HJ212协议C#代码解析实现
HJ212协议是环保中一个非常重要的标准协议(字符串协议),之前写了两篇C++ HJ212协议解析的相关博文:
- 环保 HJ212协议解析
- 基于Qt5.14.2的HJ212 TCP服务端接收解析入库程序
最近在学习C#,所以打算基于C#重新实现一遍,算是熟悉一下C#的基本语法。
HJ212协议简介
由于是做环保相关的,有时需要对212协议进行拆包和解包。HJ212协议是一种字符串协议,数据传输通讯包主要由包头、数据段长度、数据段、CRC校验、包尾组成,其中“数据段”内容包括请求编码、系统编码、命令编码、密码、设备唯一标识、总包数、包号、指令参数。请求编码为请求的时间戳,系统编码ST统一规定为22,命令编码CN为该数据包的时间类型,访问密码、设备唯一标识在对接时由平台提供,指令参数为数据内容。通讯协议的数据结构如图4所示。
图4 通讯协议的数据结构
6.1.1通讯包结构组成
名称 | 类型 | 长度 | 描述 |
---|---|---|---|
包头 | 字符 | 2 | 固定为## |
数据段长度 | 十进制整数 | 4 | 数据段的ASCII字符数。例如数据段的字符数为128,则写为“0128” |
数据段 | 字符 | 0<=n<=9999 | 变长的数据 |
CRC校验 | 十六进制 | 4 | 数据段的校验结果,例如C901,如果CRC错,即执行超时 |
包尾 | 字符 | 2 | 回车换行(\r\n) |
《污染物在线监控(监测)系统数据传输标准》简称《HJ212-2017》标准PDF文档可以从中华人民共和国生态环境部的官网下载,具体地址为:HJ212-2017》标准PDF文档
如下图所示:
目前HJ212标准协议已经发布了两个版本,一个是HJ/T 212-2005,另一个是 HJ 212-2017,最新的HJ 212-2017下载地址为:污染物在线监控(监测)系统数据传输标准(HJ 212-2017代替HJ/T 212-2005)
## 基于C#的HJ212解析类
首先创建一个基于C# .Net的库项目,名称为:HJ212ParseLibrary
,相关类实现代码如下:
(1)、通用工具类 CommonUtils
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace HJ212ParseLibrary
{/// <summary>/// 通用工具类/// </summary>public class CommonUtils{/// <summary>/// 切分数据/// </summary>/// <param name="cp"></param>/// <returns></returns>public static List<Dictionary<string, string>> SplitKV(string cp){List<Dictionary<string, string>> resultList = new List<Dictionary<string, string>>();var arr1 = cp.Split(';');foreach (var i in arr1){Dictionary<string, string> item = new Dictionary<string, string>();var arr2 = i.Split(',');foreach(var j in arr2){var arrKv = j.Split('=');if (arrKv.Length == 2){item.Add(arrKv[0], arrKv[1]); }}resultList.Add(item);}return resultList;}/// <summary>/// 组合数据/// </summary>/// <param name=""></param>/// <returns></returns>public static string JoinKV(List<Dictionary<string, string>> myList){List<string> item = new List<string>();foreach (var i in myList){List<string> arrKv = new List<string>();foreach (var j in i){arrKv.Add(j.Key + "=" + j.Value);}item.Add(Join(arrKv, ','));}return Join(item, ';');}/// <summary>/// 数据组合字符串/// </summary>/// <param name="arrKv">字符串列表</param>/// <param name="sep">分隔符</param>/// <returns></returns>/// <exception cref="NotImplementedException"></exception>private static string Join(List<string> arrKv, char sep = ' '){string result = "";bool isFirst = true;foreach (var item in arrKv){if (isFirst){result = item;isFirst = false;continue;}result += sep + item;}return result;}/// <summary>/// 将字符串数组按照sep分隔符分割之后,生成新的字符串/// </summary>/// <param name="arrKv">字符串数组</param>/// <param name="sep">分隔符</param>/// <returns></returns>private static string Join(string[] arrKv, char sep = ' '){string result = "";bool isFirst = true;foreach (var item in arrKv){if (isFirst){result = item;isFirst = false;continue;}result += sep + item;}return result;}/// <summary>/// 计算计算字节数组的CRC-16校验码/// </summary>/// <param name="byteArr">字节数组</param>/// <returns>CRC-16校验码</returns>public static uint GetCrc16(byte[] byteArr){uint crc_reg, check;int i, j;crc_reg = 0xFFFF;for (i = 0; i < byteArr.Length; i++){crc_reg = (crc_reg >> 8) ^ byteArr[i];for (j = 0; j < 8; j++){check = crc_reg & 0x0001;crc_reg >>= 1;if (check == 0x0001){crc_reg ^= 0xA001;}}}return crc_reg;}/// <summary>/// 将对应位置置1,n从1开始/// </summary>/// <param name="x"></param>/// <param name="n"></param>public static void SetBit(int x, int n){x |= 1 << (n - 1);}/// <summary>/// 将对应位置置0,n从1开始/// </summary>/// <param name="x"></param>/// <param name="n"></param>public static void CtrlBit(int x, int n){x &= ~(1 << (n - 1));}public static int GetBit(int x, int n){return x & (1 << (n - 1));}}
}
(2)、HJ212协议 CP数据部分 HJ212DataCP
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace HJ212ParseLibrary
{/// <summary>/// HJ212协议 CP数据部分/// </summary>public class HJ212DataCP{public HJ212DataCP(){}public HJ212DataCP(string s){List<Dictionary<string, string>> arr = CommonUtils.SplitKV(s);foreach (var i in arr){int kvlen = i.Count;string key = "";foreach (var j in i){// 对指定监测因子的项,统一使用因子代表if (j.Key == "PolId"){key = j.Value;this.KVDict[key] = new Dictionary<string, string>();continue;}string name, field;// 查询是否包含标准协议中的设备状态var f1 = j.Key.IndexOf("SB");var f2 = j.Key.IndexOf("-");if (f2 != -1 && f1 != 0){// a20004-Rtd name-fieldname = j.Key.Substring(0, f2);field = j.Key.Substring(f2 + 1);// i11001-Info fieldif (field == "Info"){field = name;}else{key = name;}}else{if (j.Key == "DataTime"){this.DataTime = j.Value;}name = j.Key;key = name;field = "value";}if (this.KVDict.ContainsKey(key)){// 如果存在key,则追加this.KVDict[key].Add(field, j.Value);} else{// 如果不存在该key,则创建一个字典对象,并添加到KVDict中Dictionary<string, string> dict = new Dictionary<string, string>();dict.Add(field, j.Value);this.KVDict.Add(key, dict);}}}}public void Clear(){this.KVDict.Clear();}public Dictionary<string, Dictionary<string, string>> KVDict = new Dictionary<string, Dictionary<string, string>>(); // 监测因子数据字典string DataTime;}
}
(3)、HJ212协议 数据区 HJ212Data
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace HJ212ParseLibrary
{/// <summary>/// HJ212协议 数据区/// 请求编码QN + 系统编码ST + 命令编码CN + 密码PW + 设备唯一标识MN + 标志位Flag + 总包数PNUM + 包号PNO + 指令参数CP/// 其中,指令CP字符数范围为:[0,9899],CP=&&数据区&&/// </summary>public class HJ212Data{public string QN; // 请求编码 20字符 QN=YYYYMMDDHHmmssZZZpublic string ST; // 系统编码 5字符 ST=21public string CN; // 命令编码 7字符 CN=2011public string PW; // 访问密码 9字符 PW=123456public string MN; // 设备标识 27字符 MN=[0-9A-F]public string Flag; // 标志位 8整数 Flag=7 bit:000001(协议版本)0(是否有包号)0(是否需应答)public string PNUM; // 总包数 9字符 PNUM=0000 [不分包则没有本字段]public string PNO; // 包号 8字符 PNO=0000 [不分包则没有本字段]public string CP; // 指令参数 <=9899字符 CP=&&数据区&&public HJ212DataCP CPs;private bool bValid = false;// 设置Flag,bit从1开始public void SetFlag(int bit, bool enable){int flag = 4;flag = Int32.Parse(Flag);if (enable){CommonUtils.SetBit(flag, bit);} else{CommonUtils.CtrlBit(flag, bit);}this.Flag = flag.ToString();}// 获取Flag,bit从1开始public int GetFlag(int bit){int flag = 4;if (Int32.TryParse(Flag, out flag)){return CommonUtils.GetBit(flag, bit);}return CommonUtils.GetBit(flag, bit);}// 有效性public bool IsValid(){return bValid;}// 长度public int Size(){return QN.Length + ST.Length + CN.Length + PW.Length + MN.Length +Flag.Length + PNUM.Length + PNO.Length + CP.Length;}// 构造函数public HJ212Data(){}public HJ212Data(string s){CopyStr(s);}public void CopyStr(string s){// 查找数据段int d1 = s.IndexOf("CP=&&"); // 数据段开始位置int d2 = s.IndexOf("&&", d1 + 5); // 数据段结束位置String tmpStr = "";if (d1 != -1 && d2 != -1){CP = s.Substring(d1 + 5, d2 - d1 - 5);CPs = new HJ212DataCP(CP);tmpStr = s.Substring(0, d1) + s.Substring(d2 + 2);}else{tmpStr = s;}Dictionary<string, string> strDict = new Dictionary<string, string>();var arr1 = tmpStr.Split(';');foreach (var i in arr1){var arr2 = i.Split('=');if (arr2.Length == 2){strDict.Add(arr2[0], arr2[1]);}}if (strDict.ContainsKey("QN")){QN = strDict["QN"];}if (strDict.ContainsKey("ST")){ST = strDict["ST"];}if (strDict.ContainsKey("CN")){CN = strDict["CN"];}if (strDict.ContainsKey("PW")){PW = strDict["PW"];}if (strDict.ContainsKey("MN")){MN = strDict["MN"];}if (strDict.ContainsKey("Flag")){Flag = strDict["Flag"];}if (strDict.ContainsKey("PNUM")){PNUM = strDict["PNUM"];}if (strDict.ContainsKey("PNO")){PNO = strDict["PNO"];}bValid = true;}// 获取数据项public List<Dictionary<string, string>> Cp2Object(){return CommonUtils.SplitKV(CP);}// 字符串输出public override string ToString(){string str = "";if (!string.IsNullOrEmpty(QN)){str += ("QN=" + QN + ";");}if (!string.IsNullOrEmpty(ST)){str += ("ST=" + ST + ";");}if (!string.IsNullOrEmpty(CN)){str += ("CN=" + CN + ";");}if (!string.IsNullOrEmpty(PW)){str += ("PW=" + PW + ";");}if (!string.IsNullOrEmpty(MN)){str += ("MN=" + MN + ";");}if (!string.IsNullOrEmpty(Flag)){str += ("Flag=" + Flag + ";");}if (!string.IsNullOrEmpty(PNUM)){str += ("PNUM=" + PNUM + ";");}if (!string.IsNullOrEmpty(PNO)){str += ("PNO=" + PNO + ";");}str += ("CP=&&" + CP + "&&");return str;}}
}
(4)、HJ212协议实体类 HJ212
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Threading.Tasks;namespace HJ212ParseLibrary
{/// <summary>/// HJ212协议 整包=包头+数据段长度+数据段+CRC校验+包尾组成/// ##0637ST=22;CN=2011;PW=123456;MN=LB4200001;CP=&&DataTime=20210107143500;a05002-Rtd=0,a05002-Flag=N;a21005-Rtd=0.825,a21005-Flag=N;a34006-Rtd=874,a34006-Flag=N;a24088-Rtd=0,a24088-Flag=N;a21003-Rtd=5.656,a21003-Flag=N;a21004-Rtd=9.253,a21004-Flag=N;a21002-Rtd=17.894,a21002-Flag=N;a05024-Rtd=5.803,a05024-Flag=N;a34007-Rtd=2679,a34007-Flag=N;a34002-Rtd=123.97,a34002-Flag=N;a34004-Rtd=24.82,a34004-Flag=N;a01006-Rtd=1032.3,a01006-Flag=N;a01002-Rtd=20.9,a01002-Flag=N;a21026-Rtd=0.795,a21026-Flag=N;a01007-Rtd=3.3,a01007-Flag=N;a34049-Rtd=3553,a34049-Flag=N;a01001-Rtd=-0.6,a01001-Flag=N;a99999-Rtd=0,a99999-Flag=N;a01008-Rtd=349,a01008-Flag=N&&3300/// </summary>public class HJ212{public string header = "##"; // 包头 2字符public string dataLen = "0000"; // 数据段长度 4整数,如长度128,写为"0128"HJ212Data data; // 数据段 0<=n<=9999public string crc = "0000"; // CRC校验码 4hexpublic string tailer = "\r\n"; // 包尾 2字符public string full; // 全数据,整包/// 输出public override string ToString(){string dataStr = data.ToString();uint crc16 = CommonUtils.GetCrc16(Encoding.UTF8.GetBytes(dataStr));return string.Format("{0}{1:D4}{2}{3:X4}{4}", header, dataStr.Length, dataStr, crc16, tailer); // D4标示4位10进制数字,X4表示4位16进制数}// 有效性public bool IsValid(){return bValid;}// 有效性public bool IsDataValid(){return this.data.CN == "2011" || this.data.CN == "2051" || this.data.CN == "2061"|| this.data.CN == "2031" || this.data.CN == "2041" || this.data.CN == "3020";}// 长度public int Size(){return header.Length + dataLen.Length + data.Size() + crc.Length + tailer.Length;}// 清空数据区public void ClearCp(){this.data.CP = "";this.data.CPs.Clear();}// 设置是否需要应答public void SetNeedReply(bool need){data.SetFlag(1, need);}// 设置是否有包号public void SetNeedSubPack(bool need){data.SetFlag(2, need);if (!need){data.PNUM = "";data.PNO = "";}}// 是否需应答public int IsNeedReply(){return data.GetFlag(1);}// 是否有包号public int IsNeedSubPack(){return data.GetFlag(2);}// 数据区。字段与其值用‘=’连接;// 在数据区中,同一项目的不同分类值间用‘,’来分隔,不同项目之间 用‘;’来分隔。public void Combine(){this.dataLen = string.Format("{0:D4", this.data.Size());string dataStr = this.data.ToString();uint jisuanCrc = CommonUtils.GetCrc16(Encoding.UTF8.GetBytes(dataStr));this.crc = string.Format("{0:D4", jisuanCrc);this.bValid = true;}public HJ212(string str){this.full = str;int totalSize = str.Length;dataLen = str.Substring(header.Length, dataLen.Length);int dLen = Int32.Parse(dataLen);if ((10 + dLen) > totalSize) {return;}string dataStr = str.Substring(header.Length + dataLen.Length, dLen);data = new HJ212Data(dataStr);crc = str.Substring(header.Length + dataLen.Length + dLen, crc.Length);//uint jisuanCrc16 = CommonUtils.GetCrc16(dataStr.ToCharArray());uint jisuanCrc16 = CommonUtils.GetCrc16(Encoding.UTF8.GetBytes(dataStr));int srcCrc16 = Int32.Parse(crc, System.Globalization.NumberStyles.HexNumber);if (srcCrc16 != jisuanCrc16) {return;}bValid = true;}public HJ212(HJ212 r){this.header = r.header;this.dataLen = r.dataLen;//this.data = new HJ212Data(r.data.ToString());this.data = r.data;this.crc = r.crc;this.tailer = r.tailer;this.full = r.full;this.bValid = r.bValid;}public HJ212(string st, string cn, string mn, string pw, string cp, bool needReply){this.data.QN = DateTime.Now.ToString("yyyyMMDDHHMMss000");this.data.ST = st;this.data.CN = cn;this.data.PW = pw;this.data.CP = cp;this.SetNeedReply(needReply);this.Combine();}// 获取响应报文public HJ212 GetDataResponse(string cn){HJ212 outHJ212 = new HJ212(this);outHJ212.data.ST = "91";outHJ212.data.CN = cn;outHJ212.ClearCp();outHJ212.Combine();return outHJ212;}/// <summary>/// 解析字符串,获取HJ212协议数据列表/// </summary>/// <param name="buffer">输入字符串</param>/// <returns>返回解析后的HJ212协议列表</returns>public static List<HJ212> Parse(string buffer){List<HJ212> hj212List = new List<HJ212>();int bufferSize = buffer.Length;string strTemp = "";for (int i = 0; i < bufferSize; i++){if ( i < bufferSize - 1 && buffer[i] == '#' && buffer[i + 1] == '#'){if (strTemp.Length > 0){hj212List.Add(new HJ212(strTemp));strTemp = "";}}strTemp += buffer[i];// 遍历到HJ212协议末尾,末尾以\r\n结束if (i > 0 && buffer[i - 1] == '\r' && buffer[i] == '\n'){if (strTemp.Length > 0){hj212List.Add(new HJ212(strTemp));strTemp = "";}}}if (strTemp.Length > 0){hj212List.Add(new HJ212(strTemp));strTemp = "";}return hj212List;}private bool bValid = false;}
}
测试程序
新建一个基于C# .Net的控制台程序ConsoleHJ212App
,然后输入如下测试代码:
using HJ212ParseLibrary;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace ConsoleHJ212App
{public class Program{static void Main(string[] args){string buffer = "##0637ST=22;CN=2011;PW=123456;MN=LB4200001;CP=&&DataTime=20210107143500;a05002-Rtd=0,a05002-Flag=N;a21005-Rtd=0.825,a21005-Flag=N;a34006-Rtd=874,a34006-Flag=N;a24088-Rtd=0,a24088-Flag=N;a21003-Rtd=5.656,a21003-Flag=N;a21004-Rtd=9.253,a21004-Flag=N;a21002-Rtd=17.894,a21002-Flag=N;a05024-Rtd=5.803,a05024-Flag=N;a34007-Rtd=2679,a34007-Flag=N;a34002-Rtd=123.97,a34002-Flag=N;a34004-Rtd=24.82,a34004-Flag=N;a01006-Rtd=1032.3,a01006-Flag=N;a01002-Rtd=20.9,a01002-Flag=N;a21026-Rtd=0.795,a21026-Flag=N;a01007-Rtd=3.3,a01007-Flag=N;a34049-Rtd=3553,a34049-Flag=N;a01001-Rtd=-0.6,a01001-Flag=N;a99999-Rtd=0,a99999-Flag=N;a01008-Rtd=349,a01008-Flag=N&&3300\r\n##0404ST=22;CN=2011;PW=123456;MN=LB4200001;CP=&&DataTime=20210107152600;a05002-Rtd=1.755,a05002-Flag=N;a21005-Rtd=0.699,a21005-Flag=N;a34006-Rtd=845,a34006-Flag=N;a24088-Rtd=4.945,a24088-Flag=N;a21003-Rtd=3.877,a21003-Flag=N;a21002-Rtd=14.502,a21002-Flag=N;a05024-Rtd=10.389,a05024-Flag=N;a34007-Rtd=2859,a34007-Flag=N;a21026-Rtd=1.525,a21026-Flag=N;a34049-Rtd=3704,a34049-Flag=N;a99999-Rtd=6.7,a99999-Flag=N&&9C01\r\n";List<HJ212> hj212List = HJ212.Parse(buffer);foreach (HJ212 item in hj212List){Console.WriteLine(item.ToString());}}}
}
并引用HJ212ParseLibrary
库项目,在VS2022中运行结果如下图所示:
运行结果如下:
可以看到,我们输入的数据和解析到的数据是一致的,有2个协议数据报文。