fix(iot): fix UnsupportedOperationException in JT808 codec
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled

This commit is contained in:
lzh
2026-01-16 13:24:39 +08:00
parent 8a543db18f
commit 49d6891288

View File

@@ -49,7 +49,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
public byte[] encode(IotDeviceMessage message) {
Assert.notNull(message, "消息不能为空");
Assert.notBlank(message.getMethod(), "消息方法不能为空");
try {
// 从消息中提取必要信息
String phone = extractPhoneNumber(message);
@@ -61,12 +61,12 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
// === 标准物模型方法 ===
case "thing.service.invoke" -> encodeServiceInvoke(message, phone, flowId); // 服务调用
case "thing.property.set" -> encodePropertySet(message, phone, flowId); // 属性设置
// === JT808 内部协议方法(向下兼容) ===
case "jt808.platform.commonResp", "commonResp" -> encodeCommonResp(message, phone, flowId);
case "jt808.platform.registerResp", "registerResp" -> encodeRegisterResp(message, phone, flowId);
case "jt808.platform.textDown", "textDown" -> encodeTextDown(message, phone, flowId);
default -> {
log.warn("[encode][不支持的消息方法: {}]", method);
yield new byte[0];
@@ -82,14 +82,14 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
public IotDeviceMessage decode(byte[] bytes) {
Assert.notNull(bytes, "待解码数据不能为空");
Assert.isTrue(bytes.length >= 12, "数据包长度不足");
try {
// 1. 反转义(去除首尾标识符)
byte[] unescapedBytes = protocolUtil.doEscape4Receive(bytes, 1, bytes.length - 1);
// 2. 解析为 JT808 数据包
Jt808DataPack dataPack = decoder.bytes2PackageData(unescapedBytes);
// 3. 转换为统一的 IotDeviceMessage
return convertToIotDeviceMessage(dataPack);
} catch (Exception e) {
@@ -104,26 +104,29 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
private IotDeviceMessage convertToIotDeviceMessage(Jt808DataPack dataPack) {
Jt808DataPack.PackHead head = dataPack.getPackHead();
int msgId = head.getId();
// 生成消息ID使用流水号作为标识
String messageId = head.getTerminalPhone() + "_" + head.getFlowId();
// 根据消息ID确定物模型标准方法名
String method = getStandardMethodName(msgId);
Object params = parseMessageParams(dataPack, msgId);
// 构建元数据(保留在 params 中,用于调试和追踪)
if (params instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> paramsMap = (Map<String, Object>) params;
// 确保 paramsMap 是可变的,防止 parseMessageParams 返回不可变 Map 导致 put 报错
Map<String, Object> paramsMap = new HashMap<>((Map<String, Object>) params);
Map<String, Object> metadata = new HashMap<>();
metadata.put("jt808MsgId", String.format("0x%04X", msgId));
metadata.put("terminalPhone", head.getTerminalPhone());
metadata.put("flowId", head.getFlowId());
metadata.put("encryptionType", head.getEncryptionType());
paramsMap.put("_metadata", metadata);
// 更新 params 引用,确保使用的是包含 metadata 的可变 Map
params = paramsMap;
}
// 创建 IotDeviceMessage
IotDeviceMessage message = IotDeviceMessage.of(messageId, method, params, null, null, null);
message.setReportTime(LocalDateTime.now());
@@ -132,7 +135,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
/**
* 根据 JT808 消息ID获取物模型标准方法名
*
*
* 映射关系:
* - 0x0002 心跳 -> thing.state.update设备状态更新
* - 0x0200 位置上报 -> thing.property.post属性上报
@@ -146,20 +149,20 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
return switch (msgId) {
// 设备状态类
case 0x0002 -> IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(); // 心跳 -> 状态更新
// 属性上报类
case 0x0200 -> IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(); // 位置信息汇报 -> 属性上报
case 0x0704 -> IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(); // 批量位置上传 -> 属性上报
// 事件上报类
case 0x0006 -> IotDeviceMessageMethodEnum.EVENT_POST.getMethod(); // 按键事件 -> 事件上报
// 内部协议类(不映射到标准方法,使用 JT808 特定方法名)
case 0x0001 -> "jt808.terminal.commonResp"; // 终端通用应答
case 0x0100 -> "jt808.terminal.register"; // 终端注册
case 0x0102 -> "jt808.terminal.auth"; // 终端鉴权
case 0x0003 -> "jt808.terminal.logout"; // 终端注销
// 未知消息
default -> "jt808.unknown.0x" + Integer.toHexString(msgId);
};
@@ -170,18 +173,18 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
*/
private Object parseMessageParams(Jt808DataPack dataPack, int msgId) {
byte[] bodyBytes = dataPack.getBodyBytes();
// 针对不同消息类型进行特殊解析,返回符合物模型标准的格式
return switch (msgId) {
// thing.property.post - 返回 properties
case 0x0200, 0x0704 -> parseLocationInfoAsProperties(dataPack);
// thing.state.update - 返回 state 信息
case 0x0002 -> parseHeartbeatAsState();
// thing.event.post - 返回 event 信息
case 0x0006 -> parseButtonEventAsEvent(dataPack);
// JT808 内部协议消息 - 保持原有格式
case 0x0100 -> parseRegisterInfo(dataPack);
case 0x0102 -> parseAuthInfo(bodyBytes);
@@ -201,7 +204,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
/**
* 解析按键事件为事件上报格式thing.event.post
*
*
* 物模型标准格式:
* {
* "eventId": "button_event",
@@ -217,15 +220,15 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
*/
private Map<String, Object> parseButtonEventAsEvent(Jt808DataPack dataPack) {
Jt808ButtonEvent event = decoder.toButtonEventMsg(dataPack);
Map<String, Object> result = new HashMap<>();
// 统一使用一个事件标识符,通过 isLongPress 参数区分短按和长按
result.put("eventId", "button_event");
// 事件时间戳
result.put("eventTime", System.currentTimeMillis());
// 事件参数(包含 isLongPress 字段用于区分短按和长按)
Map<String, Object> eventParams = new HashMap<>();
eventParams.put("keyId", event.getKeyId());
@@ -234,13 +237,13 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
eventParams.put("keyNumber", event.getKeyNumber());
eventParams.put("isLongPress", event.getIsLongPress());
result.put("params", eventParams);
return result;
}
/**
* 解析位置信息为属性上报格式thing.property.post
*
*
* 物模型标准格式params 直接就是属性键值对
* {
* "latitude": 31.123456,
@@ -251,10 +254,10 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
*/
private Map<String, Object> parseLocationInfoAsProperties(Jt808DataPack dataPack) {
Jt808LocationInfo locationInfo = decoder.toLocationInfoUploadMsg(dataPack);
// 物模型属性集合(所有属性平铺在同一层)
Map<String, Object> properties = new HashMap<>();
// === 基础位置信息(核心属性) ===
properties.put("latitude", locationInfo.getLatitude());
properties.put("longitude", locationInfo.getLongitude());
@@ -262,13 +265,13 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
properties.put("speed", locationInfo.getSpeed());
properties.put("direction", locationInfo.getDirection());
properties.put("time", locationInfo.getTime());
// 状态和告警字段(转为整数便于处理)
properties.put("warningFlag", locationInfo.getWarningFlagField());
properties.put("status", locationInfo.getStatusField());
// === 扩展信息(物模型属性) ===
// 电池信息
if (locationInfo.getBatteryInfo() != null) {
Jt808BatteryInfo batteryInfo = locationInfo.getBatteryInfo();
@@ -282,17 +285,17 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
properties.put("iccid", batteryInfo.getIccid());
}
}
// 里程
if (locationInfo.getMileage() != null) {
properties.put("mileage", locationInfo.getMileage());
}
// 信号强度
if (locationInfo.getSignalStrength() != null) {
properties.put("signalStrength", locationInfo.getSignalStrength());
}
// 蓝牙设备列表
if (locationInfo.getBluetoothInfos() != null && !locationInfo.getBluetoothInfos().isEmpty()) {
List<Map<String, Object>> bluetoothList = new ArrayList<>();
@@ -308,12 +311,12 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
}
properties.put("bluetoothDevices", bluetoothList);
}
// 保留原始扩展字段(用于调试和高级用途)
if (locationInfo.getExtensions() != null && !locationInfo.getExtensions().isEmpty()) {
properties.put("_rawExtensions", locationInfo.getExtensions());
}
return properties;
}
@@ -324,9 +327,9 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
Map<String, Object> result = new HashMap<>();
try {
// 解析注册消息体
com.viewsh.module.iot.gateway.codec.jt808.entity.Jt808RegisterInfo registerInfo =
com.viewsh.module.iot.gateway.codec.jt808.entity.Jt808RegisterInfo registerInfo =
decoder.toRegisterMsg(dataPack);
result.put("provinceId", registerInfo.getProvinceId());
result.put("cityId", registerInfo.getCityId());
result.put("manufacturerId", registerInfo.getManufacturerId());
@@ -365,7 +368,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
if (bodyBytes == null || bodyBytes.length == 0) {
return Map.of();
}
// 尝试解析为字符串,如果失败则返回十六进制
try {
String str = new String(bodyBytes, Jt808Constants.DEFAULT_CHARSET);
@@ -375,7 +378,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
}
} catch (Exception ignored) {
}
return Map.of("rawData", bytesToHex(bodyBytes));
}
@@ -387,7 +390,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
int replyFlowId = ((Number) params.getOrDefault("replyFlowId", 0)).intValue();
int replyId = ((Number) params.getOrDefault("replyId", 0)).intValue();
byte replyCode = ((Number) params.getOrDefault("replyCode", 0)).byteValue();
return encoder.encodeCommonResp(phone, replyFlowId, replyId, replyCode, flowId);
}
@@ -399,7 +402,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
int replyFlowId = ((Number) params.getOrDefault("replyFlowId", 0)).intValue();
byte replyCode = ((Number) params.getOrDefault("replyCode", 0)).byteValue();
String authToken = (String) params.get("authToken");
return encoder.encodeRegisterResp(phone, replyFlowId, replyCode, authToken, flowId);
}
@@ -410,15 +413,15 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
Map<String, Object> params = getParamsAsMap(message);
byte flag = ((Number) params.getOrDefault("flag", 0)).byteValue();
String content = (String) params.getOrDefault("content", "");
return encoder.encodeTextInfoDown(phone, flag, content, flowId);
}
/**
* 编码服务调用thing.service.invoke
*
*
* 根据服务标识符映射到不同的 JT808 指令
*
*
* 消息格式:
* {
* "identifier": "服务标识符",
@@ -429,29 +432,29 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
*/
private byte[] encodeServiceInvoke(IotDeviceMessage message, String phone, int flowId) {
Map<String, Object> params = getParamsAsMap(message);
// 获取服务标识符
String serviceIdentifier = (String) params.get("identifier");
if (StrUtil.isBlank(serviceIdentifier)) {
log.error("[encodeServiceInvoke][服务标识符为空]");
return new byte[0];
}
// 获取服务参数
@SuppressWarnings("unchecked")
Map<String, Object> serviceParams = (Map<String, Object>) params.getOrDefault("params", new HashMap<>());
// 根据服务标识符路由到不同的 JT808 指令
return switch (serviceIdentifier) {
case "TTS" -> {
// 语音播报服务 -> JT808 文本信息下发 (0x8300)
String ttsText = (String) serviceParams.getOrDefault("tts_text", "");
int ttsFlag = ((Number) serviceParams.getOrDefault("tts_flag", 4)).intValue(); // 默认 4-TTS 播读
log.info("[encodeServiceInvoke][TTS 语音播报] phone={}, flag={}, text={}", phone, ttsFlag, ttsText);
yield encoder.encodeTextInfoDown(phone, (byte) ttsFlag, ttsText, flowId);
}
case "locationQuery" -> {
// 位置查询服务 -> JT808 位置信息查询 (0x8201)
log.info("[encodeServiceInvoke][位置查询] phone={}", phone);
@@ -459,7 +462,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
log.warn("[encodeServiceInvoke][位置查询服务暂未实现]");
yield new byte[0];
}
default -> {
log.warn("[encodeServiceInvoke][不支持的服务标识符: {}]", serviceIdentifier);
yield new byte[0];
@@ -469,9 +472,9 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
/**
* 编码属性设置thing.property.set
*
*
* 属性设置映射到 JT808 的参数设置指令 (0x8103)
*
*
* 消息格式:
* {
* "properties": {
@@ -482,23 +485,23 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
*/
private byte[] encodePropertySet(IotDeviceMessage message, String phone, int flowId) {
Map<String, Object> params = getParamsAsMap(message);
@SuppressWarnings("unchecked")
Map<String, Object> properties = (Map<String, Object>) params.get("properties");
if (properties == null || properties.isEmpty()) {
log.error("[encodePropertySet][属性列表为空]");
return new byte[0];
}
// 将物模型属性映射到 JT808 参数
Map<Integer, Object> jt808Params = mapPropertiesToJt808Params(properties);
if (jt808Params.isEmpty()) {
log.warn("[encodePropertySet][没有可映射的 JT808 参数]");
return new byte[0];
}
log.info("[encodePropertySet][属性设置] phone={}, params={}", phone, jt808Params);
// TODO: 实现 encoder.encodeParamSettings(phone, jt808Params, flowId);
log.warn("[encodePropertySet][参数设置指令暂未实现]");
return new byte[0];
@@ -506,19 +509,19 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
/**
* 将物模型属性映射到 JT808 参数
*
*
* 映射关系参考 JT808 协议标准:
* - 0x0029: 心跳发送间隔
* - 0x0027: 位置汇报间隔
* - 0x0028: 休眠时汇报间隔
* - 等等...
*
*
* @param properties 物模型属性
* @return JT808 参数映射参数ID -> 参数值)
*/
private Map<Integer, Object> mapPropertiesToJt808Params(Map<String, Object> properties) {
Map<Integer, Object> jt808Params = new HashMap<>();
properties.forEach((identifier, value) -> {
Integer paramId = switch (identifier) {
case "heartbeatInterval" -> 0x0029; // 心跳发送间隔(单位:秒)
@@ -531,22 +534,22 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
// 更多映射关系可根据实际需求添加
default -> null;
};
if (paramId != null) {
jt808Params.put(paramId, value);
log.debug("[mapPropertiesToJt808Params][属性映射] {} -> 0x{} = {}",
log.debug("[mapPropertiesToJt808Params][属性映射] {} -> 0x{} = {}",
identifier, Integer.toHexString(paramId), value);
} else {
log.warn("[mapPropertiesToJt808Params][未知的属性标识符: {}]", identifier);
}
});
return jt808Params;
}
/**
* 提取终端手机号
*
*
* 优先级:
* 1. 从 params._deviceName 中获取下发场景IotTcpDownstreamHandler 自动注入)
* 2. 从 params._metadata.terminalPhone 中获取(上行消息回复场景)
@@ -557,9 +560,9 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
log.error("[extractPhoneNumber][params 不是 Map 类型,消息: {}]", message);
throw new IllegalArgumentException("消息参数格式错误params 必须是 Map 类型");
}
Map<?, ?> params = (Map<?, ?>) message.getParams();
// 1. 优先从 _deviceName 获取(下发场景,由 IotTcpDownstreamHandler 注入)
Object deviceName = params.get("_deviceName");
if (deviceName != null && StrUtil.isNotBlank(deviceName.toString())) {
@@ -571,7 +574,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
log.warn("[extractPhoneNumber][_deviceName 不是纯数字: {}]", deviceNameStr);
}
}
// 2. 从 metadata 中获取(上行消息回复场景)
if (params.get("_metadata") instanceof Map) {
Map<?, ?> metadata = (Map<?, ?>) params.get("_metadata");
@@ -580,7 +583,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
return terminalPhone.toString();
}
}
// 3. 从 phone 字段获取(向下兼容,不推荐)
Object phone = params.get("phone");
if (phone != null && StrUtil.isNotBlank(phone.toString())) {
@@ -589,7 +592,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
return phoneStr;
}
}
// 4. 如果都获取不到,抛出异常
log.error("[extractPhoneNumber][无法提取终端手机号params: {}]", params);
throw new IllegalArgumentException(
@@ -598,19 +601,19 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
/**
* 提取流水号
*
*
* 对于下发消息,如果没有指定流水号,则生成一个随机流水号
*/
private int extractFlowId(IotDeviceMessage message) {
if (message.getParams() instanceof Map) {
Map<?, ?> params = (Map<?, ?>) message.getParams();
// 尝试获取显式指定的流水号
Object flowId = params.get("flowId");
if (flowId instanceof Number) {
return ((Number) flowId).intValue();
}
// 尝试从 metadata 中获取(上行消息的流水号)
if (params.get("_metadata") instanceof Map) {
Map<?, ?> metadata = (Map<?, ?>) params.get("_metadata");
@@ -620,7 +623,7 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
}
}
}
// 生成随机流水号1-65535
return (int) (System.currentTimeMillis() % 65535) + 1;
}
@@ -663,8 +666,8 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec {
*/
public static boolean isJt808Format(byte[] data) {
// 检查起止标识符 0x7e
return data != null && data.length >= 12
&& data[0] == (byte) Jt808Constants.PKG_DELIMITER
return data != null && data.length >= 12
&& data[0] == (byte) Jt808Constants.PKG_DELIMITER
&& data[data.length - 1] == (byte) Jt808Constants.PKG_DELIMITER;
}