news 2026/11/4 1:01:48

MQTT温控联动系统实战:从温度传感器到制冷设备自动触发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MQTT温控联动系统实战:从温度传感器到制冷设备自动触发

1. 从一个温度传感器到一台制冷设备的完整链路

夏天机房温度飙到38度,空调还在慢悠悠地吹,等运维发现的时候硬盘已经掉线了两块。这种场景我经历过不止一次,后来干脆自己搭了一套基于 MQTT 的温控联动系统,让温度数据自己"喊"制冷设备开工。这套东西的核心逻辑特别朴素:传感器采集温度,通过 MQTT 协议把数据发到 Broker,订阅端拿到数据后判断是否超过阈值,超了就下发指令让制冷设备启动。整条链路没有任何复杂的中间件,一台树莓派加几个传感器就能跑起来。

MQTT Climate-Triggered Cooling 这个标题拆开看,三个关键词各有分量。MQTT是通信底座,负责把温度数据从采集端搬到决策端;Climate-Triggered是触发逻辑,定义了"什么条件下该制冷";Cooling是执行动作,最终落到继电器、红外发射器或者空调控制接口上。很多人一上来就纠结用什么传感器、买什么继电器,其实真正决定这套系统好不好用的,是触发策略的设计和 MQTT 主题结构的规划。我见过太多人把温度数据一股脑发到一个主题里,结果设备一多就乱成一锅粥,想加个湿度联动都无从下手。

这篇文章适合谁看?如果你手头有温湿度传感器、有一台常开的 Linux 设备(树莓派、旧笔记本、工控机都行)、想让制冷设备根据温度自动启停,那这篇内容可以直接抄作业。如果你只是想了解 MQTT 协议本身,文中关于主题设计、QoS 选择、遗嘱消息的部分也能给你不少参考。我会从协议选型讲到代码实现,再到实际部署中踩过的坑,尽量把每个决策背后的"为什么"说清楚。

提示:整套系统的可靠性不取决于最贵的传感器,而取决于最薄弱的那个环节。多数翻车案例都出在触发逻辑的边界处理上,而不是硬件本身。

2. 为什么用 MQTT 而不是 HTTP 轮询来做温控触发

2.1 轮询模式在温控场景下的三个硬伤

先说清楚为什么不用 HTTP。假设你用 HTTP 轮询,传感器端每 5 秒发一次 GET 请求到服务器,服务器返回当前是否需要制冷。这个模式在实验室里跑没问题,放到真实环境里问题就来了。第一个硬伤是实时性差:轮询间隔 5 秒意味着温度突变后最多要等 5 秒才能触发制冷,如果间隔拉到 30 秒省资源,那响应就更慢。第二个硬伤是连接开销大:每次 HTTP 请求都要重新建立 TCP 连接(除非用长连接,但长连接本质上就是在模拟 MQTT 的行为了),传感器端如果是电池供电,这个开销直接吃掉续航。第三个硬伤是单向通信:HTTP 是请求-响应模式,服务器没法主动推消息给传感器,想远程改个阈值都得等下一次轮询。

MQTT 的发布-订阅模型天然适合这种场景。传感器只管往 Broker 发消息,决策端只管从 Broker 收消息,两边互不知道对方的存在。这种解耦带来的好处是:你可以在不碰传感器代码的情况下,增加第二个决策端做数据记录,或者增加第三个决策端做告警推送。我实际部署的时候,同一个温度主题同时被三个客户端订阅——一个负责控制制冷,一个负责写入数据库,一个负责异常告警,彼此完全独立。

2.2 QoS 等级的选择直接决定触发可靠性

MQTT 有三个 QoS 等级,选哪个直接影响到"温度超阈值但制冷没启动"这种事会不会发生。QoS 0 是"最多发一次",发出去就不管了,网络抖动一下消息就丢了。QoS 1 是"至少发一次",保证消息到达但可能重复。QoS 2 是"恰好发一次",开销最大但最可靠。

温控触发场景我建议用QoS 1。原因很直接:温度数据丢一条可能刚好是那条超阈值的,导致制冷没触发;但重复一条温度数据无非是多判断一次,幂等处理一下就行。QoS 2 的四次握手在传感器资源受限的情况下反而容易出问题,而且温控场景对"恰好一次"的需求没那么强烈。下面这张表是我实测三种 QoS 在树莓派上的表现对比:

QoS 等级平均延迟丢包率(弱网)CPU 占用适用场景
QoS 012ms8.3%极低高频非关键数据
QoS 118ms0%低温控触发、告警
QoS 235ms0%中计费、不可重复操作

实测环境是树莓派 4B 通过 Wi-Fi 连接局域网内的 Mosquitto Broker,弱网模拟用 tc 命令限速到 50kbps 并注入 5% 丢包。QoS 0 在弱网下丢了将近十分之一的消息,这个丢包率在温控场景里是不可接受的。

2.3 遗嘱消息让"传感器掉线"也能被感知

MQTT 有个特别好用的特性叫遗嘱消息(Last Will and Testament)。传感器连接 Broker 的时候可以预设一条遗嘱,一旦传感器异常断开(断电、网络断了、进程崩了),Broker 会自动把这条遗嘱发到指定主题。这个机制在温控系统里价值巨大:如果温度传感器掉线了,决策端能立刻收到通知,而不是傻等着温度数据然后误以为"温度正常所以不需要制冷"。

我一般会设两条遗嘱:一条发到climate/sensor/status主题,payload 是offline;另一条发到climate/alert主题,payload 是具体的传感器 ID 和掉线时间。决策端订阅这两个主题,一旦收到离线消息就触发告警,同时把制冷设备切到手动模式或者安全默认状态。这个设计救过我一次——机房空调的温感探头被老鼠咬断了线,系统在 3 秒内就推了告警到手机,而不是等温度升到报警线才发现。

3. 主题结构设计:别让温度数据变成一锅粥

3.1 用层级主题把设备、位置、指标拆开

MQTT 主题用斜杠分层,这个设计用好了能让后续扩展轻松很多。我见过最糟糕的做法是所有传感器都往temperature这一个主题发数据,payload 里塞个 JSON 说明是谁发的。这种做法的后果是:决策端收到消息后还得解析 JSON 才知道是哪个位置的温度,而且没法用通配符做精细订阅。

推荐的主题结构是这样的:

climate/{location}/{device_type}/{device_id}/{metric}

举几个具体例子:

  • climate/server-room/sensor/temp-01/temperature— 机房1号温度传感器
  • climate/server-room/sensor/temp-01/humidity— 同一个传感器的湿度
  • climate/warehouse/sensor/temp-07/temperature— 仓库7号温度传感器
  • climate/server-room/actuator/ac-01/command— 机房1号空调的控制指令
  • climate/server-room/actuator/ac-01/state— 机房1号空调的当前状态

这种结构的好处是订阅端可以用通配符灵活筛选。比如决策端想订阅所有机房的温度数据,用climate/server-room/sensor/+/temperature就行;想订阅所有位置的温度,用climate/+/sensor/+/temperature。加号是单层通配符,井号是多层通配符,这两个符号用好了能省掉大量硬编码。

3.2 控制指令和状态反馈必须分主题

这一点特别重要,很多新手会把控制指令和状态反馈混在一个主题里,结果就是决策端发了一条"开机"指令,然后自己又收到了这条指令,误以为是设备状态更新。正确的做法是指令走 command 主题,状态走 state 主题,两者完全分开。

决策端往climate/server-room/actuator/ac-01/command发{"action":"on","target_temp":24},制冷设备订阅这个主题执行动作,执行完后往climate/server-room/actuator/ac-01/state发{"power":"on","current_temp":26,"mode":"cooling"}。决策端订阅 state 主题来确认指令是否被执行。这样就形成了一个闭环:发指令、收状态、判断是否需要重发或告警。

注意:command 主题的 QoS 建议用 1,state 主题用 QoS 0 或 1 都行。指令丢了要重发,状态丢一条影响不大,下次状态更新会覆盖。

3.3 保留消息让新上线的决策端立刻拿到最新状态

MQTT 的保留消息(Retained Message)是个容易被忽略但极其有用的特性。普通消息发出去后,如果当时没有订阅者,消息就丢了。保留消息不一样,Broker 会为这个主题保存最后一条保留消息,任何新订阅这个主题的客户端都会立刻收到它。

在温控系统里,我会让所有 state 主题的消息都带 retain 标志。这样决策端重启后,一订阅climate/server-room/actuator/ac-01/state就立刻知道空调当前是开是关、设的多少度,不用等下一次状态更新。温度数据我也建议用 retain,这样新上线的数据记录服务能立刻拿到最新温度值,而不是干等到下一次采集。

但 command 主题绝对不要用 retain。想象一下:你发了一条"开机"指令并 retain 了,结果空调执行完关机了,这时候一个新的控制端上线,立刻收到那条保留的"开机"指令,又把空调打开了。这种 bug 排查起来非常痛苦,我踩过一次,花了两个小时才定位到是 retain 惹的祸。

4. 触发逻辑的边界处理:温度在阈值附近抖动怎么办

4.1 单纯比较阈值会导致制冷设备频繁启停

假设阈值设的是 28 度,温度到 28.1 度触发制冷,降到 27.9 度停止制冷。如果温度刚好在 28 度附近波动,比如 28.1、27.9、28.2、27.8 这样来回跳,制冷设备就会疯狂启停。压缩机和继电器最怕这种频繁启停,轻则缩短寿命,重则直接烧掉。

解决办法是引入回差(Hysteresis),也叫滞回区间。具体做法是设两个阈值:启动阈值 28 度,停止阈值 26 度。温度超过 28 度才启动制冷,启动后一直制冷到温度降到 26 度以下才停止。这样中间有 2 度的缓冲带,温度在 28 度附近抖动不会导致频繁启停。

回差的大小要根据实际场景调。机房这种发热量大的地方,回差可以设 2-3 度;温室大棚这种对温度敏感的地方,回差设 0.5-1 度。回差太小起不到防抖作用,太大又会导致温度控制不精准。我一般先用 2 度起步,跑一周看启停次数,再微调。

4.2 加一个最短运行时间锁

光有回差还不够。有些场景下温度变化很快,比如开门通风导致温度瞬间飙升,触发制冷后门关了温度又迅速回落。这种情况下即使有回差,制冷设备也可能在短时间内完成一次启停循环。压缩机有个硬性要求:停机后必须等 3-5 分钟才能再次启动,否则容易损坏。

所以触发逻辑里要加一个最短运行时间锁和最短停机时间锁。制冷设备一旦启动,至少运行 N 分钟才能停;一旦停止,至少等 M 分钟才能再启动。这两个锁用简单的计时器就能实现。下面是我用的触发判断伪代码:

class CoolingController: def __init__(self): self.start_threshold = 28.0 # 启动阈值 self.stop_threshold = 26.0 # 停止阈值 self.min_run_time = 180 # 最短运行 3 分钟 self.min_off_time = 300 # 最短停机 5 分钟 self.is_cooling = False self.last_switch_time = 0 def should_start(self, current_temp, now): if self.is_cooling: return False if current_temp < self.start_threshold: return False if now - self.last_switch_time < self.min_off_time: return False return True def should_stop(self, current_temp, now): if not self.is_cooling: return False if current_temp > self.stop_threshold: return False if now - self.last_switch_time < self.min_run_time: return False return True

这段逻辑看起来简单,但每个判断条件的顺序都有讲究。先判断当前状态,再判断温度条件,最后判断时间锁,这样能避免不必要的计算,逻辑也最清晰。

4.3 温度数据要做滑动平均滤波

传感器偶尔会抽风报一个离谱的值,比如正常 27 度突然报个 45 度。如果直接拿这个值去判断,制冷设备会立刻启动,然后下一秒温度又回到 27 度,设备又停。这种毛刺数据必须过滤掉。

最简单的办法是滑动平均:维护一个长度为 N 的队列,每次新数据进来就踢掉最老的,然后算平均值。N 取 5-10 比较合适,太小起不到滤波作用,太大响应会变慢。我用的是长度 5 的滑动平均,配合回差逻辑,实测下来温度曲线非常平滑,制冷设备启停次数从每天几十次降到了每天 3-4 次。

如果对突变响应要求高,可以用中值滤波代替滑动平均:取最近 N 个数据的中位数。中值滤波对毛刺的抑制效果更好,但计算量稍大。树莓派这种设备跑中值滤波完全没压力,我后来都换成了中值滤波加滑动平均的组合,先中值去毛刺,再平均平滑。

5. 从零搭建:Broker 部署与客户端代码实现

5.1 Broker 选型与最小化配置

Broker 是整套系统的中枢,选型上我推荐Mosquitto。它足够轻量,树莓派上跑内存占用不到 10MB,配置也简单,一个配置文件几十行就能搞定。EMQX 功能更全但资源占用大,适合设备数量上千的场景;Mosquitto 在几十到几百个设备的规模下完全够用。

在 Linux 上安装 Mosquitto 很简单,以 Debian 系为例:

sudo apt update sudo apt install mosquitto mosquitto-clients

安装完 Mosquitto 默认就启动了,监听 1883 端口。但默认配置只允许本地连接,要让局域网内其他设备连上来,需要改配置文件/etc/mosquitto/mosquitto.conf:

listener 1883 0.0.0.0 allow_anonymous true persistence true persistence_location /var/lib/mosquitto/ log_dest file /var/log/mosquitto/mosquitto.log

allow_anonymous true允许匿名连接,方便调试。生产环境一定要关掉匿名并配置用户名密码,这个后面讲。persistence true开启持久化,Broker 重启后保留消息和会话不丢。改完配置重启服务:

sudo systemctl restart mosquitto sudo systemctl enable mosquitto

enable那行是设置开机自启,这个别忘,否则设备重启后整套系统就瘫了。

5.2 传感器端:用 Python 发布温度数据

传感器端我用 Python 写,因为 paho-mqtt 库足够成熟,代码量也少。先装库:

pip install paho-mqtt

假设用的是 DHT22 温湿度传感器,完整代码如下:

import time import json import paho.mqtt.client as mqtt import Adafruit_DHT BROKER = "192.168.1.100" PORT = 1883 DEVICE_ID = "temp-01" LOCATION = "server-room" TOPIC_TEMP = f"climate/{LOCATION}/sensor/{DEVICE_ID}/temperature" TOPIC_STATUS = f"climate/{LOCATION}/sensor/{DEVICE_ID}/status" def on_connect(client, userdata, flags, rc): print(f"Connected with result code {rc}") client.publish(TOPIC_STATUS, "online", qos=1, retain=True) client = mqtt.Client(client_id=DEVICE_ID) client.will_set(TOPIC_STATUS, "offline", qos=1, retain=True) client.on_connect = on_connect client.connect(BROKER, PORT, 60) client.loop_start() while True: humidity, temperature = Adafruit_DHT.read_retry(Adafruit_DHT.DHT22, 4) if temperature is not None: payload = json.dumps({ "value": round(temperature, 1), "unit": "C", "ts": int(time.time()) }) client.publish(TOPIC_TEMP, payload, qos=1, retain=True) print(f"Published: {payload}") time.sleep(10)

几个关键点解释一下。client.will_set设置了遗嘱消息,传感器掉线时 Broker 会自动发offline到 status 主题。retain=True让温度数据成为保留消息,新订阅者立刻能拿到最新值。qos=1保证消息至少到达一次。loop_start()启动后台线程处理网络收发,主循环只管读传感器和发数据。

5.3 决策端:订阅温度并控制制冷设备

决策端同样用 Python,核心逻辑是订阅温度主题,收到数据后跑触发判断,需要动作时往 command 主题发指令:

import json import time import paho.mqtt.client as mqtt BROKER = "192.168.1.100" PORT = 1883 TOPIC_TEMP = "climate/server-room/sensor/+/temperature" TOPIC_CMD = "climate/server-room/actuator/ac-01/command" TOPIC_STATE = "climate/server-room/actuator/ac-01/state" class CoolingController: def __init__(self): self.start_threshold = 28.0 self.stop_threshold = 26.0 self.min_run_time = 180 self.min_off_time = 300 self.is_cooling = False self.last_switch_time = 0 self.temp_history = [] def filter_temp(self, temp): self.temp_history.append(temp) if len(self.temp_history) > 5: self.temp_history.pop(0) return sum(self.temp_history) / len(self.temp_history) def evaluate(self, temp): now = time.time() filtered = self.filter_temp(temp) if not self.is_cooling: if filtered >= self.start_threshold and now - self.last_switch_time >= self.min_off_time: self.is_cooling = True self.last_switch_time = now return "on" else: if filtered <= self.stop_threshold and now - self.last_switch_time >= self.min_run_time: self.is_cooling = False self.last_switch_time = now return "off" return None controller = CoolingController() def on_connect(client, userdata, flags, rc): client.subscribe(TOPIC_TEMP, qos=1) client.subscribe(TOPIC_STATE, qos=1) def on_message(client, userdata, msg): if "temperature" in msg.topic: data = json.loads(msg.payload) action = controller.evaluate(data["value"]) if action: cmd = json.dumps({"action": action, "ts": int(time.time())}) client.publish(TOPIC_CMD, cmd, qos=1) print(f"Sent command: {cmd}") client = mqtt.Client(client_id="cooling-controller") client.on_connect = on_connect client.on_message = on_message client.connect(BROKER, PORT, 60) client.loop_forever()

这段代码里TOPIC_TEMP用了通配符+,能同时订阅多个温度传感器。filter_temp做了滑动平均滤波。evaluate方法里把回差判断和时间锁判断都封装好了,返回"on"、"off"或None。

5.4 执行端:收到指令后驱动继电器

执行端可以跑在另一个树莓派或者 ESP32 上,收到 command 主题的消息后驱动继电器控制空调电源:

import json import paho.mqtt.client as mqtt import RPi.GPIO as GPIO RELAY_PIN = 17 TOPIC_CMD = "climate/server-room/actuator/ac-01/command" TOPIC_STATE = "climate/server-room/actuator/ac-01/state" GPIO.setmode(GPIO.BCM) GPIO.setup(RELAY_PIN, GPIO.OUT) GPIO.output(RELAY_PIN, GPIO.LOW) def on_connect(client, userdata, flags, rc): client.subscribe(TOPIC_CMD, qos=1) def on_message(client, userdata, msg): cmd = json.loads(msg.payload) action = cmd.get("action") if action == "on": GPIO.output(RELAY_PIN, GPIO.HIGH) state = {"power": "on", "ts": cmd["ts"]} elif action == "off": GPIO.output(RELAY_PIN, GPIO.LOW) state = {"power": "off", "ts": cmd["ts"]} else: return client.publish(TOPIC_STATE, json.dumps(state), qos=1, retain=True) client = mqtt.Client(client_id="ac-01") client.on_connect = on_connect client.on_message = on_message client.connect("192.168.1.100", 1883, 60) client.loop_forever()

执行端每次动作后往 state 主题发状态,并且 retain,这样决策端重启后能立刻知道空调当前状态。这个反馈闭环很重要,没有它决策端就是"盲发指令",不知道执行端到底有没有收到。

6. 部署中真实踩过的坑与排查过程

6.1 客户端 ID 冲突导致连接反复断开

第一次部署的时候,我在两台树莓派上用了同样的代码,client_id都写死成"sensor"。结果就是两台设备轮流上线,一台连上另一台就被踢掉,日志里全是Connection refused: identifier rejected。MQTT 协议规定同一个 client_id 同时只能有一个连接,后连的会把先连的踢掉。

排查过程其实挺曲折的。一开始我以为是网络问题,换了网线换了交换机都没用。后来打开 Mosquitto 的日志看到New client connected from ... as sensor (p2, c1, k60)和Client sensor already connected, closing old connection这两行交替出现,才意识到是 client_id 冲突。解决办法很简单:client_id 里带上设备唯一标识,比如sensor-temp-01、sensor-temp-02。我后来干脆用设备的 MAC 地址做后缀,保证绝对唯一。

6.2 遗嘱消息没生效:retain 和 QoS 的配合问题

遗嘱消息我一开始设的是 QoS 0 且没加 retain,结果传感器掉线后决策端有时候收不到离线通知。原因是 QoS 0 不保证送达,而且没有 retain 的话,如果决策端当时刚好在重连,就错过了这条消息。

改成 QoS 1 加 retain 后就稳了。QoS 1 保证至少送达一次,retain 保证决策端重连后立刻能拿到最新的状态。这两个参数配合起来,离线通知的可靠性基本是 100%。这个坑让我明白一个道理:遗嘱消息的可靠性取决于 QoS 和 retain 的组合,缺一不可。

6.3 温度数据时间戳混乱引发的误判

有段时间决策端偶尔会误触发制冷,查了半天发现是时间戳问题。传感器端用的是int(time.time()),但有一台树莓派没联网,系统时间停留在出厂日期,发出来的时间戳是几年前的。决策端收到这条"来自过去"的数据,如果逻辑里有用时间戳做判断,就会出问题。

解决办法有两个:一是传感器端启动时先同步时间,用 NTP 或者从 Broker 的 CONNECT 响应里拿服务器时间;二是决策端不要依赖传感器的时间戳做关键判断,用自己收到消息的本地时间。我两个都做了,传感器端加了 NTP 同步,决策端的时间锁判断全部用本地时间。这样即使传感器时间不对,也不会影响触发逻辑。

6.4 Broker 重启后会话丢失导致指令漏发

Mosquitto 默认不持久化会话,Broker 重启后所有订阅关系都没了。有一次我升级 Mosquitto 重启了服务,决策端在重启期间发的指令全部丢失,空调该开没开,机房温度升到了 35 度。

解决方法是开启持久化并在客户端连接时设置clean_session=False:

client = mqtt.Client(client_id="cooling-controller", clean_session=False)

clean_session=False告诉 Broker 保留这个客户端的会话,包括订阅关系和未确认的 QoS 1/2 消息。配合 Broker 端的persistence true,即使 Broker 重启,决策端的订阅关系还在,重启期间发的指令也会在决策端重连后补发。这个配置对温控这种不能漏指令的场景是必须的。

6.5 弱网环境下 QoS 1 消息重复的处理

QoS 1 保证至少送达一次,意味着可能重复送达。我在弱网测试时发现同一条"开机"指令被执行端收到了三次,继电器咔咔咔响了三次。虽然结果都是开机,但继电器频繁动作会缩短寿命。

解决办法是在执行端做幂等处理:记录上一次执行的动作,如果新指令和当前状态一致就跳过。具体实现是在执行端维护一个current_state变量,收到指令后先比较,一致就不动继电器,只更新状态时间戳。这个改动很小,但效果立竿见影,弱网下继电器动作次数从几十次降到了正常水平。

7. 让系统更耐用的几个进阶配置

7.1 给 Broker 加上认证,别让匿名连接裸奔

调试阶段用匿名连接没问题,但系统一旦上线,必须加认证。Mosquitto 支持用户名密码认证,配置很简单。先创建密码文件:

sudo mosquitto_passwd -c /etc/mosquitto/passwd climate_user

然后改配置文件:

listener 1883 0.0.0.0 allow_anonymous false password_file /etc/mosquitto/passwd

重启 Broker 后,客户端连接时要带上用户名密码:

client.username_pw_set("climate_user", "your_password") client.connect(BROKER, PORT, 60)

密码文件里的密码是加密存储的,比明文安全得多。如果设备数量多,可以给每类设备分配不同的用户名,方便做权限隔离。Mosquitto 还支持 ACL(访问控制列表),能精确控制哪个用户能发布/订阅哪些主题,这个在设备多的场景下很有用。

7.2 用 systemd 管理服务,崩溃自动重启

Python 脚本跑在终端里,终端一关就没了。用 systemd 把它做成系统服务,崩溃了自动重启,开机也自动启动。创建一个服务文件/etc/systemd/system/cooling-controller.service:

[Unit] Description=MQTT Cooling Controller After=network.target mosquitto.service [Service] Type=simple User=pi WorkingDirectory=/home/pi/cooling ExecStart=/usr/bin/python3 /home/pi/cooling/controller.py Restart=always RestartSec=10 [Install] WantedBy=multi-user.target

Restart=always是关键,进程不管什么原因退出都会在 10 秒后重启。After指定了依赖关系,确保网络和 Broker 先启动。启用服务:

sudo systemctl daemon-reload sudo systemctl enable cooling-controller sudo systemctl start cooling-controller

这样即使脚本因为异常崩了,systemd 也会把它拉起来,整套系统的可用性提升一个档次。

7.3 加一个心跳主题监控全链路健康

系统跑起来后,怎么知道每个环节都活着?我加了一个心跳机制:传感器端、决策端、执行端每隔 30 秒往climate/heartbeat/{client_id}发一条带时间戳的消息。另外跑一个监控脚本订阅climate/heartbeat/+,如果某个客户端超过 90 秒没心跳就发告警。

这个机制帮我发现过好几次问题:有一次决策端的网络断了,心跳停了,监控立刻告警,我远程重启了网络就好了。还有一次执行端的 Python 进程卡死(后来查出来是 GPIO 库的 bug),心跳也停了,systemd 自动重启后恢复。心跳机制相当于给整套系统装了个"体检仪",哪个环节出问题一目了然。

7.4 温度数据落库,方便回溯和调参

光靠实时判断不够,我还想把温度数据存下来,方便回溯"为什么那天制冷没触发"这类问题。最简单的做法是决策端订阅温度主题后,除了做触发判断,还往 SQLite 写一份。SQLite 零配置,单文件,树莓派上跑完全没问题。

import sqlite3 conn = sqlite3.connect('/home/pi/cooling/temperature.db') conn.execute('''CREATE TABLE IF NOT EXISTS temp_log (ts INTEGER, device TEXT, value REAL)''') def log_temp(device, value, ts): conn.execute("INSERT INTO temp_log VALUES (?, ?, ?)", (ts, device, value)) conn.commit()

有了历史数据,调阈值和回差就有依据了。我一般跑一周后把数据导出来画个曲线,看看温度波动的规律,再决定回差设多大、最短运行时间设多长。这种数据驱动的调参比拍脑袋靠谱得多。

8. 关于这套系统我个人的几点体会

整套系统从最初的一个温度传感器加一个继电器,到现在覆盖三个机房、十几个传感器、四台制冷设备,前后迭代了差不多一年。最大的体会是:MQTT 这套发布-订阅模型的价值不在于通信本身,而在于它带来的解耦。传感器不用知道谁在消费它的数据,决策端不用知道数据从哪来,执行端不用知道指令谁发的。这种解耦让系统可以一块一块地加功能,而不用推倒重来。

另一个体会是关于边界处理。温控逻辑的核心代码可能就几十行,但真正让系统稳定运行的,是那些处理边界情况的代码——回差、时间锁、滤波、幂等、心跳。这些代码加起来比核心逻辑多好几倍,但它们才是系统能不能长期稳定跑的关键。我见过太多 demo 跑得漂亮但上线就翻车的项目,问题几乎都出在边界处理上。

最后说个实际的小技巧:阈值和回差不要一次定死,留个可以通过 MQTT 远程修改的接口。我一开始把阈值写死在代码里,每次调整都要改代码重启服务,特别麻烦。后来加了一个climate/config/threshold主题,决策端订阅它,收到新配置就热更新。这样坐在办公室里就能调参数,不用跑到机房去。这个改动不大,但日常维护的体验提升非常明显。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 1:00:48

[数字图像处理]简单的几何学图像变换与图像配准

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/4 1:00:46

浏览器即开即用:20+款ESP在线开发工具实战盘点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/3 23:59:34

LTE链路级仿真:两路发射分集从参数配置到BLER曲线全解析

简介&#xff1a;源自维也纳大学的LTE链路级仿真平台&#xff0c;完整实现了LTE下行两路发射与接收的全过程&#xff0c;涵盖发端编码、信道建模、接收译码等完整信号链&#xff0c;适合通信专业学生、算法工程师及无线通信研究人员用作链路验证与算法调试的基准工具。资源包内…

作者头像 李华
网站建设 2026/11/3 23:59:30

Maven高级应用:大型Java项目模块化与构建优化实战

1. 项目概述作为一名有十年Java开发经验的工程师&#xff0c;我深知Maven在企业级项目中的重要性。今天要分享的是Maven在实际项目中的高级应用技巧&#xff0c;这些经验都来自我参与过的多个大型分布式系统开发实战。Maven作为Java生态中最主流的构建工具&#xff0c;其基础用…

作者头像 李华
网站建设 2026/11/3 23:59:29

OpenClaw自动化工具实战解析与应用场景

1. 龙虾&#xff08;OpenClaw&#xff09;应用全景解析当技术社区首次出现"OpenClaw"这个代号时&#xff0c;很多人以为这又是某个新编程框架的酷炫命名。但经过对2110个真实案例的拆解&#xff0c;我们发现这个形似龙虾钳子的开源工具&#xff0c;正在以惊人的渗透率…

作者头像 李华
网站建设 2026/11/3 23:59:26

从10万星AI Agent项目反推软件工程经验:架构、评测与避坑指南

GitHub 上挂着 10 万星的 AI Agent 项目&#xff0c;很多人的第一反应是"这项目真火"&#xff0c;第二反应是"这代码肯定很牛"。但说实话&#xff0c;等你真正把源码翻过一遍、把架构图啃下来&#xff0c;你会发现真正值钱的不是那几行调用大模型 API 的代…

作者头像 李华