『7x24小时有问必答』

前言

之前做电表集抄项目的时候,遇到一个挺头疼的问题——通过 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.git

cd  Dlt645Master

dotnet build

dotnet  test

dotnet 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
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!

END

推荐阅读

觉得有收获?不妨分享让更多人受益
关注「DotNet技术匠」,共同提升技术实力

收藏
点赞
分享
在看
</transporterroreventargs>

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码