1.HJ212协议介绍
1.1.概述
HJ212 协议是应用于环境监测领域的一种数据传输标准协议。定义了监测设备与数据采集服务器之间的通信流程,包括数据传输、控制指令传输等环节,确保通信的可靠性和稳定性。广泛应用于大气和废气环境监测、水和废水监测、固体废弃物监测、土壤监测、生物污染监测、环境噪声监测、环境放射性监测等多个环境监测领域。例如,在污水处理厂、垃圾填埋场、工厂废气排放口等场所的监测设备,都需要按照 HJ212 协议将监测数据传输到环保部门的监控平台。
1.2.通讯协议数据结构
通讯包结构组成如下:
数据段结构组成如下:

更多详情信息请参考HJ212的协议文档。
2.数据包解析示例
系统收到设备上报的报文,打印出16进制如下:
232330333133514e3d32303234313131353139303430383030303b53543d32373b434e3d323031313b50573d3132333435363b4d4e3d415149323431313133323630303333323b466c61673d353b43503d26264461746154696d653d32303234313131353139303430383b6132313032362d5274643d3131373b6132313030342d5274643d31363b6f33313130342d5274643d32393b6132313030352d5274643d302e31343b6133343030342d5274643d363b6133343030322d5274643d363b6133343030312d5274643d363b6130313030312d5274643d32382e313b6130313030322d5274643d36383b4c412d5274643d34393b6130313030362d5274643d313030323b6130363030312d5274643d302e303b6130343030332d5274643d303b6130313030372d5274643d302e303b6130313030382d5274643d37302626393930310d0a
转出字符串如下:
对字符串进行解析说明如下:
## --包头固定为##
0127 --数据段长度;数据段的 ASCII 字符数,例如:长 255,则写为“0255”
QN=20210320163058511; --请求编码 QN;精确到毫秒的时间戳:QN=YYYYMMDDhhmmsszzz,用来唯一标识一次命令交互
ST=32; --ST=系统编码, 系统编码取值详见 6.6.1 章节的表 5《系统编码表》
CN=2081; --CN=命令编码, 命令编码取值详见 6.6.5 章节的表 9《命令编码表》
PW=123456; --PW=访问密码
MN=81733553213013; --MN=设备唯一标识,这个标识固化在设备中,用于唯一标识一个设备。MN 由 EPC-96 编码转化的字符串组成,即 MN 由 24 个 0~9,A~F 的字符组成(标头(8 bit)厂商识别代码(28 bit)对象分类代码(24 bit)序列号(36 bit))
Flag=4; --标志位,这个标志位包含标准版本号、是否拆分包、数据是否应答。V5~V0:标准版本号;Bit:000000 表示标准 HJ/T 212-2005,000001 表示本次标准修订版本号。 A:命令是否应答;Bit:1-应答,0-不应答。
D:是否有数据包序号;Bit:1-数据包中包含包号和总包数两部分,0-数据包中不包含包号和总包数两部分。 示例:Flag=7 表示标准版本为本次修订版本号,数据段需要拆分并且命令需要应答
PNUM--PNUM 指示本次通讯中总共包含的包数注:不分包时可以没有本字段,与标志位有关
PNO --PNO指示当前数据包的包号注:不分包时可以没有本字段,与标志位有关
CP=&& --CP=&&数据区&&,数据区定义见 6.3.3 章节
DataTime=20210320163058;RestartTime=20210320000006
&&
A781 --CRC校验
--这里还有一个回车换行符
3.核心代码
3.1.解析消息主体
/**
* 解析消息体
* @param ctx
* @param msgBuf
* @param out
* @throws Exception
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msgBuf, List<Object> out) throws Exception {
HJ212Message message=new HJ212Message();
//消息头
msgBuf.readUnsignedShort();
int msgBodyLen=0;
//消息体长度
byte[] msgLenArr = new byte[4];
msgBuf.readBytes(msgLenArr);
msgBodyLen=Integer.parseInt(new String(msgLenArr));
//通过分割符";"分割消息
Map<String,String> attrMap=new HashMap<>();
while (msgBuf.readableBytes() > 6) {
int index = msgBuf.bytesBefore(HJ212Constant.MSG_SPLITER);
int itemLen = index >= 0 ? index : msgBuf.readableBytes() - 6;
if (itemLen > 0) {
msgBuf.markReaderIndex();
byte[] byteArr = new byte[itemLen];
msgBuf.readBytes(byteArr);
String attrContent = new String(byteArr);
String[] attrArr = attrContent.split("=");
if (attrArr.length > 1) {
if (attrArr[0].toUpperCase().equals(HJ212Constant.DATA_ATTR)) {
msgBuf.resetReaderIndex();
byte[] dataContentArr = new byte[msgBuf.readableBytes()-6];
msgBuf.readBytes(dataContentArr);
String dataContent = CommonUtils.stringExtractWithRegex(new String(dataContentArr), "&&(.*?)&&");
//剩余的消息体长度大于“CP=&&dataContent&&”;这个7指的就是CP=&&&&部分的长度
if(dataContentArr.length>dataContent.length()+7){
//跳过CP消息内容,剩余的进入下个循环
msgBuf.resetReaderIndex();
msgBuf.skipBytes(dataContent.length()+7);
}
attrMap.put(HJ212Constant.DATA_ATTR,dataContent);
}else{
attrMap.put(attrArr[0].toUpperCase(),attrArr[1]);
msgBuf.readByte();
}
}
}
}
msgBuf.readInt();
msgBuf.readShort();
if(attrMap.containsKey("QN")){
message.setQn(attrMap.get("QN"));
}
if(attrMap.containsKey("ST")){
message.setSysId(attrMap.get("ST"));
}
if(attrMap.containsKey("CN")){
message.setMsgId(attrMap.get("CN"));
}
if(attrMap.containsKey("PW")){
message.setPassword(attrMap.get("PW"));
}
if(attrMap.containsKey("MN")){
String mn=attrMap.get("MN");
message.setTerminalNum(mn);
message.setTerminalNumArr(mn.getBytes());
}
if(attrMap.containsKey("FLAG")){
message.setFlag(Integer.parseInt(attrMap.get("FLAG")));
}
if(attrMap.containsKey("PNUM")){
message.setPNum(Integer.parseInt(attrMap.get("PNUM")));
}
if(attrMap.containsKey("PNO")){
message.setPNo(Integer.parseInt(attrMap.get("PNO")));
}
if(attrMap.containsKey("CP")){
String bodyContent =attrMap.get("CP");
message.setBodyContent(bodyContent);
message.setMsgBodyArr(bodyContent.getBytes());
message.setMsgBody(Unpooled.wrappedBuffer(bodyContent.getBytes()));
}
out.add(message);
}
3.2.解析2011消息负载
/**
* 解析实时采样数据
* @param terminalProto
* @param msg
* @param contentArr
* @return
*/
public static Hj212SampleDataProto parseRealTimeSampleData(TerminalProto terminalProto, HJ212Message msg, String [] contentArr){
Hj212SampleDataProto sampleDataProto=new Hj212SampleDataProto();
sampleDataProto.setTerminalId(terminalProto.getTerminalId());
sampleDataProto.setTerminalNum(terminalProto.getTerminalNum());
//当前时间
ZonedDateTime currentDateTime = ZonedDateTime.now(ZoneOffset.UTC);
sampleDataProto.setRecvTime(currentDateTime.toString());
sampleDataProto.setRecvTimestamp(currentDateTime.toInstant().toEpochMilli());
sampleDataProto.setMessageId(msg.getMsgId());
//解析属性值
Map<String, SampleData> dataMap=new HashMap<>();
for(String strAttr:contentArr){
SampleData sampleData=new SampleData();
String[] attrArr =strAttr.split(",");
for(String attrItem:attrArr){
String[] fieldArr =attrItem.split("=");
if(fieldArr.length>1) {
if (fieldArr[0].toLowerCase().equals("datatime")) {
ZonedDateTime zonedDateTime = CommonUtils.parseBcdTime(fieldArr[1]);
sampleDataProto.setDateTime(zonedDateTime.toString());
sampleDataProto.setDateTimestamp(zonedDateTime.toInstant().toEpochMilli());
} else {
if (fieldArr[0].toLowerCase().endsWith("-rtd")) {
sampleData.setFactor(fieldArr[0].split("-")[0]);
sampleData.setRtd(fieldArr[1]);
}else if (fieldArr[0].toLowerCase().endsWith("-sampletime")) {
sampleData.setFactor(fieldArr[0].split("-")[0]);
ZonedDateTime zonedDateTime = CommonUtils.parseBcdTime(fieldArr[1]);
sampleData.setTime(zonedDateTime.toString());
sampleData.setTimestamp(zonedDateTime.toInstant().toEpochMilli());
}else if (fieldArr[0].toLowerCase().endsWith("-flag")) {
sampleData.setFactor(fieldArr[0].split("-")[0]);
sampleData.setFlag(fieldArr[1]);
}
}
}
}
if(StringUtils.isNotBlank(sampleData.getFactor())){
dataMap.put(sampleData.getFactor(),sampleData);
}
}
if(dataMap.size()>0){
sampleDataProto.setRealTime(1);
}else{
sampleDataProto.setRealTime(0);
}
sampleDataProto.setDataMapJson(JSON.toJSONString(dataMap));
return sampleDataProto;
}
3.2.CRC循环冗余计算
/**
* CRC循环冗余计算
* @param data
* @return
*/
public static int calculateCRC16(byte[] data){
int crc_reg = 0xFFFF;
int check;
for (byte b : data) {
crc_reg = (crc_reg>>8)^b;
for (int i = 0; i < 8; i++) {
check = crc_reg & 0x0001;
crc_reg >>= 1;
if(check==0x0001){
crc_reg ^= 0xA001;
}
}
}
return crc_reg&0xFFFF;
}
4.实现效果
4.1.设备全览
4.2. 单个设备实时详情
4.3. 历史采样分析
4.4.设置报警规则,产生实时检测报警
如果有项目需求,或者源码需求的可以加我微信沟通.