embedded-host-platform

05 回调里只记录,轮询里才发送

问题

读数到达后,好几个组件要做出反应,而且反应要落到设备上:

这些组件都订阅了数据分发服务。最直接的写法是:在收到读数的回调里算出决策,然后立即下发命令。

第一版就是这么写的

结果在硬件模式下是错的,而且错得很彻底。数据回调是在接收循环读串口的过程中被触发的:

runner.run_once()
  └─ receiver 读串口、拼帧、解码
       └─ DataService.publish(读数)
            └─ 通风控制器的回调
                 └─ 设备管理器.deliver(FAN_ON)      ← 在这里下发
                      └─ 在同一个串口上等设备的应答帧

deliver() 要在同一个串口上读应答,还用着另一个拼帧缓冲。于是:

这不是推演出来的:测试进程直接崩溃(Fatal Python error: Aborted)。

决定

回调里只做内存操作,耗时的事交给轮询循环。 每个派发器拆成两个方法:

def handle_decision(self, decision):   # 在数据回调里被调用
    self._desired = decision.state     # 只记下期望状态,不发任何东西

def dispatch_pending(self):            # 由轮询循环调用,紧跟在 run_once() 之后
    if self._desired != self._applied:
        ... 真正下发命令 ...

组合代码里的轮询循环:

runner.run_once()            # 读串口、拼帧、发布读数,回调在这里触发
fan.dispatch_pending()       # 此时串口空闲,没有别的读者
alerts.dispatch_pending()
alarm_state.dispatch_pending()
answers.dispatch_pending()

所有”让设备做点什么”的组件都遵守这个形状。同一条结论后来也用在了不下发命令、但耗时的地方: 历史记录器在回调里只把读数放进内存批次,满 50 条或 5 秒才由轮询循环写一次盘—— 数据回调跑在采集线程上,一次磁盘写入就会直接坐在维持界面响应的那条路径上。

拆开之后,每个派发器可以各管各的语义

决策与发送分开以后,”什么时候发、发几次、失败怎么办”就成了派发器自己的事,而且各不相同:

派发器 携带的是 去重 失败后
风扇 状态:应该开还是关 与上次已成功下发的值比较,没变就不发 不更新”已下发”值,下一轮自然重试,不需要定时器
语音播报 事件:这一刻播一次 不去重;两次轮询之间来了多条只保留最后一条 不重试。几秒后才响的报警比不响更糟
LCD 报警位图 状态 同风扇 同风扇
LCD 问答页 最近一次答案 只下发有模板的答案种类 同风扇

另外两条共同的规矩:

风扇派发器的”已下发”初值是”关”,因为固件上电时风扇是停的——据此假设,可以避免每次启动都白发一条 FAN_OFF。

结果

代价

命令比决策最多晚一个轮询周期发出。硬件模式的轮询间隔是 0.5 秒,采集周期是 3 秒, 对通风和告警都没有可感知的影响。