HJ212分为2005年(HJ/T212-2005)和2017年(HJ212-2017)的版本,略有不同。
网上没找到非常官方的渠道下载,在这贴一份2017年版本的下载地址
TCP/IP通讯包组成
名称 | 类型 | 长度 | 描述 |
---|---|---|---|
包头 | 字符 | 2 | 固定为## |
数据段长度 | 十进制整数 | 4 | 表示数据段长度,如长度336则为0336 |
数据段 | 字符 | 0-1024 | 变长的数据,为包的传输内容 |
CRC校验 | 十六进制整数 | 4 | 用于校验数据包完整性的CRC校验值,后续附上算法 |
包尾 | 字符 | 2 | 固定为<CR><LF>(回车、换行) |
通讯包数据段组成
- HJ/T212-2005
名称 | 类型 | 长度 | 描述 |
---|---|---|---|
请求编码QN | 字符 | 20 | 精确到毫秒的时间戳:QN=YYYYMMDDhhmmsszzz,用来唯一标识一次命令交互 |
系统编码ST | 字符 | 5 | 系统编号 |
命令编号CN | 字符 | 7 | 命令编号 |
访问密码PW | 字符 | 6 | 访问密码 |
设备唯一标识MN | 字符 | 14 | 监测点编号 |
拆分包及应答标志Flag | 字符 | 3 | 见文档原文 |
总包号PNUM | 字符 | 4 | 表示本次通讯总共包含的包数 |
包号PNO | 字符 | 4 | PNO指示当前数据包的包号 |
指令参数CP | 字符 | 0-960 | CP=&&数据区&& |
- HJ212-2017
名称 | 类型 | 长度 | 描述 |
---|---|---|---|
请求编码QN | 字符 | 20 | 精确到毫秒的时间戳:QN=YYYYMMDDhhmmsszzz,用来唯一标识一次命令交互 |
系统编码ST | 字符 | 5 | 系统编号 |
命令编号CN | 字符 | 7 | 命令编号 |
访问密码PW | 字符 | 9 | 访问密码 |
设备唯一标识MN | 字符 | 27 | 监测点编号 |
拆分包及应答标志Flag | 字符 | 8 | 见文档原文 |
总包号PNUM | 字符 | 9 | 表示本次通讯总共包含的包数 |
包号PNO | 字符 | 8 | PNO指示当前数据包的包号 |
指令参数CP | 字符 | 0-950 | CP=&&数据区&& |
报文解析
因2005、2017对原始报文的解析上没有什么巨大分别,只是不同字段的长度在2017协议中有所扩展。在此仅以一例2005年规范的报文作为样例(因简书格式问题,调试时请自行补全包尾换行符):
"##0336ST=31;CN=2011;PW=123456;MN=63010000020001;CP=&&DataTime=20200108143205;B02-Rtd=9.88,B02-Flag=N;01-Rtd=10.73,01-ZsRtd=12.38,01-Flag=N;02-Rtd=0.705,02-ZsRtd=0.814,02-Flag=N;03-Rtd=69.064,03-ZsRtd=79.684,03-Flag=N;S01-Rtd=5.8,S01-Flag=N;S02-Rtd=19.38,S02-Flag=N;S03-Rtd=99.04,S03-Flag=N;S08-Rtd=-46.12,S08-Flag=N;S05-Rtd=14.66,S05-Flag=N&&8EC1"
- 代码解析
在这里不赘述如何通过TCP/IP协议获得报文再转成字符串的了,直接贴一些关键代码(C#),直接看可能比看协议文档要快一些。
本来是想封装个类库的…不过考虑我实际使用情况只有最简单的接受解析数据,没调试过收发控制命令、数据分包。单纯根据文档封装出来可能会有很大的局限性(其实是懒)。
因力求样例代码精简,可能缺乏防错及数据结构展现,大家可以针对自己实际业务需求做相应完善。
//Msg 是解析TCP/IP报文后获得的报文字符串
if (string.IsNullOrEmpty(Msg) || Msg.Length < 12 || !Msg.StartsWith("##") || !Msg.EndsWith("\r\n"))
{
Console.WriteLine("不是HJ212协议的报文!");
return false;
}
var msg_len_str = Msg.Substring(2, 4);
if (!int.TryParse(msg_len_str, out int msg_len))
{
Console.WriteLine("报文格式非法,报文长度无法解析!");
return false;
}
var content = Msg.Substring(6, msg_len);
var msg_crc = Msg.Substring(6 + msg_len, 4);
var calc_crc = CalcCRC(content);
if (calc_crc != msg_crc)
{
Console.WriteLine($"CRC校验失败! MsgCRC:{msg_crc} CalcCRC:{calc_crc}");
return false;
}
var cp = Regex.Match(content, @"CP=&&[\S]*&&");
var msg_head = string.Empty;
if (!cp.Success)
{
Console.WriteLine("未匹配到数据!");
msg_head = content;
}
msg_head = content.Substring(0, cp.Index);
var headers = msg_head.Split(';');
for (int i = 0; i < headers.Length - 1; i++)
{
var index = headers[i].IndexOf('=');
if (index == -1) continue;
Console.WriteLine($"Header key:{headers[i].Substring(0, index)} Header Value:{headers[i].Substring(index + 1, headers[i].Length - index - 1)}");
}
if (cp.Success)
{
var datadic = cp.Value.Substring(5, cp.Length - 7);
var dataarr = datadic.Split(';');
foreach (var rawdata in dataarr)
{
var items = rawdata.Split(',');
foreach (var rawitem in items)
{
var index = rawitem.IndexOf("=");
if (index == -1)
{
return false;
}
Console.WriteLine($"Data Name:{rawitem.Substring(0, index)} Data Value:{rawitem.Substring(index + 1, rawitem.Length - index - 1)}");
}
}
}
return true;
针对以上报文的解析如下:
对于Data Name
的含义,参照协议文档中的字段对照表即可
如B02-Rtd
代表废气-实时采样数据,不一定完全照搬文档,可与对接厂商根据现场情况拟定。
- CRC算法
CRC算法如下(摘自HJ212-2017,2005版本没有明确定义但实测是兼容的,语言是C):
一定要注意CRC是针对数据段进行计算的,去头(##、报文长度字符),去尾(CRC字符、包尾换行符)
unsigned int CRC16_Checkout(unsigned char *puchMsg, unsigned int usDataLen)
{
unsigned int i, j, crc_reg, check;
crc_reg = 0xFFFF;
for (i = 0; i < usDataLen; i++)
{
crc_reg = (crc_reg >> 8) ^ puchMsg[i];
for (j = 0; j < 8; j++)
{
check = crc_reg & 0x0001;
crc_reg >>= 1;
if (check == 0x0001)
{
crc_reg ^= 0xA001;
}
}
}
return crc_reg;
}