首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >以太网温湿度传感器批量采集实战:Modbus TCP与IP地址规划

以太网温湿度传感器批量采集实战:Modbus TCP与IP地址规划

原创
作者头像
HONSOR盛世宏博
发布于 2026-10-22 09:03:39
发布于 2026-10-22 09:03:39
1070
举报

以太网温湿度传感器批量采集实战:Modbus TCP与IP地址规划

物联网 · 盛世宏博 · 批量采集 · Modbus TCP · IP地址规划 · 网口温湿度变送器

关键词:以太网温湿度传感器、Modbus TCP、批量采集、IP地址规划、连接池、端口绑定、轮询调度、动环监控 标签:#物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控

一、批量采集的难点不在"读得回来",在"规模上来之后还读得回来"

单台温湿度传感器读 Modbus TCP,十行 pymodbus 就能跑。但机房里 48 台、配电室 16 台、档案库房 12 台挂在一起时,问题性质完全变了:TCP 连接数、端口扫描、超时叠加、IP 冲突、传感器固件连接池被打满,这些才是批量采集真正的敌人。

一条经验曲线:传感器 ≤8 台,直连平台问题不大;9~32 台,必须上边缘网关收敛;>32 台,网关要做主备或分区代理,否则单点故障影响面过大。


二、IP地址规划:批量部署的第一块基石

2.1 为什么不能"随手分配"

批量场景里 IP 乱分,代价会在三个时刻集中爆发:交换机端口绑定时对不上、故障时定位慢、扩容时撞地址。好的 IP 规划本身就是一张故障定位图。

2.2 结构化地址编码方案

建议采用 区域-功能-序号​ 三段编码,把位置信息烧进 IP 末段:

代码语言:javascript
复制
192.168.A.BCD
      │ │││
      │ ││└─ 序号(1~9,单区域最多9台,够了)
      │ │└── 功能位:1=温湿度 2=漏水 3=配电 4=门禁
      │ └─── 区域位:1=机房A 2=机房B 3=配电室 4=档案库
      └───── 固定:动环 VLAN 110

实际落地示例:

位置

功能

末段

IP

机房A 列头1

温湿度

111

192.168.110.111

机房A 列头2

温湿度

112

192.168.110.112

机房A 列头3

温湿度

113

192.168.110.113

机房A 进线柜

配电

121

192.168.110.121

机房A 地板漏水

漏水

131

192.168.110.131

配电室 高压室

温湿度

141

192.168.110.141

档案库 A库

温湿度

151

192.168.110.151

2.3 地址分配纪律

规则

要求

静态分配

全部静态 IP,DHCP 仅调试期开放,上线即关

网关统一

区域网关 .254,传感器网关填同 VLAN 三层接口

预留段

每个区域末段 .240~.253 留给网关/管理设备

禁止广播地址

.0 和 .255 永不分配

台账同步

IP 分配表、交换机 MAC 绑定表、网关点表三份一致

冲突检测

上线前 arping 逐 IP 扫一遍,确认无人应答

2.4 交换机端口绑定(让 IP 规划落地)

代码语言:javascript
复制
# 机房A 列头1 接入端口
interface GigabitEthernet0/0/11
 port link-type access
 port default vlan 110
 poe enable
 mac-address binding ip 192.168.110.111 vlan 110 mac 00e0-4c11-0111
 port-security max-mac-num 1

绑完之后,把笔记本插到那个口、随便改个 IP 也上不去——这才是批量采集"地址不乱"的真正保障,而不是靠 Excel 自律。


三、批量采集核心:并发模型与连接管理

3.1 三种采集模型对比

模型

做法

适用规模

风险

串行轮询

一台一台读

≤8台

慢,但稳

多线程并发

每设备或每区域一个线程

9~48台

连接数可控

异步事件循环

asyncio + 异步 Modbus

>48台

需注意库支持、超时处理

3.2 生产级批量采集器(线程池 + 连接池)

代码语言:javascript
复制
# batch_collector.py

import time
import socket
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from typing import Dict, List, Optional
import struct

@dataclass
class SensorNode:
    node_id: str
    ip: str
    unit_id: int = 1
    interval: float = 5.0
    timeout: float = 2.0

@dataclass
class PollResult:
    node_id: str
    ip: str
    success: bool
    temp: Optional[float] = None
    hum: Optional[float] = None
    quality: str = "GOOD"     # GOOD / STALE / TIMEOUT
    rtt_ms: float = 0.0
    raw_hex: str = ""

class BatchCollector:
    def __init__(self, nodes: List[SensorNode], max_workers=8,
                 batch_interval=10.0):
        self.nodes = nodes
        self.max_workers = max_workers   # 并发数,别超过传感器连接池
        self.batch_interval = batch_interval
        self._results: Dict[str, PollResult] = {}
        self._conn_pool = ConnectionPool(max_idle=30, max_lifetime=300)

    def poll_single(self, node: SensorNode) -> PollResult:
        t0 = time.perf_counter()
        try:
            sock = self._conn_pool.get(node.ip, 502, node.timeout)
            if sock is None:
                return PollResult(node.node_id, node.ip, False, quality="TIMEOUT")

            # MBAP + 读保持寄存器(0x03) 40001~40002
            tid = int(time.perf_counter() * 1000) & 0xFFFF
            mbap = struct.pack('>HHHBB', tid, 0, 6, node.unit_id, 0x03)
            pdu = struct.pack('>HHH', 0x0000, 0x0002)
            sock.sendall(mbap + pdu)
            resp = sock.recv(256)
            rtt = (time.perf_counter() - t0) * 1000

            if len(resp) >= 11:
                temp_raw = struct.unpack('>H', resp[9:11])[0]
                hum_raw = struct.unpack('>H', resp[11:13])[0]
                # 有符号处理
                if temp_raw > 32767:
                    temp_raw -= 65536
                temp = temp_raw * 0.1
                hum = hum_raw * 0.1
                raw = resp[9:13].hex()

                # 合理性校验:异常值拦截
                if not (-40 <= temp <= 85) or not (0 <= hum <= 100):
                    return PollResult(node.node_id, node.ip, False,
                                      quality="BAD_VALUE", raw_hex=raw)

                self._conn_pool.release(node.ip, sock)
                return PollResult(node.node_id, node.ip, True,
                                  temp=temp, hum=hum,
                                  quality="GOOD", rtt_ms=rtt, raw_hex=raw)

            self._conn_pool.release(node.ip, sock, close=True)
            return PollResult(node.node_id, node.ip, False, quality="BAD_FRAME")

        except socket.timeout:
            return PollResult(node.node_id, node.ip, False,
                              quality="TIMEOUT", rtt_ms=rtt)
        except Exception:
            return PollResult(node.node_id, node.ip, False, quality="ERROR")

    def poll_batch(self) -> Dict[str, PollResult]:
        """一轮全量采集"""
        with ThreadPoolExecutor(max_workers=self.max_workers) as ex:
            futures = {ex.submit(self.poll_single, n): n for n in self.nodes}
            for f in as_completed(futures, timeout=self.batch_interval):
                r = f.result()
                self._results[r.node_id] = r
        return self._results

    def run_forever(self, on_batch=None):
        """持续采集循环"""
        while True:
            t0 = time.time()
            results = self.poll_batch()
            if on_batch:
                on_batch(results)
            elapsed = time.time() - t0
            sleep_for = max(0, self.batch_interval - elapsed)
            time.sleep(sleep_for)


class ConnectionPool:
    """简单TCP连接池,避免每台每次新建握手"""
    def __init__(self, max_idle=30, max_lifetime=300):
        self._pool: Dict[str, tuple] = {}   # ip -> (sock, last_use, created)
        self.max_idle = max_idle
        self.max_lifetime = max_lifetime

    def get(self, ip, port, timeout):
        now = time.time()
        if ip in self._pool:
            sock, last, created = self._pool[ip]
            if (now - last < self.max_idle) and (now - created < self.max_lifetime):
                return sock
            else:
                try: sock.close()
                except: pass
                del self._pool[ip]
        # 新建
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(timeout)
            sock.connect((ip, port))
            self._pool[ip] = (sock, now, now)
            return sock
        except Exception:
            return None

    def release(self, ip, sock, close=False):
        if close:
            try: sock.close()
            except: pass
            self._pool.pop(ip, None)

3.3 关键参数怎么定

参数

建议值

依据

并发线程数

8(≤32台)

多数网口变送器 TCP 连接池 3~5,分区代理别打满

单台超时

2.0s

局域网 RTT <5ms,2s 足够区分真故障

采集周期

10s

机房温湿度变化慢,10s 分辨率够用

连接池 idle

30s

传感器 TCP 栈有保活,30s 内复用省握手

连接生命周期

300s

防长时间连接半开不回收


四、批量场景的五个专属坑

4.1 传感器 TCP 连接池被打满

现象:平台能连上,轮询一密集就随机 timeout。

根因:一台传感器只支持 3~5 个并发连接,多线程池 16 个线程同时建连。

解决:边缘网关做唯一代理,平台不直连;或按区域拆线程池。

4.2 全量扫描周期失控

现象:48 台串行一轮要 40 秒,平台报"采集延迟"。

计算:48台 × (2字节请求 + 2字节响应 + 网络RTT) / 带宽 + 超时惩罚。

解决:线程池并发 + 分桶读取(一次读温湿度两个寄存器)+ 超时快速失败。

4.3 IP 地址冲突(批量部署最高频)

现象:两台传感器 MAC 不同但 IP 相同,交换机 MAC 表抖动,两台交替掉线。

排查:display mac-address vlan 110 看同一 IP 对应多个端口,或 arping 收到重复应答。

解决:上线前 arping 全扫;交换机端口绑定兜底。

4.4 网络风暴把采集打死

现象:一台传感器固件异常发广播,同 VLAN 内所有传感器断连。

解决:交换机 storm-control broadcast min 5 限速;每个区域独立 VLAN 或隔离。

4.5 时间戳用"采集时刻"还是"发送时刻"

现象:网络抖动 3 秒,48 台数据时间戳挤在一起,Grafana 曲线成竖线。

解决:网关缓存里存 采集时刻(sock.recv 返回那一刻),平台写入用该时间戳,抖动不影响时序连续性。


五、平台侧写入与告警收敛

5.1 InfluxDB 写入(批量 + 质量位)

代码语言:javascript
复制
def push_batch(results: Dict[str, PollResult]):
    points = []
    for r in results.values():
        tags = {"node": r.node_id, "ip": r.ip}
        fields = {"rtt_ms": r.rtt_ms}
        if r.success:
            fields.update({"temp": r.temp, "hum": r.hum})
        fields["quality"] = r.quality
        points.append(Point("env_batch").tags(tags).fields(fields))
    write_api.write(bucket="env", record=points)

5.2 告警收敛(批量场景必做)

策略

做法

边沿触发

GOOD→STALE 发一次,持续 STALE 不再刷屏

退避抑制

同一节点 5 分钟内连续故障只升级一次

批量故障归并

10 台同时掉线 → 合并为"交换机/网络故障",不炸 10 条

恢复确认

连续 2 轮成功才判定 RECOVERED


六、上线前批量注入测试

测试项

方法

通过标准

IP 冲突测试

两台设同 IP 同时上电

交换机告警,不影响其他节点

并发压力

32 线程同时打单台传感器

连接池满时优雅拒绝,不全死

超时链路

拔一根网线观察该节点

2s 内标 TIMEOUT,不阻塞整批

网络分区

关掉 VLAN 间路由

网关本地缓存,平台侧批量离线标记

恢复补传

断网 5 分钟后恢复

补传按采集时间戳,无重复点

广播风暴

一台打 UDP 广播

storm-control 生效,其他节点 RTT <50ms


七、小结

以太网温湿度传感器批量采集,Modbus TCP 协议本身从来不是瓶颈,IP 规划是否可定位、并发是否打爆连接池、超时是否快速失败、时间戳是否忠实这四点才是。

落地三句话:

  1. IP 编码里烧进位置信息,台账、端口绑定、网关点表三份一致;
  2. 采集走边缘网关收敛,别让平台和多套系统直连传感器抢连接;
  3. 批量故障要归并告警,边沿触发 + 退避抑制,否则值班电话被打爆。

批量规模到 32 台以上,网关就要考虑主备或分区部署——这不是性能问题,是运维尊严问题。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 以太网温湿度传感器批量采集实战:Modbus TCP与IP地址规划
    • 一、批量采集的难点不在"读得回来",在"规模上来之后还读得回来"
    • 二、IP地址规划:批量部署的第一块基石
      • 2.1 为什么不能"随手分配"
      • 2.2 结构化地址编码方案
      • 2.3 地址分配纪律
      • 2.4 交换机端口绑定(让 IP 规划落地)
    • 三、批量采集核心:并发模型与连接管理
      • 3.1 三种采集模型对比
      • 3.2 生产级批量采集器(线程池 + 连接池)
      • 3.3 关键参数怎么定
    • 四、批量场景的五个专属坑
      • 4.1 传感器 TCP 连接池被打满
      • 4.2 全量扫描周期失控
      • 4.3 IP 地址冲突(批量部署最高频)
      • 4.4 网络风暴把采集打死
      • 4.5 时间戳用"采集时刻"还是"发送时刻"
    • 五、平台侧写入与告警收敛
      • 5.1 InfluxDB 写入(批量 + 质量位)
      • 5.2 告警收敛(批量场景必做)
    • 六、上线前批量注入测试
    • 七、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档