前言
之前做电表集抄项目的时候,遇到一个挺头疼的问题——通过 RS485 总线轮询多功能电能表的数据,协议是 DL/T645-2007,每次读一组数据都要走一遍"下发请求 → 等待回复 → 解析响应"的流程。现场调试的时候,手里不一定有真表,拿个表模拟又麻烦。后来干脆写了个上位机,顺带把仿真从站也做了进去,没有硬件也能跑通全链路。
.NET 8 + WPF + Prism 做的,整套代码已经开源。这篇文章会把整个项目的结构、核心实现和关键设计决策捋一遍,给做类似工控上位机的朋友做个参考。
项目介绍
一个基于 DL/T645-2007 规约的多功能电能表通信调试上位机。通过 RS485 串口轮询电能表的电量、电压、电流、功率等数据,实时展示在卡片墙和趋势图上,同时带报文监视功能。
比较特别的一点是,项目内置了仿真从站和内存回环传输,没有真表也能直接跑起来,完整走通「连接 → 轮询 → 组帧 → 解析 → 展示」这一整条链路。
仿真数据按电网物理特性做了标定:频率在 49.90~50.10 Hz 随机游走、功率因数钳制在 0.93~0.99、三相电压 209~235 V、电能只增不减单调走字。
<video width="640" height="360" controls="" data-v-74f8cad5="" poster="http://mmbiz.qpic.cn/sz_mmbiz_jpg/ApKiaLBgoS3MA10pTrUbmmvzKT59mQkiaiapw6jic4tAu4BUxw6Ppy5aklAZIvEwIQsA7mEiagBEq8iaoaTdB914XxRaa9d1crvgSXb42TIt7etDE/0?wx_fmt=jpeg&wxfrom=16" webkit-playsinline="isiPhoneShowPlaysinline" playsinline="isiPhoneShowPlaysinline" preload="metadata" crossorigin="anonymous"> 您的浏览器不支持 video 标签 </video> <!-- S 视频社交-->
项目功能
功能模块 | 说明 |
多数据项周期轮询 | 一问一答、半双工串行模式,支持单次读取 |
实时电参数卡片墙 | 按电能/电压/电流/功率与频率分组,带涨跌箭头和电压占比进度条 |
三相电压趋势图 | 基于 LiveCharts2,120 点滚动窗口,250V 红色虚线警戒线 |
报文监视 | 收发分色、时间戳、自动滚底、上限 500 条,支持导出文本文件 |
读数记录导出 CSV | UTF-8 带 BOM,Excel 直接打开中文不乱码 |
通信统计 | Tx/Rx 帧数、超时、错误、往返时延 |
深色工业 HMI 主题 | 自定义标题栏,支持拖拽/双击最大化/分屏吸附 |
离线态保留 | 断开后保留末次数值,卡片转为灰色 |
项目特点
零硬件即可运行:内置 DL/T645 仿真从站与内存回环传输,克隆后直接启动,不需要外接任何硬件就能验证协议编解码和界面交互逻辑
139 个单元测试全绿:覆盖协议编解码契约测试(含手工推演的黄金报文向量)、帧扫描器增量组帧、收发统计口径、视图模型命令状态机、波动数据源长序列值域断言、CSV 导出转义与不变文化序列化
严格分层架构:Core 纯类库(协议 + 领域模型 + 轮询服务,零框架依赖)/ Transport(真实串口 + 仿真回环)/ App(Prism + MVVM)。ITransport 接口置于 Core,服务层只依赖抽象——真实串口与仿真回环互换只改一行装配代码
项目技术
技术栈 | 版本 | 用途 |
.NET | 8.0 | 当前 LTS 运行时 |
Prism.DryIoc | 9.0.537 | MVVM 基础设施 + DI 容器 |
LiveCharts2 (SkiaSharpView.WPF) | 2.0.4 | 三相电压趋势图渲染 |
FluentAssertions | 7.2.2 | 单元测试断言(8.x 起改为 Xceed 商业许可证,此版本是最后一个 Apache-2.0 版本) |
xUnit | 2.9 | 测试框架 |
System.IO.Ports | .NET 8 内置 | RS485 串口通信 |
项目代码
架构分层
┌─────────────────────────────────────────────────────────┐│ Dlt645Master.App WPF + Prism(MVVM) ││ Views / ViewModels / 仿真链路装配 / 导出 │└──────────────┬──────────────────────────┬───────────────┘ │ 依赖 │ 依赖┌──────────────▼──────────────┐ ┌────────▼────────────────┐│ Dlt645Master.Core │ │ Dlt645Master.Transport ││ 纯类库,零框架依赖 │◄─┤ 依赖 Core ││ ├ Protocol 帧编解码/组帧 │ │ ├ Serial 真实串口 ││ ├ Models 领域模型 │ │ └ Simulation 仿真从站/ ││ ├ Services 轮询调度/统计 │ │ 内存回环/波动数据源 ││ └ Transport ITransport 抽象 │ └─────────────────────────┘└─────────────────────────────┘ITransport 抽象
服务层只依赖 ITransport 接口,不关心底层是真实串口还是仿真回环。这是消除 Core → Transport 环形依赖的关键。
public interface ITransport{ Task SendAsync(byte[] frame, CancellationToken ct); Task<byte[]> ReceiveAsync(CancellationToken ct); bool IsOpen { get; } void Open(); void Close(); event EventHandler<transporterroreventargs>? ErrorOccurred;}帧编解码器(核心逻辑)
DL/T645-2007 的帧结构:68 A0..A5 68 C L DATA... CS 16,地址 6 字节 BCD 低位在前,DATA 域每字节 +0x33 掩码,CS 为算术和校验。
public staticclassDlt645Frame{ // 组帧:从站地址 + 控制码 + 数据标识 + 数据 public static byte[] Build(byte[] address, byte control, byte[] dataId, byte[]? data = null) { // 地址域:6字节BCD,传输序(低位在前) // 数据域:每字节 +0x33 掩码 // CS:从起始符到数据域结束的算术和 } // 解析:校验 CS,解掩码,提取数据域 public static bool TryParse(byte[] frame, out Dlt645Response? response) { // 检查起始符 0x68、结束符 0x16 // 校验 CS // 数据域 -0x33 解掩码 }}仿真从站(用于测试)
没有真表的时候,仿真从站就派上用场了。它监听内存回环通道,收到请求帧后按规约组响应帧返回。仿真数据按物理特性生成:电压在 209~235V 之间浮动、电流按负荷随机变化、电能单调递增。
public class SimulatedMeter : IResponder{ public byte[]? Respond(byte[] request) { // 解析请求帧,提取数据标识 DI // 按 DI 返回对应的仿真数据 // 例如 DI=02 01 01 00 返回 A 相电压 220.5V }}主界面 ViewModel(Prism + MVVM)
使用 Prism 的 BindableBase 和 DelegateCommand 管理 UI 状态和用户交互。
public classMainViewModel : BindableBase{ privatereadonly ITransport _transport; privatereadonly PollingService _pollingService; privatebool _isConnected; publicbool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); } public DelegateCommand ConnectCommand { get; } public DelegateCommand StartPollingCommand { get; } public MainViewModel(ITransport transport, PollingService pollingService) { _transport = transport; _pollingService = pollingService; ConnectCommand = new DelegateCommand(OnConnect); StartPollingCommand = new DelegateCommand(OnStartPolling); } private void OnConnect() { _transport.Open(); IsConnected = true; // 触发轮询 }}项目效果
实际测试中,仿真模式下启动后,界面三步走:
1、点击「连接」——标题栏徽标转绿色「已连接」
2、点击「启动轮询」——卡片墙数值开始跳动、三相电压曲线滚动、右侧报文监视滚屏
3、运行一段时间后可点击「导出读数 (CSV)」或报文监视区的「导出」
通信统计面板会实时更新 Tx/Rx 帧数、超时次数和往返时延,方便评估总线通信质量。
离线状态下,卡片会转为灰色并保留最后一次收到的数值,不会直接清零,避免现场人员丢失关键信息。
项目源码
git clone https://github.com/Jayla630/Dlt645Master.gitcd Dlt645Masterdotnet builddotnet testdotnet run --project src/Dlt645Master.App项目结构
Dlt645Master/├── src/│ ├── Dlt645Master.Core/ # 纯类库:协议 + 领域模型 + 服务│ ├── Dlt645Master.Transport/ # 串口实现 + 仿真回环│ └── Dlt645Master.App/ # WPF 界面 + Prism 装配├── tests/ # 139 个单元测试└── Dlt645Master.sln总结
DL/T645 协议本身不复杂,但做上位机的时候,如果架构没捋清楚,很容易把串口操作、协议编解码、UI 刷新揉在一起,后面维护起来相当痛苦。
这个项目最核心的两个设计思路:
1、依赖倒置:ITransport 接口放在 Core 层,服务层只依赖这个抽象。真实串口和仿真回环互换只改一行装配代码,Core 层完全不感知底层实现差异。这也是 Prism 这类 MVVM 框架强调的松耦合原则。
2、测试先行:139 个单元测试覆盖了协议编解码、帧扫描器、状态机、CSV 导出等关键逻辑,后面改代码的时候敢放心改,不会担心改崩了某个边界情况。
如果你的工作也涉及到电表通信或者类似的工控上位机开发,这个项目可以直接拿来用,或者作为参考来设计自己的轮询框架。
关键词
最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号[DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!
作者:小码编匠
出处:gitee.com/smallcore/DotNetCore
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!
觉得有收获?不妨分享让更多人受益
关注「DotNet技术匠」,共同提升技术实力
</transporterroreventargs>
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!