旧上位机不肯改怎么办:原生 TCP 字节帧接入声光语音终端的改造实录

说明:面向工控与嵌入式现场:上位机/网关只会发二进制帧、不便改造为 HTTP 时的声光语音告警接入方案。报文以 Modbus TCP 帧格式逐字节拆解,Python 代码可直接联调。寄存器地址、端口、站号以设备当前固件页面为准,不放站外链接。


一、痛点:告警器要换,上位机一行代码都不能动

产线和老旧机房里有一类很现实的改造需求:

  • 原来的声光报警器/语音通知盒是某家老产品,停产了、坏了、或者只会蜂鸣不会说话,要换新
  • 上位机是十年前写的工控程序、或某套封闭的组态/SCADA、或一块改不动的嵌入式网关------它与旧报警器之间的通信是裸 TCP 二进制帧,没有 HTTP 客户端,也没有改造预算;
  • 需求是:上位机的报文结构尽量不动,只换对端,新的告警终端要能"听懂"原来那套字节。

HTTP API 那条路(见专栏对接篇)在 IT 机房里很好用,但工控现场经常走不通:老系统没有 HTTP 栈、没有 TLS、甚至没有可编程的回调出口,唯一保留的接口就是一个 TCP 端口往对端怼字节。

博灵 Q 系列为这类场景保留了原生 TCP 入口。它的协议本质一句话就能说清:借用 Modbus TCP 的二进制帧格式------上位机按功能码写寄存器,写对的瞬间终端执行对应播报。所以本文虽然叫"原生 TCP",帧却是标准的 Modbus TCP 帧,任何会发 Modbus 报文的设备(PLC、组态、网关、自研上位机)都能零成本接入。


二、协议本质:MBAP 头 + 功能码 + 数据,一个字都不神秘

一条原生 TCP 播报,就是一帧标准的 Modbus TCP 请求:

text 复制代码
[MBAP 头 7 字节]                [帧体]
事务标识符(2) 协议标识符(2) 长度(2) 单元标识符(1) 功能码(1) 起始地址(2) 寄存器数量/值
 0x0001        0x0000        0x0006  0x01         0x06      0x0063       0x0001
  • 事务标识符:随意递增即可,响应用来配对请求;
  • 协议标识符 :Modbus TCP 固定 0x0000
  • 长度:其后所有字节的字节数;
  • 单元标识符:站号,与终端页面配置一致(常见为 1);
  • 功能码0x06 写单寄存器(模板调用)、0x10 写多寄存器(自定义播报);
  • 寄存器地址 :注意偏移------本文统一用界面地址 (1 起算)描述,直接组帧时写成 0 起算的十六进制(界面地址 100 = 0x63)。

终端侧是 Server(从站),监听端口默认 502,可在页面修改。上位机每次播报就是建一条 TCP 连接(或复用长连接)写一帧,播报请求即触发。

对接前必须在终端页面完成三件事:

  1. 打开 Modbus TCP 服务开关
  2. 确认端口站号
  3. 通知模板页配好要用的模板列表(方式一要用,方式二可不用)。

三、两条路径:模板调用 vs 自定义播报

方式一:模板调用 方式二:自定义播报
功能码 0x06 写单寄存器 0x10 写多寄存器
帧长度 固定 12 字节,最好组 可变,需编码文本
播报内容 页面预设的模板(灯效+TTS) 每帧携带灯色、时长、语速、任意文本
上位机改动量 最小,值都写死 需实现文本编码
适用 告警类型有限、语句固定 告警内容动态(温度值、位置号)

改造老系统的推荐顺序:先用方式一把"能响"打通,再决定是否上方式二。多数现场其实只要十几种固定告警语义,模板方式足够,还顺便把播报文案的编辑权留在了终端页面,以后改词不用动上位机。


四、方式一:模板调用,12 字节打通

4.1 逐字节拆帧

调用界面地址 100 的 1 号模板(0 起算即 0x63),完整帧 12 字节:

text 复制代码
0x00 0x01  0x00 0x00  0x00 0x06  0x01  0x06  0x00 0x63  0x00 0x01
─事务──    ─协议──    ─长度──    ─站号─  ─写单寄存器  ─地址─     ─模板ID─
  • 寄存器值 = 模板 ID ,取值 0x01 ~ 0xFF,与终端模板页的编号对应;
  • 正常响应是请求帧原样回显,收到即认为触发成功(播报进入队列异步执行)。

4.2 Python:裸 socket 发帧

不依赖任何第三方库,工控机上往往只有标准库可用:

python 复制代码
import socket


def modbus_tcp_frame(unit: int, function: int, address: int, values: list[int]) -> bytes:
    """组一帧 Modbus TCP:0x06 写单寄存器 / 0x10 写多寄存器"""
    if function == 0x06:
        pdu = bytes([function]) + address.to_bytes(2, "big") + values[0].to_bytes(2, "big")
    elif function == 0x10:
        payload = b"".join(v.to_bytes(2, "big") for v in values)
        pdu = (
            bytes([function])
            + address.to_bytes(2, "big")
            + len(values).to_bytes(2, "big")
            + bytes([len(payload)])
            + payload
        )
    else:
        raise ValueError("仅支持 0x06 / 0x10")
    mbap = (1).to_bytes(2, "big") + b"\x00\x00" + (len(pdu) + 1).to_bytes(2, "big")
    return mbap + bytes([unit]) + pdu


def fire_template(host: str, template_id: int, port: int = 502, unit: int = 1) -> bytes:
    """模板调用:界面地址 100(0 起算 0x63)写模板 ID,返回响应帧"""
    frame = modbus_tcp_frame(unit, 0x06, 0x63, [template_id])
    with socket.create_connection((host, port), timeout=3) as sock:
        sock.sendall(frame)
        return sock.recv(256)


if __name__ == "__main__":
    resp = fire_template("192.168.0.66", template_id=1)
    print(" ".join(f"{b:02X}" for b in resp))  # 期望与请求帧一致

跑通后你会看到终端执行 1 号模板的灯效与 TTS。把 template_id 换成报警/恢复两个模板号,上位机原来"点亮/熄灭报警器"的两条分支就有了新去处。

4.3 用 pymodbus 的等价写法

如果机器上允许装库,直接用现成协议栈更省心(专栏 Modbus 对接篇同款思路):

python 复制代码
from pymodbus.client import ModbusTcpClient


def fire_template_pymodbus(host: str, template_id: int) -> None:
    client = ModbusTcpClient(host=host, port=502, timeout=3)
    if not client.connect():
        raise RuntimeError("Modbus TCP 连接失败")
    try:
        rr = client.write_register(address=100, value=template_id, slave=1)
        if rr.isError():
            raise RuntimeError(rr)
    finally:
        client.close()

注意 pymodbus 的 address 参数就是 0 起算的偏移:写 100 等价于裸帧里的 0x63 + 1,两个世界别混。


五、方式二:自定义播报,灯色和句子随帧携带

5.1 寄存器布局

从界面地址 100 起连续写多寄存器(功能码 0x10,组帧时 0 起算地址为 0x64):

界面地址 含义 取值说明
100 LED 样式 如 10=闪烁、15=呼吸,与页面样式表一致
101 LED 颜色 RGB565:红 #FF00000xF800
102 播放时长 单位 0.1 秒:600 → 60 秒
103 声音类型 0 蜂鸣 / 1 报警 / 2 提示(以页面为准)
104 语速 1~10,5 为默认
105 文本长度 寄存器对计数
106~ 文本内容 UTF-8 编码,每 3 字节拆成 2 个寄存器,余量补 0x0000

文本编码是最容易写错的一步:一个汉字 UTF-8 占 3 字节,装入寄存器时按"高 8 位 + 低 16 位"拆成两个 16 位寄存器;文本长度字段按寄存器对(即每 3 字节)计数,不是字节数。

5.2 颜色换算:RGB888 → RGB565

终端寄存器吃 RGB565。换算只需三行:

python 复制代码
def rgb888_to_rgb565(r: int, g: int, b: int) -> int:
    """#FF0000 -> 0xF800;损失低 3/2/3 位,肉眼几乎无感"""
    return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)

把通知组的色卡(critical 红 / warning 黄 / recovered 绿)在这里各算一个常量,帧里直接引用,上位机就不用管颜色语义了。

5.3 Python:文本编码 + 组帧 + 发送

python 复制代码
def encode_tts_registers(text: str) -> list[int]:
    """UTF-8 文本 -> 寄存器序列:每 3 字节拆成 [高8位, 低16位] 两个寄存器"""
    data = text.encode("utf-8")
    regs: list[int] = []
    for i in range(0, len(data), 3):
        chunk = data[i : i + 3]
        if len(chunk) < 3:          # 尾部不足 3 字节(纯 ASCII 场景)补零
            chunk = chunk.ljust(3, b"\x00")
        regs.append(chunk[0])
        regs.append((chunk[1] << 8) | chunk[2])
    return regs


def fire_custom(host: str, text: str, color: int = 0xF800, duration: int = 600,
                style: int = 15, sound: int = 1, speed: int = 5,
                port: int = 502, unit: int = 1) -> bytes:
    """自定义播报:界面地址 100 起写 LED 样式/颜色/时长/声音/语速/文本"""
    regs = encode_tts_registers(text)
    head = [style, color, duration, sound, speed, len(regs) // 2]
    frame = modbus_tcp_frame(unit, 0x10, 0x64, head + regs)
    with socket.create_connection((host, port), timeout=3) as sock:
        sock.sendall(frame)
        return sock.recv(256)


if __name__ == "__main__":
    resp = fire_custom("192.168.0.66", "三号产线二号炉温度超限",
                       color=rgb888_to_rgb565(0xFF, 0x00, 0x00))
    print(" ".join(f"{b:02X}" for b in resp))

这段代码跑通,意味着上位机在任何时刻、以任意文案触发播报------温度值、机台号、故障码都能拼进句子,这是模板方式做不到的。

5.4 抓包核对:Wireshark 是最终裁判

组帧对不对,别靠猜。在联调机上抓包比对:

bash 复制代码
# 抓 502 端口的 Modbus TCP 流量(tcpdump)
tcpdump -i eth0 -w bolin.pcap host 192.168.0.66 and port 502

Wireshark 直接把 502 端口流量按 Modbus/TCP 解码,Write Single Register / Write Multiple Registers 的地址、数量、值一目了然。官方文档给出的示例帧都可以先手动灌进去比对,再谈扩展。


六、旧设备替换四步法:从抓包到割接

真正替换老报警器时,推荐按这个顺序推进:

  1. 抓包取证:在旧链路挂 tcpdump/Wireshark,把旧上位机发给老报警器的帧全部录下来,确认功能码、地址、值的语义表(哪条是报警、哪条是恢复);
  2. 帧映射:把旧帧翻译成终端的帧。多数旧设备也是 Modbus 从站或类 Modbus 私有帧,通常只需改"地址 + 值"两个字段;如果旧帧完全私有,就在中间垫一个几十行的 Python/Node 转发进程;
  3. 模板对齐:按语义表在终端模板页建模板------旧系统的"报警"对应模板 A(红灯+急报),"恢复"对应模板 B(绿灯+恢复通知),上位机侧只改两个值;
  4. 割接与回退:先并接(新旧报警器同时在线跑一周),确认无误报漏报后摘除旧设备;保留旧帧的转发脚本作为回退预案。

这套打法的关键是语义表驱动:改造的不是字节,而是"旧语义 → 新语义"的映射关系,映射清楚了,割接只是改配置。


七、常见坑

  1. 地址差一 :文档示例帧 0x63 是 0 起算(即 99),对应界面地址 100;pymodbus 的 address=100 也是 0 起算偏移。三处口径混用是最常见的事故来源,割接前用抓包核对一次;
  2. 端口/站号不匹配:终端改过端口后上位机还在连 502,表现为 TCP 能连上但无响应或直接拒绝,先对页面再对帧;
  3. 文本长度字段按字节算:它按寄存器对计数。一句 12 个汉字 = 36 字节 = 24 个寄存器 = 长度填 12,填 24 或 36 都会播错;
  4. RGB888 直接塞寄存器:颜色会错乱,必须先转 RGB565;
  5. 长连接不处理超时:TCP 半开连接(网线被拔)后 sendall 不报错、播报全丢。用短连接或加应用层心跳,失败重连;
  6. 粘包/拆包:Modbus TCP 有 MBAP 长度字段,接收侧应按"先读 7 字节头、再按长度读体"的方式收帧,不要假定一次 recv 收完整帧;
  7. 拿响应当播报完成:响应回显只代表"帧收下了",播报在队列里异步执行;要确认播完,看设备的播报日志或首页告警列表;
  8. 无限循环无人终止:自定义播报的时长是有限的,但模板若配了循环播报,务必让恢复语义写删除/恢复模板,否则深夜喇叭长鸣。

八、验收清单

  • 模板方式:报警模板、恢复模板各触发一次,灯效、TTS、通知组语义正确;
  • 自定义方式:任意中文句子(含数字混排)播报清晰,RGB565 颜色与设计一致;
  • 响应帧与请求帧逐字节比对通过,Wireshark 解码无异常;
  • 拔网线/断电重连后,上位机自动恢复发送,无半开连接积压;
  • 旧上位机语义表(报警/恢复/测试)与新模板一一对应,留有映射文档;
  • 连续触发 50 次无队列堵塞,恢复模板可终止循环播报;
  • 并接观察期内无误报、漏报,割接与回退步骤已写入运维文档。

九、小结

原生 TCP 入口解决的是一个很"工控"的问题:告警终端换代,不该逼着上位机换代

  • 帧格式就是标准 Modbus TCP:0x06 写地址 100 调模板,0x10 从地址 100 写入灯色、时长、语速与 UTF-8 文本;
  • 改造方法论是"抓包取证 → 帧映射 → 模板对齐 → 并接割接",全程不动上位机核心逻辑;
  • 三个易错点提前锁定:地址偏移口径、文本长度按寄存器对、RGB888 转 RGB565。

至此,博灵 Q 系列的接入矩阵又补齐一块:HTTP API 面向 IT、Modbus 面向 PLC 组态、SNMP 面向网管、邮件面向安防遗留系统、云中继面向跨网段------原生 TCP 面向改不动的二进制上位机。按现场挑门,别按习惯挑门。

相关推荐
ACP广源盛139246256731 小时前
M6/M5 Pro Mac mini 端侧 AI 爆发@ACP#YLB3118 存储扩展芯片在本地 AI 服务中的机会与落地场景
大数据·网络·数据库·人工智能·嵌入式硬件·macos
程序员夏洛1 小时前
HTTP 2.0 和 3.0 有什么区别?
网络·网络协议·http
丶浅行DE时光1 小时前
管道式电磁流量计选型指南 介质腐蚀与工况适配方案推荐
大数据·网络·人工智能·科技·推荐算法
段一凡-华北理工大学2 小时前
高炉智能布料技术与炉料分布优化~系列文章03:布料矩阵解码:环位、角度、圈数与料流的量化控制
java·网络·人工智能·矩阵·高炉智能化·高炉布料矩阵优化·高炉智能布料技术
云飞云共享云桌面4 小时前
医疗器械设备研发:多人同时操作 SolidWorks,怎样依靠单台服务器替代多工作站
运维·服务器·网络·数据库·制造
Sylvia33.5 小时前
LOL实时数据接入深度解析:从WebSocket到完整数据模型
java·网络·python·websocket·网络协议
鼹鼠SDN8 小时前
没有公网 IP,如何在外面访问家里的打印机、摄像头和整个局域网?
网络·网络协议·tcp/ip
小诗懂技术10 小时前
【网络通信UDP】基础 进程间不同主机的通信
运维·服务器·网络
DM今天肝到几点?12 小时前
AI 安全进入「攻防同频」:Gemini 3.8 Flash Cyber 上线、Astra 触及关键级、HiddenLayer 融资 1 亿美元
网络·人工智能·深度学习·安全·语言模型·开源·知识图谱