首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >多库房分区管理,恒湿一体机依靠RS485协议向平台上报温湿度、消杀状态解决方案

多库房分区管理,恒湿一体机依靠RS485协议向平台上报温湿度、消杀状态解决方案

原创
作者头像
BJ盛世宏博小程
发布于 2026-10-13 15:49:20
发布于 2026-10-13 15:49:20
690
举报

多库房分区管理,恒湿一体机依靠RS485协议向平台上报温湿度、消杀状态解决方案

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


一、先说多库房和单库的本质区别

单台一体机对接平台,是"串口通了就能采"。多库房分区管理完全不同:

  • 物理上:十几到几十台设备分散在不同楼层、不同防火分区,总线不能无限拉长
  • 逻辑上:每个库房是独立控制域,但平台要统一看板、统一告警、统一审计
  • 业务上:不同库房精度等级不同(普通库 45~60%RH,特藏库 ±3%RH),策略不能一刀切
  • 安全上:A库有人不能影响B库消杀,门禁/消防是分区独立的

核心问题变成:怎么用 RS485 这个"半双工、主从、轮询"的协议,把几十台分散设备的温湿度和消杀状态可靠地汇聚到平台,同时保证分区自治和互不影响。


二、总线拓扑:三种架构选哪种

方案A:单总线全串联(小项目)

代码语言:javascript
复制
平台/网关
   │
   ├── 库房1-设备1 (slave 1)
   ├── 库房1-设备2 (slave 2)
   ├── 库房2-设备3 (slave 3)
   ├── 库房2-设备4 (slave 4)
   ├── ...
   └── 库房N-设备N (slave N)

优点

缺点

布线最简单

单点断线全总线瘫痪

网关端口最少

距离超 1200m 信号衰减

成本低

节点超 32 个需中继

不同库房混在同一碰撞域

适用:≤8 台设备,总距离 ≤500m,同一楼层。


方案B:按库房分区总线 + 多串口网关(推荐)

代码语言:javascript
复制
┌── 网关串口1 ── 库房1总线 (slave 1~3)
                    ├── 网关串口2 ── 库房2总线 (slave 4~6)
平台 ◄─── 以太网 ────├── 网关串口3 ── 库房3总线 (slave 7~9)
                    ├── 网关串口4 ── 库房4总线 (slave 10~12)
                    └── ...

优点

缺点

单库故障不扩散

网关需多串口(或级联)

每库独立 1200m

布线量稍大

节点数不受限

网关成本略高

轮询效率最优

需规划串口分配表

便于分区维护

适用:8~64 台设备,多楼层,库房间距 >50m。


方案C:RS485→TCP 分布式(大型档案馆)

代码语言:javascript
复制
库房1: 一体机群 → 串口服务器(本地) ──┐
库房2: 一体机群 → 串口服务器(本地) ──┼── 以太网 ──▶ 平台
库房3: 一体机群 → 串口服务器(本地) ──┘

优点

缺点

距离无限制

每台串口服务器需 IP

利用现有网络

网络故障影响通讯

扩展最灵活

需管理 IP 段

平台侧统一 Modbus TCP

延迟略高(网络抖动)

适用:>32 台,跨楼宇,已有综合布线。


工程建议

规模

推荐方案

≤8 台,单楼层

方案A

8~32 台,2~4 库房

方案B

32~64 台,多楼层

方案B 变体(每楼层一网关)

>64 台,跨楼宇

方案C


三、从站地址规划(多库房必须规范)

代码语言:javascript
复制
地址段划分规则:
  库房1: slave 1~8
  库房2: slave 9~16
  库房3: slave 17~24
  ...
  
  或按功能:
  恒湿机: 1~48
  备用:   49~64
  调试:   65~127

平台点表必须带分区标签:

代码语言:javascript
复制
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"

四、上报内容定义:温湿度 + 消杀状态

1. 环境量(周期上报)

点名

寄存器

类型

缩放

上报周期

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

2. 消杀状态量(事件+周期双模式)

点名

寄存器

位/值

上报策略

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周期(消杀中)

3. 状态字拆解(平台侧解析)

代码语言:javascript
复制
# 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",    # 滤网堵塞
}

五、上报机制设计:轮询 vs 主动上报

Modbus RTU 本质是从动协议

RS485 上 Modbus RTU 是纯主从轮询,设备不能"主动上报"。所以"上报"实际是:

代码语言:javascript
复制
网关按周期轮询每台设备 → 读取寄存器 → 组装 payload → 通过 MQTT/HTTP/TCP 推给平台

轮询调度策略(多库房核心)

代码语言:javascript
复制
# 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 读
        }

事件触发上报(消杀状态变更)

纯轮询有延迟。消杀状态变更需要"准实时"感知,做法:

代码语言:javascript
复制
设备侧:消杀状态变化时,拉高一个"事件标志位"(如 40020 event_flag)
网关侧:每 1s 快速扫 event_flag
        → 若 event_flag != 0 → 立即读完整消杀状态包 → 推平台 → 清 flag
代码语言:javascript
复制
# 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

六、平台侧数据模型(分区聚合)

代码语言:javascript
复制
{
  "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": {...}
    }
  }
}

七、分区联动策略

核心原则:分区自治 + 全局可见

代码语言:javascript
复制
┌─────────────────────────────────────────────────────────────┐
│ 平台全局层                                                 │
│  · 全馆温湿度看板                                          │
│  · 跨库告警汇总                                            │
│  · 消杀排程总表(避免多库同时臭氧)                        │
│  · 审计日志                                                │
├─────────────────────────────────────────────────────────────┤
│ 分区自治层(每个库房独立)                                  │
│  · 本地阈值(按库房精度等级)                              │
│  · 本地死区/错峰                                           │
│  · 本地门禁/消防互锁                                       │
│  · 单库消杀窗口(不与邻库冲突)                            │
├─────────────────────────────────────────────────────────────┤
│ 设备层(每台一体机)                                       │
│  · PID 闭环                                                │
│  · 本地自治(通讯中断时按上次设定跑)                      │
│  · 状态字/故障字实时更新                                   │
└─────────────────────────────────────────────────────────────┘

消杀排程(多库防冲突)

代码语言:javascript
复制
# 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  # 简化

八、通讯可靠性设计(多库房必做)

1. 总线故障隔离

代码语言:javascript
复制
某库房总线断线/短路 → 网关检测到该串口无响应
  → 标记该分区 offline
  → 其他分区继续正常轮询
  → 平台显示"库房X通讯中断",不误报"设备离线"
  → 修复后自动恢复,数据补传

2. 单设备掉线不影响同总线其他设备

代码语言:javascript
复制
# 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]}

3. 数据补传机制

代码语言:javascript
复制
通讯恢复后:
1. 设备侧:本地存储最近 N 条记录(环形缓冲区)
2. 平台侧:请求历史数据(如果设备支持)
3. 或:平台用最后已知值 + STALE 标记填充断线期

九、调试与验收

调试顺序

  1. 单库单台:直连串口助手,读温湿度,核 scale/endian
  2. 单库多台:挂满该库房所有设备,跑 24h,统计 CRC 错误率
  3. 跨库总线:逐库接入网关不同串口,验证分区隔离
  4. 平台联调:点表全量对接,温湿度/消杀状态/安全联锁逐点确认
  5. 故障注入:
    • 拔某库总线 → 其他库正常,平台正确标"分区通讯中断"
    • 拔单台网线 → 该台 offline,同总线其他正常
    • 消杀中开门 → 平台收到 interlock 事件,消杀立即停止
    • 断网 → 一体机本地自治,恢复后数据连续
  6. 72h 全量运行:所有库房同时在线,平台曲线无断点

验收指标

指标

合格线

单库温湿度达标率(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 删除。

目录
  • 多库房分区管理,恒湿一体机依靠RS485协议向平台上报温湿度、消杀状态解决方案
    • 一、先说多库房和单库的本质区别
    • 二、总线拓扑:三种架构选哪种
      • 方案A:单总线全串联(小项目)
      • 方案B:按库房分区总线 + 多串口网关(推荐)
      • 方案C:RS485→TCP 分布式(大型档案馆)
      • 工程建议
    • 三、从站地址规划(多库房必须规范)
    • 四、上报内容定义:温湿度 + 消杀状态
      • 1. 环境量(周期上报)
      • 2. 消杀状态量(事件+周期双模式)
      • 3. 状态字拆解(平台侧解析)
    • 五、上报机制设计:轮询 vs 主动上报
      • Modbus RTU 本质是从动协议
      • 轮询调度策略(多库房核心)
      • 事件触发上报(消杀状态变更)
    • 六、平台侧数据模型(分区聚合)
    • 七、分区联动策略
      • 核心原则:分区自治 + 全局可见
      • 消杀排程(多库防冲突)
    • 八、通讯可靠性设计(多库房必做)
      • 1. 总线故障隔离
      • 2. 单设备掉线不影响同总线其他设备
      • 3. 数据补传机制
    • 九、调试与验收
      • 调试顺序
      • 验收指标
    • 十、收口
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档