文章目录
-
- 一、项目背景与技术架构
-
- [1.1 核心技术栈](#1.1 核心技术栈)
- [1.2 整体流程架构](#1.2 整体流程架构)
- 二、环境准备(零基础适配)
-
- [2.1 STM32MP1 Linux系统基础配置](#2.1 STM32MP1 Linux系统基础配置)
- [2.2 安装Modbus与MQTT核心库](#2.2 安装Modbus与MQTT核心库)
- [2.3 硬件连接确认](#2.3 硬件连接确认)
- 三、核心功能开发(分模块实现)
-
- [3.1 Modbus数据读取模块(支持RTU/TCP双模式)](#3.1 Modbus数据读取模块(支持RTU/TCP双模式))
- [3.2 MQTT数据发布模块](#3.2 MQTT数据发布模块)
- [3.3 主程序(协议转换整合)](#3.3 主程序(协议转换整合))
- 四、配置与运行(零基础操作步骤)
-
- [4.1 配置文件修改](#4.1 配置文件修改)
- [4.2 串口权限配置(RTU模式必做)](#4.2 串口权限配置(RTU模式必做))
- [4.3 运行网关程序](#4.3 运行网关程序)
- [4.4 后台运行](#4.4 后台运行)
- 五、调试与验证
-
- [5.1 日志查看](#5.1 日志查看)
- [5.2 MQTT数据验证](#5.2 MQTT数据验证)
- [5.3 常见问题排查](#5.3 常见问题排查)
- 六、开机自启配置
一、项目背景与技术架构
在工业物联网(IIoT)场景中,大量传统工业设备通过Modbus RTU/TCP协议进行数据交互,而物联网平台普遍采用MQTT协议实现设备接入与数据传输。STM32MP1作为ST推出的异构多核边缘计算芯片,集成Cortex-A7(运行Linux)和Cortex-M4内核,非常适合作为边缘网关实现Modbus与MQTT的协议转换。
1.1 核心技术栈
- 硬件:STM32MP157开发板(已烧录Linux系统,推荐Debian/Ubuntu)
- 软件环境:Linux内核5.4+、gcc编译器、Python3.8+
- 关键库:
- pymodbus:实现Modbus协议的读写操作
- paho-mqtt:实现MQTT客户端的连接与数据收发
- serial:处理串口(Modbus RTU)通信
1.2 整体流程架构
以下是Modbus转MQTT的完整数据流转流程,流程图采用深色底、白色字体,保证视觉清晰:
Modbus寄存器值
工业设备 (Modbus RTU/TCP)
STM32MP1串口/网口 数据接收
Modbus协议解析 数据校验
数据格式转换
JSON格式封装
MQTT客户端连接 物联网平台
MQTT主题发布 转换后数据
物联网平台接收 数据存储/展示
数据解析失败 日志记录
重试/报警
二、环境准备(零基础适配)
2.1 STM32MP1 Linux系统基础配置
-
确认开发板网络连通性
bash# 查看网卡信息 ifconfig # 配置静态IP(以eth0为例) sudo ifconfig eth0 192.168.1.100 netmask 255.255.255.0 # 设置网关 sudo route add default gw 192.168.1.1 # 配置DNS sudo echo "nameserver 8.8.8.8" >> /etc/resolv.conf -
更新系统源并安装基础依赖
bash# 更新软件包列表 sudo apt update # 安装编译器、Python3及pip sudo apt install -y gcc python3 python3-pip python3-dev # 安装串口依赖 sudo apt install -y python3-serial
2.2 安装Modbus与MQTT核心库
bash
# 安装pymodbus(支持Modbus RTU/TCP)
pip3 install pymodbus==2.5.3
# 安装paho-mqtt(MQTT客户端)
pip3 install paho-mqtt==1.6.1
# 安装json处理库(系统默认自带,此处确保版本)
pip3 install simplejson==3.19.1
2.3 硬件连接确认
- Modbus RTU:将工业设备的RS485接口通过RS485转TTL模块连接到STM32MP1的UART串口(如/dev/ttySTM0)
- Modbus TCP:通过网口直连工业设备或交换机
- 网络:确保STM32MP1能访问MQTT服务器(本地/云端)
三、核心功能开发(分模块实现)
3.1 Modbus数据读取模块(支持RTU/TCP双模式)
创建modbus_reader.py文件,实现Modbus数据的通用读取功能:
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Modbus数据读取模块
支持RTU和TCP两种模式
"""
import logging
from pymodbus.client import ModbusSerialClient, ModbusTcpClient
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('modbus_reader.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger('ModbusReader')
class ModbusReader:
def __init__(self, mode, **kwargs):
"""
初始化Modbus客户端
:param mode: 模式 'rtu' 或 'tcp'
:param kwargs: 连接参数
RTU参数:port, baudrate, parity, stopbits, bytesize, timeout
TCP参数:host, port, timeout
"""
self.mode = mode.lower()
self.client = None
self.connected = False
self.kwargs = kwargs
self._init_client()
def _init_client(self):
"""初始化客户端连接"""
try:
if self.mode == 'rtu':
# RTU模式参数配置
self.client = ModbusSerialClient(
port=self.kwargs.get('port', '/dev/ttySTM0'),
baudrate=self.kwargs.get('baudrate', 9600),
parity=self.kwargs.get('parity', 'N'),
stopbits=self.kwargs.get('stopbits', 1),
bytesize=self.kwargs.get('bytesize', 8),
timeout=self.kwargs.get('timeout', 3)
)
elif self.mode == 'tcp':
# TCP模式参数配置
self.client = ModbusTcpClient(
host=self.kwargs.get('host', '192.168.1.200'),
port=self.kwargs.get('port', 502),
timeout=self.kwargs.get('timeout', 3)
)
else:
raise ValueError(f"不支持的Modbus模式:{self.mode}")
logger.info(f"Modbus {self.mode}客户端初始化完成")
except Exception as e:
logger.error(f"Modbus客户端初始化失败:{str(e)}")
raise
def connect(self):
"""建立Modbus连接"""
try:
if self.client.connect():
self.connected = True
logger.info(f"Modbus {self.mode}连接成功")
return True
else:
self.connected = False
logger.error(f"Modbus {self.mode}连接失败")
return False
except Exception as e:
logger.error(f"Modbus连接异常:{str(e)}")
self.connected = False
return False
def read_holding_registers(self, address, count, unit=1):
"""
读取保持寄存器(常用功能码03)
:param address: 寄存器起始地址
:param count: 读取寄存器数量
:param unit: 从站地址
:return: 解码后的寄存器值列表
"""
if not self.connected:
logger.warning("Modbus未连接,尝试重新连接")
if not self.connect():
return None
try:
# 读取寄存器
response = self.client.read_holding_registers(
address=address,
count=count,
unit=unit
)
if response.isError():
logger.error(f"读取寄存器失败:{response}")
return None
# 解码寄存器数据(大端序,16位)
decoder = BinaryPayloadDecoder.fromRegisters(
response.registers,
byteorder=Endian.Big,
wordorder=Endian.Big
)
# 解析每个寄存器的值
values = []
for _ in range(count):
values.append(decoder.decode_16bit_uint())
logger.info(f"成功读取寄存器[{address}-{address+count-1}],值:{values}")
return values
except Exception as e:
logger.error(f"读取寄存器异常:{str(e)}")
return None
def close(self):
"""关闭Modbus连接"""
if self.client:
self.client.close()
self.connected = False
logger.info("Modbus连接已关闭")
# 测试代码
if __name__ == "__main__":
# 测试RTU模式
rtu_reader = ModbusReader(
mode='rtu',
port='/dev/ttySTM0',
baudrate=9600,
parity='N',
stopbits=1,
bytesize=8,
timeout=3
)
if rtu_reader.connect():
rtu_values = rtu_reader.read_holding_registers(address=0, count=4, unit=1)
print(f"RTU读取结果:{rtu_values}")
rtu_reader.close()
# 测试TCP模式
tcp_reader = ModbusReader(
mode='tcp',
host='192.168.1.200',
port=502,
timeout=3
)
if tcp_reader.connect():
tcp_values = tcp_reader.read_holding_registers(address=0, count=4, unit=1)
print(f"TCP读取结果:{tcp_values}")
tcp_reader.close()
3.2 MQTT数据发布模块
创建mqtt_publisher.py文件,实现MQTT客户端的连接与数据发布:
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MQTT数据发布模块
支持QoS 0/1/2,断线重连
"""
import logging
import json
import time
import paho.mqtt.client as mqtt
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('mqtt_publisher.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger('MQTTPublisher')
class MQTTPublisher:
def __init__(self, broker, port=1883, username=None, password=None, client_id=None):
"""
初始化MQTT客户端
:param broker: MQTT服务器地址(IP/域名)
:param port: MQTT端口,默认1883
:param username: 认证用户名
:param password: 认证密码
:param client_id: 客户端ID,为空则自动生成
"""
self.broker = broker
self.port = port
self.username = username
self.password = password
self.client_id = client_id if client_id else f"stm32mp1_gateway_{int(time.time())}"
# 创建MQTT客户端
self.client = mqtt.Client(client_id=self.client_id, clean_session=True)
# 设置认证
if self.username and self.password:
self.client.username_pw_set(self.username, self.password)
# 设置回调函数
self.client.on_connect = self._on_connect
self.client.on_disconnect = self._on_disconnect
self.client.on_publish = self._on_publish
# 连接状态
self.connected = False
# 重连参数
self.reconnect_interval = 5 # 重连间隔(秒)
self.max_reconnect_attempts = 10 # 最大重连次数
def _on_connect(self, client, userdata, flags, rc):
"""连接回调函数"""
if rc == 0:
self.connected = True
logger.info(f"MQTT连接成功,返回码:{rc}")
else:
self.connected = False
logger.error(f"MQTT连接失败,返回码:{rc}")
# 返回码说明:
# 0: 连接成功
# 1: 协议版本错误
# 2: 客户端ID无效
# 3: 服务器不可用
# 4: 用户名/密码错误
# 5: 未授权
def _on_disconnect(self, client, userdata, rc):
"""断开连接回调函数"""
self.connected = False
logger.warning(f"MQTT断开连接,返回码:{rc}")
# 自动重连
if rc != 0:
attempt = 0
while attempt < self.max_reconnect_attempts and not self.connected:
logger.info(f"尝试重连MQTT服务器,第{attempt+1}次")
try:
self.client.reconnect()
time.sleep(self.reconnect_interval)
attempt += 1
except Exception as e:
logger.error(f"重连失败:{str(e)}")
time.sleep(self.reconnect_interval)
attempt += 1
def _on_publish(self, client, userdata, mid):
"""发布消息回调函数"""
logger.info(f"MQTT消息发布成功,消息ID:{mid}")
def connect(self):
"""建立MQTT连接"""
try:
# 设置断线重连
self.client.loop_start()
# 连接服务器
self.client.connect(self.broker, self.port, keepalive=60)
# 等待连接完成(最多5秒)
timeout = 5
start_time = time.time()
while not self.connected and (time.time() - start_time) < timeout:
time.sleep(0.1)
return self.connected
except Exception as e:
logger.error(f"MQTT连接异常:{str(e)}")
return False
def publish(self, topic, data, qos=0, retain=False):
"""
发布MQTT消息
:param topic: 发布主题
:param data: 发布数据(字典/字符串)
:param qos: QoS等级,0/1/2
:param retain: 是否保留消息
:return: 发布结果(成功返回mid,失败返回None)
"""
if not self.connected:
logger.warning("MQTT未连接,尝试重新连接")
if not self.connect():
return None
try:
# 数据格式转换(确保为字符串)
if isinstance(data, dict):
payload = json.dumps(data, ensure_ascii=False, indent=2)
else:
payload = str(data)
# 发布消息
result = self.client.publish(
topic=topic,
payload=payload,
qos=qos,
retain=retain
)
# 等待发布完成
result.wait_for_publish()
if result.is_published():
logger.info(f"消息发布到主题[{topic}]成功,QoS:{qos}")
return result.mid
else:
logger.error(f"消息发布到主题[{topic}]失败")
return None
except Exception as e:
logger.error(f"发布消息异常:{str(e)}")
return None
def close(self):
"""关闭MQTT连接"""
if self.client:
self.client.loop_stop()
self.client.disconnect()
self.connected = False
logger.info("MQTT连接已关闭")
# 测试代码
if __name__ == "__main__":
# 初始化MQTT发布器
mqtt_pub = MQTTPublisher(
broker='192.168.1.10', # 替换为你的MQTT服务器地址
port=1883,
username='admin', # 替换为实际用户名(无则留空)
password='123456' # 替换为实际密码(无则留空)
)
# 连接MQTT服务器
if mqtt_pub.connect():
# 构造测试数据
test_data = {
"device_id": "sensor_001",
"timestamp": int(time.time()),
"modbus_data": {
"register_0": 256,
"register_1": 1024,
"register_2": 512,
"register_3": 768
},
"gateway_id": "stm32mp1_001"
}
# 发布数据
mqtt_pub.publish(
topic="industrial/modbus2mqtt/sensor_data",
data=test_data,
qos=1,
retain=False
)
# 关闭连接
time.sleep(2)
mqtt_pub.close()
3.3 主程序(协议转换整合)
创建modbus2mqtt_gateway.py文件,整合Modbus读取和MQTT发布功能:
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Modbus转MQTT边缘网关主程序
支持定时读取、异常处理、配置文件加载
"""
import logging
import time
import json
import os
from modbus_reader import ModbusReader
from mqtt_publisher import MQTTPublisher
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('modbus2mqtt_gateway.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger('Modbus2MQTTGateway')
# 配置文件路径
CONFIG_FILE = "gateway_config.json"
# 默认配置
DEFAULT_CONFIG = {
"modbus": {
"mode": "rtu", # rtu/tcp
"rtu": {
"port": "/dev/ttySTM0",
"baudrate": 9600,
"parity": "N",
"stopbits": 1,
"bytesize": 8,
"timeout": 3
},
"tcp": {
"host": "192.168.1.200",
"port": 502,
"timeout": 3
},
"read_params": {
"address": 0,
"count": 4,
"unit": 1,
"interval": 5 # 读取间隔(秒)
}
},
"mqtt": {
"broker": "192.168.1.10",
"port": 1883,
"username": None,
"password": None,
"topic": "industrial/modbus2mqtt/sensor_data",
"qos": 1,
"retain": False
},
"gateway": {
"id": "stm32mp1_001",
"device_id": "sensor_001"
}
}
def load_config():
"""加载配置文件,不存在则创建默认配置"""
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
logger.info(f"配置文件加载成功:{CONFIG_FILE}")
return config
else:
# 创建默认配置文件
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(DEFAULT_CONFIG, f, ensure_ascii=False, indent=2)
logger.info(f"默认配置文件已创建:{CONFIG_FILE}")
return DEFAULT_CONFIG
except Exception as e:
logger.error(f"配置文件加载失败,使用默认配置:{str(e)}")
return DEFAULT_CONFIG
def main():
"""主程序入口"""
# 加载配置
config = load_config()
# 初始化Modbus读取器
modbus_config = config['modbus']
if modbus_config['mode'] == 'rtu':
modbus_reader = ModbusReader(
mode='rtu',
port=modbus_config['rtu']['port'],
baudrate=modbus_config['rtu']['baudrate'],
parity=modbus_config['rtu']['parity'],
stopbits=modbus_config['rtu']['stopbits'],
bytesize=modbus_config['rtu']['bytesize'],
timeout=modbus_config['rtu']['timeout']
)
else:
modbus_reader = ModbusReader(
mode='tcp',
host=modbus_config['tcp']['host'],
port=modbus_config['tcp']['port'],
timeout=modbus_config['tcp']['timeout']
)
# 初始化MQTT发布器
mqtt_config = config['mqtt']
mqtt_publisher = MQTTPublisher(
broker=mqtt_config['broker'],
port=mqtt_config['port'],
username=mqtt_config['username'],
password=mqtt_config['password']
)
# 连接MQTT服务器
if not mqtt_publisher.connect():
logger.error("MQTT服务器连接失败,程序退出")
return
# 连接Modbus设备
if not modbus_reader.connect():
logger.error("Modbus设备连接失败,程序退出")
mqtt_publisher.close()
return
# 读取参数
read_params = modbus_config['read_params']
address = read_params['address']
count = read_params['count']
unit = read_params['unit']
interval = read_params['interval']
# 网关信息
gateway_config = config['gateway']
gateway_id = gateway_config['id']
device_id = gateway_config['device_id']
# MQTT发布参数
mqtt_topic = mqtt_config['topic']
mqtt_qos = mqtt_config['qos']
mqtt_retain = mqtt_config['retain']
logger.info("Modbus转MQTT网关启动成功,开始数据采集与转换")
try:
while True:
# 读取Modbus数据
modbus_data = modbus_reader.read_holding_registers(
address=address,
count=count,
unit=unit
)
if modbus_data is not None and len(modbus_data) > 0:
# 构造MQTT发布数据
publish_data = {
"gateway_id": gateway_id,
"device_id": device_id,
"timestamp": int(time.time()),
"modbus": {
"address": address,
"count": count,
"unit": unit,
"values": modbus_data
}
}
# 发布MQTT消息
mqtt_publisher.publish(
topic=mqtt_topic,
data=publish_data,
qos=mqtt_qos,
retain=mqtt_retain
)
# 等待下一次读取
time.sleep(interval)
except KeyboardInterrupt:
logger.info("用户中断程序,开始关闭资源")
except Exception as e:
logger.error(f"程序运行异常:{str(e)}")
finally:
# 关闭连接
modbus_reader.close()
mqtt_publisher.close()
logger.info("Modbus转MQTT网关已停止")
if __name__ == "__main__":
main()
四、配置与运行(零基础操作步骤)
4.1 配置文件修改
-
查看生成的
gateway_config.json文件:bashcat gateway_config.json -
根据实际环境修改配置:
- Modbus部分:
mode:根据设备类型选择rtu或tcp- RTU模式:修改
port(串口设备,如/dev/ttySTM0)、baudrate(波特率)等参数 - TCP模式:修改
host(Modbus TCP设备IP)、port(默认502) interval:设置数据读取间隔(秒)
- MQTT部分:
broker:修改为你的MQTT服务器IP/域名username/password:MQTT服务器认证信息(无则留空)topic:自定义MQTT发布主题
- Gateway部分:修改
gateway_id和device_id为实际设备ID
- Modbus部分:
4.2 串口权限配置(RTU模式必做)
bash
# 将当前用户添加到dialout组(访问串口需要)
sudo usermod -aG dialout $USER
# 重启生效(或重新登录)
sudo reboot
# 验证串口权限
ls -l /dev/ttySTM0
4.3 运行网关程序
bash
# 赋予脚本执行权限
chmod +x modbus2mqtt_gateway.py
# 运行程序
python3 modbus2mqtt_gateway.py
4.4 后台运行
bash
# 使用nohup后台运行,日志输出到gateway.log
nohup python3 modbus2mqtt_gateway.py > gateway.log 2>&1 &
# 查看运行状态
ps -ef | grep modbus2mqtt_gateway.py
# 停止程序
kill -9 [进程ID]
五、调试与验证
5.1 日志查看
bash
# 实时查看Modbus读取日志
tail -f modbus_reader.log
# 实时查看MQTT发布日志
tail -f mqtt_publisher.log
# 实时查看网关主日志
tail -f modbus2mqtt_gateway.log
5.2 MQTT数据验证
使用MQTT.fx或mosquitto_sub工具订阅主题,验证数据是否正常接收:
bash
# 安装mosquitto客户端
sudo apt install -y mosquitto-clients
# 订阅MQTT主题
mosquitto_sub -h 192.168.1.10 -t "industrial/modbus2mqtt/sensor_data" -v
5.3 常见问题排查
-
Modbus连接失败:
- 检查串口/网口硬件连接
- 确认Modbus从站地址、波特率、寄存器地址等参数正确
- 测试命令:
minicom -D /dev/ttySTM0 -b 9600(RTU)/ping 192.168.1.200(TCP)
-
MQTT发布失败:
- 检查MQTT服务器地址、端口、认证信息
- 确认网络连通性:
telnet 192.168.1.10 1883 - 检查MQTT服务器是否开启,防火墙是否放行1883端口
-
数据解析异常:
- 确认寄存器数据类型(16位无符号/有符号、32位浮点等)
- 修改
modbus_reader.py中的解码方式(如decode_16bit_int/decode_32bit_float)
六、开机自启配置
创建系统服务文件,实现网关程序开机自启:
bash
# 创建服务文件
sudo nano /etc/systemd/system/modbus2mqtt.service
写入以下内容:
ini
[Unit]
Description=Modbus to MQTT Gateway Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/home/root/modbus2mqtt # 替换为你的程序目录
ExecStart=/usr/bin/python3 /home/root/modbus2mqtt/modbus2mqtt_gateway.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
启用并启动服务:
bash
# 重新加载系统服务
sudo systemctl daemon-reload
# 设置开机自启
sudo systemctl enable modbus2mqtt
# 启动服务
sudo systemctl start modbus2mqtt
# 查看服务状态
sudo systemctl status modbus2mqtt
总结
- 本教程基于STM32MP1的Linux系统,实现了Modbus RTU/TCP到MQTT的协议转换,包含完整的代码实现和零基础操作步骤,可直接落地部署。
- 核心流程为:Modbus设备数据读取 → 协议解析与数据校验 → JSON格式封装 → MQTT协议发布 → 物联网平台接收,全流程包含异常处理和自动重连机制。
- 关键配置项包括Modbus连接参数(串口/网口)、MQTT服务器信息、数据读取间隔等,可通过配置文件灵活调整,支持开机自启和后台运行。