在工业现场,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 触发模板告警
下面使用标准库 socket 和 struct 构造 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 侧加入状态机、边沿触发和恢复逻辑,避免扫描周期造成告警风暴。
参考资料: