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-field
name = j.Key.Substring(0, f2);
field = j.Key.Substring(f2 + 1);
// i11001-Info field
if (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=YYYYMMDDHHmmssZZZ
public string ST; // 系统编码 5字符 ST=21
public string CN; // 命令编码 7字符 CN=2011
public string PW; // 访问密码 9字符 PW=123456
public 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<=9999
public string crc = "0000"; // CRC校验码 4hex
public 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个协议数据报文。