在GB/T32960-2016協(xié)議中規(guī)定了電動汽車遠(yuǎn)程服務(wù)于管理系統(tǒng)中協(xié)議結(jié)構(gòu)、通信連接、數(shù)據(jù)包結(jié)構(gòu)與定義、數(shù)據(jù)單元格式與定義。本協(xié)議采用大端模式的網(wǎng)絡(luò)字節(jié)序來傳遞字和雙字。
協(xié)議中傳輸?shù)臄?shù)據(jù)類型如下:
通訊包結(jié)構(gòu)組成如下:
命令標(biāo)識定義如下:
應(yīng)答標(biāo)志定義如下:
時間定義如下:
數(shù)據(jù)單元格式和定義
本次只有車輛登入的報文解析,車輛登入的數(shù)據(jù)格式和定義如下:
更多關(guān)于GB/T32960-2016的內(nèi)容可點擊查看。
本次協(xié)議解析基于研博工業(yè)物聯(lián)網(wǎng)統(tǒng)一接入系統(tǒng)(stew-ot)協(xié)議擴(kuò)展規(guī)范開發(fā)。示例只針對GB/T32960-2016協(xié)議的車輛登入數(shù)據(jù)解碼,不涉及對該類設(shè)備的控制。
新建類com.yanboot.iot.protocol.tcp.GBT32960.GBT32960ProtocolCodec
,根據(jù)SDK包開發(fā)規(guī)范完成協(xié)議報文的解析工作。
實現(xiàn)com.yanboot.iot.sdk.protocol.ProtocolCodec
接口,重寫support
方法,指定協(xié)議的唯一標(biāo)識、名稱、特性等內(nèi)容。
@Override
public ProtocolSupport support() {
return new ProtocolSupport(TransportProtocol.TCP)
.id("GB/T 32960.3-2016")
.name("GB/T 32960-2016《電動汽車遠(yuǎn)程服務(wù)與管理技術(shù)規(guī)范》")
.feature(new ProtocolFeature().keepOnline(true).keepOnlineTimeoutSeconds(1800))
.description("GB/T 32960-2016《電動汽車遠(yuǎn)程服務(wù)與管理技術(shù)規(guī)范》")
;
}
實現(xiàn)com.yanboot.iot.sdk.protocol.ProtocolCodec
接口的decode
方法,完成協(xié)議的解碼工作。
@Override
public void decode(OperatorSupplier supplier, DeviceSession deviceSession, ProtocolMessage message, MessageExporter> messageExporter) {
TcpProtocolMessage tcpProtocolMessage = (TcpProtocolMessage) message;
byte[] payload = tcpProtocolMessage.payloadAsBytes();
//起始符
if (START_BIT != payload[0] || START_BIT != payload[1]) {
log.error("數(shù)據(jù)包起始符不正確,請檢查數(shù)據(jù)包數(shù)據(jù)!{}", Arrays.toString(payload));
return;
}
//校驗碼
if (!checkSum(payload)) {
log.error("數(shù)據(jù)包校驗碼校驗不正確,請檢查數(shù)據(jù)包數(shù)據(jù)!{}", Arrays.toString(payload));
return;
}
//命令標(biāo)識
String command = COMMAND_MAP.get(payload[2] + "");
//應(yīng)答標(biāo)志
String reply = ANSWER_MAP.get(payload[3] + "");
//唯一識別碼
StringBuffer vin = new StringBuffer();
for (int i = 4; i < 21; i++) {
vin.append(payload[i]);
}
//數(shù)據(jù)單元加密方式
String encrypt = ENCRYPT_MAP.get(payload[21] + "");
//數(shù)據(jù)單元長度
int dataLen = Integer.parseInt(payload[22] + "" + payload[23], 16);
//數(shù)據(jù)單元
byte[] data = Arrays.copyOfRange(payload, 24, 24 + dataLen);
Map dataMap = parseData(command, data);
if (ObjectUtil.isNull(dataMap)) {
return;
}
dataMap.put("command", command);
dataMap.put("reply", reply);
dataMap.put("encrypt", encrypt);
messageExporter.export(new ReportPropertyMessage().deviceId(vin.toString()).properties(dataMap).timestamp(Long.parseLong(dataMap.getOrDefault("datetime",System.currentTimeMillis()).toString())));
}
完整代碼如下:
package com.yanboot.iot.protocol.tcp.GBT32960;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSON;
import com.yanboot.iot.core.constant.TransportProtocol;
import com.yanboot.iot.core.message.device.DeviceMessage;
import com.yanboot.iot.core.message.device.impl.ReportPropertyMessage;
import com.yanboot.iot.core.message.protocol.ProtocolMessage;
import com.yanboot.iot.core.message.protocol.impl.TcpProtocolMessage;
import com.yanboot.iot.core.operator.OperatorSupplier;
import com.yanboot.iot.core.session.DeviceSession;
import com.yanboot.iot.sdk.protocol.MessageExporter;
import com.yanboot.iot.sdk.protocol.ProtocolCodec;
import com.yanboot.iot.sdk.protocol.ProtocolFeature;
import com.yanboot.iot.sdk.protocol.ProtocolSupport;
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
*
*/
@Slf4j
public class GBT32960ProtocolCodec implements ProtocolCodec {
private final static byte START_BIT = 0X23;
private final static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyMMddHHmmss");
//命令標(biāo)識
private final static Map COMMAND_MAP = new HashMap();
//應(yīng)答標(biāo)識
private final static Map ANSWER_MAP = new HashMap();
//加密方式
private final static Map ENCRYPT_MAP = new HashMap();
static {
// 定義命令標(biāo)識
COMMAND_MAP.put("01", "車輛登入");
COMMAND_MAP.put("02", "實時信息上報");
COMMAND_MAP.put("03", "補發(fā)信息上報");
COMMAND_MAP.put("04", "車輛登出");
COMMAND_MAP.put("05", "平臺登入");
COMMAND_MAP.put("06", "平臺登出");
// 定義應(yīng)答標(biāo)識
ANSWER_MAP.put("01", "成功");
ANSWER_MAP.put("02", "錯誤");
ANSWER_MAP.put("03", "VIN重復(fù)");
ANSWER_MAP.put("FE", "命令");
// 定義加密方式
ENCRYPT_MAP.put("01", "數(shù)據(jù)不加密");
ENCRYPT_MAP.put("02", "數(shù)據(jù)經(jīng)過RSA算法加密");
ENCRYPT_MAP.put("03", "數(shù)據(jù)經(jīng)過AES128位算法加密");
ENCRYPT_MAP.put("FE", "異常");
ENCRYPT_MAP.put("FF", "無效");
}
@Override
public ProtocolSupport support() {
return new ProtocolSupport(TransportProtocol.TCP)
.id("GB/T 32960.3-2016")
.name("GB/T 32960-2016《電動汽車遠(yuǎn)程服務(wù)與管理技術(shù)規(guī)范》")
.feature(new ProtocolFeature().keepOnline(true).keepOnlineTimeoutSeconds(30))
.description("GB/T 32960-2016《電動汽車遠(yuǎn)程服務(wù)與管理技術(shù)規(guī)范》")
;
}
/**
* 解碼
*/
@Override
public void decode(OperatorSupplier supplier, DeviceSession deviceSession, ProtocolMessage message, MessageExporter> messageExporter) {
TcpProtocolMessage tcpProtocolMessage = (TcpProtocolMessage) message;
byte[] payload = tcpProtocolMessage.payloadAsBytes();
//起始符
if (START_BIT != payload[0] || START_BIT != payload[1]) {
log.error("數(shù)據(jù)包起始符不正確,請檢查數(shù)據(jù)包數(shù)據(jù)!{}", Arrays.toString(payload));
return;
}
//校驗碼
if (!checkSum(payload)) {
log.error("數(shù)據(jù)包校驗碼校驗不正確,請檢查數(shù)據(jù)包數(shù)據(jù)!{}", Arrays.toString(payload));
return;
}
//命令標(biāo)識
String command = COMMAND_MAP.get(payload[2] + "");
//應(yīng)答標(biāo)志
String reply = ANSWER_MAP.get(payload[3] + "");
//唯一識別碼
StringBuffer vin = new StringBuffer();
for (int i = 4; i < 21; i++) {
vin.append(payload[i]);
}
//數(shù)據(jù)單元加密方式
String encrypt = ENCRYPT_MAP.get(payload[21] + "");
//數(shù)據(jù)單元長度
int dataLen = Integer.parseInt(payload[22] + "" + payload[23], 16);
//數(shù)據(jù)單元
byte[] data = Arrays.copyOfRange(payload, 24, 24 + dataLen);
Map dataMap = parseData(command, data);
if (ObjectUtil.isNull(dataMap)) {
return;
}
dataMap.put("command", command);
dataMap.put("reply", reply);
dataMap.put("encrypt", encrypt);
messageExporter.export(new ReportPropertyMessage().deviceId(vin.toString()).properties(dataMap).timestamp(Long.parseLong(dataMap.getOrDefault("datetime",System.currentTimeMillis()).toString())));
}
/**
* 解析數(shù)據(jù)
*
* @param command 命令標(biāo)識
* @param data 數(shù)據(jù)單元
* @return 解析結(jié)果
*/
private Map parseData(String command, byte[] data) {
switch (command) {
case "01" -> {
return carLogin(data);
}
}
return null;
}
/**
* 車輛登入
*/
private Map carLogin(byte[] data) {
Map dataMap = new HashMap<>();
StringBuilder datetime = new StringBuilder();
for (int i = 0; i < 6; i++) {
byte datum = data[i];
if (datum < 10) {
datetime.append("0");
}
datetime.append(datum);
}
dataMap.put("datetime", LocalDateTime.parse(datetime.toString(),dtf).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
dataMap.put("loginSequenceNumber", Integer.parseInt(data[6] + "" + data[7], 16) + "");
StringBuilder iccid = new StringBuilder();
for (int i = 8; i < 28; i++) {
iccid.append(data[i]);
}
dataMap.put("ICCID", iccid.toString());
dataMap.put("energyStorageNumber", Integer.parseInt(data[28] + "", 16) + "");
dataMap.put("energyStorageLength", Integer.parseInt(data[29] + "", 16) + "");
int length = data.length;
StringBuilder energyStorageCode = new StringBuilder();
for (int i = 30; i < length; i++) {
energyStorageCode.append(data[i]);
}
dataMap.put("energyStorageCode", energyStorageCode.toString());
return dataMap;
}
/**
* 校驗碼校驗
*/
public static boolean checkSum(byte[] payload) {
int checkCode = Integer.parseInt(payload[payload.length - 1] + "", 16);
int a = 0;
for (int i = 2; i < payload.length - 2; i++) {
a = a ^ Integer.parseInt(payload[i] + "", 16);
}
return checkCode == a;
}
}
GB/T 32960-2016《電動汽車遠(yuǎn)程服務(wù)與管理技術(shù)規(guī)范》適用于以下場景中:
(1):電動汽車健康監(jiān)測;
(2):車輛使用記錄;
(3):故障診斷;
(4):車企和政府的數(shù)據(jù)共享平臺等。