package xiaozhi.modules.parent.mqtt; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import xiaozhi.common.exception.RenException; import xiaozhi.modules.sys.service.SysParamsService; @Slf4j @Component @RequiredArgsConstructor public class ParentMqttPublisher { private static final int PAYLOAD_SOFT_MAX = 7800; private final SysParamsService sysParamsService; private final ObjectMapper objectMapper; public void publishCtl(String macAddress, String action, Object payload) { String host = StringUtils.trimToEmpty(sysParamsService.getValue("server.parent_mqtt_host", true)); String portText = StringUtils.trimToEmpty(sysParamsService.getValue("server.parent_mqtt_port", true)); String username = StringUtils.trimToEmpty(sysParamsService.getValue("server.parent_mqtt_username", true)); String password = StringUtils.trimToEmpty(sysParamsService.getValue("server.parent_mqtt_password", true)); if (StringUtils.isBlank(host) || "null".equalsIgnoreCase(host)) { throw new RenException("未配置 server.parent_mqtt_host,无法下发到设备"); } int port = 2100; try { if (StringUtils.isNotBlank(portText) && !"null".equalsIgnoreCase(portText)) { port = Integer.parseInt(portText); } } catch (NumberFormatException ex) { throw new RenException("server.parent_mqtt_port 无效"); } String mac = normalizeMac(macAddress); String topic = "snlp/v1/" + mac + "/cmd/ctl"; Map header = new LinkedHashMap<>(); header.put("version", "1.0"); header.put("device_id", mac); header.put("req_id", UUID.randomUUID().toString()); header.put("timestamp", System.currentTimeMillis() / 1000L); header.put("ttl", 60); Map body = new LinkedHashMap<>(); body.put("header", header); body.put("action", action); body.put("payload", payload == null ? Map.of() : payload); String json; try { json = objectMapper.writeValueAsString(body); } catch (Exception ex) { throw new RenException("家长任务 JSON 序列化失败"); } if (json.length() > PAYLOAD_SOFT_MAX) { throw new RenException("下发内容超过设备 MQTT 8KB 上限,请减少计划条数(不要下发展开后的课文清单)"); } String broker = "tcp://" + host + ":" + port; String clientId = "parent-api-" + UUID.randomUUID(); try (MqttClient client = new MqttClient(broker, clientId, new MemoryPersistence())) { MqttConnectOptions options = new MqttConnectOptions(); options.setAutomaticReconnect(false); options.setCleanSession(true); options.setConnectionTimeout(8); if (StringUtils.isNotBlank(username) && !"null".equalsIgnoreCase(username)) { options.setUserName(username); } if (StringUtils.isNotBlank(password) && !"null".equalsIgnoreCase(password)) { options.setPassword(password.toCharArray()); } client.connect(options); MqttMessage message = new MqttMessage(json.getBytes(java.nio.charset.StandardCharsets.UTF_8)); message.setQos(0); message.setRetained(false); client.publish(topic, message); client.disconnect(); log.info("parent mqtt {} -> {} bytes={}", action, topic, json.length()); } catch (MqttException ex) { log.error("parent mqtt publish failed: {}", ex.getMessage()); throw new RenException("下发设备失败: " + ex.getMessage()); } } public void publishLessonManifest(String macAddress, String courseId, String baseUrl, List files) { if (files == null || files.isEmpty()) { return; } String course = StringUtils.defaultIfBlank(courseId, "lesson-sync").trim(); if (!course.matches("[A-Za-z0-9_-]{1,31}")) { course = "lesson-sync"; } String base = StringUtils.trimToEmpty(baseUrl).replaceAll("/+$", ""); if (StringUtils.isBlank(base) || "null".equalsIgnoreCase(base) || base.contains("你")) { throw new RenException("未配置课文音频下载地址 server.lesson_audio_public_url"); } Map payload = new LinkedHashMap<>(); payload.put("course_id", course); payload.put("base_url", base); payload.put("files", files); publishCtl(macAddress, "UPDATE_MANIFEST", payload); } public static String normalizeMac(String macAddress) { return StringUtils.trimToEmpty(macAddress).toLowerCase().replace('-', ':'); } }