让 PLC“开口说话”:基于 Modbus TCP 的声光语音告警设计与 Python 调试

在工业现场,PLC 检测到温度超限、设备停机、缺料或安全门打开后,通常会驱动指示灯、蜂鸣器或组态画面产生告警。

这种方式虽然可靠,但存在一个明显问题:传统指示灯只能表达"有故障",很难直接说明故障发生在哪里、具体是什么问题以及操作人员应该如何处理。

如果将 PLC 与支持 Modbus TCP、全彩 LED 和 TTS 语音合成的智能声光告警终端连接,就可以把一个寄存器值转换成完整语音,例如:

二号生产线电机温度超过八十度,请停止设备并检查冷却系统。

本文以博灵 Q 系列语音通知终端为例,介绍两种 Modbus TCP 对接方式,并使用原生 Python Socket 构造调试报文。

本文示例用于测试环境。寄存器地址、功能范围和数据结构应以实际设备版本的官方文档为准。

一、适合使用 Modbus TCP 的场景

Modbus TCP 方案适用于:

  • PLC 控制系统;
  • SCADA 平台;
  • 工业组态软件;
  • 边缘计算网关;
  • 生产线 Andon 系统;
  • 环境监测设备;
  • 不方便调用 HTTP API 的工控网络。

典型应用包括:

PLC 状态 现场动作
电机温度超限 红灯闪烁并播报温度告警
生产线缺料 黄灯闪烁并提醒补料
安全门打开 红灯旋转并重复播报
AGV 到达工位 蓝灯常亮并播报到达信息
故障恢复 绿灯常亮并播报恢复通知
工单完成 播放提示音并播报任务编号

相比只控制蜂鸣器,语音通知终端能够把告警代码转换成操作人员可以立即理解的信息。

二、整体架构

一套典型的 PLC 声光语音告警架构如下:

复制代码
┌──────────────────────────────────────┐
│            传感器与设备              │
│                                      │
│ 温度 / 压力 / 液位 / 安全门 / 电机   │
└─────────────────┬────────────────────┘
                  │ DI / AI / 工业总线
                  ▼
┌──────────────────────────────────────┐
│                 PLC                  │
│                                      │
│ 阈值判断 → 状态机 → 告警码 → 去重    │
└─────────────────┬────────────────────┘
                  │ Modbus TCP
                  │ 0x06 / 0x10
                  ▼
┌──────────────────────────────────────┐
│       博灵 Q 系列语音通知终端        │
│                                      │
│ 模板匹配 / 数据解析 / 告警队列       │
│ LED灯光 / 提示音 / TTS语音播报       │
└─────────────────┬────────────────────┘
                  ▼
          现场操作人员确认处理

PLC 侧不应该每个扫描周期都发送一次告警,而应在状态发生变化时触发:

复制代码
正常 → 故障:发送故障语音
故障 → 故障:不重复发送
故障 → 恢复:发送恢复语音

如果需要持续提醒,应由告警终端创建周期播报,或者由 PLC 使用受控的定时逻辑触发,不能简单地在扫描循环中持续写寄存器。

三、两种 Modbus TCP 对接方式

博灵 Q 系列文档提供两种主要思路。

方式一:写入模板 ID

先在语音通知终端中配置模板:

复制代码
模板 ID:10
播报内容:二号生产线电机温度超过阈值,请检查
通知组:严重告警

PLC 触发时只需写入数值 10,不需要编码中文内容。

这种方式具有以下优势:

  • PLC 程序简单;
  • 报文长度短;
  • 调试难度低;
  • 语音文本集中管理;
  • 适合固定类型的工业告警。

其限制是播报内容预先确定,无法直接包含实时温度、压力或工单编号。

方式二:写多个寄存器

PLC 或工业网关动态构造以下内容:

  • LED 样式;
  • LED 颜色;
  • 播报时长;
  • 提示音编号;
  • TTS 语速;
  • 灯光样式参数;
  • UTF-8 编码的语音文本。

这种方式可以生成动态语音,但涉及寄存器排列、UTF-8 编码、字节序和报文长度计算,适合具备较强协议开发能力的项目。

建议的选择原则是:

复制代码
固定告警内容       → 模板 ID
少量动态内容       → 多个模板组合
完全动态语音       → 多寄存器写入
复杂业务系统       → HTTP API

四、模板 ID 的寄存器与报文结构

根据 Q 系列 Modbus TCP 文档,模板触发使用逻辑地址 100

需要注意,Modbus 报文中的寄存器偏移通常从 0 开始,因此:

复制代码
文档逻辑地址:100
协议报文偏移:99
十六进制:0x0063

触发模板 10 的完整报文为:

复制代码
00 01 00 00 00 06 01 06 00 63 00 0A

拆解如下:

字节 含义
00 01 Transaction ID 1
00 00 Protocol ID 固定为 0
00 06 后续长度 6 字节
01 Unit ID 1
06 功能码 写单寄存器
00 63 寄存器偏移 99
00 0A 模板 ID 10

语音通知终端正确处理后,通常会返回相同结构的响应报文。

五、使用 Python 触发模板告警

下面使用标准库 socketstruct 构造 Modbus TCP 报文,不依赖第三方库。

复制代码
import socket
import struct


DEVICE_IP = "192.168.0.66"
DEVICE_PORT = 502

UNIT_ID = 1
FUNCTION_WRITE_SINGLE = 0x06

# 文档逻辑地址100,对应协议偏移99
TEMPLATE_REGISTER = 99


def build_template_request(
    template_id: int,
    transaction_id: int = 1
) -> bytes:
    if not 1 <= template_id <= 0xFFFF:
        raise ValueError("模板ID超出寄存器范围")

    protocol_id = 0
    following_length = 6

    return struct.pack(
        ">HHHBBHH",
        transaction_id,
        protocol_id,
        following_length,
        UNIT_ID,
        FUNCTION_WRITE_SINGLE,
        TEMPLATE_REGISTER,
        template_id
    )


def parse_template_response(
    response: bytes,
    expected_transaction_id: int,
    expected_template_id: int
):
    if len(response) < 12:
        raise RuntimeError(
            f"响应长度异常:{len(response)}"
        )

    (
        transaction_id,
        protocol_id,
        length,
        unit_id,
        function_code,
        register_address,
        register_value
    ) = struct.unpack(">HHHBBHH", response[:12])

    if transaction_id != expected_transaction_id:
        raise RuntimeError("Transaction ID不匹配")

    if protocol_id != 0:
        raise RuntimeError("Protocol ID异常")

    if function_code & 0x80:
        raise RuntimeError(
            f"设备返回Modbus异常:0x{function_code:02X}"
        )

    if register_address != TEMPLATE_REGISTER:
        raise RuntimeError("响应寄存器地址不匹配")

    if register_value != expected_template_id:
        raise RuntimeError("响应模板ID不匹配")

    return {
        "transaction_id": transaction_id,
        "length": length,
        "unit_id": unit_id,
        "template_id": register_value
    }


def trigger_template(template_id: int):
    transaction_id = 1
    request = build_template_request(
        template_id,
        transaction_id
    )

    print("发送报文:", request.hex(" "))

    with socket.create_connection(
        (DEVICE_IP, DEVICE_PORT),
        timeout=5
    ) as client:
        client.sendall(request)
        response = client.recv(1024)

    print("响应报文:", response.hex(" "))

    return parse_template_response(
        response,
        transaction_id,
        template_id
    )


if __name__ == "__main__":
    result = trigger_template(10)
    print("模板触发成功:", result)

运行后,如果模板 10 已在设备中正确配置,终端将按照对应通知组执行声光语音播报。

六、多寄存器动态语音的数据结构

当告警文本必须包含动态值时,可以使用功能码 0x10 写多个保持寄存器。

Q 系列文档中的主要逻辑地址如下:

逻辑地址 内容
101 LED 样式
102~109 最多四组 LED 颜色
110 播报时长
111 提示音编号
112 TTS 语速
113 LED 样式参数
114~115 保留
116~223 UTF-8 文本内容

写入起始逻辑地址 101 时,Modbus 报文中的起始偏移是:

复制代码
101 - 1 = 100 = 0x0064

文档规定自定义播报区域使用多寄存器写入,最多一次写入 123 个寄存器。

七、UTF-8 文本如何写入寄存器

文档中的编码方式是:一个字符使用两个寄存器存储。

以汉字"你"为例:

复制代码
UTF-8:E4 BD A0

高位寄存器:0x00E4
低位寄存器:0xBDA0

汉字"好":

复制代码
UTF-8:E5 A5 BD

高位寄存器:0x00E5
低位寄存器:0xA5BD

可以使用以下函数进行转换:

复制代码
def encode_text_to_registers(
    text: str,
    max_chars: int = 54
) -> list[int]:
    if len(text) > max_chars:
        raise ValueError(
            f"文本超过{max_chars}个字符"
        )

    registers = []

    for char in text:
        encoded = char.encode("utf-8")

        if len(encoded) > 4:
            raise ValueError(
                f"字符无法编码:{char}"
            )

        value = int.from_bytes(
            encoded,
            byteorder="big"
        )

        high_word = (value >> 16) & 0xFFFF
        low_word = value & 0xFFFF

        registers.extend([
            high_word,
            low_word
        ])

    return registers


registers = encode_text_to_registers(
    "设备温度超过阈值"
)

print([
    f"0x{value:04X}"
    for value in registers
])

正式使用前,需要在实际设备上测试中文、英文、数字和标点符号。对于表情符号或生僻字符,应确认设备 TTS 和协议编码是否支持。

八、构造写多寄存器报文

下面给出一个通用的 Modbus TCP 写多寄存器函数:

复制代码
import socket
import struct


def build_write_multiple_request(
    registers: list[int],
    start_address: int = 100,
    transaction_id: int = 1,
    unit_id: int = 1
) -> bytes:
    quantity = len(registers)

    if not 1 <= quantity <= 123:
        raise ValueError(
            "寄存器数量必须在1~123之间"
        )

    for value in registers:
        if not 0 <= value <= 0xFFFF:
            raise ValueError(
                f"非法寄存器值:{value}"
            )

    function_code = 0x10
    byte_count = quantity * 2

    # Length包含Unit ID和后续PDU
    length = 7 + byte_count

    header = struct.pack(
        ">HHH",
        transaction_id,
        0,
        length
    )

    pdu_header = struct.pack(
        ">BBHHB",
        unit_id,
        function_code,
        start_address,
        quantity,
        byte_count
    )

    register_data = b"".join(
        struct.pack(">H", value)
        for value in registers
    )

    return header + pdu_header + register_data

构造一条动态语音:

复制代码
def build_alarm_registers(text: str) -> list[int]:
    led_style = 15

    # #8A2BE2拆分为高8位和低16位
    colors = [
        0x008A,
        0x2BE2,
        0x0000,
        0x0000,
        0x0000,
        0x0000,
        0x0000,
        0x0000
    ]

    play_time = 0
    tone = 0xFFFF
    tts_speed = 5

    # 对应样式参数[0.03, 1]
    led_flag = 0x0301

    reserved = [0x0000, 0x0000]
    text_registers = encode_text_to_registers(text)

    return (
        [led_style]
        + colors
        + [play_time, tone, tts_speed, led_flag]
        + reserved
        + text_registers
    )


registers = build_alarm_registers(
    "二号生产线电机温度超过八十度"
)

request = build_write_multiple_request(
    registers=registers,
    start_address=100
)

with socket.create_connection(
    ("192.168.0.66", 502),
    timeout=5
) as client:
    client.sendall(request)
    response = client.recv(1024)

print("响应:", response.hex(" "))

上述代码用于说明报文结构。上线前还需要严格验证响应功能码、寄存器地址和写入数量。

九、PLC 程序应如何避免重复告警

PLC 的扫描周期可能只有数毫秒。如果把"故障位为真"直接作为 Modbus 写入条件,同一个模板可能在短时间内被触发数百次。

建议使用上升沿和状态锁:

复制代码
IF Alarm = TRUE AND LastAlarm = FALSE THEN
    发送故障模板
END_IF

IF Alarm = FALSE AND LastAlarm = TRUE THEN
    发送恢复模板
END_IF

LastAlarm := Alarm

更完整的工业告警状态机可以设计为:

复制代码
NORMAL
  │ 检测到异常
  ▼
DELAY_CONFIRM
  │ 连续异常达到阈值
  ▼
ALARM_ACTIVE
  │ 发送一次告警模板
  │
  ├─ 异常持续:不重复发送
  │
  └─ 状态恢复
       ▼
RECOVERY
  │ 发送恢复模板
  ▼
NORMAL

对于需要周期提醒的严重故障,可以在终端中配置周期播报,并在恢复后发送对应删除指令。

十、工业网络的安全要求

Modbus TCP 本身通常不提供完善的身份认证和传输加密,不能直接暴露在办公网或公网中。

建议:

  • 将 PLC 和告警终端部署在工业 VLAN;
  • 使用工业防火墙限制源 IP 和目标端口;
  • 只允许指定 PLC 或网关访问 Modbus 服务;
  • 不把 502 端口映射到公网;
  • 限制能够修改模板和通知组的管理人员;
  • 保留 PLC 告警触发日志;
  • 设置合理的调用频率;
  • 避免多个控制器同时写同一终端;
  • 在正式投产前完成异常报文和断线测试;
  • 为终端离线准备备用告警方式。

如果需要跨区域控制,建议通过 VPN、安全网关或经过认证的应用层接口连接,而不是直接转发 Modbus TCP 端口。

十一、上线测试清单

正式投产前至少验证以下项目:

  • PLC 与语音通知终端网络是否互通;
  • Modbus TCP 服务端口是否正确;
  • Unit ID 是否为当前设备要求的值;
  • 模板 ID 与语音内容是否对应;
  • 红、黄、绿通知组是否正确;
  • 相同故障是否只触发一次;
  • 恢复时是否发送恢复通知;
  • 终端离线时 PLC 是否记录通信异常;
  • 中文、数字和单位播报是否清晰;
  • 告警队列积压后如何处理;
  • 设备重启后模板是否仍然存在;
  • 工业防火墙是否只开放必要通信。

结语

通过 Modbus TCP,PLC 不再只能驱动蜂鸣器或单色指示灯,而是可以把设备状态转换成包含故障位置、原因和处置建议的语音信息。

博灵 Q 系列语音通知终端提供模板 ID 和多寄存器自定义播报两种方案:前者适合快速、稳定地接入固定工业告警,后者适合需要实时文本和动态灯光效果的项目。

在实际实施中,建议优先选择模板 ID,把复杂性控制在最低水平;只有确实需要动态语音时,再开发多寄存器编码。同时必须在 PLC 侧加入状态机、边沿触发和恢复逻辑,避免扫描周期造成告警风暴。

参考资料:

相关推荐
今日热点1 小时前
微信小程序主体变更公证全流程实操解析:场景条件、材料规范、驳回避坑与Python校验脚本实现
python·微信小程序·小程序
O。O蛋黄酥啊1 小时前
GraphRAG 和 LightRAG 详解与对比
人工智能·python·算法·rag·graphrag·lightrag
Buke..1 小时前
【小程序逆向】某游快爆 AI 逆向 sign参数:AI 辅助分析与 MCP 工具链实战
前端·人工智能·爬虫·python·小程序·notepad++
一只旭宝1 小时前
预约系统版本2(pyhton+flask可视化版本)
服务器·数据库·c++·笔记·python·flask
小飞学编程...1 小时前
【equals 、Comparable、Comparator 三者的区别】
java·python
青 春 记 忆2 小时前
零基础入门python21:Flask账本的数据模型——用户、分类和账目
python·flask·后端开发
hy.z_7772 小时前
【软件测试】8. 自动化测试实践
python
楠楠子呀2 小时前
从零构建高性能 WhatsApp 链接生成器与重定向服务:架构与路由解析
redis·python·容器·架构·flink·散列表·flume