1. 项目背景与需求拆解
1.1 为什么这个项目值得单独拿出来讲
做过工业现场数据采集的人都有一个共识:传感器本身不难选,难的是怎么把数据稳定、低延迟、低耦合地送进上层系统。以太网温湿度变送器就是典型例子——设备本身带RJ45口,支持Modbus TCP或者私有TCP协议,看起来"插上网线就能用",但真正到了项目现场,你会发现上位机可能只认SNMP,SCADA系统走的是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', port=502) while True: try: rr = client.read_holding_registers(0, 2, slave=1) 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(f"poll error: {e}") time.sleep(2)注意read_holding_registers的第一个参数是起始地址,这里填0对应手册里的40001。slave=1是从站地址。除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)都放到配置文件里,不要硬编码。现场改参数时不用重新打包程序,改完重启服务就行。这个习惯能省下大量来回沟通的时间。