使用 C# 實現 CJ-T188 水錶協議和 DL-T645 電錶協議的解析與編碼

来源:https://www.cnblogs.com/myzony/archive/2019/05/21/10897895.html
-Advertisement-
Play Games

一、協議的定義 要對某種協議進行編解碼操作,就必須知道協議的基本定義,首先我們來看一下 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 代碼打包下載

上述代碼實現均已打包為壓縮文件,點擊我 即可直接下載。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ASP.NET的優勢 ASP.NET背後有一個完整的.NET Framework支撐 什麼是ASP.NET? ASP.NET是建立在公共語言運行庫上的編程框架,可用於在伺服器上生成功能強大的Web應用程式。與以前的Web開發模型相比,ASP.NET提供了數個重要的優點: MVC模式的優勢 mvc是一 ...
  • 實現DynamicProxy前,先介紹幾個必要的輔助類: 一、切麵上下文類AspectContext 該類是作為切麵特性類的OnEntry和OnEixt方法的參數用的,該類包含了被代理對象Sender、當前切入的方法名稱(Name)、調用方法的參數列表(Args)以及返回值(Result) 二、切麵 ...
  • 最近需要有一個完全自主的基於C#語言的Aop框架,查了一下資料實現方式主要分為:靜態織入和動態代理,靜態織入以Postshop為代表,而動態代理又分為: 1、普通反射 2、Emit反射 3、微軟提供的.Net Remoting和RealProxy (微軟官方例子https://msdn.micros ...
  • ​一、前言 從進行到軟體開發這個行業現在已經有幾年了,在整理出這個套開發框架之前自己做了不少重覆造輪子的事。每次有新的項目總是要耗費不少時間在UI、許可權和系統通用模塊上面,自己累得要死,老闆還罵沒效率。為了能提高開發效率,同時也多拿拿獎金、多存點私房錢,我就著手做了一套以許可權管理為主的快速開發框架。 ...
  • C# -- LinkedList的使用 class Person { public Person() { } public Person(string name, int age, string sex) { this.Name = name; this.Age = age; this.Sex = ...
  • string.Format("{0:t}", DateTime.Parse("2019-1-1 08:30:00")); 轉換為 8:30 這種格式 string.Format("{0:hh:mm}", DateTime.Parse("2019-1-1 08:30:00")); 可以轉換為 08:3 ...
  • 在SQL語句查詢過程中,Sqlserver支持使用LEFT()、RIGHT()、SUBSTRING()等幾個函數對字元串進行截取操作,SubString函數相對於其他兩個函數來說更靈活,使用場景更多,可以指定截取開始的位置以及截取的長度,SubString函數的格式為SubString(expres ...
  • 500.30 ANCM In-Process Handler Load Failure ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...