物联网 · 盛世宏博 · 批量采集 · 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 乱分,代价会在三个时刻集中爆发:交换机端口绑定时对不上、故障时定位慢、扩容时撞地址。好的 IP 规划本身就是一张故障定位图。
建议采用 区域-功能-序号 三段编码,把位置信息烧进 IP 末段:
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 |
规则 | 要求 |
|---|---|
静态分配 | 全部静态 IP,DHCP 仅调试期开放,上线即关 |
网关统一 | 区域网关 .254,传感器网关填同 VLAN 三层接口 |
预留段 | 每个区域末段 .240~.253 留给网关/管理设备 |
禁止广播地址 | .0 和 .255 永不分配 |
台账同步 | IP 分配表、交换机 MAC 绑定表、网关点表三份一致 |
冲突检测 | 上线前 arping 逐 IP 扫一遍,确认无人应答 |
# 机房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 自律。
模型 | 做法 | 适用规模 | 风险 |
|---|---|---|---|
串行轮询 | 一台一台读 | ≤8台 | 慢,但稳 |
多线程并发 | 每设备或每区域一个线程 | 9~48台 | 连接数可控 |
异步事件循环 | asyncio + 异步 Modbus | >48台 | 需注意库支持、超时处理 |
# 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)参数 | 建议值 | 依据 |
|---|---|---|
并发线程数 | 8(≤32台) | 多数网口变送器 TCP 连接池 3~5,分区代理别打满 |
单台超时 | 2.0s | 局域网 RTT <5ms,2s 足够区分真故障 |
采集周期 | 10s | 机房温湿度变化慢,10s 分辨率够用 |
连接池 idle | 30s | 传感器 TCP 栈有保活,30s 内复用省握手 |
连接生命周期 | 300s | 防长时间连接半开不回收 |
现象:平台能连上,轮询一密集就随机 timeout。
根因:一台传感器只支持 3~5 个并发连接,多线程池 16 个线程同时建连。
解决:边缘网关做唯一代理,平台不直连;或按区域拆线程池。
现象:48 台串行一轮要 40 秒,平台报"采集延迟"。
计算:48台 × (2字节请求 + 2字节响应 + 网络RTT) / 带宽 + 超时惩罚。
解决:线程池并发 + 分桶读取(一次读温湿度两个寄存器)+ 超时快速失败。
现象:两台传感器 MAC 不同但 IP 相同,交换机 MAC 表抖动,两台交替掉线。
排查:display mac-address vlan 110 看同一 IP 对应多个端口,或 arping 收到重复应答。
解决:上线前 arping 全扫;交换机端口绑定兜底。
现象:一台传感器固件异常发广播,同 VLAN 内所有传感器断连。
解决:交换机 storm-control broadcast min 5 限速;每个区域独立 VLAN 或隔离。
现象:网络抖动 3 秒,48 台数据时间戳挤在一起,Grafana 曲线成竖线。
解决:网关缓存里存 采集时刻(sock.recv 返回那一刻),平台写入用该时间戳,抖动不影响时序连续性。
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)策略 | 做法 |
|---|---|
边沿触发 | GOOD→STALE 发一次,持续 STALE 不再刷屏 |
退避抑制 | 同一节点 5 分钟内连续故障只升级一次 |
批量故障归并 | 10 台同时掉线 → 合并为"交换机/网络故障",不炸 10 条 |
恢复确认 | 连续 2 轮成功才判定 RECOVERED |
测试项 | 方法 | 通过标准 |
|---|---|---|
IP 冲突测试 | 两台设同 IP 同时上电 | 交换机告警,不影响其他节点 |
并发压力 | 32 线程同时打单台传感器 | 连接池满时优雅拒绝,不全死 |
超时链路 | 拔一根网线观察该节点 | 2s 内标 TIMEOUT,不阻塞整批 |
网络分区 | 关掉 VLAN 间路由 | 网关本地缓存,平台侧批量离线标记 |
恢复补传 | 断网 5 分钟后恢复 | 补传按采集时间戳,无重复点 |
广播风暴 | 一台打 UDP 广播 | storm-control 生效,其他节点 RTT <50ms |
以太网温湿度传感器批量采集,Modbus TCP 协议本身从来不是瓶颈,IP 规划是否可定位、并发是否打爆连接池、超时是否快速失败、时间戳是否忠实这四点才是。
落地三句话:
批量规模到 32 台以上,网关就要考虑主备或分区部署——这不是性能问题,是运维尊严问题。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。