物联网 · 盛世宏博 · 多库房分区

单台一体机对接平台,是"串口通了就能采"。多库房分区管理完全不同:
核心问题变成:怎么用 RS485 这个"半双工、主从、轮询"的协议,把几十台分散设备的温湿度和消杀状态可靠地汇聚到平台,同时保证分区自治和互不影响。
平台/网关
│
├── 库房1-设备1 (slave 1)
├── 库房1-设备2 (slave 2)
├── 库房2-设备3 (slave 3)
├── 库房2-设备4 (slave 4)
├── ...
└── 库房N-设备N (slave N)优点 | 缺点 |
|---|---|
布线最简单 | 单点断线全总线瘫痪 |
网关端口最少 | 距离超 1200m 信号衰减 |
成本低 | 节点超 32 个需中继 |
不同库房混在同一碰撞域 |
适用:≤8 台设备,总距离 ≤500m,同一楼层。
┌── 网关串口1 ── 库房1总线 (slave 1~3)
├── 网关串口2 ── 库房2总线 (slave 4~6)
平台 ◄─── 以太网 ────├── 网关串口3 ── 库房3总线 (slave 7~9)
├── 网关串口4 ── 库房4总线 (slave 10~12)
└── ...优点 | 缺点 |
|---|---|
单库故障不扩散 | 网关需多串口(或级联) |
每库独立 1200m | 布线量稍大 |
节点数不受限 | 网关成本略高 |
轮询效率最优 | 需规划串口分配表 |
便于分区维护 |
适用:8~64 台设备,多楼层,库房间距 >50m。
库房1: 一体机群 → 串口服务器(本地) ──┐
库房2: 一体机群 → 串口服务器(本地) ──┼── 以太网 ──▶ 平台
库房3: 一体机群 → 串口服务器(本地) ──┘优点 | 缺点 |
|---|---|
距离无限制 | 每台串口服务器需 IP |
利用现有网络 | 网络故障影响通讯 |
扩展最灵活 | 需管理 IP 段 |
平台侧统一 Modbus TCP | 延迟略高(网络抖动) |
适用:>32 台,跨楼宇,已有综合布线。
规模 | 推荐方案 |
|---|---|
≤8 台,单楼层 | 方案A |
8~32 台,2~4 库房 | 方案B |
32~64 台,多楼层 | 方案B 变体(每楼层一网关) |
>64 台,跨楼宇 | 方案C |
地址段划分规则:
库房1: slave 1~8
库房2: slave 9~16
库房3: slave 17~24
...
或按功能:
恒湿机: 1~48
备用: 49~64
调试: 65~127平台点表必须带分区标签:
devices:
- slave: 3
zone: "库房A-普通档案"
precision: "±5%RH"
model: "HME-ARCHIVE-Pro"
location: "东侧第三列密集架旁"
rs485_port: "COM2"
- slave: 11
zone: "库房B-特藏库"
precision: "±3%RH"
model: "HME-ARCHIVE-Elite"
location: "中央通道北端"
rs485_port: "COM3"点名 | 寄存器 | 类型 | 缩放 | 上报周期 |
|---|---|---|---|---|
room_temp | 30001 | INT16 | 0.1℃ | 2s |
room_rh | 30002 | UINT16 | 0.1%RH | 2s |
dewpoint | 30003 | INT16 | 0.1℃ | 5s |
pm25 | 30004 | UINT16 | 1 μg/m³ | 5s |
tvoc | 30005 | UINT16 | 0.01 mg/m³ | 5s |
ozone | 40012 | UINT16 | 0.001 ppm | 5s |
点名 | 寄存器 | 位/值 | 上报策略 |
|---|---|---|---|
disinfect_mode | 40010 | 0关/1UV/2臭氧/3等离子 | 变更即报+30s周期 |
disinfect_status | status_word bit6 | 1=消杀中 | 变更即报 |
disinfect_timer | 40011/40012 | 时段设定 | 变更即报 |
safety_interlock | 40013 | bit0~5 | 变更即报 |
disinfect_run_hours | 40017+40018 | UINT32 | 60s周期 |
uv_lamp_hours | 自定义 | UINT32 | 60s周期 |
ozone_concentration | 40012 | 实时值 | 5s周期(消杀中) |
# status_word (40009) 位定义
STATUS_BITS = {
0: "running", # 运行中
1: "fault", # 故障
2: "humidifying", # 加湿中
3: "dehumidifying", # 除湿中
4: "reheating", # 再热中
5: "purifying", # 净化中
6: "disinfecting", # 消杀中 ← 关键
7: "local_lock", # 本地锁定
8: "comm_lost", # 内部通讯异常
}
# safety_interlock (40013) 位定义
SAFETY_BITS = {
0: "door_open", # 门开 → 禁UV/臭氧
1: "human_present", # 有人 → 禁强消杀
2: "smoke_fire", # 消防 → 急停
3: "emergency_stop", # 急停按钮
4: "water_level_err", # 水位异常
5: "filter_blocked", # 滤网堵塞
}RS485 上 Modbus RTU 是纯主从轮询,设备不能"主动上报"。所以"上报"实际是:
网关按周期轮询每台设备 → 读取寄存器 → 组装 payload → 通过 MQTT/HTTP/TCP 推给平台# polling_scheduler.py
"""多库房分区轮询调度器"""
import time
from collections import defaultdict
class ZonePollingScheduler:
def __init__(self):
# {zone: [slave_ids]}
self.zones = defaultdict(list)
# {slave_id: {port, baudrate, ...}}
self.slave_config = {}
self.poll_intervals = {
"critical": 1, # 消杀状态/安全联锁
"normal": 2, # 温湿度
"slow": 30, # 累计时长/滤网
}
self.last_poll = defaultdict(dict)
def register_device(self, zone: str, slave: int, port: str, **kwargs):
self.zones[zone].append(slave)
self.slave_config[slave] = {"port": port, **kwargs}
def poll_cycle(self):
"""一轮完整轮询"""
results = {}
for zone, slaves in self.zones.items():
zone_results = {}
for slave in slaves:
now = time.time()
cfg = self.slave_config[slave]
# 快变量:温湿度(2s)
if self._should_poll(slave, "normal", now):
zone_results[slave] = self._poll_normal(slave, cfg)
self.last_poll[slave]["normal"] = now
# 快变量:消杀状态/安全联锁(1s,消杀中时)
if self._is_disinfecting(zone_results.get(slave, {})):
if self._should_poll(slave, "critical", now):
disinfect_data = self._poll_disinfect(slave, cfg)
zone_results[slave].update(disinfect_data)
self.last_poll[slave]["critical"] = now
# 慢变量:累计时长(30s)
if self._should_poll(slave, "slow", now):
slow_data = self._poll_slow(slave, cfg)
if slave in zone_results:
zone_results[slave].update(slow_data)
self.last_poll[slave]["slow"] = now
results[zone] = zone_results
return results
def _should_poll(self, slave: int, level: str, now: float) -> bool:
last = self.last_poll.get(slave, {}).get(level, 0)
interval = self.poll_intervals[level]
return (now - last) >= interval
def _is_disinfecting(self, data: dict) -> bool:
if not data:
return False
status = data.get("status_word", 0)
return bool(status & (1 << 6)) # bit6 = disinfecting
def _poll_normal(self, slave: int, cfg: dict) -> dict:
"""读温湿度+运行状态"""
# 实际调用 pymodbus 或串口库
# 这里返回模拟结构
return {
"slave": slave,
"room_temp": None, # 从 30001 读
"room_rh": None, # 从 30002 读
"status_word": 0, # 从 40009 读
"ts": time.time(),
}
def _poll_disinfect(self, slave: int, cfg: dict) -> dict:
"""读消杀详细状态"""
return {
"disinfect_mode": 0, # 从 40010 读
"safety_interlock": 0, # 从 40013 读
"ozone": 0.0, # 从 40012 读
}
def _poll_slow(self, slave: int, cfg: dict) -> dict:
"""读运维量"""
return {
"run_hours": 0, # 从 40017+40018 读
"filter_remain": 0, # 从 40016 读
"fault_word": 0, # 从 40015 读
}纯轮询有延迟。消杀状态变更需要"准实时"感知,做法:
设备侧:消杀状态变化时,拉高一个"事件标志位"(如 40020 event_flag)
网关侧:每 1s 快速扫 event_flag
→ 若 event_flag != 0 → 立即读完整消杀状态包 → 推平台 → 清 flag# event_trigger.py
"""消杀状态变更事件检测"""
class DisinfectEventMonitor:
def __init__(self, gateway):
self.gateway = gateway
self.last_state = {} # {slave: {mode, interlock, ozone}}
def check_events(self) -> list:
events = []
for slave in self.gateway.get_slaves():
current = self.gateway.read_disinfect_status(slave)
prev = self.last_state.get(slave, {})
# 消杀模式变更
if current.get("mode") != prev.get("mode"):
events.append({
"slave": slave,
"type": "disinfect_mode_change",
"from": prev.get("mode", 0),
"to": current.get("mode"),
"ts": time.time(),
})
# 安全联锁触发
if current.get("interlock", 0) & ~prev.get("interlock", 0):
new_bits = current["interlock"] & ~prev.get("interlock", 0)
events.append({
"slave": slave,
"type": "safety_interlock_triggered",
"bits": new_bits,
"ts": time.time(),
})
# 臭氧浓度超阈
if current.get("ozone", 0) > 0.1: # 0.1ppm 阈值
events.append({
"slave": slave,
"type": "ozone_high",
"value": current["ozone"],
"ts": time.time(),
})
self.last_state[slave] = current
return events{
"archive": "市档案馆",
"zones": {
"普通档案库A": {
"target": {"temp": [14, 24], "rh": [45, 60]},
"devices": [
{
"slave": 3,
"name": "一体机-03",
"location": "东侧第三列",
"online": true,
"last_seen": "2026-10-13T15:30:00+08:00",
"data": {
"room_temp": 21.5,
"room_rh": 53.2,
"dewpoint": 11.8,
"pm25": 35,
"tvoc": 0.12,
"ozone": 0.02,
"status": {
"running": true,
"humidifying": false,
"dehumidifying": false,
"purifying": true,
"disinfecting": false
},
"safety": {
"door_open": false,
"human_present": false,
"smoke_fire": false
},
"disinfect": {
"mode": 0,
"scheduled_start": 1320,
"scheduled_end": 1440,
"run_hours": 1280
}
},
"quality": "GOOD",
"freshness_ms": 1200
}
],
"zone_summary": {
"avg_temp": 21.8,
"avg_rh": 52.4,
"any_disinfecting": false,
"any_fault": false,
"online_count": 3,
"total_count": 3
}
},
"特藏库B": {
"target": {"temp": [20, 22], "rh": [48, 52]},
"devices": [...],
"zone_summary": {...}
}
}
}┌─────────────────────────────────────────────────────────────┐
│ 平台全局层 │
│ · 全馆温湿度看板 │
│ · 跨库告警汇总 │
│ · 消杀排程总表(避免多库同时臭氧) │
│ · 审计日志 │
├─────────────────────────────────────────────────────────────┤
│ 分区自治层(每个库房独立) │
│ · 本地阈值(按库房精度等级) │
│ · 本地死区/错峰 │
│ · 本地门禁/消防互锁 │
│ · 单库消杀窗口(不与邻库冲突) │
├─────────────────────────────────────────────────────────────┤
│ 设备层(每台一体机) │
│ · PID 闭环 │
│ · 本地自治(通讯中断时按上次设定跑) │
│ · 状态字/故障字实时更新 │
└─────────────────────────────────────────────────────────────┘# disinfect_scheduler.py
"""多库房消杀排程——避免臭氧/UV 同时开"""
class DisinfectScheduler:
def __init__(self, zones: list):
self.zones = zones
# 每个库房的消杀窗口(分钟,0~1439)
self.zone_windows = {}
def assign_windows(self):
"""自动分配消杀窗口,相邻库房错开"""
total_zones = len(self.zones)
window_duration = 120 # 每库 2 小时窗口
for i, zone in enumerate(self.zones):
start = 1320 + i * window_duration # 从 22:00 开始排
if start >= 1440:
start -= 1440
end = start + window_duration
if end > 1440:
end -= 1440
self.zone_windows[zone] = {"start": start, "end": end}
return self.zone_windows
def can_start(self, zone: str, mode: int) -> bool:
"""判断某库能否启动消杀"""
window = self.zone_windows.get(zone, {})
# 强消杀模式(UV/臭氧)必须在本库窗口内
if mode in (1, 2): # UV / 臭氧
now_min = self._current_minutes()
start, end = window["start"], window["end"]
if start < end:
in_window = start <= now_min < end
else: # 跨午夜
in_window = now_min >= start or now_min < end
if not in_window:
return False
# 检查相邻库房是否正在强消杀
current_zone_idx = self.zones.index(zone)
for neighbor_idx, neighbor in enumerate(self.zones):
if neighbor_idx == current_zone_idx:
continue
if self._is_neighbor(zone, neighbor):
if self._neighbor_disinfecting(neighbor, strong_only=True):
return False
return True
def _current_minutes(self) -> int:
import datetime
now = datetime.datetime.now()
return now.hour * 60 + now.minute
def _is_neighbor(self, zone1: str, zone2: str) -> bool:
# 判断两库是否相邻(共享墙体/走廊)
# 实际项目中从建筑平面图提取
return False # 简化
def _neighbor_disinfecting(self, zone: str, strong_only: bool = False) -> bool:
# 查该库消杀状态
return False # 简化某库房总线断线/短路 → 网关检测到该串口无响应
→ 标记该分区 offline
→ 其他分区继续正常轮询
→ 平台显示"库房X通讯中断",不误报"设备离线"
→ 修复后自动恢复,数据补传# fault_isolation.py
"""单设备故障隔离——不拖垮整条总线"""
class DeviceFaultIsolator:
def __init__(self, max_retries=3, isolation_timeout=300):
self.retry_count = {}
self.isolated = {} # {slave: isolated_until_ts}
self.max_retries = max_retries
self.isolation_timeout = isolation_timeout
def should_skip(self, slave: int) -> bool:
if slave in self.isolated:
if time.time() < self.isolated[slave]:
return True
else:
# 隔离期满,尝试恢复
del self.isolated[slave]
self.retry_count[slave] = 0
return False
def report_success(self, slave: int):
self.retry_count[slave] = 0
def report_failure(self, slave: int):
self.retry_count[slave] = self.retry_count.get(slave, 0) + 1
if self.retry_count[slave] >= self.max_retries:
self.isolated[slave] = time.time() + self.isolation_timeout
return {"isolated": True, "slave": slave, "duration": self.isolation_timeout}
return {"isolated": False, "retries": self.retry_count[slave]}通讯恢复后:
1. 设备侧:本地存储最近 N 条记录(环形缓冲区)
2. 平台侧:请求历史数据(如果设备支持)
3. 或:平台用最后已知值 + STALE 标记填充断线期指标 | 合格线 |
|---|---|
单库温湿度达标率(72h) | ≥95% 时间在 45~60%RH / 14~24℃ |
通讯可用率 | ≥99.5% |
消杀状态变更上报延迟 | ≤5s |
单设备掉线恢复时间 | ≤30s |
分区故障隔离 | 单库断线不影响其他库 |
平台/设备/便携表三方一致 | 误差 ≤±0.3℃ / ±3%RH |
多库房分区管理下,恒湿一体机通过 RS485 向平台上报温湿度和消杀状态,本质是:
物理上按库分区布线 → 逻辑上按 zone 建模 → 轮询按优先级分层(温湿 2s、消杀 1s、运维 30s)→ 事件靠状态字变更触发 → 安全联锁实时拦截 → 单设备/单分区故障不扩散 → 断网本地自治 → 恢复后补传。
RS485 是"老协议",但配上合理的分区架构、轮询调度、事件检测和隔离机制,照样撑得起档案馆几十台设备的可靠上报。
关键词:多库房分区,恒湿一体机,RS485,Modbus RTU,消杀状态上报,分区轮询,事件触发,故障隔离,消杀排程,本地自治
标签:#多库房分区 #恒湿一体机 #RS485 #ModbusRTU #消杀状态 #分区轮询 #事件上报 #故障隔离 #消杀排程 #智慧档案
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。