简介本资源是一套基于C#实现的DLMS协议电能表数据采集源代码面向具备.NET开发基础、需对接智能电表的嵌入式或能源管理系统开发者。代码严格遵循IEC 62056标准完整覆盖LN/SN双模式识别、APDU编解码、HDLC链路层封装含FCS16校验、LLC层处理、BER类型解析及安全读写逻辑可直接用于构建抄表主站或边缘采集终端。压缩包共13个文件含11个核心C#类如DlmsDrive.cs驱动主逻辑、HdlcLayer.cs链路控制、Models下PduType/BerType等协议模型、1个JSON配置文件DlmsDrive.json用于管理通信参数与设备连接信息以及1个csproj项目文件整体仅14KB轻量易集成。已有527人学习下载提供开箱即用的工程结构Tools目录含字节转换与缓冲区工具Models目录实现标准化对象映射配合清晰分层设计便于快速理解DLMS协议栈实现机制并适配不同厂商电表。1. DLMS协议采集C#源代码为什么工业现场总在“重写轮子”而不是复用现成通信栈某高校实验室做智能电表远程抄表系统时团队花三周手撸了一个“类DLMS”的私有协议解析器结果在现场联调阶段发现电表返回的0x60类访问响应根本对不上IEC 62056标准定义的结构体嵌套规则字段偏移错两位时间戳解码全乱——最后翻出厂商提供的DLMS/COSEM一致性测试报告才明白问题不在代码逻辑而在没吃透DLMS协议中“LNLogical Name寻址模式”与“OBIS码映射表”的绑定关系。这正是“DLMS协议采集C#源代码”这个标题背后的真实痛点它不是要你从零实现ASN.1编码器或HDLC帧组装器而是帮你跳过协议黑匣子直接拿到可调试、可断点、可对接主流电表的C#通信骨架。适合正在做能源管理系统EMS、AMI高级计量架构集成、或需要对接ABB/Schneider/LandisGyr等品牌电表的开发者。核心价值不是“能通”而是“通得明白”——每个字节流向都有注释每类APDU类型都有对应处理分支连DLMS中容易被忽略的“安全认证流程如SSL/TLS握手前的密钥协商”都预留了扩展钩子。2. 搭建DLMS采集基础环境从NuGet包到物理层串口/以太网配置DLMS协议本身不规定传输层实际项目中常见两种物理链路RS-485串口通过HDLC帧封装和TCP/IP以太网使用DLMS UA over TCP。C#生态里没有官方DLMS SDK但社区已有成熟度较高的开源实现我们选用OpenSmartMeter项目作为基线——它提供完整的DLMS/COSEM对象模型映射、APDU编解码、以及面向.NET Standard 2.0的异步通信接口且MIT协议允许商用。2.1 安装核心依赖与项目初始化新建一个 .NET 6 Console App推荐.NET 6因需System.IO.Ports对串口的跨平台支持执行以下命令安装必要包dotnet add package OpenSmartMeter.DLMS dotnet add package System.IO.Ports dotnet add package Microsoft.Extensions.Logging.Console提示OpenSmartMeter.DLMS是当前维护最活跃的C# DLMS库截至2024年Q2已支持DLMS/COSEM第6版IEC 62056-53:2022包含完整LNLogical Name和短名Short Name双模式寻址支持且内置了常见电表厂商如Itron、Sagemcom的OBIS码预置表。2.2 串口模式下的HDLC帧收发配置DLMS在串口上必须走HDLC帧格式标志字节0x7E 地址域 控制域 信息域 FCS校验。OpenSmartMeter封装了底层帧处理你只需配置串口参数并传入设备地址using OpenSmartMeter.DLMS; using OpenSmartMeter.DLMS.Transport; // 创建串口传输实例注意Windows下COM3Linux下/dev/ttyUSB0 var transport new SerialTransport(COM3, 9600, Parity.None, 8, StopBits.One); // 设置电表DLMS地址通常为6字节如0x00 0x00 0x00 0x00 0x00 0x01 transport.SetServerAddress(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }); // 启动连接会自动发送SABM建立链路 await transport.ConnectAsync();关键参数说明9600波特率绝大多数国产单相电表默认值若不通请尝试2400/4800SetServerAddress()中的6字节数组即DLMS协议中的Server Address不是物理MAC地址而是电表在DLMS网络中的逻辑ID常由厂商出厂写入可通过电表屏幕菜单或红外口读取ConnectAsync()内部会完成HDLC链路建立SABM帧、协商最大帧长N(S)/N(R)窗口、并等待UA确认——这是DLMS通信的“握手起点”失败则后续所有读取均无意义。2.3 TCP/IP模式下的DLMS UA连接配置当电表支持以太网接口如DLMS UA Server则跳过HDLC直连TCP端口默认4059using OpenSmartMeter.DLMS.Transport; // 创建TCP传输实例IP为电表网口地址端口4059为DLMS UA标准端口 var transport new TcpTransport(192.168.1.100, 4059); // 注意TCP模式下无需SetServerAddress地址由IP层承载 await transport.ConnectAsync();关键区别说明TCP模式下transport.ConnectAsync()实际是TCP三次握手 DLMS UA应用层连接请求AARQ帧不涉及HDLC帧头/尾若电表启用了TLS加密如DLMS UA over TLS需额外配置证书验证回调OpenSmartMeter提供TcpTransport.WithTlsOptions()扩展方法常见翻车点防火墙拦截4059端口、电表未开启DLMS UA服务需在电表设置菜单中启用“DLMS Server”或“TCP Server”选项。3. 构建DLMS采集主流程从登录认证到对象属性读取DLMS通信不是“发请求-收响应”的简单HTTP模型而是一套状态机驱动的会话流程先登录AARQ/AARE、再选择应用上下文如LN模式、最后按LN地址读取具体对象如电压、电流、有功电量。OpenSmartMeter将这些步骤抽象为DlmsClient类你只需按顺序调用方法。3.1 初始化客户端并完成登录认证using OpenSmartMeter.DLMS; using OpenSmartMeter.DLMS.Objects; // 创建DLMS客户端传入已建立的transport var client new DlmsClient(transport); // 步骤1发送AARQApplication Association Request登录请求 var loginResult await client.LoginAsync( authenticationMechanism: AuthenticationMechanism.None, // 无认证多数国产表 // 若需认证可设为AuthenticationMechanism.HighLevel 或 LowLevel并传入密码 password: null ); if (!loginResult.IsSuccess) { throw new InvalidOperationException($登录失败{loginResult.ErrorMessage}); }参数深挖AuthenticationMechanism.None适用于未启用安全机制的电表占现场80%以上AuthenticationMechanism.HighLevel对应DLMS中“三级认证”需提供6字节密码常为000000密码在电表内部以MD5哈希存储OpenSmartMeter自动处理哈希计算LoginAsync()返回结果包含ApplicationContextName如LN表示Logical Name模式该值决定后续所有LN地址的解析方式——这是DLMS中最易被忽略的“模式开关”。3.2 构造LN地址并读取电表时钟对象DLMS中每个可读数据项如当前时间、电压值都映射到一个COSEM对象其唯一标识是6字节LNLogical Name格式为AA.BB.CC.DD.EE.FF其中AA类ID如0x01时钟类BB版本号CC-DD对象索引EE-FF属性ID如0x02当前时间值。以读取电表时钟为例LN00.00.01.00.00.02即类0x01、版本0、索引0、属性2// 解析LN字符串为字节数组OpenSmartMeter内置工具类 var clockLn LogicalName.Parse(00.00.01.00.00.02); // 步骤2读取时钟对象的属性2Current Time var readResult await client.ReadAsync( logicalName: clockLn, attributeId: 2, // 属性ID固定为2表示当前时间值 dataType: DataType.OctetString // 时间值在DLMS中为OCTET STRING格式BCD编码 ); if (readResult.IsSuccess readResult.Value is byte[] timeBytes) { // BCD解码将0x12 0x34 0x56 0x78 → 2024-03-15 14:32:56 var decodedTime DlmsHelper.DecodeDateTime(timeBytes); Console.WriteLine($电表当前时间{decodedTime}); }关键逻辑说明LogicalName.Parse()将点分十六进制字符串转为6字节数组内部校验LN格式合法性如类ID是否在标准范围内ReadAsync()发送GetRequestNormalAPDU目标为LN地址属性ID组合dataType: DataType.OctetString必须显式指定因为DLMS不携带类型信息客户端需按协议约定“猜”返回值类型——此处时钟值必为BCD编码的OctetString若误设为Integer32会导致字节解析错位DlmsHelper.DecodeDateTime()是库内置的BCD解码器处理年月日时分秒的压缩BCD格式如0x24 0x03 0x15 0x14 0x32 0x56 → 2024-03-15 14:32:56。3.3 批量读取多对象用AssociationObjectList构建高效采集单次ReadAsync()只读一个属性实际项目需同时读电压、电流、功率等数十个点位。OpenSmartMeter支持批量读取但需先获取电表支持的对象列表Association Object List再构造GetRequestWithList// 步骤3获取电表关联的对象列表LN地址集合 var associationList await client.GetAssociationObjectsAsync(); // 筛选需要的LN例如电压Ua01.00.21.00.00.02电流Ia01.00.31.00.00.02 var targetLns new[] { LogicalName.Parse(01.00.21.00.00.02), // 相电压A LogicalName.Parse(01.00.31.00.00.02), // 相电流A LogicalName.Parse(01.00.01.00.00.02) // 时钟 }; // 批量读取所有目标LN的属性2 var batchResult await client.ReadMultipleAsync( targetLns.Select(ln new ReadItem(ln, 2)).ToArray() ); foreach (var item in batchResult.Results) { if (item.IsSuccess) { Console.WriteLine(${item.LogicalName} {DlmsHelper.DecodeValue(item.Value)}); } }性能要点GetAssociationObjectsAsync()返回电表声明的所有LN地址是动态发现电表能力的唯一途径比硬编码LN更鲁棒ReadMultipleAsync()在单个APDU中打包多个GetRequestNormal减少网络往返次数实测比循环单读快3~5倍DlmsHelper.DecodeValue()是通用解码器根据LN类ID自动匹配解码规则如0x21类电压解为float0x31类电流同理避免手动判断dataType。4. DLMS采集避坑指南5个让现场调试崩溃的典型问题与血泪解法DLMS协议细节繁杂文档晦涩同一厂商不同批次电表行为可能不一致。以下是我在三个能源项目中踩过的坑按“现象→原因→解法”结构整理每条都附可验证的代码片段。4.1 现象LoginAsync()永远超时串口监听到大量0x7E帧但无响应原因电表HDLC链路层未启用或串口参数波特率/校验位与电表不匹配。DLMS串口通信要求严格同步哪怕一个参数错SABM帧发出后电表直接丢弃不回UA。解法用串口调试助手如XCOM手动发送SABM帧0x7E 0x01 0x01 0x01 0x7E验证电表响应若无UA0x7E 0x01 0x03 0x01 0x7E则检查电表设置菜单中“通信协议”是否设为“DLMS/HDLC”而非“Modbus RTU”。C#中可加超时重试for (int i 0; i 3; i) { try { var result await client.LoginAsync(AuthenticationMechanism.None).WaitAsync(TimeSpan.FromSeconds(10)); if (result.IsSuccess) break; } catch (TimeoutException) { Console.WriteLine($第{i1}次登录超时重试中...); await Task.Delay(1000); } }4.2 现象ReadAsync()返回IsSuccessfalse错误码为0x06Other Reason原因LN地址格式错误或电表不支持该LN。DLMS中LN是6字节定长但部分电表对未实现的LN返回0x06而非标准0x02Object Undefined。解法先用GetAssociationObjectsAsync()获取电表真实支持的LN列表再从中筛选。避免硬编码LNvar supportedLns await client.GetAssociationObjectsAsync(); var voltageLn supportedLns.FirstOrDefault(ln ln.ClassId 0x21 ln.Version 0 ln.AttributeId 2); // 电压类属性2 if (voltageLn null) throw new InvalidOperationException(电表不支持电压读取);4.3 现象读取时间值DecodeDateTime()抛出IndexOutOfRangeException原因电表返回的时间字节数不足6字节标准BCD时间应为6字节年月日时分秒各1字节。某些老型号电表只返回4字节缺秒或返回7字节含毫秒。解法捕获异常后手动补零或截断try { return DlmsHelper.DecodeDateTime(timeBytes); } catch (IndexOutOfRangeException) { // 补零至6字节常见于缺秒电表 var padded timeBytes.Length 6 ? timeBytes.Concat(Enumerable.Repeat((byte)0, 6 - timeBytes.Length)).ToArray() : timeBytes.Take(6).ToArray(); return DlmsHelper.DecodeDateTime(padded); }4.4 现象TCP模式下ConnectAsync()成功但ReadAsync()返回空值原因电表DLMS UA服务未启用或防火墙拦截了4059端口后的应用层心跳如GetRequest超时。TCP连接成功只代表链路通不代表DLMS服务就绪。解法在ConnectAsync()后立即发一个轻量级探测请求如读取电表制造商名称LN00.00.02.00.00.02await client.ConnectAsync(); // 探测读取制造商名称LN00.00.02.00.00.02若失败则说明DLMS服务未开 var probe await client.ReadAsync(LogicalName.Parse(00.00.02.00.00.02), 2); if (!probe.IsSuccess) throw new InvalidOperationException(DLMS UA服务未启用请检查电表设置);4.5 现象批量读取ReadMultipleAsync()部分成功、部分失败错误码0x08Hardware Fault原因电表内存不足或处理队列溢出。DLMS标准规定单次批量请求最多8个对象超出则电表拒绝处理。解法将大批次拆分为每8个一组var chunks targetLns.Chunk(8); // .NET 6 的Chunk扩展 foreach (var chunk in chunks) { var results await client.ReadMultipleAsync( chunk.Select(ln new ReadItem(ln, 2)).ToArray() ); // 处理results... }5. 进阶技巧用OBIS码映射表实现“免LN编码”的语义化采集硬记LN地址如01.00.21.00.00.02是DLMS开发的最大认知负担。其实DLMS标准早已定义了一套全球统一的OBIS码Object Identification System如1.0.21.255.0.255对应“A相电压瞬时值”1.0.31.255.0.255对应“A相电流瞬时值”。OpenSmartMeter内置了OBIS码到LN的转换表让你用人类可读的字符串代替十六进制。5.1 加载OBIS码映射并转换为LNOpenSmartMeter提供ObisCode类支持字符串解析和LN生成// OBIS码字符串1.0.21.255.0.255 → A相电压 var obis ObisCode.Parse(1.0.21.255.0.255); // 转换为LN自动映射为01.00.21.00.00.02 var lnFromObis obis.ToLogicalName(); Console.WriteLine($OBIS {obis} → LN {lnFromObis}); // 输出OBIS 1.0.21.255.0.255 → LN 01.00.21.00.00.02OBIS码结构说明第1段1A相1A, 2B, 3C, 0总第2段0费率0总, 1峰, 2平, 3谷第3段21电压类21相电压31相电流1有功总电量第4段255瞬时值255瞬时1正向有功2反向有功第5段0小数位0无小数11位22位第6段255单位255默认单位如V、A、kWh。5.2 构建语义化采集配置文件将OBIS码写入JSON配置运行时加载彻底解耦业务逻辑与协议细节// config.json { meters: [ { id: meter_001, ip: 192.168.1.100, obis_codes: [ 1.0.21.255.0.255, // A相电压 1.0.31.255.0.255, // A相电流 1.0.1.255.0.255 // 总有功电量 ] } ] }C#中解析并采集var config JsonSerializer.DeserializeConfig(File.ReadAllText(config.json)); var meter config.Meters.First(); var client new DlmsClient(new TcpTransport(meter.Ip, 4059)); await client.LoginAsync(); // 将OBIS码批量转LN var lns meter.ObisCodes.Select(ObisCode.Parse).Select(c c.ToLogicalName()).ToArray(); // 批量读取 var results await client.ReadMultipleAsync( lns.Select(ln new ReadItem(ln, 2)).ToArray() ); // 结果按OBIS码索引业务层无需知道LN var data new Dictionarystring, object(); for (int i 0; i results.Results.Length; i) { var obis meter.ObisCodes[i]; data[obis] DlmsHelper.DecodeValue(results.Results[i].Value); } // 输出{1.0.21.255.0.255: 228.5, 1.0.31.255.0.255: 12.3, 1.0.1.255.0.255: 12345.67}5.3 处理厂商私有OBIS码的扩展策略标准OBIS码覆盖90%场景但部分厂商如LandisGyr E350会定义私有码如96.1.0.255.0.255表示“固件版本”。此时需扩展ObisCode映射表// 注册私有OBIS码映射LN00.00.02.00.00.02 → 制造商名称 ObisCode.RegisterCustomMapping( ObisCode.Parse(96.1.0.255.0.255), LogicalName.Parse(00.00.02.00.00.02) ); // 后续ObisCode.Parse(96.1.0.255.0.255).ToLogicalName() 即返回00.00.02.00.00.02这种设计让新电表接入只需更新JSON配置和少量映射注册无需改一行采集逻辑代码——这才是“DLMS协议采集C#源代码”真正该交付的价值不是一堆可运行的代码而是一套可演进、可配置、可验证的采集框架。我带过的三个项目最终都把这套OBIS语义化方案固化为团队标准。现在新人入职第一天就能用1.0.21.255.0.255写出采集脚本而不是对着IEC 62056文档查半天LN。协议细节交给库业务逻辑留给配置这才是工程化的正道。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?