环保在线监控数据接入的事实标准。这篇笔记把 HJ 212-2017 的报文结构、交互机制、编码体系,以及实际对接中最容易踩的坑,一次性梳理清楚。
一、这份标准解决什么问题
HJ 212-2017《污染物在线监控(监测)系统数据传输标准》由国家环境保护部于 2017 年 4 月 24 日发布、2017 年 5 月 1 日实施,代替 HJ/T 212-2005。它规定了污染物在线监控(监测)系统、污染物排放过程(工况)自动监控系统与监控中心之间的数据传输过程,明确了参数命令、交互命令、数据命令、控制命令的格式与代码定义,同时也规定了在线监控仪器仪表与数据采集传输仪(数采仪)之间的传输格式。
相比 2005 版,这次修订有几个关键变化,做兼容时必须注意:
- 适用范围扩展,新增污染物排放过程(工况)自动监控设备与监控中心之间的数据交换;
- 新增数采仪与在线监控仪器仪表之间的通讯协议;
- 新增现场机与上位机之间的数据传输网络;
- 修改了设备唯一标识 MN 的描述;
- 修改了是否拆分包及应答标志 Flag 的描述;
- 删除了报警相关的字段、命令字与通讯示例;
- 增加工况监控监测因子编码规则,明确系统编码与命令的分类并扩充;
- 扩充执行结果定义表、请求返回表,新增实时数据标记定义表;
- 单独列出《声环境监测因子编码表》,完善污水、烟气排放量及均值计算方法。
标准允许扩展,但扩展内容不得与标准中已使用或保留的控制命令冲突。
二、系统结构与协议层次
系统自下而上分三层:现场机 → 传输网络 → 上位机。有两种典型构成方式:
- 一台(套)现场机集监控(监测)、存储、通讯传输为一体,直接经传输网络与上位机交互;
- 现场有多套仪器仪表,经数字输出接口连接到独立的数采仪,上位机与数采仪通讯。
协议层次上有一个关键定位:HJ 212 对应 ISO/OSI 模型中的应用层,基础传输层建构在 TCP/IP 之上,整个应用层协议与具体通讯介质无关。也就是说 GPRS、ADSL、CDMA、WCDMA、TD-SCDMA、CDMA2000、PLC、TD-LTE、FDD-LTE、WiMAX 都可以承载。现网设备大多已换成 4G 或专线,但协议报文本身一字不用改——这也是这套标准能沿用至今的原因。
三、报文结构:外层五段,数据段九个字段
所有通讯包由 ASCII 字符组成(汉字除外,采用 UTF-8 编码,8 位 1 字节)。
3.1 外层结构
| 名称 | 类型 | 长度 | 说明 |
|---|---|---|---|
| 包头 | 字符 | 2 | 固定为 ## |
| 数据段长度 | 十进制整数 | 4 | 数据段的 ASCII 字符数,如长度 255 写作 0255 |
| 数据段 | 字符 | 0 ≤ n ≤ 1024 | 变长,结构见下表 |
| CRC 校验 | 十六进制整数 | 4 | 对数据段计算的结果,算法见附录 A |
| 包尾 | 字符 | 2 | 固定为 <CR><LF>(回车、换行) |
3.2 数据段结构
注意表里的"长度"包含字段名称 + = + 字段内容三部分。
| 名称 | 长度 | 说明 |
|---|---|---|
| 请求编码 QN | 20 | 精确到毫秒的时间戳 YYYYMMDDhhmmsszzz,用来唯一标识一次命令交互 |
| 系统编码 ST | 5 | 取值见《系统编码表》 |
| 命令编码 CN | 7 | 取值见《命令编码表》 |
| 访问密码 PW | 9 | 设备访问密码 |
| 设备唯一标识 MN | 27 | 固化在设备中,由 EPC-96 编码转化而来,即 24 位 0~9、A~F 字符 |
| 拆分包及应答标志 Flag | 8 | 位域,见下 |
| 总包数 PNUM | 9 | 分包时才有,与 Flag 有关 |
| 包号 PNO | 8 | 分包时才有,与 Flag 有关 |
| 指令参数 CP | 0 ≤ n ≤ 950 | CP=&&数据区&& |
3.3 Flag 位域:一个字节塞了三件事
Flag 是 0~255 的整数,位定义如下:
| 位 | 含义 |
|---|---|
| V5~V0 | 标准版本号。000000 表示 HJ/T 212-2005,000001 表示本次修订版本 |
| A | 命令是否应答:1 应答,0 不应答 |
| D | 是否包含数据包序号:1 数据段中含包号和总包数,0 不含 |
标准给的例子是 Flag=7:标准版本为本次修订版、数据段需要拆分、命令需要应答。实际对接中 Flag=5(本次修订版 + 不拆分 + 需要应答)最常见。
3.4 数据区语法
- 字段与其值用
=连接; - 同一项目的不同分类值之间用
,分隔; - 不同项目之间用
;分隔; - 字段名区分大小写,单词首字符大写,其余小写。
数据类型记法需要先建立概念:C4 表示最多 4 位字符型字符串;N5 表示最多 5 位数字型字符串;N14.2 表示 14 位整数 + 2 位小数、带符号、最大长度 18 的可变长数字型;此外还有 YYYY/MM/DD/hh/mm/ss/zzz 分别表示日期的年、月、日与时间的小时、分、秒、毫秒。
四、交互机制:请求—应答 + 超时重发
4.1 应答模式
一条完整的命令由请求方发起、响应方应答组成,共六步:
- 请求方发送请求命令给响应方;
- 响应方接到请求后,向请求方发送请求应答(握手完成);
- 请求方收到请求应答后,等待响应方回应执行结果;未收到请求应答则按请求回应超时处理;
- 响应方执行请求操作;
- 响应方发送执行结果给请求方;
- 请求方收到执行结果,命令完成;没有接收到则按执行超时处理。
这里要划重点:"请求应答"和"执行结果"是两条独立消息。收到应答只代表对方愿意干活,业务是否成功要看执行结果里的 ExeRtn。
4.2 超时与重发
- 请求命令发出后,在规定时间内未收到回应视为超时;
- 超时后重发,重发超过规定次数仍未收到回应,视为通讯不可用,通讯结束;
- 超时时间与重发次数均可根据通讯方式和任务性质自定义。
标准给出的缺省值:
| 通讯类型 | 缺省超时(秒) | 重发次数 |
|---|---|---|
| GPRS | 10 | 3 |
| CDMA | 10 | 3 |
| ADSL | 5 | 3 |
| WCDMA | 10 | 3 |
| TD-SCDMA | 10 | 3 |
| CDMA2000 | 10 | 3 |
| PLC | 10 | 3 |
| TD-LTE | 10 | 3 |
| FDD-LTE | 10 | 3 |
| WIMAX | 10 | 3 |
五、编码体系:系统、命令、返回码、数据标记
5.1 系统编码 ST
分四类:10~29 环境质量、30~49 环境污染源、50~69 工况、91~99 系统交互,A0~Z9 用于扩展。
| 系统名称 | 编码 | 系统名称 | 编码 |
|---|---|---|---|
| 地表水质量监测 | 21 | 大气环境污染源 | 31 |
| 空气质量监测 | 22 | 地表水体环境污染源 | 32 |
| 声环境质量监测 | 23 | 地下水体环境污染源 | 33 |
| 地下水质量监测 | 24 | 海洋环境污染源 | 34 |
| 土壤质量监测 | 25 | 土壤环境污染源 | 35 |
| 海水质量监测 | 26 | 声环境污染源 | 36 |
| 挥发性有机物监测 | 27 | 振动环境污染源 | 37 |
| 放射性环境污染源 | 38 | 工地扬尘污染源 | 39 |
| 电磁环境污染源 | 41 | 烟气排放过程监控 | 51 |
| 污水排放过程监控 | 52 | 系统交互(现场机与上位机交互) | 91 |
5.2 命令编码 CN
共四类命令(请求、上传、通知、交互),编码分四组:
1000~1999初始化命令和参数命令;2000~2999数据命令;3000~3999控制命令;9000~9999交互命令。
常用的几条:
| 命令名称 | 编码 | 方向 |
|---|---|---|
| 设置超时时间及重发次数 | 1000 | 上位 → 现场(请求) |
| 提取现场机时间 | 1011 | 上位 → 现场(请求) |
| 上传现场机时间 | 1011 | 现场 → 上位(上传) |
| 设置现场机时间 | 1012 | 上位 → 现场(请求) |
| 现场机时间校准请求 | 1013 | 现场 → 上位(通知) |
| 提取 / 上传实时数据间隔 | 1061 | 请求 / 上传 |
| 设置实时数据间隔 | 1062 | 上位 → 现场(请求) |
| 提取 / 上传分钟数据间隔 | 1063 | 请求 / 上传 |
| 设置分钟数据间隔 | 1064 | 上位 → 现场(请求) |
| 设置现场机密码 | 1072 | 上位 → 现场(请求) |
| 取 / 上传污染物实时数据 | 2011 | 请求 / 上传 |
| 停止察看污染物实时数据 | 2012 | 通知 |
| 请求应答 | 9011 | 响应方 → 请求方 |
| 执行结果应答 | 9012 | 响应方 → 请求方 |
5.3 请求命令返回 QnRtn
1 准备执行请求、2 请求被拒绝、3 PW 错误、4 MN 错误、5 ST 错误、6 Flag 错误、7 QN 错误、8 CN 错误、9 CRC 校验错误、100 未知错误。
排查现场问题时这张表非常好用——对方返回 3 就是密码不对,返回 9 就是你的 CRC 算错了,几乎可以做到秒级定位。
5.4 执行结果 ExeRtn
1 执行成功、2 执行失败但不知道原因、3 命令请求条件错误、4 通讯超时、5 系统繁忙不能执行、6 系统故障、100 没有数据。
5.5 数据标记:最容易被忽略、却最重要的一张表
| 标记 | 含义 |
|---|---|
| N | 在线监控(监测)仪器仪表工作正常 |
| F | 在线监控(监测)仪器仪表停运 |
| M | 仪器仪表处于维护期间产生的数据 |
| S | 手工输入的设定值 |
| D | 在线监控(监测)仪器仪表故障 |
| C | 仪器仪表处于校准状态 |
| T | 仪器仪表采样数值超过测量上限 |
| B | 仪器仪表与数采仪通讯异常 |
做数据接入时,绝不能只存数值、丢掉标记。同一个 -Rtd 值,标记是 N 和标记是 C,在有效性判别和报表统计里完全是两回事。
六、CRC16:算错一个字节,整包作废
标准采用 ANSI CRC16,简称 CRC16。生成步骤:
- CRC16 校验寄存器赋值为
0xFFFF; - 取被校验串的第一个字节赋给临时寄存器;
- 临时寄存器与 CRC16 校验寄存器的高位字节做"异或"运算,结果赋给 CRC16 校验寄存器;
- 取 CRC16 校验寄存器最后一位赋给检测寄存器;
- 把 CRC16 校验寄存器右移一位;
- 若检测寄存器值为 1,则 CRC16 校验寄存器与多项式
0xA001做"异或"运算,结果赋给 CRC16 校验寄存器; - 重复步骤 4~6,直至移出 8 位;
- 取被校验串的下一个字节赋给临时寄存器;
- 重复步骤 3~8,直至所有字节均被校验;
- 返回 CRC16 校验寄存器的值。
校验码按照先高字节后低字节的顺序存放。
标准附录 A 给出的参考实现:
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;
}
生产代码里建议替换为查表法,避免在实时上报链路上产生不必要的 CPU 开销。
七、拆一条完整报文
标准附录 A 里那条经典示例:
##0101QN=20160801085857223;ST=32;CN=1062;PW=100000;MN=010000A8900016F000169DC0;Flag=5;CP=&&RtdInterval=30&&1C80\r\n
逐段拆开看:
##—— 包头;0101—— 数据段长度为 101 个 ASCII 字符(实测数据段确实是 101 字节,与字段一致);QN=20160801085857223—— 2016 年 8 月 1 日 8 时 58 分 57 秒 223 毫秒触发的一次命令交互;ST=32—— 地表水体环境污染源;CN=1062—— 设置实时数据间隔;PW=100000—— 访问密码;MN=010000A8900016F000169DC0—— 设备唯一标识;Flag=5—— 本次修订版本号、不分包、需要应答;CP=&&RtdInterval=30&&—— 实时采样数据上报间隔 30 秒(标准规定取值 30 ≤ n ≤ 3600);1C80—— 数据段的 CRC16 校验码;\r\n—— 包尾。
顺便说个细节:标准正文在解释这条示例时写的是"1C08 为 CRC16 校验码",但按附录 A 的算法对数据段实际计算,结果应为 0x1C80,报文示例中给出的也是 1C80。以算法和报文示例为准——这种正文与示例不一致的地方,实现时一定要用实测数据兜底,别照抄文本。
再来看一条实时数据上传(CN=2011)的数据区:
CP=&&DataTime=20160801085857;w01001-Rtd=7.1,w01001-Flag=N;w01018-SampleTime=20160801070000,01018-Rtd=2.2,w01018-Flag=N,w01018-EFlag=A01;…&&
DataTime—— 本组数据的数据时间;w01001-Rtd—— 某监测因子的实时值,w前缀表示水类因子;w01001-Flag=N—— 该因子的仪器数据标记,N表示仪器工作正常;w01018-SampleTime—— 采样时间(非连续测量仪器才有意义);w01018-Flag=N—— 该因子的仪器数据标记;w01018-EFlag=A01—— 该因子的扩充数据标记,由仪器仪表厂商自行定义。
7.1 顺带说清一个极易混淆的点:-Flag 与 -EFlag 不是一回事
《字段对照表》里这两个是并列的独立字段,不是同一个东西的两种写法:
| 字段 | 含义 | 类型 | 取值说明 |
|---|---|---|---|
xxxxxx-Flag |
监测仪器数据标记 | A-Z/0-9,C1 | 见表 8,即 N/F/M/S/D/C/T/B |
xxxxxx-EFlag |
监测仪器扩充数据标记 | A-Z/0-9,C4 | 由在线监控(监测)仪器仪表设备自行定义 |
区别很关键:
-Flag的语义由标准统一规定,解析时必须按表 8 翻译,全行业一致;-EFlag是留给厂商的扩展位,标准不定义含义,同一个值在 A 厂和 B 厂可能完全是两回事,且"可以没有此项,根据实际情况确定"。
所以对接不同厂家的设备时,-EFlag 一定要逐家问清定义,绝不能想当然地当成"超标标记"来用——这是现场很常见的一个误读。
可以看到,同一因子通过 -Rtd、-Flag、-SampleTime、-EFlag 成组出现,用 , 分隔;不同因子之间用 ; 分隔,正好对应第 3.4 节的语法规则。
八、工程落地:七个必须处理好的点
- 分包重组。
Flag的 D 位决定数据段里是否带PNUM/PNO。接收端必须先按 QN + 包序号完成重组,再交给业务层,否则长报文(比如历史数据补传)一定解析失败。 - QN 的复用与配对。一次命令交互的请求、请求应答、执行结果必须共用同一个 QN,它是配对的唯一依据。重发时不要重新生成 QN,否则对端会当成新命令。
- 区分应答与执行结果。
CN=9011只代表"我收到并准备执行",CN=9012的ExeRtn才是业务结果。把它们混为一谈是极常见的实现错误。 - 编码规范。
MN固定 24 位十六进制;汉字用 UTF-8;数字型字段注意N14.2这类"带符号、带小数点、最大 18 位"的表述,不要按定长截断。 - CRC 校验失败的包直接丢弃。不要试图"修复"或跳过校验,否则脏数据会污染整个数据链。
- 数据标记必须入库。
N/F/M/S/D/C/T/B是后续解释异常值、做有效数据判别的唯一依据。 - 落库要做幂等。重发机制天然决定了同一份数据可能重复到达,建议按(MN + CN + DataTime + 因子编码)建唯一约束,把去重交给数据库。
九、小结
HJ 212-2017 的设计其实非常克制:外层只用一个 ## 包头 + 长度 + 数据段 + CRC + 包尾,数据段用 字段=值 的扁平结构,靠 QN 串联交互、靠 Flag 表达控制语义、靠编码表承载业务含义。理解了这四件事——报文骨架、QN 配对、Flag 位域、编码表——整套协议就没有黑盒了。
真正决定对接效率的往往不是协议本身,而是边角:分包重组有没有做、CRC 是不是严格校验、数据标记有没有保留、重发幂等有没有考虑。这几点做扎实,现场调试基本就是"通一遍就过"。
关于作者
我长期从事环保在线监控(监测)数据接入与上位机平台研发,熟练使用 HJ 212-2017 数据传输标准,对该标准的报文结构、应答与超时重发机制、系统编码与命令编码体系、分包与重组逻辑、CRC16 校验实现,以及水、气、声各类监测因子编码都有完整的实战经验;同时具备 HJ/T 212-2005 历史报文的兼容处理能力。
在工程实现上,我能独立完成从 TCP 长连接接入、报文解析与校验、分包重组、上下行命令交互(含 2011/2051 等数据命令与 1000/1011/1061 等参数命令)、异常数据标记判别,到数据落库与幂等设计的全链路开发,技术栈以 Java / Spring Boot 为主,并熟悉 SQL Server、Kingbase 等数据库。如果你正在做数采接入、协议对接或环保数据平台,欢迎交流。