一、协议的定义

要对某种协议进行编解码操作,就必须知道协议的基本定义,首先我们来看一下 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. 1、web爬虫,requests请求

    requests请求,就是用python的requests模块模拟浏览器请求,返回html源码 模拟浏览器请求有两种,一种是不需要用户登录或者验证的请求,一种是需要用户登录或者验证的请求 一.不需要用 ...

  2. SpringBoot运行异常时捕获

    一.目录展示 二.FirstController 三.ExceptionHandler 捕获异常类 四.效果展示

  3. 运维工具ansible-使用与介绍(转)

    转自 Linux轻量级自动运维工具-Ansible浅析 - ~微风~ - 51CTO技术博客http://weiweidefeng.blog.51cto.com/1957995/1895261 Ans ...

  4. python del和垃圾回收

    1. del是删除对象 2. python中的垃圾回收是删除引用计数

  5. Spark家族:Win10系统下搭建Scala开发环境

    一.Scala环境基础 Scala对Java相关的类,接口进行了包装,所以依赖Jvm环境. Jdk 1.8 scala 依赖 scala 2.11 安装版本 idea 2017.3 开发工具 二.配置 ...

  6. Python 调用 ES、Solr、Phoenix

    #!/usr/bin/env python # -*- coding:utf-8 -*- # ************************************* # @Time : 2019/ ...

  7. Java入门系列之StringBuilder、StringBuffer(三)

    前言 上一节我们讲解了字符串的特性,除了字符串类外,还有两个我们也会经常用到的类,那就是StringBuffer和StringBuilder.因为字符串不可变,所以我们每次对字符串的修改比如通过连接c ...

  8. django简单密码加密和效验

    通过django自带的类库,来加密解密很方便,下面来简单介绍下: 导入包: from django.contrib.auth.hashers import make_password, check_p ...

  9. 使用laravel-amdin调用文件上传阿里oss注意点

    开发者工作中,项目代码开发提高效率,往往会使用一些github上面的一些扩展类,这里举例说明一下遇到的情况. 一.使用laravel-admin框架开发管理后台文件或者图片上传 情景:运营或者产品通过 ...

  10. JMeter命令行执行+生成HTML报告

    1.为什么用命令行模式 使用GUI方式启动jmeter,运行线程较多的测试时,会造成内存和CPU的大量消耗,导致客户机卡死: 所以一般采用的方式是在GUI模式下调整测试脚本,再用命令行模式执行: 命令 ...