3、数据采集基础:Modbus TCP协议解析、使用pymodbus读取BMS数据、数据校验与异常处理
好,咱们今天来啃一块硬骨头——Modbus TCP协议。说实话,做储能系统这么多年,我见过太多人在这个环节翻车了。BMS(电池管理系统)的数据读不上来,整个监控系统就是个摆设。你想想看,一个储能电站,几十个电池簇,几百个从站,要是通讯断了,那画面太美我不敢看。
我个人习惯,不管项目多急,先把协议吃透再动手。磨刀不误砍柴工嘛。
3.1 Modbus TCP协议,说白了就是“主从问答”
Modbus TCP本质上是一个请求/响应协议。主站(咱们的监控系统)发一个请求帧,从站(BMS)回一个响应帧。就这么简单。
但简单归简单,里面的门道可不少。我刚开始做的时候,以为就是个socket通讯,结果被坑得够呛。
一个标准的Modbus TCP帧长这样:
| 事务标识符(2B) | 协议标识符(2B) | 长度(2B) | 单元标识符(1B) | 功能码(1B) | 数据(nB) |
咱们拆开来看:
- 事务标识符:用来匹配请求和响应。我一般用递增的计数器,简单粗暴。
- 协议标识符:固定为0x0000,表示Modbus协议。
- 长度:后面所有字节的总长度。
- 单元标识符:也就是从站地址。BMS通常从1开始编号。
- 功能码:读线圈用0x01,读保持寄存器用0x03。读BMS数据,99%的情况用0x03。
- 数据:起始地址和读取数量。
核心要点:Modbus TCP的报文结构是固定的,但数据内容完全取决于BMS厂商的寄存器映射表。没有这张表,你啥也干不了。
3.2 使用pymodbus读取BMS数据
Python里操作Modbus TCP,我首选pymodbus。这个库封装得很好,几行代码就能搞定通讯。
先安装:
pip install pymodbus
然后写个简单的读取示例:
from pymodbus.client import ModbusTcpClient
# 连接BMS
client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()
# 读取保持寄存器,从地址0开始,读10个寄存器
result = client.read_holding_registers(0, 10, unit=1)
if not result.isError():
# 数据在result.registers里
print(f"读取成功: {result.registers}")
else:
print(f"读取失败: {result}")
client.close()
嗯,这里要注意。很多BMS的寄存器地址是从0开始的,但有些厂商从1开始。我遇到过一家厂商,文档写的是地址1,实际通讯时要传0。你说坑不坑?
所以我的建议是:先读一个已知值验证一下。比如BMS的型号寄存器,读出来跟文档对一下,确认地址映射没问题。
3.3 数据校验,别信BMS发来的原始数据
BMS发来的数据,你敢直接用吗?反正我不敢。
我在项目里遇到过,BMS的电压数据突然跳变,从3.2V直接蹦到4.5V。要是直接拿这个数据去做告警,那整个电站都得跳闸。
所以数据校验这一步,必须做。我一般做三层校验:
- 范围校验:每个数据都有合理的物理范围。比如单体电压,正常在2.5V~4.2V之间。超出这个范围,直接丢弃。
- 变化率校验:相邻两次采集的数据,变化不能太离谱。比如电压1秒内变化超过0.5V,那肯定有问题。
- CRC校验:Modbus TCP本身没有CRC,但有些BMS会在数据域里加校验。这个要看厂商文档。
代码实现起来也不复杂:
def validate_voltage(voltage):
"""单体电压校验"""
if voltage < 2.5 or voltage > 4.2:
return False
return True
def validate_change_rate(old_val, new_val, max_rate=0.5):
"""变化率校验"""
if abs(new_val - old_val) > max_rate:
return False
return True
避坑指南:我曾经遇到过BMS返回的数据字节序是反的。Modbus默认是大端模式,但有些BMS用的小端。pymodbus里可以用word_order参数调整,或者自己手动交换高低字节。
3.4 异常处理,通讯断了怎么办?
通讯异常是常态,不是异常。你想想看,储能电站环境那么恶劣,电磁干扰、网线松动、BMS死机,什么情况都可能发生。
我的异常处理策略是:
- 超时重试:默认超时3秒,重试3次。每次重试间隔递增,避免雪崩。
- 断线重连:如果连续3次都失败,主动断开连接再重连。
- 数据保持:通讯中断期间,用最后一次有效数据填充,并打上“陈旧”标记。
- 告警上报:通讯中断超过30秒,必须上报告警。
代码示例:
import time
from pymodbus.client import ModbusTcpClient
class BMSReader:
def __init__(self, host, port=502, timeout=3):
self.host = host
self.port = port
self.timeout = timeout
self.client = None
self.retry_count = 0
self.max_retry = 3
def read_with_retry(self, address, count, unit=1):
"""带重试的读取"""
for i in range(self.max_retry):
try:
if not self.client or not self.client.is_socket_open():
self.connect()
result = self.client.read_holding_registers(
address, count, unit=unit
)
if not result.isError():
self.retry_count = 0
return result.registers
except Exception as e:
print(f"读取失败,第{i+1}次重试: {e}")
time.sleep(2 ** i) # 指数退避
self.retry_count += 1
return None
def connect(self):
"""建立连接"""
self.client = ModbusTcpClient(self.host, port=self.port, timeout=self.timeout)
self.client.connect()
重要提醒:不要在主线程里做同步读取。Modbus通讯是IO操作,会阻塞主线程。一定要用异步或者多线程。我一般用asyncio配合pymodbus的异步客户端,效果很好。
3.5 知识体系总览
说了这么多,咱们用一张图把整个流程串起来。这张图是我做项目时自己画的,每次带新人都会拿出来讲一遍。
这张图把整个流程分成了四个阶段:请求、响应、校验、异常处理。每个阶段都有对应的代码实现。我个人建议,刚开始做的时候,先把校验和异常处理写好,再去调通讯。顺序别搞反了。
好了,这一节的内容就到这儿。记住一句话:Modbus TCP不难,难的是把数据采准、采稳。下一节咱们会深入BMS的寄存器映射表,看看那些电压、温度、SOC数据到底藏在哪儿。
公众号:蓝海资料掘金营,微信deep3321