diff --git a/viewshanghai-module-iot/viewshanghai-module-iot-gateway/src/main/java/com/viewshanghai/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java b/viewshanghai-module-iot/viewshanghai-module-iot-gateway/src/main/java/com/viewshanghai/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java index 01cc47e..2dc3438 100644 --- a/viewshanghai-module-iot/viewshanghai-module-iot-gateway/src/main/java/com/viewshanghai/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java +++ b/viewshanghai-module-iot/viewshanghai-module-iot-gateway/src/main/java/com/viewshanghai/module/iot/gateway/codec/jt808/IotJt808DeviceMessageCodec.java @@ -68,8 +68,12 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { case "jt808.platform.textDown", "textDown" -> encodeTextDown(message, phone, flowId); // === 上行消息方法(不应用于下行) === + // 注意:正常情况下不应该到达这里,因为: + // 1. 业务层已跳过 JT808 协议的回复消息生成 + // 2. 协议处理器会自动生成通用应答 + // 这里保留作为防御性检查 case "thing.property.post", "thing.property.report" -> { - log.warn("[encode][JT808 协议不支持下行属性上报方法: {},请使用 thing.property.set 进行属性设置]", method); + log.warn("[encode][JT808 协议不支持下行属性上报方法: {},请使用 thing.property.set 进行属性设置。如果这是回复消息,应使用 jt808.platform.commonResp 方法]", method); yield new byte[0]; } @@ -83,6 +87,16 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { throw new RuntimeException("JT808 消息编码失败: " + e.getMessage(), e); } } + + /** + * 判断是否为上行消息方法 + */ + private boolean isUpstreamMethod(String method) { + return "thing.property.post".equals(method) + || "thing.property.report".equals(method) + || "thing.event.post".equals(method) + || "thing.state.update".equals(method); + } @Override public IotDeviceMessage decode(byte[] bytes) { @@ -259,38 +273,41 @@ public class IotJt808DeviceMessageCodec implements IotDeviceMessageCodec { /** * 解析按键事件为事件上报格式(thing.event.post) - * - * 物模型标准格式: + *

+ * 根据 JT808 协议: + * - 短按:keyId = 0x01-0x0A(对应1-10号键),keyState = 按键次数 + * - 长按:keyId = 0x0B-0x0E(对应长按1-4号键),keyState = 按键次数 + *

+ * 物模型标准格式(简化版,只保留 keyId 和 keyState): * { * "eventId": "button_event", * "eventTime": 1234567890, * "params": { - * "keyId": 1, - * "keyState": 1, - * "keyType": "short_press", - * "keyNumber": 1, - * "isLongPress": false + * "keyId": 1, // 0x01=短按1号键, 0x0B=长按1号键 + * "keyState": 1 // 按键次数(0x01=按键一次) * } * } + *

+ * 判断长按/短按:通过 keyId 判断 + * - keyId < 0x0B:短按 + * - keyId >= 0x0B:长按 */ private Map parseButtonEventAsEvent(Jt808DataPack dataPack) { Jt808ButtonEvent event = decoder.toButtonEventMsg(dataPack); Map result = new HashMap<>(); - // 统一使用一个事件标识符,通过 isLongPress 参数区分短按和长按 + // 统一使用一个事件标识符 result.put("eventId", "button_event"); // 事件时间戳 result.put("eventTime", System.currentTimeMillis()); - // 事件参数(包含 isLongPress 字段用于区分短按和长按) + // 事件参数(只保留 keyId 和 keyState 两个字段) + // 通过 keyId 可以判断是长按还是短按:keyId >= 0x0B 为长按 Map eventParams = new HashMap<>(); eventParams.put("keyId", event.getKeyId()); eventParams.put("keyState", event.getKeyState()); - eventParams.put("keyType", event.getKeyType()); - eventParams.put("keyNumber", event.getKeyNumber()); - eventParams.put("isLongPress", event.getIsLongPress()); result.put("params", eventParams); return result; diff --git a/viewshanghai-module-iot/viewshanghai-module-iot-gateway/src/main/java/com/viewshanghai/module/iot/gateway/protocol/tcp/handler/Jt808ProtocolHandler.java b/viewshanghai-module-iot/viewshanghai-module-iot-gateway/src/main/java/com/viewshanghai/module/iot/gateway/protocol/tcp/handler/Jt808ProtocolHandler.java index 9b397e5..bdadd89 100644 --- a/viewshanghai-module-iot/viewshanghai-module-iot-gateway/src/main/java/com/viewshanghai/module/iot/gateway/protocol/tcp/handler/Jt808ProtocolHandler.java +++ b/viewshanghai-module-iot/viewshanghai-module-iot-gateway/src/main/java/com/viewshanghai/module/iot/gateway/protocol/tcp/handler/Jt808ProtocolHandler.java @@ -113,10 +113,108 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { log.info("[handleBusinessMessage][JT808 业务消息已发送,clientId: {}, method: {}, messageId: {}]", clientId, message.getMethod(), message.getId()); + // JT808 协议要求:对每个上行消息都要回复通用应答(0x8001) + // 从消息的 requestId 中提取流水号和消息ID(格式:terminalPhone_flowId_timestamp) + // 从消息的 metadata 中提取终端手机号和原始消息ID + String terminalPhone = extractTerminalPhone(message); + int flowId = extractFlowId(message); + int msgId = extractJt808MsgId(message); + + if (terminalPhone != null && flowId > 0 && msgId > 0) { + // 发送通用应答(成功) + sendCommonResp(socket, terminalPhone, flowId, msgId, (byte) 0, codecType, message.getRequestId()); + } else { + log.warn("[handleBusinessMessage][无法提取 JT808 消息信息,跳过通用应答,terminalPhone: {}, flowId: {}, msgId: {}]", + terminalPhone, flowId, msgId); + } + } catch (Exception e) { log.error("[handleBusinessMessage][JT808 业务消息处理异常,clientId: {}]", clientId, e); } } + + /** + * 从消息中提取 JT808 流水号 + *

+ * 提取优先级: + * 1. 从 metadata.flowId 中提取(最可靠) + * 2. 从 requestId 中提取(格式:terminalPhone_flowId_timestamp) + */ + @SuppressWarnings("unchecked") + private int extractFlowId(IotDeviceMessage message) { + // 优先从 metadata 中提取 + if (message.getParams() instanceof Map) { + Map params = (Map) message.getParams(); + if (params.get("_metadata") instanceof Map) { + Map metadata = (Map) params.get("_metadata"); + Object flowId = metadata.get("flowId"); + if (flowId instanceof Number) { + return ((Number) flowId).intValue(); + } + } + } + + // 如果 metadata 中没有,尝试从 requestId 中提取 + String requestId = message.getRequestId(); + if (requestId != null && requestId.contains("_")) { + try { + String[] parts = requestId.split("_"); + if (parts.length >= 2) { + return Integer.parseInt(parts[1]); + } + } catch (Exception e) { + log.debug("[extractFlowId][从 requestId 提取流水号失败: {}]", requestId); + } + } + + return 0; + } + + /** + * 从消息中提取 JT808 消息ID + * 从 metadata 中获取原始 JT808 消息ID(格式:0x%04X,如 "0x0200") + */ + @SuppressWarnings("unchecked") + private int extractJt808MsgId(IotDeviceMessage message) { + Object params = message.getParams(); + if (params instanceof Map) { + Map paramsMap = (Map) params; + // 尝试从 _metadata 中获取 jt808MsgId(格式:0x%04X) + Object metadata = paramsMap.get("_metadata"); + if (metadata instanceof Map) { + Map metadataMap = (Map) metadata; + Object jt808MsgIdObj = metadataMap.get("jt808MsgId"); + if (jt808MsgIdObj instanceof String) { + String jt808MsgIdStr = (String) jt808MsgIdObj; + try { + // 解析 "0x0200" 格式的字符串 + if (jt808MsgIdStr.startsWith("0x") || jt808MsgIdStr.startsWith("0X")) { + return Integer.parseInt(jt808MsgIdStr.substring(2), 16); + } else { + return Integer.parseInt(jt808MsgIdStr, 16); + } + } catch (NumberFormatException e) { + log.debug("[extractJt808MsgId][解析 jt808MsgId 失败: {}]", jt808MsgIdStr); + } + } + } + } + // 如果无法从 metadata 中提取,尝试根据 method 映射到 JT808 消息ID + String method = message.getMethod(); + return mapMethodToJt808MsgId(method); + } + + /** + * 将物模型方法名映射到 JT808 消息ID + */ + private int mapMethodToJt808MsgId(String method) { + return switch (method) { + case "thing.property.post", "thing.property.report" -> 0x0200; // 位置信息汇报 + case "thing.event.post" -> 0x0006; // 按键事件 + case "thing.state.update" -> 0x0002; // 心跳 + default -> 0x0001; // 默认使用通用应答 + }; + } @Override public void sendResponse(NetSocket socket, IotDeviceMessage originalMessage, @@ -522,28 +620,6 @@ public class Jt808ProtocolHandler extends AbstractProtocolHandler { return null; } - /** - * 从消息中提取流水号 - *

- * 流水号存储在消息的 _metadata.flowId 字段中 - * - * @param message 设备消息 - * @return 流水号,提取失败返回 1 - */ - @SuppressWarnings("unchecked") - private int extractFlowId(IotDeviceMessage message) { - if (message.getParams() instanceof Map) { - Map params = (Map) message.getParams(); - if (params.get("_metadata") instanceof Map) { - Map metadata = (Map) params.get("_metadata"); - Object flowId = metadata.get("flowId"); - if (flowId instanceof Number) { - return ((Number) flowId).intValue(); - } - } - } - return 1; - } /** * 从消息中提取鉴权码