首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >TCP 与 SNMP 双协议:RJ45 温湿度记录仪对接机房服务器监控大屏实战

TCP 与 SNMP 双协议:RJ45 温湿度记录仪对接机房服务器监控大屏实战

原创
作者头像
HONSOR盛世宏博
发布于 2026-09-24 11:21:58
发布于 2026-09-24 11:21:58
370
举报

TCP 与 SNMP 双协议:RJ45 温湿度记录仪对接机房服务器监控大屏实战

关键词:RJ45温湿度记录仪、Modbus TCP、SNMP、双协议接入、机房监控大屏、Grafana、Telegraf、设备侧代理、数据融合、告警联动 标签:#物联网 #Modbus #TCP/IP #POE供电 #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控 #边缘计算 #SNMP

一、为什么是双协议,而不是上一轮的TCP/UDP

前几篇把采集面拆开讲过:Modbus TCP 轮询、UDP 主动上报、双模式切换、断网补传。本篇落到交付现场——把 RJ45 PoE 温湿度记录仪接进机房服务器监控大屏,运营和运维要在同一面板上看到服务器指标和环境量。

现场约束决定了协议分工:

  • Modbus TCP(设备面):记录仪原生暴露温湿度、状态字、配置寄存器。读实时值、下发设定值、触发校准、拉取本地存储补传,都走它。请求-响应,可控、可校验、可重试。
  • SNMP(设施/监控面):机房监控大屏背后通常是现有 NMS 或 Zabbix/PRTG/Telegraf 体系,交换机、PDU、UPS 已纳入 SNMP 轮询。把环境量也挂进同一轮询域,复用既有采集管道、告警路由、权限模型,避免再起一套采集 agent。

别把两件事混为一谈:Modbus TCP 是设备南向协议,SNMP 是北向/监控面集成协议。双协议不是设备同时跑两套采集,而是采集层做桥接——一端轮询设备 Modbus TCP,另一端以 SNMP agent 或 SNMP 可轮询代理暴露给大屏侧 NMS;或反向,大屏侧直接轮询设备 SNMP 子代理。

二、架构与数据流

2.1 两种落地形态

形态A:边缘桥接代理(推荐)

代码语言:javascript
复制
RJ45记录仪 ──Modbus TCP──▶ 边缘采集服务 ──本地状态库──▶ SNMP子代理 ──SNMP GET──▶ NMS/Grafana+Telegraf
                                              │
                                              └──HTTP/InfluxDB直推(并行)
  • 边缘服务轮询记录仪(502/TCP),归一化后写入本地状态库(内存表+WAL)。
  • SNMP 子代理(snmpd + pass_persist 或 gosnmpd/net-snmp扩展)暴露同一份状态,OID 树自定义。
  • NMS 按既有 SNMP 轮询周期拉取,大屏侧 Telegraf snmp input → InfluxDB → Grafana。

形态B:设备侧原生SNMP(少数设备支持)

代码语言:javascript
复制
记录仪 ──SNMP agent内置──▶ NMS直接轮询
       └─Modbus TCP保留给配置/补传
  • 设备固件内置 SNMP agent,MIB 私有。接入成本低但 MIB 维护、社区字符串、v3 凭证、冷启动 trap 都要管,边缘桥接更可控。

本文按形态A展开,工程上更通用,不改设备固件即可交付。

二、设备侧:Modbus TCP接入

2.1 点表核对与归一化

记录仪常见寄存器布局(需以厂家点表为准,下面为典型示例):

寄存器(0-based)

类型

含义

缩放

0x0000

RO holding

温度×10, int16

/10

0x0001

RO holding

湿度×10, int16

/10

0x0002

RO holding

露点×10, int16

/10

0x0003

RO holding

状态字 uint16

bit0=传感器OK,bit1=PoE供电OK,bit2=存储写满,bit3=UDP/TCP模式

0x0004

RO holding

最后采样时标 epoch sec uint32(或分高低字)

—

0x0010

RW holding

设定温度×10

/10

0x0011

RW holding

设定湿度×10

/10

0x0012

RW holding

上报周期 s

—

0x0080

RW coil

远程复位

—

注意:保持寄存器 vs 输入寄存器、字序、IEEE754 32bit 时高低字顺序、符号扩展。上线前用 mbpoll 验证。

代码语言:javascript
复制
# 轮询单台验证
mbpoll -a -t 3 -r 1 -c 4 -P 502 -u 1 10.20.1.51
# -t 3 = holding registers, -r 1 注意mbpoll是1-based,映射到0-based偏移

2.2 Python采集服务

代码语言:javascript
复制
"""
env_bridge.py - 边缘采集 + SNMP子代理状态供给
"""

import asyncio, logging, time
from pymodbus.client import AsyncModbusTcpClient
from dataclasses import dataclass, asdict
import json

DEVICES = {
    "A01": {"host": "10.20.1.51", "unit": 1},
    "A02": {"host": "10.20.1.52", "unit": 1},
}
POLL_PERIOD = 5.0
STATE_TTL = 30.0   # 状态库老化阈值

@dataclass
class EnvSample:
    dev: str
    temp: float
    hum: float
    dewpoint: float
    status: int
    ts: float
    reachable: bool = True

class Bridge:
    def __init__(self):
        self.clients = {}
        self.state = {}        # dev -> EnvSample
        self._stop = False

    async def ensure_client(self, dev):
        cfg = DEVICES[dev]
        c = self.clients.get(dev)
        if c is None or not c.connected:
            c = AsyncModbusTcpClient(cfg["host"], port=502)
            await c.connect()
            self.clients[dev] = c
        return c

    async def poll_one(self, dev):
        cfg = DEVICES[dev]
        try:
            c = await self.ensure_client(dev)
            rr = await c.read_holding_registers(0, 5, slave=cfg["unit"])
            if rr.isError():
                raise RuntimeError(str(rr))
            v = rr.registers
            temp = v[0]/10.0 if v[0] < 0x8000 else (v[0]-0x10000)/10.0
            hum = v[1]/10.0
            dp = v[2]/10.0 if v[2] < 0x8000 else (v[2]-0x10000)/10.0
            status = v[3]
            ts = time.time()
            self.state[dev] = EnvSample(dev, temp, hum, dp, status, ts)
        except Exception as e:
            logging.warning("poll %s failed: %s", dev, e)
            old = self.state.get(dev)
            if old:
                old.reachable = False
            else:
                self.state[dev] = EnvSample(dev, float('nan'), float('nan'), float('nan'), 0, time.time(), False)

    async def poll_loop(self):
        while not self.stop:
            await asyncio.gather(*(self.poll_one(d) for d in DEVICES))
            await asyncio.sleep(POLL_PERIOD)

    # ---- SNMP子代理侧拉取接口 ----
    def snapshot_json(self) -> str:
        out = {}
        now = time.time()
        for dev, s in self.state.items():
            stale = (now - s.ts) > STATE_TTL
            out[dev] = {
                "temp": s.temp, "hum": s.hum, "dewpoint": s.dewpoint,
                "status": s.status, "reachable": s.reachable and not stale,
                "age_s": round(now - s.ts, 1)
            }
        return json.dumps(out)

if __name__ == "__main__":
    b = Bridge()
    loop = asyncio.get_event_loop()
    loop.create_task(b.poll_loop())
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        b.stop = True

生产环境补:连接池复用、超时(asyncio.wait_for 3s)、重试退避、多设备并发限制、状态库落盘/WAL。

三、北向:SNMP暴露给大屏侧

3.1 选集成方式

  • 复用 net-snmpd + pass_persist:边缘节点跑 snmpd,Telegraf/NMS 轮询 161/UDP。pass_persist 脚本从桥接服务拉状态,映射为 OID 叶子。
  • 轻量自研 SNMP agent:用 gosnmpd / snmpusm,自定义 MIB 视图。Go 实现更稳,但交付期长。
  • Telegraf 直接拉 Modbus,Grafana 直连 InfluxDB:这其实绕开了 SNMP。如果大屏侧已接受 Telegraf 采集,就没必要套 SNMP。SNMP 的价值在于——大屏侧 NMS 不支持 Modbus,或合规要求统一监控面。

下面给 net-snmp pass_persist 方案,落地快。

3.2 MIB/OID设计

私有子树,挂在企业OID下,例如 .1.3.6.1.4.1.4711.1.1.x:

代码语言:javascript
复制
envKitEntry ::= SEQUENCE {
  envKitIndex      INTEGER,      -- x.1.<idx>
  envKitId         OCTET STRING, -- x.2.<idx>
  envKitTemp       Gauge32,      -- x.3.<idx>  ×10 后返回,或返回原生
  envKitHum        Gauge32,      -- x.4.<idx>
  envKitDewpoint   INTEGER,      -- x.5.<idx>
  envKitStatus     INTEGER,      -- x.6.<idx>
  envKitReachable  INTEGER,      -- x.7.<idx>  1=up 2=down
  envKitAge        Gauge32       -- x.8.<idx>  s
}

注意 SNMPv2c/3 传输:NMS 轮询 UDP/161。pass_persist 通过 STDIN/STDOUT 与 snmpd 通信,snmpd 代发。

3.3 pass_persist 桥接脚本

代码语言:javascript
复制
#!/usr/bin/env python3
"""
snmp_pass_persist.py - net-snmp pass_persist handler
从env_bridge拉取JSON状态,映射为SNMP叶子
"""
import sys, json, time

BRIDGE_STATE_PATH = "/run/env_bridge/state.json"   # 或由本地HTTP拉取

OID_BASE = ".1.3.6.1.4.1.4711.1.1"
INDEX_MAP = {}   # dev -> idx

def load_state():
    try:
        with open(BRDIGE_STATE_PATH) as f:
            return json.load(f)
    except Exception:
        return {}

def build_rows(state):
    rows = []
    idx = 1
    INDEX_MAP.clear()
    for dev, m in state.items():
        INDEX_MAP[dev] = idx
        prefix = f"{OID_BASE}.3.{idx}"
        rows.append((f"{OID_BASE}.1.{idx}", "INTEGER", str(idx)))
        rows.append((f"{OID_BASE}.2.{idx}", "OCTETSTR", dev.encode().hex()))
        rows.append((f"{OID_BASE}.3.{idx}", "GAUGE", str(int(round(m.get('temp',0)*10)))))
        rows.append((f"{OID_BASE}.4.{idx}", "GAUGE", str(int(round(m.get('hum',0)*10)))))
        rows.append((f"{OID_BASE}.5.{idx}", "INTEGER", str(int(round(m.get('dewpoint',0)*10)))))
        rows.append((f"{OID_BASE}.6.{idx}", "INTEGER", str(m.get('status',0))))
        rows.append((f"{OID_BASE}.7.{idx}", "INTEGER", "1" if m.get('reachable') else "2"))
        rows.append((f"{OID_BASE}.8.{idx}", "GAUGE", str(int(m.get('age_s',0)))))
        idx += 1
    return rows

def handle_pdu():
    # pass_persist协议:snmpd发送 "ping\nPONG\n" 或 "get\n<oid>\n" / "getnext\n<oid>\n"
    while True:
        line = sys.stdin.readline()
        if not line:
            break
        cmd = line.strip()
        if cmd == "ping":
            sys.stdout.write("PONG\n")
            sys.stdout.flush()
            continue
        if cmd in ("get", "getnext", "set"):
            oid = sys.stdin.readline().strip()
            state = load_state()
            rows = build_rows(state)
            keymap = {o:(t,v) for (o,t,v) in rows}
            if cmd == "getnext":
                # 简化:返回下一个>=oid的叶子;生产用完备字典序
                cand = sorted(keymap.keys())
                hit = next((k for k in cand if k >= oid), None)
                if hit:
                    t,v = keymap[hit]
                    sys.stdout.write(f"{t}\n{hit}\n{v}\n")
                else:
                    sys.stdout.write("NONE\n\n")
            else:
                if oid in keymap:
                    t,v = keymap[oid]
                    sys.stdout.write(f"{t}\n{oid}\n{v}\n")
                else:
                    sys.stdout.write("NONE\n\n")
            sys.stdout.flush()
            continue

if __name__ == "__main__":
    handle_pdu()

更稳妥的做法:桥接服务暴露本地 HTTP/unix socket,pass_persist 脚本拉取后缓存;snmpd 配置:

代码语言:javascript
复制
pass_persist .1.3.6.1.4.1.4711.1.1 /usr/local/bin/snmp_pass_persist.py

重启 snmpd,验证:

代码语言:javascript
复制
snmpwalk -v2c -c public localhost .1.3.6.1.4.1.4711.1.1
snmpget -v2c -c public localhost .1.3.6.1.4.1.4711.1.1.3.1

注意 SELinux/AppArmor 放行、snmpd 社区字符串、UDP 防火墙、pass_persist 超时(缺省够用,状态拉取失败要降级)。

3.4 不想碰 pass_persist?用 Telegraf 直接采 + SNMP 仅做设施面

很多现场大屏已用 Grafana+Telegraf+InfluxDB。两条并行管道:

  • Telegraf modbus input → InfluxDB → Grafana 面板。
  • Telegraf snmp input 采交换机/PDU/UPS → 同一 InfluxDB → 同一仪表盘。

交付上这就够了,但用户点名双协议对接大屏,重点在让大屏侧 NMS 通过 SNMP 拿到环境量。所以保留桥接代理;同时把数据推一份进 InfluxDB,Grafana 直接渲染,避免 NMS 轮询延迟。

四、大屏侧:Grafana 渲染与联动

4.1 数据接入

  • InfluxDB datasource,bucket/measurement env。
  • Telegraf snmp input 配置(大屏侧 NMS 节点或边缘节点跑):
代码语言:javascript
复制
[[inputs.snmp.table]]
  oid = ".1.3.6.1.4.1.4711.1.1"
  name = "envkit"
  [[inputs.snmp.table.field]]
    oid = ".1.3.6.1.4.1.4711.1.1.2.1"
    name = "dev_id"
    is_tag = true
  [[inputs.snmp.table.field]]
    oid = ".1.3.6.1.4.1.4711.1.1.3.1"
    name = "temp_x10"
  [[inputs.snmp.table.field]]
    oid = ".1.3.6.1.4.1.4711.1.1.4.1"
    name = "hum_x10"
  [[inputs.snmp.table.field]]
    oid = ".1.3.6.1.4.1.4711.1.1.7.1"
    name = "reachable"

更实际:Telegraf snmp 采整个表,用 agent_host tag 区分。InfluxDB 中字段 temp_x10/10.0 在面板表达式处理,或在 Telegraf processor 转换。

  • 服务器监控面:node_exporter / telegraf host metrics 已在 Grafana。环境量叠加到同一行面板或独立 Row。

4.2 Grafana 面板

  • 热力图/散点:机房平面分区,每区记录仪一点,颜色=湿度带。
  • 时序:temp/hum 双 Y 轴,叠加设定带(阈值线)。
  • 状态表:设备、可达性、年龄、状态字解码。
  • 联动:Grafana Unified Alerting 或外部告警路由。

示例面板变量:

代码语言:javascript
复制
$room, $rack_row, $sensor
FROM env WHERE room=$room

4.3 告警联动

  • 阈值:温度 >28℃ 或 <18℃;湿度 >60%RH 或 <40%RH;连续 2 个采样越限触发。
  • 状态字 bit 解码:PoE 掉电、存储写满、模式切换。
  • 联动动作:大屏闪烁、Webhook→运维平台、SNMP trap 上行 NMS、触发空调/DDC 设定值下发(经桥接服务 Modbus 写回)。

写回示例(桥接服务暴露 HTTP/gRPC,或直接在采集服务内):

代码语言:javascript
复制
async def set_setpoint(dev, t_sp, h_sp):
    cfg = DEVICES[dev]
    c = await ensure_client(dev)
    await c.write_registers(0x10, [int(t_sp*10), int(h_sp*10)], slave=cfg["unit"])

五、双协议一致性、故障域与切换

5.1 数据一致性

  • 时标:以采集端写入时标为准,设备时标仅校验;InfluxDB 写时加 host/dev tag。
  • 单位:SNMP 侧返回×10 整数,Grafana 面板除10;别在两处各除一次。
  • 可达性:Modbus 轮询失败 ≠ SNMP 不可达。SNMP 代理存活但状态库陈旧时,暴露 age_s + reachable,大屏区分"设备离线"和"采集代理故障"。
  • 竞争写回:大屏侧经 SNMP SET?不建议。写回走南向 Modbus TCP 通道,北向 SNMP 只读暴露。明确控制面边界。

5.2 故障域隔离

  • snmpd 挂了:NMS 轮询失败,但 Modbus 采集仍在,InfluxDB 直推兜底。
  • 边缘桥接服务挂了:SNMP 暴露陈旧状态(age_s 增长),大屏告警;本地 WAL 缓冲,恢复后补传。
  • 网络分区:Modbus 轮询超时,状态库标记 unreachable;SNMP 代理仍响应,但值陈旧——大屏需结合 age_s 判定。
  • NMS 轮询周期与采集周期错开,避免惊群;SNMP 轮询周期建议 10~15s,Modbus 轮询 5s。

5.3 切换/降级

  • SNMPv3 凭证轮换、community 泄露:agent 侧热更新,不影响 Modbus 采集。
  • 大屏侧不支持 SNMP:切 Telegraf Modbus 直采,同一 InfluxDB,Grafana 不变。
  • 设备固件升级后寄存器表变更:版本化点表,采集服务热加载,SNMP MIB 同步更新。

六、安全与运维

  • SNMP:禁用默认 community,v3 USM/AES,ACL 限制 NMS 源地址;net-snmpd 视图限制到私有子树,别暴露系统组。
  • Modbus TCP:防火墙限采集网段,502 不暴露到业务网;单元ID校验。
  • 边缘节点:systemd 托管,Watchdog,core dump 保留;状态库定期落盘。
  • 观测:暴露 metrics——poll_latency, poll_err, snmpd_up, bridge_age, inflx_write_err。
  • 交付物:点表版本、MIB 文件、OID 对照表、Grafana dashboard JSON、Telegraf 配置、回滚脚本。

七、典型坑

  1. net-snmp pass_persist 协议细节:输出格式、换行、PONG 响应,调试时 snmpd -f -Le 前台跑。
  2. OID 字典序:getnext 遍历私有表时索引处理错,Telegraf table 采集漏列——用 snmpwalk 验证 MIB 视图。
  3. 多设备索引:SNMP 表需稳定索引,别用动态 idx 导致 NMS 缓存错位;用 dev_id hash 映射或固定索引表。
  4. 符号扩展:负温度寄存器值处理错,Grafana 出现 65000+ 值。
  5. 并发轮询:Telegraf snmp + 桥接服务 Modbus 各自轮询,双倍负载;统一采集周期或合并。
  6. UTF-8/OCTETSTRING:dev_id 返回 hex,Grafana 变量解析错;直接映射 index。
  7. snmpd 社区/源地址:NMS 跨网段,agent 侧 ACL 放行 UDP/161,防火墙状态表放行响应。
  8. InfluxDB 批写阻塞拖慢轮询循环——采集与写入解耦,队列化。
  9. 写回竞态:大屏侧误操作下发设定值,未做权限/确认;建议 dry-run + 审计日志。
  10. 状态库陈旧未老化:代理一直返回旧值,大屏无感知;强制 age_s 超阈标记 stale。

八、交付检查清单

  • [ ] Modbus TCP 点表验证(mbpoll/PyModbus)
  • [ ] 边缘桥接服务运行、重连、重试
  • [ ] snmpd pass_persist 集成,snmpwalk 验证
  • [ ] NMS/Telegraf snmp input 采集进 InfluxDB
  • [ ] Grafana dashboard 导入,变量、面板、阈值线
  • [ ] 告警规则 + Webhook/通知路由
  • [ ] 写回通道测试(设定值下发→设备回读)
  • [ ] 故障注入:停 snmpd、停桥接服务、断设备网线、PoE 断电恢复
  • [ ] 运维文档 + 回滚

九、小结

双协议对接的本质是控制面与监控面解耦:南向 Modbus TCP 管设备,北向 SNMP 融进既有监控大屏体系。工程落地关键在边缘桥接层——把设备状态归一化后,既供 SNMP 轮询,又推时序库直渲。别让 NMS 直接戳设备私有协议,也别为大屏另起一套采集栈;复用管道、明确边界、老化判定到位,交付就能闭环。

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

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

目录
  • TCP 与 SNMP 双协议:RJ45 温湿度记录仪对接机房服务器监控大屏实战
    • 一、为什么是双协议,而不是上一轮的TCP/UDP
    • 二、架构与数据流
      • 2.1 两种落地形态
    • 二、设备侧:Modbus TCP接入
      • 2.1 点表核对与归一化
      • 2.2 Python采集服务
    • 三、北向:SNMP暴露给大屏侧
      • 3.1 选集成方式
      • 3.2 MIB/OID设计
      • 3.3 pass_persist 桥接脚本
      • 3.4 不想碰 pass_persist?用 Telegraf 直接采 + SNMP 仅做设施面
    • 四、大屏侧:Grafana 渲染与联动
      • 4.1 数据接入
      • 4.2 Grafana 面板
      • 4.3 告警联动
    • 五、双协议一致性、故障域与切换
      • 5.1 数据一致性
      • 5.2 故障域隔离
      • 5.3 切换/降级
    • 六、安全与运维
    • 七、典型坑
    • 八、交付检查清单
    • 九、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档