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;
}