一、协议的定义

要对某种协议进行编解码操作,就必须知道协议的基本定义,首先我们来看一下 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 代码打包下载

上述代码实现均已打包为压缩文件,点击我 即可直接下载。

使用 C# 实现 CJ-T188 水表协议和 DL-T645 电表协议的解析与编码的更多相关文章

  1. 经常使用传感器协议1:CJ/T-188 水表协议解析1

          本文以实例说明CJ/T-188水表协议的解析过程,下面数据未经特殊说明,均指十六进制. 数据发送:         FE FE FE FE 68 10 44 33 22 11 00 33 ...

  2. 比较下OceanBase的选举协议和Raft的选举协议的区别

    阿里技术大讲堂OceanBase专场中曾有专门一场讲座介绍OB自己实现的分布式选举算法:<分布式选举-破解数据库高可用性难题> 这里简单列一下这个选举算法和raft论文中提到的选举算法的区 ...

  3. 【原创】锐捷实现OSPF路由协议和NAT地址转换协议

    路由网络设计与实施 [锐捷设备实现OSPF路由协议与NAT地址转换] 说明:   本文是在多VLAN双星型交换网络的基础之上发展的.关于组建多VLAN双星型交换网络,请参阅: <思科和锐捷组建多 ...

  4. 计算机网络【1】—— OSI七层协议和TCP/IP四层协议

    新开一贴,专门用来记录计算机网络相关知识. 一.OSI七层协议 物理层.数据链路层.网络层.传输层.会话层.表示层.应用层 二.TCP/IP四层协议 网络接口层.网际层.运输层.应用层 三.五层协议 ...

  5. OSI七层协议和TCP/IP四层协议

    1. OSI七层和TCP/IP四层的关系 1.1 OSI引入了服务.接口.协议.分层的概念,TCP/IP借鉴了OSI的这些概念建立TCP/IP模型. 1.2 OSI先有模型,后有协议,先有标准,后进行 ...

  6. 页面解耦—— 统跳协议和Rewrite引擎

    原文: http://pingguohe.net/2015/11/24/Navigator-and-Rewrite.html 解耦神器 —— 统跳协议和Rewrite引擎 Nov 24, 2015 • ...

  7. http协议和web应用有状态和无状态浅析

    http协议和web应用有状态和无状态浅析 (2013-10-14 10:38:06) 转载▼ 标签: it   我们通常说的web应用程序的无状态性的含义是什么呢? 直观的说,“每次的请求都是独立的 ...

  8. 在线聊天室的实现(1)--websocket协议和javascript版的api

    前言: 大家刚学socket编程的时候, 往往以聊天室作为学习DEMO, 实现简单且上手容易. 该Demo被不同语言实现和演绎, 网上相关资料亦不胜枚举. 以至于很多技术书籍在讲解网络相关的编程时, ...

  9. http协议和web本质

    转载:http://www.cnblogs.com/dinglang/archive/2012/02/11/2346430.html http协议和web本质 当你在浏览器地址栏敲入“http://w ...

随机推荐

  1. laravel实现多模块

    一.这里使用Caffienate Modules 网址:modules maintained by caffeinated 二.根据自己的版本选择包的版本 三.在项目composer.json文件中加 ...

  2. 【安富莱】STM32H7用户手册发布,重在BSP驱动包设计方法,HAL库的框架学习,授人以渔,更新至63章(2019-07-21)

    说明: 1.本教程重在BSP驱动包设计方法和HAL库的框架学习,并将HAL库里面的各种弯弯绕捋顺,从而方便我们的程序设计. 2.由于是基于HAL库的文档,所以不限制H7系列,其它F1,F2,F3,F4 ...

  3. Vue 小练习01

    有红, 黄, 蓝三个按钮, 以及一个200X200px的矩形box, 点击不同的按钮, box的颜色会被切换为指定的颜色 <!DOCTYPE html> <html lang=&qu ...

  4. 软件文档写作-plantuml画用例图和时序图

    背景 当下的软件开发人员,不可避免的需要输出一些软件设计文档,作为一个软件工程专业毕业的工程师,最常用的设计工具就是UML,使用UML工具绘制一些软件相关的图,是必备技能,也是输出的技术文档中的重要组 ...

  5. 什么是面向对象编程(OOP)?

    Java 程序员第一个要了解的基础概念就是:什么是面向对象编程(OOP)? 玩过 DOTA2 (一款推塔杀人的游戏)吗?里面有个齐天大圣的角色,欧洲战队玩的很溜,国内战队却不怎么会玩,自家人不会玩自家 ...

  6. Java中间消息件——ActiveMQ入门级运用

    先来说一说我们为什么要用这个东西啊! 比如,我们现在有这样了个问题要解决: 这样,我们就要用到中间消息间了 然后我们就说一下什么是中间消息间吧. 采用消息传送机制/消息队列 的中间件技术,进行数据交流 ...

  7. ASP.NET MVC5基础 – MVC文件架构

    创建MVC项目 首先,我们使用Visual Studio2019创建一个MVC架构的应用程序.步骤如下:首先打开VS2019,在启动页选择[创建新项目].然后选择创建 ASP.NET Web 应用程序 ...

  8. 理解并运用TP5.1-Facade

    1.内容介绍 深入解析tp5.1与laravel 中Facade底层原理实现 1. 什么是Facade 2. 为什么需要有什么好处 3.  Facade实现原理 4. 功能实现. 5. 容器注入 2. ...

  9. git命令教程

    git教程笔记 Git是什么? Git是一个分布式版本控制系统 版本控制方式 集中式版本控制:从版本库中先取得最新的版本,改完之后再上传到版本库中,需要联网 分布式版本控制:每个合作者电脑上都有一个版 ...

  10. [转]UiPath教程:UiPath及其组件介绍

    本文转自:http://www.rpa-cn.com/UiPathxuexirenzheng/UiPathzaixianxueyuan/2019-06-05/937.html 根据德勤2018年的调查 ...