☰
Java解析HJ212协议实战:报文结构、CRC校验与编码处理
2026/9/25 23:54:49 网站建设 项目流程

简介:本资源面向环保监测系统开发工程师与Java学习者,提供国标HJ212协议(污染源在线自动监控数据传输标准)的完整解析实现,可直接导入Eclipse项目调用。包内共308个文件,以197个class编译文件与100个java源码为主,另含少量xml、properties、classpath等配置与说明文件,压缩包约327KB,结构紧凑便于快速集成。内容覆盖协议数据结构、编码解码、XML/JSON处理、Socket网络通信、异常处理与JUnit测试等关键环节,并封装了T212Mapper、SegmentParser、DataConverter、T212Factory等核心解析类,读者可据此理解协议字段映射与数据转换逻辑,直接复用解析方法或按需扩展。目前已有5072人学习下载,适合需要快速落地HJ212数据采集与解析的开发者参考。

1. HJ212 协议在 Java 里到底难在哪:从一条报文说起

如果你手上有环保监测设备的对接任务,大概率绕不开 HJ212。它全称是《污染物在线监控(监测)系统数据传输标准》,现场数采仪、CEMS、水质分析仪往上位机传数据,走的基本都是这套报文格式。很多人第一次拿到抓包数据会愣一下:##0131QN=20240101120000001;ST=32;CN=2011;PW=123456;MN=HY0001;CP=&&DataTime=20240101120000;a01001-Rtd=12.5,a01001-Flag=N&&C4E1,看着像字符串拼接,真动手解析才发现坑不少。

Java 做这块的诉求很集中:把这一长串文本稳定地拆成对象,把字段映射到业务模型,再回一条应答报文。难点不在语法,而在协议本身的几个特性——##和&&双重分隔、CP字段里还嵌了一层键值对、CRC 校验要按字节算、中文编码在不同厂商设备上还不统一。这篇文章就按我实际做过的路子,把 Java 解析 HJ212 从报文结构、解析器实现、应答构造到踩坑排查讲透,适合正在做环保数据接入的后端和物联网方向的人。

2. 拆解 HJ212 报文结构:先搞清楚每一段是什么

2.1 报文的分层结构

HJ212 的报文是纯文本,但它是分层的。最外层用##开头,后面跟 4 位数据段长度,然后是数据段,最后是 4 位 CRC 校验。数据段内部用;分隔各个字段,字段是KEY=VALUE形式。其中CP这个字段特殊,它的值被&&包起来,里面又是一层用;分隔的键值对,比如CP=&&DataTime=20240101120000;a01001-Rtd=12.5;a01001-Flag=N&&。

理解这个两层结构是写解析器的前提。我一般把它抽象成三层:帧层(##、长度、CRC)、命令层(QN、ST、CN、PW、MN、CP 这些)、数据层(CP 内部的监测因子)。很多人翻车就翻在把 CP 当成普通字符串处理,结果里面的;和外层的;混在一起,split 出来全乱套。

层级组成分隔符说明
帧层##+ 长度 + 数据段 + CRC固定位置长度是数据段的字节数,CRC 是数据段的校验
命令层QN/ST/CN/PW/MN/CP 等;请求命令标识、系统编码、命令编码
数据层CP 内部键值对;和&&监测因子编码、值、标记

2.2 关键字段的含义和取值

QN 是请求编号,格式是YYYYMMDDHHmmssSSS加 3 位序列号,用来做请求应答的匹配,这个字段必须原样回传。ST 是系统编码,常见 21 是地表水、22 是污水、31 是烟气、32 是环境空气。CN 是命令编码,2011 是上传污染物数据,2051 是取实时数据,9011 是心跳。MN 是设备唯一标识,PW 是访问密码,这两个通常用来做设备鉴权。

CP 里的监测因子编码是国标定义的,比如a01001是烟尘、a01002是二氧化硫、w01001是 pH。每个因子后面跟-Rtd表示实时值,-Flag表示数据标记(N 正常、T 超时、D 故障等)。这些编码建议做成枚举或者配置表,不要硬编码在解析逻辑里,否则新增因子就得改代码。

提示:不同厂商对 CP 内部字段的顺序和数量处理不一致,有的会省略 Flag,有的会加自定义字段,解析时不要假设字段一定存在。

3. 用 Java 写一个能跑的 HJ212 解析器

3.1 先定义数据模型

动手写解析之前,先把模型定下来。我一般分两个类:一个Hj212Frame表示整帧,一个Hj212Command表示命令层,CP 内部的数据用一个Map<String, String>或者专门的Hj212Data类承载。这样职责清晰,后面加字段也方便。

public class Hj212Frame { private int length; // 数据段长度 private String dataSegment; // 数据段原文 private String crc; // 4位CRC private Hj212Command command; // getter/setter 省略 } public class Hj212Command { private String qn; // 请求编号 private String st; // 系统编码 private String cn; // 命令编码 private String pw; // 密码 private String mn; // 设备标识 private String cp; // CP 原文 private Map<String, String> data = new LinkedHashMap<>(); // CP 解析结果 // getter/setter 省略 }

模型里 CP 原文和解析后的 data 都保留,是因为排查问题时经常要对照原文。用LinkedHashMap而不是HashMap,是为了保持字段顺序,回写报文时顺序一致,方便和原始报文做 diff。

3.2 帧层解析:长度和 CRC 怎么处理

帧层解析的核心是定位数据段边界和校验。数据段长度是 4 位十进制数,紧跟在##后面。拿到长度后,从第 7 个字符开始截取对应字节数的内容就是数据段,再往后 4 位是 CRC。

public static Hj212Frame parseFrame(String raw) { if (raw == null || !raw.startsWith("##")) { throw new IllegalArgumentException("报文必须以 ## 开头"); } // 长度字段:第 3-6 位 int length = Integer.parseInt(raw.substring(2, 6)); // 数据段:从第 7 位开始,长度按字节算 byte[] rawBytes = raw.getBytes(StandardCharsets.UTF_8); // 注意:长度是字节数,不是字符数,中文场景要按字节截 String dataSegment = new String(rawBytes, 6, length, StandardCharsets.UTF_8); // CRC:数据段之后 4 位 String crc = new String(rawBytes, 6 + length, 4, StandardCharsets.UTF_8); Hj212Frame frame = new Hj212Frame(); frame.setLength(length); frame.setDataSegment(dataSegment); frame.setCrc(crc); return frame; }

这里有个血泪经验:长度字段是字节数,不是字符数。如果报文里有中文(比如某些厂商在 CP 里塞了中文描述),用substring按字符截会错位。上面代码先转成字节数组再按字节截,能规避这个问题。CRC 校验算法是 CRC-16/CCITT,多项式0x1021,初始值0xFFFF,具体实现见下一节。

3.3 命令层解析:split 的边界在哪

命令层用;分隔,但 CP 的值里也有;,所以不能直接对整个数据段 split。正确做法是先找到CP=&&的位置,把它之前的部分按;拆,CP 单独处理。

public static Hj212Command parseCommand(String dataSegment) { Hj212Command cmd = new Hj212Command(); int cpIndex = dataSegment.indexOf("CP=&&"); String head = cpIndex >= 0 ? dataSegment.substring(0, cpIndex) : dataSegment; String cpPart = cpIndex >= 0 ? dataSegment.substring(cpIndex) : ""; // 解析 CP 之前的字段 for (String kv : head.split(";")) { if (kv.isEmpty()) continue; int eq = kv.indexOf('='); if (eq < 0) continue; String key = kv.substring(0, eq); String value = kv.substring(eq + 1); switch (key) { case "QN": cmd.setQn(value); break; case "ST": cmd.setSt(value); break; case "CN": cmd.setCn(value); break; case "PW": cmd.setPw(value); break; case "MN": cmd.setMn(value); break; default: break; } } // 解析 CP 内部 if (cpPart.startsWith("CP=&&")) { String inner = cpPart.substring(5); int end = inner.indexOf("&&"); if (end >= 0) inner = inner.substring(0, end); cmd.setCp(inner); for (String kv : inner.split(";")) { if (kv.isEmpty()) continue; int eq = kv.indexOf('='); if (eq < 0) continue; cmd.getData().put(kv.substring(0, eq), kv.substring(eq + 1)); } } return cmd; }

逻辑说明:先定位CP=&&,把数据段切成头部和 CP 两部分,头部按;拆没问题,因为头部不含嵌套。CP 部分去掉&&后再按;拆,得到监测因子。参数上要注意indexOf找的是第一个CP=&&,正常报文只有一个,如果厂商拼了多个 CP 就得改成循环处理。inner.indexOf("&&")找结束标记,防止后面还有多余内容。

3.4 CRC 校验实现

CRC 是很多人容易忽略的一步,但现场设备如果校验不过会直接丢包。HJ212 用的是 CRC-16/CCITT-FALSE,实现如下。

public static String crc16(String data) { byte[] bytes = data.getBytes(StandardCharsets.UTF_8); int crc = 0xFFFF; for (byte b : bytes) { crc ^= (b & 0xFF) << 8; for (int i = 0; i < 8; i++) { if ((crc & 0x8000) != 0) { crc = (crc << 1) ^ 0x1021; } else { crc <<= 1; } crc &= 0xFFFF; } } return String.format("%04X", crc); }

参数说明:初始值0xFFFF,多项式0x1021,不反转输入输出,结果取低 16 位格式化成 4 位大写十六进制。校验的对象是数据段,不含##和长度字段。如果算出来和报文里的 CRC 不一致,先确认编码是不是 UTF-8,有些老设备用 GBK,字节不同 CRC 自然不同。

4. 构造应答报文和编码处理

4.1 应答报文的组装顺序

设备上传数据后,上位机要回一条应答,否则设备可能重传。应答的 CN 通常是上传命令加 1,比如收到 2011 回 2012。QN 必须原样带回,这是请求应答匹配的关键。组装顺序是:##+ 长度 + 数据段 + CRC,长度和 CRC 都要重新算。

public static String buildAck(Hj212Command req) { String qn = req.getQn(); String st = req.getSt(); String cn = String.valueOf(Integer.parseInt(req.getCn()) + 1); String mn = req.getMn(); String pw = req.getPw(); // 应答的 CP 一般带确认结果 String cp = "&&DataTime=" + qn.substring(0, 14) + ";ExeRtn=1&&"; String dataSegment = "QN=" + qn + ";ST=" + st + ";CN=" + cn + ";PW=" + pw + ";MN=" + mn + ";CP=" + cp; String crc = crc16(dataSegment); int len = dataSegment.getBytes(StandardCharsets.UTF_8).length; return "##" + String.format("%04d", len) + dataSegment + crc; }

逻辑说明:CN 加 1 是国标约定,2011 的应答是 2012。ExeRtn=1表示执行成功,0 表示失败。长度用字节数,和解析时保持一致。这里 QN 截前 14 位当 DataTime 是个简化处理,严格来说应该用当前时间,但很多场景下设备只校验 QN 匹配,用请求时间也能过。

4.2 编码问题的处理

编码是 HJ212 对接里最玄学的一环。国标没强制规定字符集,现场设备有用 GBK 的,有用 UTF-8 的,还有用 ASCII 但塞了中文的。我的做法是:解析时先按 UTF-8 试,如果 CRC 对不上或者出现乱码,再按 GBK 重试。配置层面给每个设备留一个编码字段,不要全局写死。

public static String decode(byte[] bytes, String charset) { try { return new String(bytes, charset); } catch (Exception e) { return new String(bytes, StandardCharsets.UTF_8); } }

实际项目里我会在设备接入配置表加一列charset,默认 UTF-8,遇到问题设备单独改。这样比在代码里到处 try-catch 要干净。另外注意,长度字段是按字节算的,所以解析和组装时都要用同一套编码,否则长度对不上,CRC 也会错。

5. 避坑与排查:那些让我加班到凌晨的问题

5.1 CRC 校验总是不通过

现象:解析出来的数据段看着没问题,但 CRC 和报文里的对不上。原因通常是编码不一致,或者校验对象搞错了。有人把##和长度字段也算进 CRC,那肯定错。解决:确认校验对象是纯数据段,确认编

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询