一、协议介绍
在GB/T32960-2016协议中规定了电动汽车远程服务于管理系统中协议结构、通信连接、数据包结构与定义、数据单元格式与定义。本协议采用大端模式的网络字节序来传递字和双字。
协议中传输的数据类型如下:
通讯包结构组成如下:
起始字节 | 定义 | 数据类型 | 描述及要求 | |
0 | 起始符 | STRING | 固定为ASCII字符‘##’,用“0x23,0x23”表示。 | |
2 | 命令单元 | 命令标识 | BYTE | 命令单元定义见后续说明。 |
3 | 应答标志 | BYTE | ||
4 | 唯一识别码 | STRING | 当传输车辆数据时,应使用车辆VIN,其字码应符合GB16735的规定。如传输其他数据,则使用唯一自定义编码。 | |
21 | 数据单元加密方式 | BYTE | 0x01:数据不加密;0x02:数据经过RSA算法加密;0x03:数据经过AES128位算法加密;0xFE标识异常;0xFF表示无效,其他预留。 | |
22 | 数据单元长度 | WORD | 数据单元的总字节数,有效值范围:0~65531。 | |
24 | 数据单元 | — | 数据单元格式和定义见后续说明。 | |
倒数第1位 | 校验码 | BYTE | 采用BCC法,校验范围从命令单元的第一个字节开始,同后一个字节异或,知道校验码前一字节位置,校验码占用一个字节,当数据单元存在加密时,应先加密后校验,先校验后解密。 |
命令标识定义如下:
应答标志定义如下:
时间定义如下:
数据单元格式和定义
本次只有车辆登入的报文解析,车辆登入的数据格式和定义如下:
更多关于GB/T32960-2016的内容可点击查看。
二、协议解析开发
本次协议解析基于研博工业物联网统一接入系统(stew-ot)协议扩展规范开发。示例只针对GB/T32960-2016协议的车辆登入数据解码,不涉及对该类设备的控制。
新建类com.yanboot.iot.protocol.tcp.GBT32960.GBT32960ProtocolCodec
,根据SDK包开发规范完成协议报文的解析工作。
- 实现
com.yanboot.iot.sdk.protocol.ProtocolCodec
接口,重写support
方法,指定协议的唯一标识、名称、特性等内容。
@Override
public ProtocolSupport support() {
return new ProtocolSupport(TransportProtocol.TCP)
.id("GB/T 32960.3-2016")
.name("GB/T 32960-2016《电动汽车远程服务与管理技术规范》")
.feature(new ProtocolFeature().keepOnline(true).keepOnlineTimeoutSeconds(1800))
.description("GB/T 32960-2016《电动汽车远程服务与管理技术规范》")
;
}
- 实现
com.yanboot.iot.sdk.protocol.ProtocolCodec
接口的decode
方法,完成协议的解码工作。
@Override
public void decode(OperatorSupplier supplier, DeviceSession deviceSession, ProtocolMessage<?> message, MessageExporter<DeviceMessage<?>> messageExporter) {
TcpProtocolMessage tcpProtocolMessage = (TcpProtocolMessage) message;
byte[] payload = tcpProtocolMessage.payloadAsBytes();
//起始符
if (START_BIT != payload[0] || START_BIT != payload[1]) {
log.error("数据包起始符不正确,请检查数据包数据!{}", Arrays.toString(payload));
return;
}
//校验码
if (!checkSum(payload)) {
log.error("数据包校验码校验不正确,请检查数据包数据!{}", Arrays.toString(payload));
return;
}
//命令标识
String command = COMMAND_MAP.get(payload[2] + "");
//应答标志
String reply = ANSWER_MAP.get(payload[3] + "");
//唯一识别码
StringBuffer vin = new StringBuffer();
for (int i = 4; i < 21; i++) {
vin.append(payload[i]);
}
//数据单元加密方式
String encrypt = ENCRYPT_MAP.get(payload[21] + "");
//数据单元长度
int dataLen = Integer.parseInt(payload[22] + "" + payload[23], 16);
//数据单元
byte[] data = Arrays.copyOfRange(payload, 24, 24 + dataLen);
Map<String, Object> 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");
//命令标识
private final static Map<String, String> COMMAND_MAP = new HashMap<String, String>();
//应答标识
private final static Map<String, String> ANSWER_MAP = new HashMap<String, String>();
//加密方式
private final static Map<String, String> ENCRYPT_MAP = new HashMap<String, String>();
static {
// 定义命令标识
COMMAND_MAP.put("01", "车辆登入");
COMMAND_MAP.put("02", "实时信息上报");
COMMAND_MAP.put("03", "补发信息上报");
COMMAND_MAP.put("04", "车辆登出");
COMMAND_MAP.put("05", "平台登入");
COMMAND_MAP.put("06", "平台登出");
// 定义应答标识
ANSWER_MAP.put("01", "成功");
ANSWER_MAP.put("02", "错误");
ANSWER_MAP.put("03", "VIN重复");
ANSWER_MAP.put("FE", "命令");
// 定义加密方式
ENCRYPT_MAP.put("01", "数据不加密");
ENCRYPT_MAP.put("02", "数据经过RSA算法加密");
ENCRYPT_MAP.put("03", "数据经过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《电动汽车远程服务与管理技术规范》")
.feature(new ProtocolFeature().keepOnline(true).keepOnlineTimeoutSeconds(30))
.description("GB/T 32960-2016《电动汽车远程服务与管理技术规范》")
;
}
/**
* 解码
*/
@Override
public void decode(OperatorSupplier supplier, DeviceSession deviceSession, ProtocolMessage<?> message, MessageExporter<DeviceMessage<?>> messageExporter) {
TcpProtocolMessage tcpProtocolMessage = (TcpProtocolMessage) message;
byte[] payload = tcpProtocolMessage.payloadAsBytes();
//起始符
if (START_BIT != payload[0] || START_BIT != payload[1]) {
log.error("数据包起始符不正确,请检查数据包数据!{}", Arrays.toString(payload));
return;
}
//校验码
if (!checkSum(payload)) {
log.error("数据包校验码校验不正确,请检查数据包数据!{}", Arrays.toString(payload));
return;
}
//命令标识
String command = COMMAND_MAP.get(payload[2] + "");
//应答标志
String reply = ANSWER_MAP.get(payload[3] + "");
//唯一识别码
StringBuffer vin = new StringBuffer();
for (int i = 4; i < 21; i++) {
vin.append(payload[i]);
}
//数据单元加密方式
String encrypt = ENCRYPT_MAP.get(payload[21] + "");
//数据单元长度
int dataLen = Integer.parseInt(payload[22] + "" + payload[23], 16);
//数据单元
byte[] data = Arrays.copyOfRange(payload, 24, 24 + dataLen);
Map<String, Object> 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())));
}
/**
* 解析数据
*
* @param command 命令标识
* @param data 数据单元
* @return 解析结果
*/
private Map<String, Object> parseData(String command, byte[] data) {
switch (command) {
case "01" -> {
return carLogin(data);
}
}
return null;
}
/**
* 车辆登入
*/
private Map<String, Object> carLogin(byte[] data) {
Map<String, Object> 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《电动汽车远程服务与管理技术规范》适用于以下场景中:
(1):电动汽车健康监测;
(2):车辆使用记录;
(3):故障诊断;
(4):车企和政府的数据共享平台等。