一、協議的定義 要對某種協議進行編解碼操作,就必須知道協議的基本定義,首先我們來看一下 CJ/T188 的數據幀定義(協議定義),瞭解請求數據與響應數據的基本結構。 1.1 CJ/T188 水錶通訊協議 請求幀: | 位元組 | 值 | 描述 | | | | | | 0 | 0x68 | 數據幀開始標 ...
一、協議的定義
要對某種協議進行編解碼操作,就必須知道協議的基本定義,首先我們來看一下 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 代碼打包下載
上述代碼實現均已打包為壓縮文件,點擊我 即可直接下載。