議實戰(zhàn):報文結構、CRC校驗與編碼處理)
簡介本資源面向環(huán)保監(jiān)測系統(tǒng)開發(fā)工程師與Java學習者提供國標HJ212協(xié)議污染源在線自動監(jiān)控數據傳輸標準的完整解析實現可直接導入Eclipse項目調用。包內共308個文件以197個class編譯文件與100個java源碼為主另含少量xml、properties、classpath等配置與說明文件壓縮包約327KB結構緊湊便于快速集成。內容覆蓋協(xié)議數據結構、編碼解碼、XML/JSON處理、Socket網絡通信、異常處理與JUnit測試等關鍵環(huán)節(jié)并封裝了T212Mapper、SegmentParser、DataConverter、T212Factory等核心解析類讀者可據此理解協(xié)議字段映射與數據轉換邏輯直接復用解析方法或按需擴展。目前已有5072人學習下載適合需要快速落地HJ212數據采集與解析的開發(fā)者參考。1. HJ212 協(xié)議在 Java 里到底難在哪從一條報文說起如果你手上有環(huán)保監(jiān)測設備的對接任務大概率繞不開 HJ212。它全稱是《污染物在線監(jiān)控監(jiān)測系統(tǒng)數據傳輸標準》現場數采儀、CEMS、水質分析儀往上位機傳數據走的基本都是這套報文格式。很多人第一次拿到抓包數據會愣一下##0131QN20240101120000001;ST32;CN2011;PW123456;MNHY0001;CPDataTime20240101120000;a01001-Rtd12.5,a01001-FlagNC4E1看著像字符串拼接真動手解析才發(fā)現坑不少。Java 做這塊的訴求很集中把這一長串文本穩(wěn)定地拆成對象把字段映射到業(yè)務模型再回一條應答報文。難點不在語法而在協(xié)議本身的幾個特性——##和雙重分隔、CP字段里還嵌了一層鍵值對、CRC 校驗要按字節(jié)算、中文編碼在不同廠商設備上還不統(tǒng)一。這篇文章就按我實際做過的路子把 Java 解析 HJ212 從報文結構、解析器實現、應答構造到踩坑排查講透適合正在做環(huán)保數據接入的后端和物聯網方向的人。2. 拆解 HJ212 報文結構先搞清楚每一段是什么2.1 報文的分層結構HJ212 的報文是純文本但它是分層的。最外層用##開頭后面跟 4 位數據段長度然后是數據段最后是 4 位 CRC 校驗。數據段內部用;分隔各個字段字段是KEYVALUE形式。其中CP這個字段特殊它的值被包起來里面又是一層用;分隔的鍵值對比如CPDataTime20240101120000;a01001-Rtd12.5;a01001-FlagN。理解這個兩層結構是寫解析器的前提。我一般把它抽象成三層幀層##、長度、CRC、命令層QN、ST、CN、PW、MN、CP 這些、數據層CP 內部的監(jiān)測因子。很多人翻車就翻在把 CP 當成普通字符串處理結果里面的;和外層的;混在一起split 出來全亂套。層級組成分隔符說明幀層## 長度 數據段 CRC固定位置長度是數據段的字節(jié)數CRC 是數據段的校驗命令層QN/ST/CN/PW/MN/CP 等;請求命令標識、系統(tǒng)編碼、命令編碼數據層CP 內部鍵值對;和監(jiān)測因子編碼、值、標記2.2 關鍵字段的含義和取值QN 是請求編號格式是YYYYMMDDHHmmssSSS加 3 位序列號用來做請求應答的匹配這個字段必須原樣回傳。ST 是系統(tǒng)編碼常見 21 是地表水、22 是污水、31 是煙氣、32 是環(huán)境空氣。CN 是命令編碼2011 是上傳污染物數據2051 是取實時數據9011 是心跳。MN 是設備唯一標識PW 是訪問密碼這兩個通常用來做設備鑒權。CP 里的監(jiān)測因子編碼是國標定義的比如a01001是煙塵、a01002是二氧化硫、w01001是 pH。每個因子后面跟-Rtd表示實時值-Flag表示數據標記N 正常、T 超時、D 故障等。這些編碼建議做成枚舉或者配置表不要硬編碼在解析邏輯里否則新增因子就得改代碼。提示不同廠商對 CP 內部字段的順序和數量處理不一致有的會省略 Flag有的會加自定義字段解析時不要假設字段一定存在。3. 用 Java 寫一個能跑的 HJ212 解析器3.1 先定義數據模型動手寫解析之前先把模型定下來。我一般分兩個類一個Hj212Frame表示整幀一個Hj212Command表示命令層CP 內部的數據用一個MapString, 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; // 系統(tǒng)編碼 private String cn; // 命令編碼 private String pw; // 密碼 private String mn; // 設備標識 private String cp; // CP 原文 private MapString, String data new LinkedHashMap(); // CP 解析結果 // getter/setter 省略 }模型里 CP 原文和解析后的 data 都保留是因為排查問題時經常要對照原文。用LinkedHashMap而不是HashMap是為了保持字段順序回寫報文時順序一致方便和原始報文做 diff。3.2 幀層解析長度和 CRC 怎么處理幀層解析的核心是定位數據段邊界和校驗。數據段長度是 4 位十進制數緊跟在##后面。拿到長度后從第 7 個字符開始截取對應字節(jié)數的內容就是數據段再往后 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 位開始長度按字節(jié)算 byte[] rawBytes raw.getBytes(StandardCharsets.UTF_8); // 注意長度是字節(jié)數不是字符數中文場景要按字節(jié)截 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; }這里有個血淚經驗長度字段是字節(jié)數不是字符數。如果報文里有中文比如某些廠商在 CP 里塞了中文描述用substring按字符截會錯位。上面代碼先轉成字節(jié)數組再按字節(jié)截能規(guī)避這個問題。CRC 校驗算法是 CRC-16/CCITT多項式0x1021初始值0xFFFF具體實現見下一節(jié)。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 部分去掉后再按;拆得到監(jiān)測因子。參數上要注意indexOf找的是第一個CP正常報文只有一個如果廠商拼了多個 CP 就得改成循環(huán)處理。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字節(jié)不同 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) ;ExeRtn1; 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。ExeRtn1表示執(zhí)行成功0 表示失敗。長度用字節(jié)數和解析時保持一致。這里 QN 截前 14 位當 DataTime 是個簡化處理嚴格來說應該用當前時間但很多場景下設備只校驗 QN 匹配用請求時間也能過。4.2 編碼問題的處理編碼是 HJ212 對接里最玄學的一環(huán)。國標沒強制規(guī)定字符集現場設備有用 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 要干凈。另外注意長度字段是按字節(jié)算的所以解析和組裝時都要用同一套編碼否則長度對不上CRC 也會錯。5. 避坑與排查那些讓我加班到凌晨的問題5.1 CRC 校驗總是不通過現象解析出來的數據段看著沒問題但 CRC 和報文里的對不上。原因通常是編碼不一致或者校驗對象搞錯了。有人把##和長度字段也算進 CRC那肯定錯。解決確認校驗對象是純數據段確認編本文還有配套的精品資源點擊獲取