一个分区超温就全棚浇水,控制系统怎样按区下策略

温室控制舱里有个数字让我盯着看了半天:番茄区 26.2℃,只比目标 26℃ 高了 0.2℃,系统就把四个分区全部打开滴灌。香草区这会儿才 23.1℃,目标 22℃,本来就偏凉,再浇一遍水,根直接泡烂。0.2℃ 的越限,让 74% 的水肥洒进了根本不需要水的分区------这就是第一版全局阈值策略的后果。

第一版的逻辑特别直:定一个全局温度上限 26℃,任何一个分区读数超过它,就 trigger 全棚灌溉。代码也就三行。坏就坏在"任何一个分区超,全棚都浇"这个假设上。四个分区种的不是同一种作物:育苗区、番茄区、香草区、花卉区各有各的目标温度和湿度,一个统一阈值根本套不住。

每个分区一张阈值表

改法是每个分区一张阈值表,各算各的动作。先把分区画像建出来。

ts 复制代码
export interface ZoneProfile {
  id: string          // 'tomato'
  name: string        // '番茄区'
  tempC: number       // 当前 26.2
  tempTarget: number  // 目标 26
  tempMax: number     // 硬上限 28
  tempHysteresis: number // 迟滞 0.5
  humidPct: number    // 72
  ecTarget: number    // 营养液 EC 2.1
  coolCrop: boolean   // 是否喜凉作物
}

export type ZoneAction = 'IRRIGATE' | 'VENT' | 'SHADE' | 'NONE'

每个分区的判定逻辑独立。温度超过硬上限先通风,超过目标加迟滞才浇水,其余不动。

ts 复制代码
export function evaluateZone(z: ZoneProfile): ZoneAction {
  if (z.tempC > z.tempMax) {
    return 'VENT'
  }
  if (z.tempC > z.tempTarget + z.tempHysteresis) {
    return 'IRRIGATE'
  }
  return 'NONE'
}

番茄区 26.2 对比 26 + 0.5 = 26.5,没到阈值,返回 NONE,不浇水。香草区 23.1 离自己的目标 22 还有距离,也不会动。四个分区的阈值各不相同:

分区 当前温度 目标 硬上限 迟滞
育苗区 24.8 25 28 0.5
番茄区 26.2 26 28 0.5
香草区 23.1 22 27 0.5
花卉区 25.4 25 28 0.5

全棚浇水的问题消失了------动作按区下发,各分区只对自己的阈值负责。

阈值表定完之后,还得管住动作在临界点上别来回抖。

那 0.2℃ 的越限到底要不要处理?这里补一个迟滞带。没有迟滞的话,温度在 26℃ 上下抖,传感器噪声 ±0.1℃ 会让阀门在开和关之间来回跳,阀门磨损、水肥浪费。加 0.5℃ 的迟滞,越过 26.5 才浇水,降到 25.5 以下才停,中间一段抖动被吸收掉。

迟滞带还有一个更实际的意义:灌溉阀门有机械寿命,频繁启停比持续浇水更费。0.5℃ 的迟滞把启停频率压下来,配合"滴灌每 3 小时"的策略,一次浇水任务能覆盖一个完整的迟滞周期。动作判定得有记忆------已经开启的阀门不能因为一次读数回落就立刻关,得等温度真正降到下限以下。这里用一个带状态的执行器,Schmitt 触发器式的开合:

ts 复制代码
export class ZoneActuator {
  private active = new Map<string, boolean>()

  decide(z: ZoneProfile): ZoneAction {
    const on = this.active.get(z.id) ?? false
    if (on && z.tempC > z.tempTarget - z.tempHysteresis) {
      return 'IRRIGATE'
    }
    if (!on && z.tempC > z.tempTarget + z.tempHysteresis) {
      this.active.set(z.id, true)
      return 'IRRIGATE'
    }
    this.active.set(z.id, false)
    return 'NONE'
  }
}

已开启的阀门在温度降到 target - hysteresis 之前保持浇水,未开启的要越过 target + hysteresis 才启动,中间那段抖动执行器不动。

ts 复制代码
export function planZoneActions(zones: ZoneProfile[]): Map<string, ZoneAction> {
  const plan = new Map<string, ZoneAction>()
  for (const z of zones) {
    plan.set(z.id, evaluateZone(z))
  }
  return plan
}

计划算出来之后,还要量化"按区下策略"到底省了多少。我写了个对比函数,统计全局阈值规则会浇几个分区、按区规则实际浇几个。

ts 复制代码
export function savedZones(zones: ZoneProfile[], globalMax: number): number {
  let naive = 0
  let perZone = 0
  for (const z of zones) {
    if (z.tempC > globalMax) {
      naive++
    }
    if (evaluateZone(z) === 'IRRIGATE') {
      perZone++
    }
  }
  return naive - perZone
}

界面底部那个"预计节水 18.6%"就是这么估出来的:全局规则下番茄区越限要浇全棚 4 个分区,按区规则下只有真正需要水的分区触发,省下来的灌溉量换算成百分比。18.6% 是演示值,真实数字取决于各分区的阈值和当天温差。

异步轮询与失败跳过

传感器读数不是同步的,各分区探头有自己的上报节奏。我用一个异步轮询循环拉各区温度,读到就重算该区动作,不去等全部到齐再统一算,这样某个分区探头离线不会拖累别的区。

截图里番茄区的环境策略面板挂着通风"26℃ 启动"、滴灌"每 3 小时"、补光"17:30---20:00"、营养液"EC 2.1",右侧还有顶窗 45%、遮阳 20%、滴灌 ON 的实时状态。这些策略项都挂在各自分区下面,跟按区阈值表一一对应。EC 2.1 是营养液电导率,超出该分区作物耐受范围就要稀释或停供,这条跟温度浇水是并行的另一路判定,同样按区配置。CO₂ 684 ppm、光照 38 klx 是另一组环境量,阈值判定同样按区分开。

ts 复制代码
export async function pollZones(
  zones: ZoneProfile[],
  readTemp: (id: string) => Promise<number>,
  onPlan: (plan: Map<string, ZoneAction>) => void
): Promise<void> {
  while (true) {
    for (const z of zones) {
      try {
        z.tempC = await readTemp(z.id)
      } catch (e) {
        continue
      }
    }
    onPlan(planZoneActions(zones))
    await delay(30_000)
  }
}

function delay(ms: number): Promise<void> {
  return new Promise<void>((resolve) => {
    setTimeout(resolve, ms)
  })
}

readTemp 失败时 continue 跳过这个分区,保留上一次读数,等下一轮重试,30 秒一次。真实温室的传感器走 @ohos.sensor 或网关协议,这里抽象成函数注入,方便换数据源。

复盘记录

这个 bug 的完整复盘按时间顺序记一遍。

第一版错在哪:全局阈值定死 26℃,任何一个分区读数超过它就 trigger 全棚灌溉,代码统共三行,逻辑直得不能再直。直错的根子在于"任何一个分区超,全棚都浇"这个假设------四个分区种的是四种作物,育苗区目标 25、番茄区 26、香草区 22、花卉区 25,一个统一数字根本套不住四张卷子。当时想着阈值先跑起来,配置项以后再加,这一拖就是半个月。

怎么发现的:那天下午看浇水记录,番茄区 26.2℃ 只越限 0.2℃,四个分区的阀门却在同一秒全部打开。我把四个区的阀门开启日志按时间对了一遍,全棚 4 个分区无一幸免,74% 的水肥洒进了根本不需要水的分区。香草区 23.1℃ 本来就偏凉,再浇一遍水,根直接泡烂。0.2℃ 的越限换来一次全棚误灌,这个代价当时没算过。

怎么改的:分两步。第一步把阈值从全局搬到分区,每个 ZoneProfile 带上自己的 tempTarget 和 tempMax,evaluateZone 只认自己这张表;第二步补迟滞带,0.5℃ 的抖动吸收区间,配合 Schmitt 触发器式的执行器,阀门不再在开关之间跳。改完之后同样的读数再跑一遍,只有番茄区返回 IRRIGATE,其余三个区 NONE。动作按区下发,各分区只对自己的阈值负责,谁越限谁动,互不牵连。

复盘会开完我还做了一件事:把四个分区的阀门开启日志导出来存档,每条带时间戳、分区、持续秒数。后面再有人嘀咕"按区策略是不是过度设计",直接翻日志------上个月全棚误灌 3 次,按区策略上线之后 0 次。数据比解释管用,这也是这次复盘留给流程的东西。

复盘里还有条教训:全局阈值省下的配置工夫,后面连本带利还了回去。阈值表看起来多了四份配置,可每份都是明写的数据,出问题对着表就能查;全局那一个数字反而是暗逻辑,藏在 trigger 条件里,看代码看不出来它管着四个区。

工程结构与边界

工程目录长这样,阈值表和动作判定集中在一个文件。

text 复制代码
GreenhouseOS/
├─ entry/src/main/ets/
│  ├─ pages/Index.ets           # 分区列表、环境面板、趋势
│  ├─ features/ZonePolicy.ets   # 每分区阈值 + 动作判定
│  └─ features/ZonePoller.ets   # 异步轮询与降级
├─ entry/src/main/resources/    # 分区色板与文案
└─ build-profile.json5

这件事给我的教训是:全局阈值是懒做法,省了配置,赔了水肥。把阈值下放到每个分区,判定逻辑反而更简单------每区只对自己的目标温度负责,越限了该通风通风、该浇水浇水,互不牵连。

补一句边界。本文的温度、湿度、EC 阈值都是演示默认值,真实温室控制要按具体作物品种、生长阶段和当地气候标定,本文不做农业种植建议。自动策略上线前必须保留手动模式兜底,界面里那个"自动策略运行中 / 手动模式"的开关就是留给突发情况的,任何时候人工可以接管。

相关推荐
前端架构师-老李1 小时前
HTML与CSS基础第二课—初识HTML
前端·css·html
云上工程笔记1 小时前
2026年大模型Token成本测算推荐清单:输入输出、缓存、并发与峰值用量
人工智能·spring boot·缓存·token
视***间1 小时前
视程空间Edge工业边缘盒:给工业机器人装一颗耐造的“本地大脑“
前端·edge·机器人·边缘计算·智慧工业·工业视觉·视程空间
SelectDB技术团队1 小时前
ClickHouse SQL 好学吗?从 MySQL 迁移 Doris 的完整兼容验证
人工智能·sql·mysql·clickhouse·开源·apache doris·增量计算
AI_AGENT_DEV_AI1 小时前
AI英语在线考试系统的开发
人工智能·语音识别
stereohomology1 小时前
Kimi K3.1 Pro试用:可称之曰能
人工智能·workbuddy
AI日报派送佬1 小时前
2026年10月27日AI行业日报|Anthropic递交IPO招股书、GPT-6.1 Terra降价、Kimi K3.1 Pro即将开源
人工智能·openai·kimi·deepseek·anthropic·ai日报·人工智能每日速报
中伟视界1 小时前
边缘端行为识别部署实战:AI算法分析盒子落地皮带场景
人工智能·算法