ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

开关柜局放监测软件:Modbus RTU 数据落地与 PRPD 图谱可视化

开关柜局放监测软件:Modbus RTU 数据落地与 PRPD 图谱可视化

简介:该资源是一套面向电力系统运维与电气工程学习者的开关柜局部放电监测软件,基于Modbus_RTU协议连接TEV、超声波与UHF传感器,实时读取遥测数据和图谱数据,并通过图形界面完成可视化展示,可用于开关柜局部放电状态监测与故障预警。资源包共11个文件,包含5个Python源码、3个Markdown说明文档、2个文本文件及1份docx文档,压缩包约64KB,源码覆盖多传感器读取、UHF监测及PyQt5向PySide6迁移等模块,文档则提供环境依赖与使用说明。目前已有145人学习下载。读者可据此掌握Modbus_RTU通信、多传感器数据采集与图形界面开发思路,理解局部放电事件识别、分级报警与趋势分析逻辑,并参考迁移说明完成界面框架适配,适合作为电力监测类课程设计或工程原型的实践参考。

1. 开关柜局放监测软件:从 TEV 到 UHF 的 Modbus 数据落地

变电站里开关柜的绝缘劣化,往往先表现为局部放电,等闻到焦糊味或者保护动作,基本已经晚了。这套软件解决的就是把 TEV、超声波、UHF 三类传感器的数据通过 Modbus RTU 拉回来,实时显示遥测值和图谱,让运维人员在缺陷早期就能看到趋势。它适合做电力设备状态监测的二次开发人员、想给现有开关柜加装在线监测的集成商,以及需要一套可改可调的局放上位机做算法验证的工程师。软件本身不绑定特定厂家硬件,只要传感器支持标准 Modbus RTU 寄存器映射,就能接进来。图谱数据这块是重点,PRPD、PRPS 这类相位分布图不是简单存个数组,涉及相位同步和周期对齐,后面会拆开讲。

2. Modbus RTU 通信层:串口参数、寄存器映射与轮询策略

2.1 为什么选 Modbus RTU 而不是 Modbus TCP

开关柜现场电磁环境复杂,TEV 传感器和超声波传感器通常装在柜内或柜面,走 RS485 总线到集中器,再由集中器转以太网。如果传感器直接带网口,Modbus TCP 当然更省事,但实际项目里大量存量传感器只有 RS485 接口,Modbus RTU 是唯一选择。RTU 模式用二进制传输,比 ASCII 模式效率高,CRC 校验对现场干扰的容忍度也够用。这套软件在通信层做了两件事:一是把串口参数做成可配置,二是把寄存器地址映射抽成配置文件,换传感器不用改代码。

常见做法是用 Python 的 pymodbus 库搭通信层,配合 serial 库管理串口。下面是一个最小可用的 RTU 读取封装,我一般会把它单独放一个模块里,方便后面换成多线程或异步。

import serial from pymodbus.client import ModbusSerialClient from pymodbus.exceptions import ModbusIOException class PDModbusReader: def __init__(self, port, baudrate=9600, parity='N', stopbits=1, bytesize=8, timeout=1): # 串口参数按传感器手册填,9600-8-N-1 是局放传感器最常见的默认值 self.client = ModbusSerialClient( port=port, baudrate=baudrate, parity=parity, stopbits=stopbits, bytesize=bytesize, timeout=timeout ) self.connected = False def connect(self): self.connected = self.client.connect() return self.connected def read_telemetry(self, slave_id, start_addr, count): # 遥测数据一般是连续的保持寄存器,一次读回来再按偏移解析 try: rr = self.client.read_holding_registers( address=start_addr, count=count, slave=slave_id ) if rr.isError(): return None return rr.registers except ModbusIOException: return None def close(self): self.client.close()

逻辑说明:ModbusSerialClient把串口打开和 Modbus 帧封装在一起,read_holding_registers发的是功能码 0x03。参数slave是传感器地址,现场如果一条总线上挂多个传感器,地址不能冲突。count是一次读的寄存器数量,注意不要超过传感器允许的最大连续读取长度,有些低端传感器超过 16 个寄存器就返回异常。超时设 1 秒是保守值,现场如果总线长、波特率低,可以放到 2 秒,但轮询周期会拉长。

2.2 寄存器映射表怎么定

不同厂家的传感器寄存器定义差别很大,但结构上逃不出三类:遥测值(放电幅值、次数、相位)、状态字(传感器自检、通信状态)、图谱数据(PRPD 数组或波形采样点)。我一般先跟厂家要一份寄存器表,然后做成 YAML 或 JSON 映射文件,软件启动时加载。下面是一个示例映射,覆盖 TEV 和超声波两种传感器。

sensors: - type: TEV slave_id: 1 telemetry: - name: discharge_amplitude addr: 0x0000 scale: 0.1 unit: dBmV - name: discharge_count addr: 0x0001 scale: 1 unit: 次/秒 status: - name: self_test addr: 0x0010 bit: 0 prpd: start_addr: 0x0100 length: 128 phase_bins: 64 - type: Ultrasonic slave_id: 2 telemetry: - name: amplitude addr: 0x0000 scale: 0.01 unit: dB - name: frequency addr: 0x0001 scale: 1 unit: kHz

参数说明:scale是原始寄存器值到工程值的换算系数,比如寄存器读到 452,scale 0.1,实际就是 45.2 dBmV。bit用于状态字按位解析。prpd段的length是图谱数组占用的寄存器数量,phase_bins是相位分箱数,常见 64 或 128。这个映射文件的好处是,换传感器只改 YAML,不动 Python 代码。

2.3 轮询策略与多传感器调度

一条 RS485 总线上挂多个传感器时,不能同时发请求,必须串行轮询。轮询周期取决于传感器响应时间和寄存器数量。我一般把遥测和状态放在快轮询里,200ms 到 500ms 一次;图谱数据量大,放在慢轮询里,1s 到 2s 一次。下面是一个简单的轮询调度片段。

import time from collections import deque class PollScheduler: def __init__(self, reader, sensor_configs): self.reader = reader self.sensors = sensor_configs self.fast_interval = 0.3 # 遥测轮询间隔,秒 self.slow_interval = 1.5 # 图谱轮询间隔,秒 self.last_slow = 0 def poll_once(self): results = {} for s in self.sensors: sid = s['slave_id'] tele = self.reader.read_telemetry(sid, 0x0000, 2) if tele: results[sid] = {'telemetry': tele} now = time.time() if now - self.last_slow >= self.slow_interval: for s in self.sensors: if 'prpd' in s: sid = s['slave_id'] prpd = self.reader.read_telemetry( sid, s['prpd']['start_addr'], s['prpd']['length'] ) if prpd: results.setdefault(sid, {})['prpd'] = prpd self.last_slow = now return results

逻辑说明:快轮询只读前两个寄存器,保证实时性;慢轮询读图谱数组,避免总线被长帧占满。results用字典按 slave_id 聚合,后面交给界面层。注意如果某个传感器掉线,read_telemetry返回 None,调度器不会阻塞,但要在界面上标红提示。

3. 图谱数据解析与可视化:PRPD 相位分布怎么画

3.1 PRPD 数据的结构

局部放电的 PRPD 图谱本质是一个二维直方图:横轴是相位 0 到 360 度,纵轴是放电幅值或放电次数。传感器传回来的通常是一段连续寄存器,每个寄存器代表一个相位分箱的计数值或幅值。比如 64 个相位分箱,每个分箱用 1 个寄存器存放电次数,那 64 个寄存器就是一张 PRPD 图。有些传感器还会传 PRPS 数据,那是按工频周期排列的相位序列,数据量更大,一般用于实验室分析,现场监测用 PRPD 就够了。

解析的时候要注意字节序。Modbus 寄存器是 16 位大端,但有些传感器把两个寄存器拼成一个 32 位浮点数,这时候要按 IEEE 754 解析。下面是一个解析函数,同时处理 16 位整数和 32 位浮点两种格式。

import struct def parse_registers(registers, fmt='H'): """ fmt='H' 表示每个寄存器独立解析为无符号16位整数 fmt='f' 表示每两个寄存器拼成一个32位浮点数 """ if fmt == 'H': return list(registers) elif fmt == 'f': values = [] for i in range(0, len(registers) - 1, 2): # Modbus 大端,两个寄存器高在前 raw = struct.pack('>HH', registers[i], registers[i+1]) values.append(struct.unpack('>f', raw)[0]) return values else: raise ValueError('unsupported format')

参数说明:registers是从 Modbus 读回来的整数列表。fmt='H'适合放电次数这类整数;fmt='f'适合幅值、相位这类浮点。注意有些传感器用小端浮点,那就把>f改成<f,这个必须拿实际数据试,试错了图谱会完全乱掉。

3.2 用 Matplotlib 嵌入 PyQt 做实时刷新

这套软件如果做成桌面端,常见方案是 PyQt5 加 Matplotlib 的 FigureCanvas。图谱刷新不能每来一帧就重绘整个画布,那样 CPU 扛不住。我一般用 blit 或者只更新数据源的方式。下面是一个简化的 PRPD 绘制类。

import numpy as np from matplotlib.figure import Figure from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas class PRPDCanvas(FigureCanvas): def __init__(self, parent=None): self.fig = Figure(figsize=(5, 4), dpi=100) self.ax = self.fig.add_subplot(111) super().__init__(self.fig) self.setParent(parent) self.phase_bins = 64 self.phase_axis = np.linspace(0, 360, self.phase_bins, endpoint=False) self.bar = None def update_prpd(self, counts): # counts 长度必须等于 phase_bins if len(counts) != self.phase_bins: return if self.bar is None: self.bar = self.ax.bar( self.phase_axis, counts, width=360/self.phase_bins, color='#2c7fb8', edgecolor='none' ) self.ax.set_xlim(0, 360) self.ax.set_xlabel('相位 (°)') self.ax.set_ylabel('放电次数') else: for rect, h in zip(self.bar, counts): rect.set_height(h) self.draw_idle()

逻辑说明:第一次调用时创建柱状图对象,后续只更新每个柱子的高度,避免重复创建。draw_idle比draw温和,不会阻塞界面事件循环。相位轴用linspace生成 0 到 360 的等分点,endpoint=False保证最后一个分箱不重叠。如果传感器传的是幅值而不是次数,把 y 轴标签改成 dBmV 即可。

3.3 图谱数据的存储与回放

实时监测之外,图谱数据要落盘,不然出了故障没法回溯。我一般用 SQLite 存遥测和状态,图谱数据量大的话用 HDF5 或单独的文件目录。下面是一个 SQLite 建表和插入的示例。

import sqlite3 import time def init_db(path='pd_monitor.db'): conn = sqlite3.connect(path) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS telemetry ( ts REAL, slave_id INTEGER, amplitude REAL, count INTEGER )''') c.execute('''CREATE TABLE IF NOT EXISTS prpd ( ts REAL, slave_id INTEGER, phase_bins INTEGER, data BLOB )''') conn.commit() return conn def save_prpd(conn, slave_id, counts): import array blob = array.array('H', counts).tobytes() conn.execute( 'INSERT INTO prpd (ts, slave_id, phase_bins, data) VALUES (?, ?, ?, ?)', (time.time(), slave_id, len(counts), blob) ) conn.commit()

参数说明:array.array('H', counts)把整数列表转成二进制,比 JSON 省空间。回放的时候用array.frombytes还原。注意 SQLite 的 BLOB 字段不要存太大的数组,单条超过 1MB 会影响查询性能,PRPD 一般 64 到 128 个点,完全没问题。

4. 避坑与排查:现场调试最容易翻车的五个点

4.1 串口能打开但读不到数据

现象:client.connect()返回 True,但read_holding_registers一直超时或返回异常。原因通常是 A/B 线接反,或者波特率、校验位跟传感器不一致。解决:先用串口调试助手手动发一帧读命令,确认传感器有回复。如果手动能通,代码不通,检查 pymodbus 的slave参数是不是写成了unit,不同版本参数名不一样。

4.2 遥测值跳变或明显偏大

现象:放电幅值在 0 和满量程之间乱跳。原因多半是寄存器解析格式错了,比如传感器传的是有符号 16 位,代码按无符号解析,负值就变成 65535 附近的大数。解决:查手册确认数据类型,有符号用struct.unpack('>h', ...),浮点确认大小端。另外检查scale系数,有些传感器出厂默认 0.1,但实际是 0.01。

4.3 图谱相位对不齐

现象:PRPD 图上放电集中在固定相位,但跟工频相位对不上,或者每次刷新图形左右漂移。原因是没有做相位同步。TEV 和超声波传感器内部通常有工频参考,但 Modbus 读回来的相位是相对值,需要软件根据工频周期做对齐。常见做法是读一个工频同步寄存器,或者用系统时间戳做插值。如果传感器不支持同步,图谱只能看趋势,不能做相位诊断。

4.4 多传感器轮询时丢包

现象:总线上挂 3 个以上传感器,轮询几轮后某个传感器数据不再更新。原因是轮询间隔太短,传感器还没处理完上一帧就收到下一帧。解决:拉长快轮询间隔到 500ms 以上,或者在每次请求之间加 50ms 延时。RS485 半双工,收发切换也需要时间,有些转换器切换慢,延时不够就丢帧。

4.5 界面卡死

现象:图谱刷新时界面无响应。原因是 Modbus 读取和绘图都在主线程。解决:把轮询放到 QThread 或 Python 的 threading 里,通过信号槽把数据传给界面。注意 Matplotlib 的绘图必须在主线程,子线程只负责读数据。如果数据量大,可以用队列缓冲,界面定时从队列取最新一帧。

5. 进阶技巧:用配置文件驱动多类型传感器混接

这套软件真正省事的地方在于,TEV、超声波、UHF 三种传感器可以混在同一条总线上,只要它们的波特率和数据格式一致。我一般把传感器配置写成 JSON,启动时遍历生成对应的读取任务和界面面板。下面是一个混接配置的片段。

{ "serial": {"port": "COM3", "baudrate": 9600, "parity": "N"}, "sensors": [ {"type": "TEV", "slave_id": 1, "telemetry_addrs": [0, 1], "prpd_addr": 256, "prpd_len": 64}, {"type": "Ultrasonic", "slave_id": 2, "telemetry_addrs": [0, 1], "prpd_addr": 256, "prpd_len": 64}, {"type": "UHF", "slave_id": 3, "telemetry_addrs": [0, 1, 2], "prpd_addr": 512, "prpd_len": 128} ] }

参数说明:telemetry_addrs是遥测寄存器起始地址列表,不同传感器长度不同。prpd_addr和prpd_len决定图谱读取范围。UHF 传感器数据量大,prpd_len给到 128。加载配置后,界面按传感器数量动态生成选项卡,每个选项卡里放遥测数值和 PRPD 图。

验证方法:先只接一个传感器,确认遥测和图谱都正常,再逐个增加。每加一个,观察轮询周期是否还在可接受范围。如果周期超过 2 秒,就要考虑把图谱轮询拆到独立线程,或者降低图谱刷新频率。我习惯在软件里加一个通信质量统计,记录每个传感器的超时次数和平均响应时间,调试的时候一眼就能看出哪条链路有问题。

从那以后我每次拿到新传感器,都强制先用手动发帧的方式确认寄存器映射,再写进配置文件,绝不直接猜。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进