HJ212协议标准报文解析

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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容