使用 C# 实现 CJ-T188 水表协议和 DL-T645 电表协议的解析与编码
一、协议的定义
要对某种协议进行编解码操作,就必须知道协议的基本定义,首先我们来看一下 CJ/T188 的数据帧定义(协议定义),了解请求数据与响应数据的基本结构。
1.1 CJ/T188 水表通讯协议
请求帧:
字节 | 值 | 描述 |
---|---|---|
0 | 0x68 | 数据帧开始标识。 |
1 | T | 表计类型代码,详细信息请参考 表计类型表 。 |
2-8 | A0-A6 | 表计地址,水表设备的具体地址,这里是 BCD 形式。 |
9 | CTR_01 | 协议控制码,例如 0x1 就是读表数据。 |
10 | 0x3 | 数据域长度。 |
11-12 | 0x1F,0x90 | 数据标识 DI0-DI1。 |
13 | 0x00 | 序列号,一般为 0x00,序列号也被作为整个数据域的长度。 |
14 | CS | 表示校验和数据,即 0-13 位置的所有字节的累加和。 |
15 | 0x16 | 数据帧的结束标识。 |
例如有以下请求帧数据(读取水表数据):
68 10 01 00 00 05 08 00 00 01 03 1F 90 00 39 16
对应的解释如下。
顺序 | 0 | 1 | 2-8 | 9 | 10 | 11-12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|
说明 | 帧头 | 类型 | 地址 | CTR_0 | 长度 | 数据标识 | 序列号 | 校验和 | 帧尾 |
实例 | 68 | 10 | 01 00 00 05 08 00 00 | 01 | 03 | 1F 90 | 00 | 39 | 16 |
表计类型表:
值 | 含义 |
---|---|
10 | 冷水水表 |
11 | 生活热水水表 |
12 | 直饮水水表 |
13 | 中水水表 |
20 | 热量表 (记热量) |
21 | 热量表 (记冷量) |
30 | 燃气表 |
40 | 电度表 |
响应帧(读表操作):
字节 | 值 | 描述 |
---|---|---|
0 | 0x68 | 数据帧开始标识。 |
1 | T | 表计类型代码,详细信息请参考 表计类型表 。 |
2-8 | A0-A6 | 表计地址,水表设备的具体地址,这里是 BCD 形式。 |
9 | CTR_1 | 协议控制码,在返回帧含义即是请求帧的控制码加上 0x80。 |
10 | L | 数据域长度。 |
11-12 | 0x1F,0x90 | 数据标识 DI0-DI1。 |
13 | 0x00 | 序列号,一般为 0x00。 |
14-17 | ALL DATA | 累计用量,以 BCD 形式进行存储。 |
18 | 单位 | 计量单位,具体含义可以参考 计量单位表 。 |
19-22 | MONTH DATA | 本月用量,以 BCD 形式进行存储。 |
23 | 单位 | 计量单位,具体含义可以参考 计量单位表 。 |
24-30 | 时间 | 表示实际时间,以 BCD 形式存储,格式为 ss mm HH dd MM yy yy。 |
31 | 状态 1 | 状态字段。 |
32 | 状态 2 | 保留字节,一般置为 0xFF。 |
33 | CS | 表示校验和数据,即 0-32 位置的所有字节的累加和。 |
34 | 0x16 | 数据帧的结束标识。 |
例如有以下响应帧数据:
68 10 44 33 22 11 00 33 78 81 16 1F 90 00 00 77 66 55 2C 00 77 66 55 2C 31 01 22 11 05 15 20 21 84 6D 16
对应的解释如下:
顺序 | 0 | 1 | 2-8 | 9 | 10 | 11-12 | 13 |
---|---|---|---|---|---|---|---|
说明 | 帧头 | 类型 | 地址 | 控制码 | 长度 | 标识 | 序列号 |
实例 | 68 | 10 | 44 33 22 11 00 33 78 | 81 | 16 | 1F 90 | 00 |
顺序 | 14-17 | 18 | 19-22 | 23 | 24-30 |
---|---|---|---|---|---|
说明 | 累计用量 | 单位 | 本月用量 | 单位 | 时间 |
实例 | 00 77 66 55 | 2C | 00 77 66 55 | 2C | 31 01 22 11 05 15 20 |
顺序 | 31 | 32 | 33 | 34 |
---|---|---|---|---|
说明 | 状态 1 | 状态 2 | 校验和 | 帧尾 |
实例 | 00 | FF | 6D | 16 |
计量单位表:
单位 | 值 |
---|---|
Wh | 0x2 |
KWh | 0x5 |
MWh | 0x8 |
MWh * 100 | 0xA |
J | 0x1 |
KJ | 0xB |
MJ | 0xE |
GJ | 0x11 |
GJ * 100 | 0x13 |
W | 0x14 |
KW | 0x17 |
MW | 0x1A |
L | 0x29 |
\[m^3\] | 0x2C |
\[ L/h \] | 0x32 |
\[m^3/h\] | 0x35 |
2.2 DL/T645 多功能电能表通信协议
请求帧:
字节 | 值 | 描述 |
---|---|---|
0 | 0x68 | 数据帧开始标识。 |
1-6 | A0-A5 | 电表设备地址,以 BCD 码形式存储。 |
7 | 0x68 | 帧起始符。 |
8 | C | 控制码。 |
9 | L | 数据域长度。 |
10 | DATA | 数据域。 |
11 | CS | 校验码,从 0-10 字节的累加和。 |
12 | 0x16 | 数据帧结束标识。 |
读取电表的当前正向有功总电量,表号为 12345678。
68 78 56 34 12 00 00 68 11 04 33 33 34 33 C6 16
顺序 | 0 | 1-6 | 7 | 8 | 9 | 10-13 |
---|---|---|---|---|---|---|
说明 | 帧头 | 地址 | 帧头 | 控制码 | 长度 | 数据域 |
实例 | 68 | 78 56 34 12 00 00 | 68 | 11 | 04 |
顺序 | 14 | 15 |
---|---|---|
说明 | 累加和 | 帧尾 |
实例 | C6 | 16 |
这里需要注意的是,33 33 34 33 是 00 01 00 00 加上 0x33 之后的值,因为传输的时候是低位在前,高位在后,所以就是 00 00 01 00 每字节加上 0x33,00 01 00 00 即代表要读取当前正向有功总电能,也有其他的标识,这里不再叙述。
响应帧(读表操作):
68 78 56 34 12 00 00 68 91 08 33 33 34 33 A4 56 79 38 F5 16
顺序 | 0 | 1-6 | 7 | 8 | 9 |
---|---|---|---|---|---|
说明 | 帧头 | 地址 | 帧头 | 控制码,这里即 0x11 + 0x80 | 长度 |
实例 | 68 | 78 56 34 12 00 00 | 68 | 91 | 08 |
顺序 | 10-17 | 18 | 19 |
---|---|---|---|
说明 | 数据域 | 累加和 | 帧尾 |
实例 | 33 33 34 33 A4 56 79 38 | F5 | 16 |
这里只说明一下数据域,在这里 33 33 34 33 可以理解成寄存器地址,而 A4 56 79 38 则是具体的电量数据,在这里就是分别减去 0x33,即 71 23 46 5,因为其精度是两位,且是 BCD 码的形式,最后的结果就是 54623.71 度。
2.3 前导字节
前导字节并非水/电表协议强制规定的协议组,所谓前导字节是在数据帧的头部增加 1-4 组 0xFE,例如以下数据帧就是增加了前导字节。
FE FE FE FE 68 10 44 33 22 11 00 33 78 01 03 1F 90 00 80 16
所以在处理的协议的时候,某些厂家可能会加入前导字节,在处理的时候一定要注意。
2.4 小结
水/电表协议的请求帧与响应帧其实结构一致,区别仅在于不同的响应,其具体的数据域值也不同,所以在处理的时候可以用一个字典/列表来存储数据域。
二、代码的实现
2.1 工具类的编码
为了方便我们对协议的解析与组装,我们需要编写一个工具类实现对字节组的某些特殊操作,例如校验和、BCD 转换、十六进制数据的校验等。
2.1.1 累加和计算功能
首先我们来实现累加和的计算,累加和就是一堆字节相加的结果,不过这个结果可能超过一个字节的大小,我们需要对 256 取模,使其结果刚好能被 1 个字节存储。
/// <summary>
/// 计算一组二进制数据的累加和。
/// </summary>
/// <param name="waitCalcBytes">等待计算的二进制数据。</param>
public static byte CalculateAccumulateSum(byte[] waitCalcBytes)
{int ck = 0;foreach (var @byte in waitCalcBytes) ck = (ck + @byte);// 对 256 取余,获得 1 个字节的数据。return (byte)(ck % 0x100);
}
2.1.2 十六进制字符串转字节数组
首先我们需要校验一个字符串是否是一个规范合法的十六进制字符串。
/// <summary>
/// 判断输入的字符串是否是有效的十六进制数据。
/// </summary>
/// <param name="hexStr">等待判断的十六进制数据。</param>
/// <returns>符合规范则返回 True,不符合则返回 False。</returns>
public static bool IsIllegalHexadecimal(string hexStr)
{var validStr = hexStr.Replace("-", string.Empty).Replace(" ", string.Empty);if (validStr.Length % 2 != 0) return false;if (string.IsNullOrEmpty(hexStr) || string.IsNullOrWhiteSpace(hexStr)) return false;return new Regex(@"[A-Fa-f0-9]+$").IsMatch(hexStr);
}
校验之后我们才能够将这个字符串用于转换。
/// <summary>
/// 将 16 进制的字符串转换为字节数组。
/// </summary>
/// <param name="hexStr">等待转换的 16 进制字符串。</param>
/// <returns>转换成功的字节数组。</returns>
public static byte[] HexStringToBytes(string hexStr)
{// 处理干扰,例如空格和 '-' 符号。var str = hexStr.Replace("-", string.Empty).Replace(" ", string.Empty);return Enumerable.Range(0, str.Length).Where(x => x % 2 == 0).Select(x => Convert.ToByte(str.Substring(x, 2), 16)).ToArray();
}
2.1.3 BCD 数据的转换
关于 BCD 码的介绍,网上有诸多解释,这里不再赘述,这里只讲一下编码实现。
/// <summary>
/// BCD 码转换成 <see cref="double"/> 类型。
/// </summary>
/// <param name="sourceBytes">等待转换的 BCD 码数据。</param>
/// <param name="precisionIndex">精度位置,用于指示小数点所在的索引。</param>
/// <returns>转换成功的值。</returns>
public static double BCDToDouble(byte[] sourceBytes, int precisionIndex)
{var sb = new StringBuilder();var reverseBytes = sourceBytes.Reverse().ToArray();for (int index = 0; index < reverseBytes.Length; index++){sb.Append(reverseBytes[index] >> 4 & 0xF);sb.Append(reverseBytes[index] & 0xF);if (index == precisionIndex - 1) sb.Append('.');}return Convert.ToDouble(sb.ToString());
}/// <summary>
/// BCD 码转换成 <see cref="string"/> 类型。
/// </summary>
/// <param name="sourceBytes">等待转换的 BCD 码数据。</param>
/// <returns>转换成功的值。</returns>
public static string BCDToString(byte[] sourceBytes)
{var sb = new StringBuilder();var reverseBytes = sourceBytes.Reverse().ToArray();for (int index = 0; index < reverseBytes.Length; index++){sb.Append(reverseBytes[index] >> 4 & 0xF);sb.Append(reverseBytes[index] & 0xF);}return sb.ToString();
}
2.2 协议的实现
协议分为发送帧与响应帧,发送帧是通过传入一系列参数构建一个 byte
数组,而响应帧则需要我们从一个 byte
数组转换为方便读写的对象。
根据以上特点,我们编写一个 IProtocol
接口,该接口拥有两个方法,即编码 (Encode) 和解码 (Decode) 方法。
public interface IProtocol
{byte[] Encode();IProtocol Decode(byte[] sourceBytes);List<DataDefine> DataDefines { get;}
}
接着我们可以使用一个类型来表示每个数据域的数据,这里我定义了一个 DataDefine
类型。
public class DataDefine
{public string Name { get; set; }public byte[] Data { get; set; }public int Length { get; set; }
}
这里我以水表的读表操作为例,定义了一个抽象基类,在抽象基类里面定义了数据帧的基本接口,并且实现了编码/解码方法。在这里 DataDefines
的作用就体现了,他主要是用于
public abstract class CJT188Protocol : IProtocol
{protected const byte FrameHead = 0x68;public byte DeviceType { get; protected set; }public byte[] Address { get; protected set; }public byte ControlCode { get; protected set; }public int DataLength { get; protected set; }public byte[] DataArea { get; private set; }public List<DataDefine> DataDefines { get;}public byte AccumulateSum { get; protected set; }protected const byte FrameEnd = 0x16;public CJT188Protocol(){DataDefines = new List<DataDefine>();}public DataDefine this[string key]{get{return DataDefines.FirstOrDefault(x => x.Name == key);}}public virtual byte[] Encode(){// 校验协议数据。if(Address.Length != 7) throw new ArgumentException($"水表地址 {BitConverter.ToString(Address)} 的长度不正确,长度不等于 7 个字节。");BuildDataArea();using (var mem = new MemoryStream()){mem.WriteByte(FrameHead);mem.WriteByte(DeviceType);mem.Write(Address);mem.WriteByte(ControlCode);mem.WriteByte((byte)DataLength);mem.Write(DataArea);AccumulateSum = ByteUtils.CalculateAccumulateSum(mem.ToArray());mem.WriteByte(AccumulateSum);mem.WriteByte(FrameEnd);return mem.ToArray();}}public virtual IProtocol Decode(byte[] sourceBytes){using (var mem = new MemoryStream(sourceBytes)){using (var reader = new BinaryReader(mem)){reader.ReadByte();DeviceType = reader.ReadByte();Address = reader.ReadBytes(7);ControlCode = reader.ReadByte();DataLength = reader.ReadByte();foreach (var dataDefine in DataDefines){dataDefine.Data = reader.ReadBytes(dataDefine.Length);}AccumulateSum = reader.ReadByte();}}return this;}protected virtual void BuildDataArea(){// 构建数据域。using (var dataMemory = new MemoryStream()){foreach (var data in DataDefines){if(data==null) continue;dataMemory.Write(data.Data);}DataArea = dataMemory.ToArray();DataLength = DataArea.Length;}}
}
最后我们定义了两个具体的协议类,分别是读表的请求帧和读表的响应帧,在其构造方法分别定义了具体的数据域。
public class CJT188_Read_Request : CJT188Protocol
{public CJT188_Read_Request(string address,byte type){Address = ByteUtils.HexStringToBytes(address).Reverse().ToArray();ControlCode = 0x1;DeviceType = type;DataDefines.Add(new DataDefine{Name = "Default",Length = 2});DataDefines.Add(new DataDefine{Name = "Seq",Length = 1});}
}public class CJT188_Read_Response : CJT188Protocol
{public CJT188_Read_Response(){DataDefines.Add(new DataDefine{Name = "Default",Length = 2});DataDefines.Add(new DataDefine{Name = "Seq",Length = 1});DataDefines.Add(new DataDefine{Name = "AllData",Length = 4});DataDefines.Add(new DataDefine{Name = "AllDataUnit",Length = 1});DataDefines.Add(new DataDefine{Name = "MonthData",Length = 4});DataDefines.Add(new DataDefine{Name = "MonthDataUnit",Length = 1});DataDefines.Add(new DataDefine{Name = "DateTime",Length = 7});DataDefines.Add(new DataDefine{Name = "Status1",Length = 1});DataDefines.Add(new DataDefine{Name = "Status2",Length = 1});}
}
测试代码:
class Program
{static void Main(string[] args){// 发送水表读表数据。var sendProtocol = new CJT188_Read_Request("00000805000001",0x10);sendProtocol["Default"].Data = new byte[] {0x1F, 0x90};sendProtocol["Seq"].Data = new byte[] {0x00};Console.WriteLine(BitConverter.ToString(sendProtocol.Encode()));// 解析水表响应数据。var receiveProtocol = new CJT188_Read_Response().Decode(ByteUtils.HexStringToBytes("68 10 78 06 12 18 20 00 00 81 16 90 1F 00 00 01 00 00 2C 00 01 00 00 2C 00 00 00 00 00 00 00 01 FF E0 16"));Console.ReadLine();}
}
2.3 代码打包下载
上述代码实现均已打包为压缩文件,点击我 即可直接下载。
转载于:https://www.cnblogs.com/myzony/p/10897895.html
使用 C# 实现 CJ-T188 水表协议和 DL-T645 电表协议的解析与编码相关推荐
- 经常使用传感器协议1:CJ/T-188 水表协议解析1
本文以实例说明CJ/T-188水表协议的解析过程,下面数据未经特殊说明,均指十六进制. 数据发送: FE FE FE FE 68 10 44 33 22 11 00 33 78 0 ...
- 7层网络协议和4层网络协议有什么区别和联系?
层网络协议和4层网络协议是指网络通信模型中的不同层次. 网络协议是一种规则,用于控制网络设备之间的通信.通常,网络协议被划分为不同的层次,每一层负责处理特定的通信任务. 在网络协议层次模型中,有两种常 ...
- osi七层协议和tcp/ip四层协议
(大部分内容为转载) OSI(Open System Interconnection)是一个开放性的通行系统互连参考模型,他是一个定义的非常好的协议规范,共包含七层协议.OSI七层协议是由ISO (I ...
- 基于PT分析ARP协议和Wireshark分析ARP协议。
一.Ip层主要的协议:ARP,ICMP,ip,IGMP ICMP:ping命令.(可以这么理解,先简单这么理解叭) 二.ARP技术原理(图片为网络图片,侵权即删) 首先我们得明白ARP工作在网络层.( ...
- OSI七层协议和TCP/IP四层协议比较
网络协议设计者不应当设计一个单一.巨大的协议来为所有形式的通信规定完整的细节,而应把通信问题划分成多个小问题,然后为每一个小问题设计一个单独的协议.这样做使得每个协议的设计.分析.时限和测试比较容易. ...
- 比较下OceanBase的选举协议和Raft的选举协议的区别
阿里技术大讲堂OceanBase专场中曾有专门一场讲座介绍OB自己实现的分布式选举算法:<分布式选举-破解数据库高可用性难题> 这里简单列一下这个选举算法和raft论文中提到的选举算法的区 ...
- OSI七层协议和Tcp/IP五层协议,路由器交换机和HUB的区别
在网络历史的早期,国际标准化组织(ISO)和国际电报电话咨询委员会(CCITT)共同出版了开放系统互联的七层参考模型.一台计算机操作系统中 的网络过程包括从应用请求(在协议栈的顶部)到网络介质(底部) ...
- OSI七层协议和TCP/IP四层协议之比较
转载来自 :少年阿宾 http://www.blogjava.net/stevenjohn/archive/2013/03/28/397117.html TCP/IP: 数据链路层:ARP,RARP ...
- 精述IBM的MQTT协议和MQTT-S协议
一.MQTT简介 MQTT (Message Queuing Telemetry Transport) 是由IBM研发的构建在TCP/IP之上的简单轻量的消息协议,目标使用场景为受限制环境,如低带宽. ...
- SQL Server 网络协议和 TDS 端点
SQL Server 网络协议 SQL Server支持四种协议,Shared Memory:TCP/IP:Named Pipes和Virtual Interface Adapter(VIA).打开S ...
最新文章
- 10个顶级的CSS UI开源框架
- git-fork下来的项目(拷贝到本地 根据原来的库更新)
- UA MATH636 信息论7 并行高斯信道简介
- 利用Python进行简单的图像识别(验证码)
- 牛客网笔记之数组(一)
- 剑指Offer - 面试题33. 二叉搜索树的后序遍历序列(递归)
- java.lang.class_关于Java.lang.Class的一些疑问
- 12. 信息系统项目的采购管理
- 一文带你掌握抓包工具的使用-科来
- 10个优秀的思维导图软件,各种需求都能满足!!!
- latex复杂表格排版
- java field.gettype_Java Field getGenericType()用法及代码示例
- ios dyld: Library not loaded: @rpath/xxx.framework/xxx 之根本原因
- 高中信息技术计算机网络教案,信息技术 - 第八册计算机教案(全册)-四年级...
- 用格式工厂旋转手机视频
- 键盘定位板图纸_客制化键盘
- 微信跨公众号进行支付
- 快捷键----------快人快语
- python100个常用术语_Python 常用术语
- zbb20181224 win7 下的open live writer代码插件
热门文章
- 云计算的未来畅想(五) -- matrix(终篇)
- 数学建模模型_浅谈对数学建模,数据挖掘,数据分析和数据模型的区别
- Win10 全屏截图快捷键
- android打开系统文件怎么打开方式,Android调用系统应用打开任意文件
- 深入解析J.U.C并发包(十五)—— Thread - Specific Storage(ThreadLocal)模式
- JavaScript的document对象详解
- tornado线程阻塞的解决
- 基于Springboot外卖系统01:技术构成+功能模块介绍
- java中instant_Instant
- 一起写RPC框架(七)RPC网络模块的搭建五 Netty模块