1. 项目背景与需求拆解1.1 为什么这个项目值得单独拿出来讲做过工业现场数据采集的人都有一个共识传感器本身不难选难的是怎么把数据稳定、低延迟、低耦合地送进上层系统。以太网温湿度变送器就是典型例子——设备本身带RJ45口支持Modbus TCP或者私有TCP协议看起来插上网线就能用但真正到了项目现场你会发现上位机可能只认SNMPSCADA系统走的是Modbus TCP而运维平台又想通过TCP长连接拿实时数据。三种协议、三套接口、三个对接方如果每个都单独写一套采集程序后期维护成本会高到让人崩溃。这个项目的核心思路就是用一台网关设备或者一台跑网关服务的工控机作为协议转换中枢把以太网温湿度变送器的数据同时以SNMP和TCP两种协议对外暴露内部通过Modbus TCP或Modbus RTU采集原始数据。这样上位机、SCADA、运维平台各取所需变送器只需要接一次线、配一次网。适合阅读这篇内容的人包括做工业物联网集成的工程师、负责机房动环监控的运维人员、需要把温湿度数据接入自研平台的开发者以及正在选型网关设备的技术负责人。不管你之前有没有接触过SNMP只要懂基本的TCP/IP和Modbus概念下面的内容都能直接参考复现。1.2 现场常见的三种对接诉求先把这个项目里会遇到的对接方梳理清楚后面方案设计才有依据。第一种是SNMP管理平台。很多机房、数据中心、弱电间已经有一套基于SNMP的网管系统比如Zabbix、SolarWinds或者国产的网管软件。这些平台习惯通过OID去轮询设备指标温湿度作为环境参数天然适合挂到SNMP的私有MIB或者标准MIB下。运维人员不需要写代码配一个OID就能出曲线和告警。第二种是TCP长连接客户端。自研平台或者边缘计算盒子通常更愿意用TCP长连接因为可以做到服务端主动推送延迟低数据格式自定义解析灵活。变送器如果直接支持TCP输出当然好但很多老设备只支持Modbus TCP这时候就需要网关做一次转换。第三种是Modbus TCP/RTU采集。这是工业现场最通用的方式PLC、组态软件、Modbus Poll调试工具都认这个协议。变送器原生支持Modbus的话网关可以直接透传或者做寄存器映射。三种诉求并存就是双协议接入这个标题的由来。注意这里说的是双协议接入网关不是双协议变送器意味着协议转换的逻辑落在网关侧变送器保持原样。1.3 方案选型的几个关键取舍在动手之前有几个选型问题必须先想清楚否则后面会反复返工。网关形态选硬件还是软件硬件网关比如带串口和网口的协议转换器优点是稳定、免维护、上电即用缺点是灵活性差私有协议或者特殊映射改不了。软件网关跑在Linux工控机或树莓派上优点是随便改缺点是多了个操作系统要维护。我个人的经验是如果现场只有温湿度这一路数据硬件网关足够如果后面还要接漏水、烟感、门禁直接上软件网关扩展性完全不是一个量级。采集侧走Modbus TCP还是Modbus RTU以太网变送器一般直接支持Modbus TCP走网线就行不需要额外转换。但如果变送器只有RS485口那就得加一个串口服务器或者网关自带RS485口。这个项目标题里写的是以太网温湿度变送器所以采集侧优先走Modbus TCP省掉一层转换。SNMP用哪个版本SNMPv1太老安全性差SNMPv3最安全但配置复杂很多国产网管平台支持得并不好。实测下来SNMPv2c是兼容性和易用性最平衡的选择community string相当于密码配置简单主流平台都支持。如果甲方明确要求v3再单独处理。TCP服务端还是客户端网关作为TCP服务端等上位机来连适合上位机主动拉取的场景网关作为TCP客户端主动连上位机适合上位机在NAT后面或者需要穿透的场景。这个项目里我建议网关同时开一个TCP Server端口上位机连上来之后按自定义帧格式请求数据这样最灵活。2. 核心协议原理与数据流设计2.1 Modbus TCP采集侧的工作原理Modbus TCP的本质是把Modbus RTU的报文去掉CRC校验加上一个7字节的MBAP头然后塞进TCP payload里。MBAP头包含事务标识、协议标识、长度和单元标识。事务标识用来匹配请求和响应协议标识固定为0长度表示后续字节数单元标识在TCP场景下通常填1或者设备地址。以太网温湿度变送器一般会把温度、湿度放在保持寄存器里比如40001放温度单位0.1℃40002放湿度单位0.1%RH。具体寄存器地址和数据类型必须查设备手册不同厂家差异很大。有的用浮点数占两个寄存器有的用整型有的还有符号位处理。这一步如果搞错后面所有数据都是错的。采集频率方面温湿度变化慢1秒到5秒轮询一次完全够用。轮询太快反而会增加网络和CPU负担尤其是网关同时跑SNMP和TCP服务的时候。我一般设2秒兼顾实时性和资源占用。2.2 SNMP Agent侧的数据组织方式SNMP的核心概念是OID树和MIB。网关要作为SNMP Agent响应网管平台的GET请求就需要把温湿度值映射到某个OID上。有两种做法一种是使用标准MIB。比如RFC 4133定义的ENTITY-MIB里有温度传感器相关的OID但湿度没有标准定义而且标准MIB的结构比较复杂配置起来不直观。另一种是自定义私有MIB。企业OID分支下自己定义比如.1.3.6.1.4.1.xxxxx.1.1.0表示温度.1.3.6.1.4.1.xxxxx.1.2.0表示湿度。这种做法的好处是清晰、好记、好维护网管平台导入MIB文件后就能看到有意义的名称。缺点是每个项目都要维护一份MIB文件。实测下来如果甲方网管平台支持自定义OID直接用私有MIB最省事。如果不支持就退而求其次用标准MIB里能塞进去的节点。SNMP的GET请求是网管平台主动发起的网关只需要被动响应不需要主动上报。如果要支持告警可以用SNMP Trap但那是另一个话题了。2.3 TCP服务侧的自定义帧设计TCP服务端这块自由度最大也最容易埋坑。我的建议是设计一个简单的请求-响应帧格式包含帧头、命令字、数据长度、数据和校验。比如帧头(2B) | 命令(1B) | 长度(2B) | 数据(NB) | CRC16(2B)命令字0x01表示读全部温湿度0x02表示读温度0x03表示读湿度。数据部分用大端序浮点数或者整型。CRC16可以用Modbus CRC算法网上有现成的查表法实现几行代码就能搞定。为什么要加CRC因为TCP虽然保证可靠传输但应用层的数据解析错误、粘包问题依然存在。加一个CRC可以在解析前先校验避免脏数据进入业务逻辑。粘包问题通过长度字段解决收到数据后先读帧头再根据长度字段读完整帧。2.4 双协议并行的数据流架构整个数据流是这样的网关内部维护一个采集线程每隔2秒通过Modbus TCP从变送器读取温度和湿度更新到共享内存或者全局变量里。SNMP Agent线程监听161端口收到GET请求时从共享变量取值并组装SNMP响应。TCP Server线程监听自定义端口比如8888收到请求帧后从共享变量取值并组装响应帧。三个线程之间通过读写锁或者无锁队列同步数据。温湿度数据量很小用读写锁完全够用不需要上复杂的消息队列。关键是采集线程写数据时要加写锁SNMP和TCP线程读数据时加读锁避免读到半更新状态的数据。这个架构的好处是采集和协议服务解耦。如果后面要加MQTT或者HTTP接口只需要再加一个服务线程采集逻辑完全不用动。3. 现场实操从接线到双协议跑通3.1 硬件连接与网络规划先确认变送器的供电和接口。大多数以太网温湿度变送器支持DC 12V或24V供电RJ45口同时走数据和供电PoE的型号也有但需要确认交换机是否支持PoE。如果不支持就得单独拉电源线。网络规划上建议给变送器、网关、上位机分配同一网段的静态IP。比如变送器192.168.1.10网关192.168.1.20上位机192.168.1.100。不要用DHCP现场调试时IP变来变去会让人抓狂。网关如果跑Linux用nmcli或者直接改/etc/network/interfaces配置静态IP。接线顺序先接电源确认变送器指示灯正常再接网线用笔记本ping一下变送器IP确认网络通最后配置网关。这个顺序可以避免把网络问题和供电问题混在一起排查。3.2 变送器Modbus寄存器实测用Modbus Poll或者mbpoll命令行工具先单独测试变送器。假设变送器IP是192.168.1.10端口502从站地址1温度在40001湿度在40002。用mbpoll的命令mbpoll -m tcp -a 1 -t 4 -r 1 -c 2 192.168.1.10这条命令的意思是TCP模式从站地址1保持寄存器function code 4从地址1开始读2个寄存器。返回的数值需要根据手册换算比如返回235和567可能表示23.5℃和56.7%RH。如果读不到数据先检查端口是否502从站地址是否正确寄存器地址是0-based还是1-based。Modbus协议本身是0-based但很多手册写的是1-based差一位就会读错。这个坑我踩过不止一次。3.3 网关采集程序的核心逻辑网关侧用Python写采集程序最省事pymodbus库直接支持Modbus TCP客户端。核心代码大概长这样from pymodbus.client import ModbusTcpClient import threading import time data_lock threading.Lock() sensor_data {temperature: 0.0, humidity: 0.0} def poll_sensor(): client ModbusTcpClient(192.168.1.10, port502) while True: try: rr client.read_holding_registers(0, 2, slave1) if not rr.isError(): temp rr.registers[0] / 10.0 humi rr.registers[1] / 10.0 with data_lock: sensor_data[temperature] temp sensor_data[humidity] humi except Exception as e: print(fpoll error: {e}) time.sleep(2)注意read_holding_registers的第一个参数是起始地址这里填0对应手册里的40001。slave1是从站地址。除10是因为寄存器单位是0.1。这些换算必须对着手册来不能想当然。采集线程启动后SNMP和TCP服务线程就可以从sensor_data里读数据了。读写锁保证不会读到写了一半的数据。3.4 SNMP Agent的配置与验证Python里可以用pysnmp库实现SNMP Agent。核心是定义一个MIB对象把OID和取值函数绑定。比如from pysnmp.hlapi import * from pysnmp.entity import engine, config from pysnmp.entity.rfc3413 import cmdrsp, context from pysnmp.carrier.asyncore.dgram import udp实际代码比较长核心思路是注册一个MIB标量getValue回调里返回sensor_data里的值。community string设为public或者自定义。配置完成后用snmpget命令验证snmpget -v2c -c public 192.168.1.20 .1.3.6.1.4.1.99999.1.1.0如果返回温度值说明SNMP Agent工作正常。然后在网管平台里添加这个OID配置轮询间隔和告警阈值。注意SNMP默认端口161是UDP不是TCP。防火墙规则要放行UDP 161很多人只放TCP导致SNMP不通。3.5 TCP Server的实现与联调TCP Server用Python的socketserver或者asyncio都能实现。核心逻辑是接收请求帧解析命令字从sensor_data取值组装响应帧返回。import socketserver import struct class Handler(socketserver.BaseRequestHandler): def handle(self): while True: data self.request.recv(1024) if not data: break # 解析帧头、命令、长度、CRC # 组装响应 resp build_response(cmd, sensor_data) self.request.sendall(resp)联调时先用netcat或者Python脚本发一个请求帧看返回是否正确。然后再用上位机的实际客户端测试。粘包问题在测试阶段就要验证连续发多个请求看服务端是否能正确拆分。3.6 双协议同时压测与稳定性观察两个协议单独跑通之后要同时跑一段时间观察CPU、内存和网络。我一般会跑至少24小时用top和iftop监控资源占用。温湿度采集频率2秒SNMP轮询30秒TCP请求按需正常情况下CPU占用应该低于5%。如果发现SNMP响应变慢可能是采集线程阻塞了GIL。Python的GIL在多线程IO密集型场景下问题不大但如果采集线程里有耗时计算考虑用多进程或者异步IO重构。4. 常见问题排查与避坑经验4.1 协议层问题速查表现象可能原因排查方法Modbus读不到数据端口/从站地址/寄存器地址错误用mbpoll逐项排除SNMP超时UDP 161未放行或community错误本地snmpget测试TCP连接被拒端口未监听或防火墙拦截netstat检查监听状态数据值明显异常寄存器换算系数错误对照手册重新计算双协议同时跑时丢包线程锁竞争或缓冲区不足降低轮询频率观察4.2 现场踩过的三个坑第一个坑寄存器地址0-based和1-based混淆。手册写40001程序里填40001读不到填0反而对了。这个问题的根源是Modbus协议本身用0-based但很多手册为了兼容传统PLC习惯用1-based。解决办法是先用调试工具确认再写进代码。第二个坑SNMP community string大小写敏感。有的网管平台默认用public有的用Public大小写不一致直接超时。配置时两边必须完全一致包括前后空格。第三个坑TCP粘包导致解析错位。上位机连续发请求时服务端一次recv可能收到两个半帧。解决办法是维护一个接收缓冲区按长度字段循环解析不够长度就继续recv。这个逻辑在handle方法里要写对否则数据会越来越乱。4.3 长期运行的经验建议网关程序一定要加日志记录每次采集失败、SNMP请求异常、TCP连接断开等事件。日志按天切割保留7天足够。没有日志现场出问题只能靠猜。另外建议加一个看门狗机制。如果采集线程连续多次失败自动重启Modbus连接如果整个进程挂掉用systemd或者supervisor拉起。工业现场无人值守稳定性比功能丰富更重要。最后所有配置参数IP、端口、寄存器地址、换算系数、community string都放到配置文件里不要硬编码。现场改参数时不用重新打包程序改完重启服务就行。这个习惯能省下大量来回沟通的时间。
