diff --git a/LogBook.md b/LogBook.md index 5acc02b..e88053e 100644 --- a/LogBook.md +++ b/LogBook.md @@ -135,23 +135,20 @@ - 从管理员控制台页面 (`charging_web_app/src/app/(authenticated)/admin/dashboard/page.tsx`) 移除了"会话管理"导航卡片按钮,以匹配后端不再提供管理员直接删除会话功能的调整。 ## YYYY-MM-DD (请替换为今天的实际日期) -- **MQTT 服务器域名更新**: - - 将后端 (`springboot-init-main/src/main/resources/application.yml`) 和单片机 (`mqtt_esp32_client/mqtt_esp32_client.ino`) 的 MQTT Broker 地址从 `broker.emqx.io` 更新为 `yuyun-hk1.stormrain.cn`。 - - 确保设备与自建 MQTT 服务器的通信正常进行。 +- 新建 MG995 舵机测试项目 `mg995_servo_test`。 + - 目标:测试 MG995 舵机与 ESP32-S3 的集成。 + - 硬件配置: + - 主控:ESP32-S3 + - 舵机:MG995 + - 连接引脚:舵机信号线连接到 GPIO 18。 + - 在 `mg995_servo_test/` 目录下创建了 `mg995_servo_test.ino` 文件,实现了舵机在 0-180 度之间往复运动的测试程序。 ## YYYY-MM-DD (请替换为今天的实际日期) -- **修复后端 MQTT 消息处理逻辑**: - - 修正 `springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttMessageHandler.java`,确保在机器人完成 `MOVE_TO_SPOT` 任务后,能够正确调用 `ChargingSessionService.handleRobotArrival` 方法更新充电会话状态。 - - **具体变更**: - - 在 `MqttMessageHandler` 中注入 `ChargingSessionService`。 - - 在 `handleRobotStatusUpdate` 方法中,当接收到 `COMPLETED` 状态的 `RobotTask` 消息,并且该任务的 `commandType` 为 `MOVE_TO_SPOT` 时,调用 `chargingSessionService.handleRobotArrival`,传入相关的会话 ID 和任务 ID。 - -## YYYY-MM-DD (请替换为今天的实际日期) -- **修复单片机 MQTT ACK 消息格式**: - - 修改 `mqtt_esp32_client/mqtt_esp32_client.ino`,使其发送的 ACK 消息符合后端 `MqttMessageHandlerImpl` 的期望格式。 - - **具体变更**: - - 删除旧的 `publish_ack_message` 函数。 - - 修改 `publish_status_update` 函数,使其只发送通用状态更新和心跳消息。 - - 新增一个 `publish_ack_message` 函数,用于构建包含 `command_ack`(指令中文描述)、`task_id`(数字类型)和 `success`(布尔类型)的 JSON 结构。 - - 在 `STOP_CHARGE` 指令的 ACK 中添加 `energy_kwh` 和 `duration_s` 字段。 - - 更新 `callback` 函数中所有 `publish_ack_message` 的调用,以使用新的函数签名和参数。 \ No newline at end of file +- 新建 Arduino Nano 的 MG995 舵机测试项目 `mg995_nano_test`。 + - 目标:将舵机控制方案切换到 Arduino Nano 平台。 + - 硬件配置: + - 主控:Arduino Nano + - 舵机:MG995 + - 连接引脚:舵机信号线连接到 Nano 的 D9 PWM 引脚。 + - 在 `mg995_nano_test/` 目录下创建了 `mg995_nano_test.ino` 文件,使用标准 `Servo.h` 库实现舵机往复运动。 + - **重要提示**:同样需要为 MG995 舵机提供独立的 5V 外部电源,并与 Nano 共地。 \ No newline at end of file diff --git a/mqtt_esp32_client/mqtt_esp32_client.ino b/mqtt_esp32_client/mqtt_esp32_client.ino index 3adab7e..5171f07 100644 --- a/mqtt_esp32_client/mqtt_esp32_client.ino +++ b/mqtt_esp32_client/mqtt_esp32_client.ino @@ -130,7 +130,7 @@ void callback(char *topic, byte *payload, unsigned int length) { if (cmdType == nullptr || taskId == nullptr) { Serial.println("指令JSON缺少 commandType/command 或 taskId 字段。"); - publish_ack_message(0, "指令解析失败", false, "Command JSON invalid (missing commandType/command or taskId)"); + publish_ack_message(taskId, false, "Command JSON invalid (missing commandType/command or taskId)", nullptr); return; } @@ -170,7 +170,7 @@ void callback(char *topic, byte *payload, unsigned int length) { chargeStartTimeMillis = millis(); Serial.println("状态更新: CHARGING (已到达目标车位 " + currentSpotId + ",视为开始充电)"); - publish_ack_message(taskId, "移动到指定点", true, "Robot arrived at spot and started charging (simulated)"); + publish_ack_message(taskId, true, "Robot arrived at spot and started charging (simulated)", currentSpotId.c_str()); publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr); @@ -196,7 +196,7 @@ void callback(char *topic, byte *payload, unsigned int length) { currentSessionId = ""; } Serial.println("模拟: 充电已启动。会话ID: " + currentSessionId + ", 车位: " + currentSpotId); - publish_ack_message(taskId, "开始充电", true, "Charging started successfully"); + publish_ack_message(taskId, true, "Charging started successfully", currentSessionId.c_str()); publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr); } else if (strcmp(cmdType, "STOP_CHARGE") == 0) { @@ -214,7 +214,7 @@ void callback(char *topic, byte *payload, unsigned int length) { currentSessionId = ""; // 在ACK中上报准确的充电时长,如果需要的话,可以通过修改 publish_ack_message 或在 message 字段中添加 // For now, the generic ACK is sent. - publish_ack_message(taskId, "停止充电", true, ("Charging stopped. Duration: " + String(chargeDuration) + "s").c_str(), currentEnergyConsumed, chargeDuration); + publish_ack_message(taskId, true, ("Charging stopped. Duration: " + String(chargeDuration) + "s").c_str(), previousSessionId.c_str()); publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr); } // Add other commandType handling here if needed, e.g., "QUERY_STATUS" @@ -225,7 +225,7 @@ void callback(char *topic, byte *payload, unsigned int length) { // } else { Serial.println("未知指令 commandType: " + String(cmdType)); - publish_ack_message(taskId, "未知指令", false, ("Unknown commandType: " + String(cmdType)).c_str()); + publish_ack_message(taskId, false, ("Unknown commandType: " + String(cmdType)).c_str(), nullptr); } Serial.println("-----------------------"); } @@ -254,7 +254,13 @@ void publish_status_update(bool isAckOrTaskUpdate, const char* ackTaskId, const doc["robotUid"] = spotUid; if (isAckOrTaskUpdate) { - if (ackTaskId) doc["taskId"] = ackTaskId; + if (ackTaskId) { + // 关键修复:同时提供 taskId 和 activeTaskId 字段 + // taskId 用于 robot_task 表的状态更新 + // activeTaskId 用于 charging_session 的业务流程推进 + doc["taskId"] = ackTaskId; + doc["activeTaskId"] = ackTaskId; + } if (ackStatus) doc["status"] = ackStatus; if (ackMessage) doc["message"] = ackMessage; // 根据用户要求,ACK中不发送errorCode @@ -323,30 +329,18 @@ void publish_heartbeat() { lastHeartbeatTime = millis(); } -// 新增ACK消息发布函数,以符合后端期望的格式 -void publish_ack_message(long taskId, const char* commandAckStr, bool success, const char* message, float energyKwh = -1.0f, int durationSeconds = -1) { - StaticJsonDocument<256> doc; // 调整大小以适应所有字段 - - doc["robotUid"] = spotUid; - doc["command_ack"] = commandAckStr; // 指令的中文描述 - doc["task_id"] = taskId; // 任务ID (数字类型) - doc["success"] = success; // 成功状态 (布尔类型) - - if (message && strlen(message) > 0) { - doc["message"] = message; +// Simplified ACK message function +void publish_ack_message(const char* taskId, bool success, const char* message, const char* contextInfo) { + if (!taskId || strlen(taskId) == 0) { + Serial.println("无法发送ACK: taskId 为空"); + return; } - - // 针对 STOP_CHARGE 指令的额外字段 - if (strcmp(commandAckStr, "停止充电") == 0) { - if (energyKwh >= 0) { - doc["energy_kwh"] = energyKwh; - } - if (durationSeconds >= 0) { - doc["duration_s"] = durationSeconds; - } - } - - publish_message(topic_uplink_to_backend, doc, "ACK"); + // Use the main publish_status_update function formatted as an ACK + // For contextInfo, we can pass spotId if relevant, or sessionId if that's what backend expects for ACKs. + // The 'true' indicates it's an ACK. + // The ackErrorCode field in publish_status_update will be set to "SUCCESS_ACK" or "FAILURE_ACK" + // 根据用户要求,ACK中的errorCode也暂时简化或移除。如果保留,确保含义清晰。 + publish_status_update(true, taskId, success ? "COMPLETED" : "FAILED", message, nullptr, contextInfo); } void setup() { diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/dto/mqtt/RobotStatusMessage.java b/springboot-init-main/src/main/java/com/yupi/project/model/dto/mqtt/RobotStatusMessage.java index dd3f005..4a4ca3a 100644 --- a/springboot-init-main/src/main/java/com/yupi/project/model/dto/mqtt/RobotStatusMessage.java +++ b/springboot-init-main/src/main/java/com/yupi/project/model/dto/mqtt/RobotStatusMessage.java @@ -4,18 +4,15 @@ import lombok.Data; @Data public class RobotStatusMessage { - - // Fields for Task ACK / specific message context - private Long taskId; // ID of the task this message might be an ACK for - private String status; // Status related to the taskId (e.g., "PROCESSING", "COMPLETED", "FAILED") - private String message; // General message, or error message for a task - private String errorCode; // Error code for a task failure - - // Fields for general robot status reporting (heartbeat, unsolicited status) - private String robotUid; // UID of the robot sending this status - private String actualRobotStatus; // The robot's current operational status (e.g., from RobotStatusEnum) - private String location; // Robot's current location - private Integer batteryLevel; // Robot's current battery level - private Long activeTaskId; // ID of the task the robot is currently busy with (if any) - -} \ No newline at end of file + private Long taskId; + private String status; + private String message; + private String errorCode; + private String robotUid; + private String actualRobotStatus; + private String location; + private Integer batteryLevel; + + // 关键修复:添加此字段以从MQTT消息中正确接收任务ID + private Long activeTaskId; +} \ No newline at end of file diff --git a/springboot-init-main/src/main/java/com/yupi/project/model/enums/CommandTypeEnum.java b/springboot-init-main/src/main/java/com/yupi/project/model/enums/CommandTypeEnum.java index 8f8db35..88afad1 100644 --- a/springboot-init-main/src/main/java/com/yupi/project/model/enums/CommandTypeEnum.java +++ b/springboot-init-main/src/main/java/com/yupi/project/model/enums/CommandTypeEnum.java @@ -27,6 +27,25 @@ public enum CommandTypeEnum { public String getAckValue() { return ackValue; } + + /** + * 根据 value 获取枚举 + * + * @param value + * @return + */ + public static CommandTypeEnum getEnumByValue(String value) { + if (value == null) { + return null; + } + for (CommandTypeEnum anEnum : CommandTypeEnum.values()) { + if (anEnum.value.equals(value)) { + return anEnum; + } + } + return null; + } + /** * 根据ackValue获取枚举 @@ -45,4 +64,4 @@ public enum CommandTypeEnum { } return null; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/impl/ChargingRobotServiceImpl.java b/springboot-init-main/src/main/java/com/yupi/project/service/impl/ChargingRobotServiceImpl.java index 8013307..571d8a2 100644 --- a/springboot-init-main/src/main/java/com/yupi/project/service/impl/ChargingRobotServiceImpl.java +++ b/springboot-init-main/src/main/java/com/yupi/project/service/impl/ChargingRobotServiceImpl.java @@ -6,10 +6,18 @@ import com.yupi.project.common.ErrorCode; import com.yupi.project.exception.BusinessException; import com.yupi.project.mapper.ChargingRobotMapper; import com.yupi.project.model.entity.ChargingRobot; +import com.yupi.project.model.entity.ParkingSpot; +import com.yupi.project.model.entity.RobotTask; +import com.yupi.project.model.enums.ParkingSpotStatusEnum; import com.yupi.project.model.enums.RobotStatusEnum; import com.yupi.project.service.ChargingRobotService; +import com.yupi.project.service.ParkingSpotService; +import com.yupi.project.service.RobotTaskService; +import com.yupi.project.service.ChargingSessionService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,6 +35,19 @@ import java.util.List; public class ChargingRobotServiceImpl extends ServiceImpl implements ChargingRobotService { + private final ParkingSpotService parkingSpotService; + private final RobotTaskService robotTaskService; + private final ChargingSessionService chargingSessionService; + + @Autowired + public ChargingRobotServiceImpl(ParkingSpotService parkingSpotService, + RobotTaskService robotTaskService, + @Lazy ChargingSessionService chargingSessionService) { + this.parkingSpotService = parkingSpotService; + this.robotTaskService = robotTaskService; + this.chargingSessionService = chargingSessionService; + } + @Override @Transactional public ChargingRobot registerRobot(String robotUid, RobotStatusEnum initialStatus) { @@ -57,8 +78,8 @@ public class ChargingRobotServiceImpl extends ServiceImpl() + .eq("id", assignedRobot.getId()) + .set("current_task_id", moveTask.getId()) + ); + if (!robotUpdated) { + // 如果更新失败,这是严重的内部错误,需要回滚所有操作 + log.error("为会话 {} 关联机器人任务ID失败!机器人ID: {}", sessionId, assignedRobot.getId()); + // 抛出异常以触发 @Transactional 回滚 + throw new BusinessException(ErrorCode.OPERATION_ERROR, "关联机器人任务失败"); + } + // 4. 更新充电会话信息 session.setRobotId(assignedRobot.getId()); session.setRobotUidSnapshot(assignedRobot.getRobotUid()); @@ -253,32 +269,58 @@ public class ChargingSessionServiceImpl extends ServiceImpl validPreviousStates = Arrays.asList( + ChargingSessionStatusEnum.CHARGING_STARTED.getValue() + ); + if (!validPreviousStates.contains(session.getStatus())) { + log.warn("处理充电结束事件失败,会话状态 ({}) 不正确 for session {}", session.getStatus(), sessionId); + // 如果已经是COMPLETED,直接返回成功,避免重复处理 + if (session.getStatus().equals(ChargingSessionStatusEnum.CHARGING_COMPLETED.getValue())) { + return true; + } return false; } + log.info("充电结束,开始处理会话 {} 的收尾工作. 关联任务ID: {}", sessionId, robotTaskId); + + // 1. 更新会话状态和核心数据 session.setStatus(ChargingSessionStatusEnum.CHARGING_COMPLETED.getValue()); session.setChargeEndTime(new Date()); - session.setEnergyConsumedKwh(energyConsumedKwh); - session.setTotalDurationSeconds(durationSeconds); - session.setUpdateTime(new Date()); - boolean updated = this.updateById(session); + session.setEnergyConsumedKwh(energyConsumedKwh); // 从机器人实际上报或系统估算 - if (updated) { - log.info("充电结束,会话 {} 状态更新为 CHARGING_COMPLETED. 电量:{} kWh, 时长:{}s. 关联任务ID: {}", - sessionId, energyConsumedKwh, durationSeconds, robotTaskId); - // 释放机器人和车位 - chargingRobotService.updateRobotStatus(session.getRobotUidSnapshot(), RobotStatusEnum.IDLE, null, null, null, new Date()); - parkingSpotService.updateSpotStatus(session.getSpotUidSnapshot(), ParkingSpotStatusEnum.AVAILABLE, null); // 车位变为空闲 - - // 执行计费和最终化会话逻辑 - this.calculateCostAndFinalizeSession(sessionId); + // 如果外部传入了时长,使用它;否则自己计算 + if (durationSeconds > 0) { + session.setTotalDurationSeconds(durationSeconds); + } else if (session.getChargeStartTime() != null) { + long durationMillis = System.currentTimeMillis() - session.getChargeStartTime().getTime(); + session.setTotalDurationSeconds((int) (durationMillis / 1000)); } - return updated; + session.setUpdateTime(new Date()); + + boolean updated = this.updateById(session); + if (!updated) { + log.error("更新会话 {} 状态为COMPLETED失败", sessionId); + return false; + } + + // 2. 释放资源 + // 释放机器人,使其变为空闲 + if(session.getRobotId() != null) { + chargingRobotService.releaseRobot(session.getRobotId()); + log.info("已释放机器人 {} for session {}", session.getRobotUidSnapshot(), sessionId); + } + // 释放车位 + if(session.getSpotId() != null) { + parkingSpotService.releaseSpot(session.getSpotId()); + log.info("已释放车位 {} for session {}", session.getSpotUidSnapshot(), sessionId); + } + + // 3. 进行计费 + log.info("开始为会话 {} 进行计费...", sessionId); + calculateCostAndFinalizeSession(sessionId); + + return true; } @Override diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttMessageHandler.java b/springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttMessageHandler.java index 39dc596..a0b9eb9 100644 --- a/springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttMessageHandler.java +++ b/springboot-init-main/src/main/java/com/yupi/project/service/impl/MqttMessageHandler.java @@ -66,43 +66,49 @@ public class MqttMessageHandler implements IMqttMessageListener { if (taskNewStatus == null) { log.error("Invalid task status '{}' received for task {} from robot {}. Payload: {}", statusMessage.getStatus(), statusMessage.getTaskId(), robotUIDFromTopicSource, payloadJson); - return; - } - boolean taskUpdated; // Defined outside switch - switch (taskNewStatus) { - case PROCESSING: - taskUpdated = robotTaskService.markTaskAsProcessing(statusMessage.getTaskId(), new Date(), statusMessage.getMessage()); - break; - case COMPLETED: - taskUpdated = robotTaskService.markTaskAsCompleted(statusMessage.getTaskId(), new Date(), statusMessage.getMessage()); - break; - case FAILED: - String combinedTaskErrorMessage = statusMessage.getErrorCode() != null ? - statusMessage.getErrorCode() + ": " + statusMessage.getMessage() : - statusMessage.getMessage(); - taskUpdated = robotTaskService.markTaskAsFailed(statusMessage.getTaskId(), combinedTaskErrorMessage, new Date()); - break; - default: - log.warn("Received unhandled RobotTaskStatusEnum: {} for task {} from robot {}. Ignoring.", - taskNewStatus, statusMessage.getTaskId(), robotUIDFromTopicSource); - return; - } - if (taskUpdated) { - log.info("Successfully updated task {} to status {} for robot {} based on MQTT message.", - statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource); + // Do not return, general status might still need processing. } else { - log.warn("Failed to update task {} to status {} for robot {} (or task not found/invalid state transition). Message: {}", - statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource, statusMessage.getMessage()); + boolean taskUpdated; // Defined outside switch + switch (taskNewStatus) { + case PROCESSING: + taskUpdated = robotTaskService.markTaskAsProcessing(statusMessage.getTaskId(), new Date(), statusMessage.getMessage()); + break; + case COMPLETED: + taskUpdated = robotTaskService.markTaskAsCompleted(statusMessage.getTaskId(), new Date(), statusMessage.getMessage()); + break; + case FAILED: + String combinedTaskErrorMessage = statusMessage.getErrorCode() != null ? + statusMessage.getErrorCode() + ": " + statusMessage.getMessage() : + statusMessage.getMessage(); + taskUpdated = robotTaskService.markTaskAsFailed(statusMessage.getTaskId(), combinedTaskErrorMessage, new Date()); + break; + default: + log.warn("Received unhandled RobotTaskStatusEnum: {} for task {} from robot {}. Ignoring.", + taskNewStatus, statusMessage.getTaskId(), robotUIDFromTopicSource); + taskUpdated = false; // To prevent logging success + break; + } + if (taskUpdated) { + log.info("Successfully updated task {} to status {} for robot {} based on MQTT message.", + statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource); + } else { + log.warn("Failed to update task {} to status {} for robot {} (or task not found/invalid state transition). Message: {}", + statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource, statusMessage.getMessage()); + } } } - else if (statusMessage.getActualRobotStatus() != null) { + + // Scenario 2: Message contains a general robot status update. + // This is now a separate "if", not an "else if", so it can execute even if a taskId was present. + // This is crucial for ACK messages that also convey a change in the robot's main status (e.g., from MOVING to CHARGING). + if (statusMessage.getActualRobotStatus() != null) { log.debug("Handling general status update from robot {}", robotUIDFromTopicSource); String actualRobotUIDToUse = StringUtils.isNotBlank(statusMessage.getRobotUid()) ? statusMessage.getRobotUid() : robotUIDFromTopicSource; if (StringUtils.isBlank(actualRobotUIDToUse)) { log.warn("Cannot determine a valid robot UID for general status update (from topic: {}, from message body: {}). Ignoring update.", robotUIDFromTopicSource, statusMessage.getRobotUid()); - return; + return; // Critical to have a UID, so we can return here. } RobotStatusEnum robotStatus = RobotStatusEnum.getEnumByValue(statusMessage.getActualRobotStatus()); @@ -120,7 +126,8 @@ public class MqttMessageHandler implements IMqttMessageListener { chargingRobotService.updateRobotStatus(actualRobotUIDToUse, robotStatus, location, batteryLevel, currentRobotTask, new Date()); } - else { + // A final check for completely empty/un-actionable messages. + else if (statusMessage.getTaskId() == null) { log.warn("RobotStatusMessage for robot {} has no taskId and no general status fields. Payload: {}. Ignoring.", robotUIDFromTopicSource, payloadJson); } diff --git a/springboot-init-main/src/main/java/com/yupi/project/service/impl/RobotTaskServiceImpl.java b/springboot-init-main/src/main/java/com/yupi/project/service/impl/RobotTaskServiceImpl.java index d6ff177..0cd3c39 100644 --- a/springboot-init-main/src/main/java/com/yupi/project/service/impl/RobotTaskServiceImpl.java +++ b/springboot-init-main/src/main/java/com/yupi/project/service/impl/RobotTaskServiceImpl.java @@ -7,9 +7,12 @@ import com.yupi.project.exception.BusinessException; import com.yupi.project.model.entity.RobotTask; import com.yupi.project.model.enums.CommandTypeEnum; import com.yupi.project.model.enums.RobotTaskStatusEnum; +import com.yupi.project.service.ChargingSessionService; import com.yupi.project.service.RobotTaskService; import com.yupi.project.mapper.RobotTaskMapper; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,6 +30,13 @@ import java.util.List; public class RobotTaskServiceImpl extends ServiceImpl implements RobotTaskService { + private final ChargingSessionService chargingSessionService; + + @Autowired + public RobotTaskServiceImpl(@Lazy ChargingSessionService chargingSessionService) { + this.chargingSessionService = chargingSessionService; + } + @Override public boolean hasPendingOrSentTask(String robotId) { if (robotId == null) { @@ -288,35 +298,26 @@ public class RobotTaskServiceImpl extends ServiceImpl