From 6e49c7e779d43f447f0bb97fb3c8d6d968d39aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E6=99=93=E4=B8=9C?= <17363321594@163.com> Date: Mon, 27 Oct 2025 15:55:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=85=B7=E7=AE=A1=E7=90=86=E6=94=B9?= =?UTF-8?q?=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dryingroom/alarm/AlarmLightService.java | 357 +++++++++++++++--- .../collect/AutoCollectService.java | 8 +- .../com/shgx/dryingroom/mqtt/MqttConsole.java | 12 +- 3 files changed, 324 insertions(+), 53 deletions(-) diff --git a/shgx-dryingroom/src/main/java/com/shgx/dryingroom/alarm/AlarmLightService.java b/shgx-dryingroom/src/main/java/com/shgx/dryingroom/alarm/AlarmLightService.java index fdf2bcf..2d94401 100644 --- a/shgx-dryingroom/src/main/java/com/shgx/dryingroom/alarm/AlarmLightService.java +++ b/shgx-dryingroom/src/main/java/com/shgx/dryingroom/alarm/AlarmLightService.java @@ -11,14 +11,14 @@ import java.util.List; import java.util.Map; /** - * 报警灯控制服务(多设备版本) + * 报警灯控制服务(多设备版本,支持声光控制+设备语音播放) */ @Service public class AlarmLightService { - private static final Logger log = LoggerFactory.getLogger(MqttService.class); + private static final Logger log = LoggerFactory.getLogger(AlarmLightService.class); - // 注入客户端管理器,替代原直接注入单个AlarmLightTcpClient + // 注入客户端管理器,管理多台报警灯TCP连接 @Autowired private AlarmLightTcpClientManager tcpClientManager; @@ -28,25 +28,71 @@ public class AlarmLightService { @Autowired private AlarmLightConfig alarmLightConfig; - // 预定义指令映射(保留原逻辑,支持多设备共用指令) + // 1. 预定义指令映射(含声光控制、音量、语音模式) private Map commandMap = new HashMap<>(); + // 2. 设备编码与语音序号映射(1-26:干燥机;27-30:除湿干燥机) + // 后续设备编码变更时,仅需修改此Map + private Map equipCodeToVoiceNum = new HashMap() {{ + // 26台干燥机(对应语音1-26号) + put("GZ-011", 1); + put("GZ-026", 2); + put("CS-001", 3); + put("CS-002", 4); + put("GZ-021", 5); + put("GZ-024", 6); + put("GZ-019", 7); + put("GZ-010", 8); + put("GZ-025", 9); + put("GZ-002", 10); + put("GZ-004", 11); + put("GZ-016", 12); + put("CS-003", 13); + put("GZ-005", 14); + put("CS-004", 15); + put("GZ-006", 16); + put("GZ-014", 17); + put("GZ-001", 18); + put("GZ-012", 19); + put("GZ-015", 20); + put("GZ-020", 21); + put("GZ-022", 22); + put("GZ-018", 23); + put("GZ-013", 24); + put("GZ-017", 25); + put("GZ-008", 26); + // 4台除湿干燥机(对应语音27-30号) + put("GZ-007", 27); + put("GZ-009", 28); + put("GZ-003", 29); + put("GZ-023", 30); + }}; + @PostConstruct public void init() { - initCommands(); // 初始化指令 + initCommands(); // 初始化指令集 subscribeMqttTopic(); // 订阅MQTT控制主题 } + /** + * 初始化报警灯指令集(按硬件提供的串口指令配置) + */ private void initCommands() { - commandMap.put("TURN_ON", "01 06 04 0E 00 03 A9 38"); // 打开报警灯 - commandMap.put("TURN_OFF", "01 06 04 0E 00 00 E9 39"); // 关闭报警灯 - commandMap.put("QUERY_STATUS", "01 03 00 00 00 01 84 0A"); // 查询状态 - commandMap.put("SLOW_BLINK", "此处替换为慢闪的十六进制指令"); // 补充慢闪指令 - commandMap.put("FAST_BLINK", "此处替换为快闪的十六进制指令"); // 补充快闪指令 + commandMap.put("TURN_ON", "01 06 04 0E 00 03 A9 38"); // 打开声音和灯光 + commandMap.put("TURN_OFF", "01 06 04 0E 00 00 E9 39"); // 关闭声光 + commandMap.put("TURN_ON_SOUND", "01 06 04 0E 00 01 28 F9"); // 单独打开声音 + commandMap.put("TURN_ON_LIGHT", "01 06 04 0E 00 02 68 F8"); // 单独打开灯光 + commandMap.put("VOLUME_LEVEL_1", "01 06 04 0F 00 01 79 39"); // 音量1级 + commandMap.put("VOLUME_LEVEL_30", "01 06 04 0F 00 1E 38 F1"); // 音量30级 + commandMap.put("SINGLE_LOOP", "01 06 04 11 00 01 19 3F"); // 单曲循环模式 + commandMap.put("SINGLE_PLAY", "01 06 04 11 00 02 59 3E"); // 单曲播放模式 + commandMap.put("QUERY_STATUS", "01 03 00 00 00 01 84 0A"); // 查询报警灯状态 + commandMap.put("SLOW_BLINK", ""); // 若有慢闪指令,替换为空字符串 + commandMap.put("FAST_BLINK", ""); // 若有快闪指令,替换为空字符串 } /** - * 订阅MQTT控制主题(保留原逻辑) + * 订阅MQTT控制主题(接收外部控制指令) */ private void subscribeMqttTopic() { mqttService.addMessageListener(message -> { @@ -56,7 +102,7 @@ public class AlarmLightService { } /** - * 处理MQTT控制消息(核心修改:按deviceId分发指令) + * 处理MQTT控制消息(支持:声光控制、语音播放) */ private void handleMqttControlMessage(String payload) { try { @@ -65,41 +111,94 @@ public class AlarmLightService { if ("alarm_light_control".equals(type)) { String command = json.getString("command"); - String deviceId = json.getString("deviceId"); // 从MQTT消息获取目标设备ID - - // 1. 根据deviceId获取对应客户端 + String deviceId = json.getString("deviceId"); AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); + + // 校验客户端是否存在 if (client == null) { - sendControlResult(deviceId, command, false, "未找到对应设备"); + sendControlResult(deviceId, command, false, "未找到对应报警灯设备"); return; } - // 2. 发送指令(修改为通过客户端实例发送) + // 分支1:处理语音播放指令(需传入equipmentCode) + if ("PLAY_VOICE".equals(command)) { + String equipmentCode = json.getString("equipmentCode"); + if (equipmentCode == null || equipmentCode.trim().isEmpty()) { + sendControlResult(deviceId, command, false, "缺少参数:equipmentCode(设备编码)"); + return; + } + // 生成语音播放指令并发送 + String voiceCommand = generateVoiceCommand(equipmentCode); + if (voiceCommand == null) { + sendControlResult(deviceId, command, false, "语音指令生成失败(设备编码无效)"); + return; + } + boolean success = client.sendHexCommand(voiceCommand); + sendControlResult(deviceId, command, success, + success ? "语音指令发送成功" : "语音指令发送失败"); + return; + } + + // 分支2:处理普通声光控制指令(TURN_ON/TURN_OFF等) boolean success = sendCommand(client, command); - // 3. 发送控制结果(保留原逻辑) - sendControlResult(deviceId, command, success, success ? "指令发送成功" : "指令发送失败"); + sendControlResult(deviceId, command, success, + success ? "指令发送成功" : "指令发送失败"); } } catch (Exception e) { - log.error("处理MQTT控制消息失败: {}", e.getMessage()); + log.error("处理MQTT控制消息异常: {}", e.getMessage(), e); } } /** - * 发送指令(修改为接收客户端实例,而非单例) + * 发送预定义指令(支持普通声光控制) */ public boolean sendCommand(AlarmLightTcpClient client, String command) { try { - // 先从预定义指令获取,没有则直接当作十六进制指令 + // 优先从预定义指令获取,无匹配则当作自定义十六进制指令 String hexCommand = commandMap.getOrDefault(command.toUpperCase(), command); + if (hexCommand.trim().isEmpty()) { + log.warn("设备[{}]指令为空,指令类型:{}", client.getDeviceId(), command); + return false; + } return client.sendHexCommand(hexCommand); } catch (Exception e) { - log.error("设备[{}]发送指令失败: {}", client.getDeviceId(), e.getMessage()); + log.error("设备[{}]发送指令失败,指令类型:{}", client.getDeviceId(), command, e); return false; } } /** - * 发送控制结果到MQTT(抽取为独立方法,便于复用) + * 生成语音播放指令(修改后:适配001-030的3位文件编号) + * 指令格式:01 06 04 10 [文件夹(01)] [3位文件编号转十六进制] [CRC16校验位] + */ + private String generateVoiceCommand(String equipmentCode) { + // 1. 获取语音序号(1-30,对应文件编号001-030) + Integer voiceNum = equipCodeToVoiceNum.get(equipmentCode); + if (voiceNum == null || voiceNum < 1 || voiceNum > 30) { + log.error("设备编码[{}]无匹配语音序号(需1-30,对应文件001-030)", equipmentCode); + return null; + } + + // 2. 关键修改:将序号格式化为3位数字字符串(如1→"001",30→"030") + String voiceFileNum = String.format("%03d", voiceNum); // 3位文件编号 + // 3. 将3位数字字符串转十六进制(如"001"→0x0001→"00 01","030"→0x001E→"00 1E") + int numInt = Integer.parseInt(voiceFileNum); + String numHex = String.format("%04X", numInt); // 转4位十六进制(对应2字节) + String numHexSplit = numHex.substring(0, 2) + " " + numHex.substring(2, 4); // 拆分为2字节,加空格 + + // 4. 构建指令前缀(文件夹固定为02) + String folderHex = "01"; // 语音文件存放在第二个文件夹 + String prefix = "01 06 04 10 " + folderHex + " " + numHexSplit; // 前6字节指令 + + // 5. 计算CRC16-Modbus校验位(小端模式,逻辑不变) + String crcHex = Crc16Calculator.calculate(prefix.replaceAll(" ", "")); + + // 6. 拼接完整指令(前缀+校验位) + return prefix + " " + crcHex; + } + + /** + * 发送控制结果到MQTT(供前端/其他服务监听) */ private void sendControlResult(String deviceId, String command, boolean success, String msg) { String resultTopic = "alarm_light/" + deviceId + "/control_result"; @@ -110,11 +209,90 @@ public class AlarmLightService { result.put("message", msg); result.put("timestamp", System.currentTimeMillis()); - mqttService.sendMessage(resultTopic, result.toJSONString()); - log.info("设备[{}]控制结果已发布到MQTT: {}", deviceId, result); + try { + mqttService.sendMessage(resultTopic, result.toJSONString()); + log.info("报警灯[{}]控制结果已发布,主题:{},内容:{}", deviceId, resultTopic, result); + } catch (Exception e) { + log.error("发布报警灯[{}]控制结果失败", deviceId, e); + } } - // ---------------------- 以下为单设备快捷方法(按需保留) ---------------------- + // ---------------------- 批量控制方法(报警触发核心) ---------------------- + /** + * 批量打开所有报警灯(声光同时开启) + */ + public void turnOnAll() { + List devices = alarmLightConfig.getDevices(); + if (devices == null || devices.isEmpty()) { + log.warn("无配置的报警灯设备,无法执行批量打开"); + return; + } + + for (AlarmLightConfig.DeviceConfig device : devices) { + String deviceId = device.getDeviceId(); + AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); + if (client == null) { + log.warn("报警灯[{}]客户端不存在,跳过批量打开", deviceId); + continue; + } + boolean success = sendCommand(client, "TURN_ON"); + log.info("报警灯[{}]批量打开结果:{}", deviceId, success ? "成功" : "失败"); + } + } + + /** + * 批量关闭所有报警灯(声光同时关闭) + */ + public void turnOffAll() { + List devices = alarmLightConfig.getDevices(); + if (devices == null || devices.isEmpty()) { + log.warn("无配置的报警灯设备,无法执行批量关闭"); + return; + } + + for (AlarmLightConfig.DeviceConfig device : devices) { + String deviceId = device.getDeviceId(); + AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); + if (client == null) { + log.warn("报警灯[{}]客户端不存在,跳过批量关闭", deviceId); + continue; + } + boolean success = sendCommand(client, "TURN_OFF"); + log.info("报警灯[{}]批量关闭结果:{}", deviceId, success ? "成功" : "失败"); + } + } + + /** + * 批量播放指定设备的语音(所有报警灯同时播放同一设备语音) + */ + public void playVoiceOnAll(String equipmentCode) { + List devices = alarmLightConfig.getDevices(); + if (devices == null || devices.isEmpty()) { + log.warn("无配置的报警灯设备,无法执行批量语音播放"); + return; + } + + // 生成当前设备的语音指令(所有报警灯共用同一指令) + String voiceCommand = generateVoiceCommand(equipmentCode); + if (voiceCommand == null) { + log.error("设备[{}]语音指令生成失败,终止批量播放", equipmentCode); + return; + } + + for (AlarmLightConfig.DeviceConfig device : devices) { + String deviceId = device.getDeviceId(); + AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); + if (client == null) { + log.warn("报警灯[{}]客户端不存在,跳过语音播放", deviceId); + continue; + } + boolean success = client.sendHexCommand(voiceCommand); + log.info("报警灯[{}]播放设备[{}]语音结果:{},指令:{}", + deviceId, equipmentCode, success ? "成功" : "失败", voiceCommand); + } + } + + // ---------------------- 单设备控制方法(按需调用) ---------------------- public boolean turnOn(String deviceId) { AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); return client != null && sendCommand(client, "TURN_ON"); @@ -125,14 +303,34 @@ public class AlarmLightService { return client != null && sendCommand(client, "TURN_OFF"); } - public boolean slowBlink(String deviceId) { + public boolean turnOnSound(String deviceId) { AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); - return client != null && sendCommand(client, "SLOW_BLINK"); + return client != null && sendCommand(client, "TURN_ON_SOUND"); } - public boolean fastBlink(String deviceId) { + public boolean turnOnLight(String deviceId) { AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); - return client != null && sendCommand(client, "FAST_BLINK"); + return client != null && sendCommand(client, "TURN_ON_LIGHT"); + } + + public boolean setVolumeLevel1(String deviceId) { + AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); + return client != null && sendCommand(client, "VOLUME_LEVEL_1"); + } + + public boolean setVolumeLevel30(String deviceId) { + AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); + return client != null && sendCommand(client, "VOLUME_LEVEL_30"); + } + + public boolean setSingleLoop(String deviceId) { + AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); + return client != null && sendCommand(client, "SINGLE_LOOP"); + } + + public boolean setSinglePlay(String deviceId) { + AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); + return client != null && sendCommand(client, "SINGLE_PLAY"); } public boolean queryStatus(String deviceId) { @@ -150,33 +348,106 @@ public class AlarmLightService { return client != null && client.isConnected(); } - // 新增:批量控制所有报警灯(同时打开) - public void turnOnAll() { - // 获取配置中的所有设备ID + /** + * 核心报警方法:批量切换所有报警灯到目标设备语音 → 再打开声光(确保语音对应) + * @param equipmentCode 报警设备编码(如GZ-011) + */ + public void triggerAlarmWithVoice(String equipmentCode) { List devices = alarmLightConfig.getDevices(); if (devices == null || devices.isEmpty()) { - System.out.println("⚠️ 未配置任何报警灯设备"); + log.warn("无配置的报警灯设备,无法触发报警"); return; } - // 遍历所有deviceId,逐个发送“打开”指令 + + // 1. 生成目标设备的语音切换指令(第一步:切语音) + String voiceSwitchCommand = generateVoiceCommand(equipmentCode); + if (voiceSwitchCommand == null) { + log.error("设备[{}]语音切换指令生成失败,终止报警", equipmentCode); + return; + } + + // 2. 遍历所有报警灯,执行“切语音→开声光” for (AlarmLightConfig.DeviceConfig device : devices) { String deviceId = device.getDeviceId(); - boolean success = turnOn(deviceId); // 调用单个设备的打开方法 - System.out.println("设备[" + deviceId + "] 批量打开" + (success ? "成功" : "失败")); + AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); + if (client == null) { + log.warn("报警灯[{}]客户端不存在,跳过报警", deviceId); + continue; + } + + // 步骤1:先发送语音切换指令(切到目标设备语音) + boolean switchSuccess = client.sendHexCommand(voiceSwitchCommand); + if (!switchSuccess) { + log.error("报警灯[{}]切换语音失败,指令:{}", deviceId, voiceSwitchCommand); + continue; + } + + // 步骤2:再发送打开声光指令(播放已切换好的语音) + boolean alarmSuccess = sendCommand(client, "TURN_ON"); + log.info("报警灯[{}]处理结果:语音切换{},声光开启{},目标设备:{}", + deviceId, switchSuccess ? "成功" : "失败", + alarmSuccess ? "成功" : "失败", equipmentCode); } } - // 可选:批量关闭所有报警灯 - public void turnOffAll() { + /** + * 批量关闭所有报警灯(关闭声光,重置状态) + */ + public void stopAllAlarm() { List devices = alarmLightConfig.getDevices(); if (devices == null || devices.isEmpty()) { - System.out.println("⚠️ 未配置任何报警灯设备"); + log.warn("无配置的报警灯设备,无法关闭报警"); return; } + for (AlarmLightConfig.DeviceConfig device : devices) { String deviceId = device.getDeviceId(); - boolean success = turnOff(deviceId); - System.out.println("设备[" + deviceId + "] 批量关闭" + (success ? "成功" : "失败")); + AlarmLightTcpClient client = tcpClientManager.getClientByDeviceId(deviceId); + if (client == null) { + log.warn("报警灯[{}]客户端不存在,跳过关闭", deviceId); + continue; + } + + // 发送关闭声光指令 + boolean success = sendCommand(client, "TURN_OFF"); + log.info("报警灯[{}]关闭声光结果:{}", deviceId, success ? "成功" : "失败"); + } + } + + // ---------------------- 内部工具类:CRC16-Modbus校验位计算(小端模式) ---------------------- + private static class Crc16Calculator { + private static final int POLYNOMIAL = 0x8005; // CRC16-Modbus多项式 + private static final int INITIAL_VALUE = 0xFFFF; // 初始值 + + /** + * 计算十六进制字符串的CRC16校验位(返回小端模式的十六进制字符串,空格分隔) + */ + public static String calculate(String hexString) { + try { + // 十六进制字符串转字节数组 + byte[] bytes = new byte[hexString.length() / 2]; + for (int i = 0; i < bytes.length; i++) { + int pos = i * 2; + bytes[i] = (byte) Integer.parseInt(hexString.substring(pos, pos + 2), 16); + } + + // 计算CRC16 + int crc = INITIAL_VALUE; + for (byte b : bytes) { + crc ^= (b & 0xFF); + for (int i = 0; i < 8; i++) { + crc = (crc & 0x0001) != 0 ? (crc >> 1) ^ POLYNOMIAL : crc >> 1; + } + } + + // 转换为小端模式(低字节在前,高字节在后) + int crcLow = (crc & 0xFF); // 低8位 + int crcHigh = (crc >> 8) & 0xFF; // 高8位 + return String.format("%02X %02X", crcLow, crcHigh); + } catch (Exception e) { + log.error("CRC16校验位计算失败,输入十六进制字符串:{}", hexString, e); + return ""; + } } } } \ No newline at end of file diff --git a/shgx-dryingroom/src/main/java/com/shgx/dryingroom/collect/AutoCollectService.java b/shgx-dryingroom/src/main/java/com/shgx/dryingroom/collect/AutoCollectService.java index 6fd7903..2c6e097 100644 --- a/shgx-dryingroom/src/main/java/com/shgx/dryingroom/collect/AutoCollectService.java +++ b/shgx-dryingroom/src/main/java/com/shgx/dryingroom/collect/AutoCollectService.java @@ -287,8 +287,8 @@ public class AutoCollectService { updateWrapper.eq(DryEquipmentInfo::getEquipmentCode, data.getEquipmentCode()) .set(DryEquipmentInfo::getStatus, '2'); equipmentInfoMapper.update(updateWrapper); - // MQTT向报警灯发送报警 - alarmLightService.turnOnAll(); + /** MQTT向报警灯发送报警 */ + alarmLightService.triggerAlarmWithVoice(data.getEquipmentCode()); } if (parseThresholdValue(threshold.getMinValue()) > parseThresholdValue(data.getDataValue())) { // 插入报警日志 @@ -305,8 +305,8 @@ public class AutoCollectService { // 修改设备状态 equipmentInfoMapper.update(updateWrapper); logMapper.insert(log); - // MQTT向报警灯发送报警 - alarmLightService.turnOnAll(); + /** MQTT向报警灯发送报警 */ + alarmLightService.triggerAlarmWithVoice(data.getEquipmentCode()); } else { return; } diff --git a/shgx-dryingroom/src/main/java/com/shgx/dryingroom/mqtt/MqttConsole.java b/shgx-dryingroom/src/main/java/com/shgx/dryingroom/mqtt/MqttConsole.java index 21fba2a..7aa461d 100644 --- a/shgx-dryingroom/src/main/java/com/shgx/dryingroom/mqtt/MqttConsole.java +++ b/shgx-dryingroom/src/main/java/com/shgx/dryingroom/mqtt/MqttConsole.java @@ -151,12 +151,12 @@ public class MqttConsole implements CommandLineRunner { case "OFF": success = alarmLightService.turnOff(deviceId); break; - case "SLOW": - success = alarmLightService.slowBlink(deviceId); - break; - case "FAST": - success = alarmLightService.fastBlink(deviceId); - break; +// case "SLOW": +// success = alarmLightService.slowBlink(deviceId); +// break; +// case "FAST": +// success = alarmLightService.fastBlink(deviceId); +// break; case "STATUS": success = alarmLightService.queryStatus(deviceId); break;