单片机互通调试初步完成

This commit is contained in:
2025-05-27 16:44:45 +08:00
parent 22e1109d81
commit 6aaae06bac
22 changed files with 1678 additions and 129 deletions

View File

@@ -26,19 +26,26 @@ WiFiClient espClient;
PubSubClient client(espClient);
// 模拟硬件状态 (实际项目中需要从传感器或硬件逻辑获取)
const char* currentDeviceStatus = "IDLE"; // 设备当前状态: IDLE, CHARGING, FAULTED 等
const char* currentDeviceStatus = "IDLE"; // 设备当前状态: IDLE, CHARGING, COMPLETED, FAULTED 等
float currentVoltage = 220.0;
float currentCurrent = 0.0;
float currentPower = 0.0;
float currentEnergyConsumed = 0.0;
int currentErrorCode = 0;
String currentSessionId = ""; // 当前充电会话ID
String currentSimulatedLocation = "BASE_STATION"; // 新增:模拟当前位置
int currentSimulatedBattery = 95; // 新增:模拟当前电量
String currentTargetSpot = ""; // 新增:用于移动状态的目标车位
String currentSpotId = ""; // 新增:当前所在车位 (用于充电等状态)
unsigned long chargeStartTimeMillis = 0; // 新增:用于计算充电时长
// 定时发送相关
unsigned long lastStatusUpdateTime = 0;
unsigned long lastHeartbeatTime = 0;
const long statusUpdateInterval = 30000; // 状态上报间隔 (例如: 30秒)
const long heartbeatInterval = 60000; // 心跳间隔 (例如: 60秒)
unsigned long lastBatteryUpdateTime = 0; // 新增:用于模拟电池电量变化的时间戳
const long batteryUpdateInterval = 5000; // 新增电池状态更新间隔例如5秒
void setup_mqtt_topics() {
String backend_status_base = "yupi_mqtt_power_project/robot/status/";
@@ -113,50 +120,102 @@ void callback(char *topic, byte *payload, unsigned int length) {
return;
}
const char* cmdType = doc["commandType"]; // 例如: "START_CHARGE", "STOP_CHARGE"
const char* taskId = doc["taskId"]; // 用于ACK
const char* cmdType = nullptr;
if (doc.containsKey("commandType")) {
cmdType = doc["commandType"];
} else if (doc.containsKey("command")) {
cmdType = doc["command"];
}
const char* taskId = doc["taskId"];
if (cmdType == nullptr || taskId == nullptr) {
Serial.println("指令JSON缺少 commandType 或 taskId 字段。");
publish_ack_message(taskId, false, "Command JSON invalid", nullptr); // 尝试ACK错误
Serial.println("指令JSON缺少 commandType/command 或 taskId 字段。");
publish_ack_message(taskId, false, "Command JSON invalid (missing commandType/command or taskId)", nullptr);
return;
}
if (strcmp(cmdType, "MOVE_TO_SPOT") == 0) {
Serial.println("接收到 [移动到车位] 指令");
// const char* targetSpotUid = doc["target_spot_uid"]; // 可选: 从payload中获取目标车位ID (如果存在且需要进一步处理)
// if (targetSpotUid) {
// Serial.println("目标车位UID: " + String(targetSpotUid));
// }
// 模拟机器人移动到指定位置的动作
Serial.println("模拟: 机器人正在移动到目标车位...");
delay(1000); // 模拟移动耗时 (缩短演示时间)
Serial.println("模拟: 机器人已到达目标车位。");
publish_ack_message(taskId, true, "Robot arrived at spot (simulated)", nullptr);
// 注意:此时设备状态 currentDeviceStatus 可以保持不变,或根据业务逻辑更新
// 例如: currentDeviceStatus = "IDLE_AT_SPOT";
// 如果需要,可以立即上报一次状态: publish_regular_status_update();
const char* spotIdFromCommand = nullptr;
if (doc.containsKey("spotId")) {
spotIdFromCommand = doc["spotId"].as<const char*>();
} else if (doc.containsKey("target_spot_uid")) {
spotIdFromCommand = doc["target_spot_uid"].as<const char*>();
}
} else if (strcmp(cmdType, "START_CHARGE") == 0) {
if (strcmp(cmdType, "MOVE_TO_SPOT") == 0 || strcmp(cmdType, "MOVE") == 0) {
Serial.println("接收到 [移动] 指令 (MOVE_TO_SPOT 或 MOVE)");
if (spotIdFromCommand) {
currentTargetSpot = String(spotIdFromCommand);
currentSpotId = "";
} else {
currentTargetSpot = "UNKNOWN_SPOT";
}
currentDeviceStatus = "MOVING";
currentSimulatedLocation = "EN_ROUTE_TO_" + currentTargetSpot;
Serial.println("状态更新: MOVING (前往目标车位: " + currentTargetSpot + ")");
publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr);
Serial.println("模拟: 机器人正在移动到目标车位 " + currentTargetSpot + "...");
// 模拟移动过程中的电量消耗,更细致的可以在 loop 中做
if (currentSimulatedBattery > 10) currentSimulatedBattery -= 5; // 假设移动固定消耗一些电
else currentSimulatedBattery = 5; // 最低电量
delay(3000);
Serial.println("模拟: 机器人已到达目标车位 " + currentTargetSpot + "");
currentDeviceStatus = "CHARGING";
currentSpotId = currentTargetSpot;
currentTargetSpot = "";
currentSimulatedLocation = currentSpotId;
chargeStartTimeMillis = millis();
Serial.println("状态更新: CHARGING (已到达目标车位 " + currentSpotId + ",视为开始充电)");
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);
} else if (strcmp(cmdType, "START_CHARGE") == 0) {
Serial.println("接收到 [启动充电] 指令");
currentDeviceStatus = "CHARGING";
if (strcmp(currentDeviceStatus, "CHARGING") != 0) { // 仅当未在充电时才响应
currentDeviceStatus = "CHARGING";
if (spotIdFromCommand) {
currentSpotId = String(spotIdFromCommand);
} else if (currentSpotId.length() == 0) {
currentSpotId = "DEFAULT_SPOT"; // 如果没有从指令获取且之前也没有,给个默认值
}
currentSimulatedLocation = currentSpotId;
chargeStartTimeMillis = millis();
Serial.println("状态更新: CHARGING (指令启动于 " + currentSpotId + ")");
} else {
Serial.println("设备已在充电中,忽略 START_CHARGE 指令。");
}
if (doc.containsKey("sessionId")) {
currentSessionId = String(doc["sessionId"].as<const char*>());
} else {
currentSessionId = ""; //确保没有sessionId时清空
currentSessionId = "";
}
Serial.println("模拟: 充电已启动。会话ID: " + currentSessionId);
Serial.println("模拟: 充电已启动。会话ID: " + currentSessionId + ", 车位: " + currentSpotId);
publish_ack_message(taskId, true, "Charging started successfully", currentSessionId.c_str());
publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr); // 立即更新状态
publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr);
} else if (strcmp(cmdType, "STOP_CHARGE") == 0) {
Serial.println("接收到 [停止充电] 指令");
bool wasCharging = strcmp(currentDeviceStatus, "CHARGING") == 0;
currentDeviceStatus = "COMPLETED";
Serial.println("模拟: 充电已停止。");
String previousSessionId = currentSessionId; // 保存一下以防ACK需要
currentSimulatedLocation = currentSpotId;
unsigned long chargeDuration = 0;
if (chargeStartTimeMillis > 0 && wasCharging) {
chargeDuration = (millis() - chargeStartTimeMillis) / 1000;
}
chargeStartTimeMillis = 0;
Serial.println("模拟: 充电已停止。车位: " + currentSpotId + ", 本次充电时长约: " + String(chargeDuration) + "s");
String previousSessionId = currentSessionId;
currentSessionId = "";
publish_ack_message(taskId, true, "Charging stopped successfully", previousSessionId.c_str());
publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr); // 立即更新状态
// 在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(), previousSessionId.c_str());
publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr);
}
// Add other commandType handling here if needed, e.g., "QUERY_STATUS"
// else if (strcmp(cmdType, "QUERY_STATUS") == 0) {
@@ -196,26 +255,47 @@ void publish_status_update(bool isAckOrTaskUpdate, const char* ackTaskId, const
if (isAckOrTaskUpdate) {
if (ackTaskId) doc["taskId"] = ackTaskId;
if (ackStatus) doc["status"] = ackStatus; // e.g., "SUCCESS", "FAILURE" or task-specific status
if (ackStatus) doc["status"] = ackStatus;
if (ackMessage) doc["message"] = ackMessage;
if (ackErrorCode) doc["errorCode"] = ackErrorCode;
// actualRobotStatus should still be sent to reflect current state after ACK
// 根据用户要求ACK中不发送errorCode
// if (ackErrorCode) doc["errorCode"] = ackErrorCode;
doc["actualRobotStatus"] = currentDeviceStatus;
if (ackSessionId && strlen(ackSessionId) > 0) doc["activeTaskId"] = ackSessionId; // Assuming activeTaskId can hold sessionId for context in ACKs
// Or, if RobotStatusMessage is extended for sessionId in future.
// For now, activeTaskId might be a way to correlate, or it might be ignored by backend for ACKs.
if (ackSessionId && strlen(ackSessionId) > 0) {
// For ACKs, if we have a sessionId or a spotId that's relevant for context, we can add it.
// Let's use "spotId" as suggested by requirements for charging related ACKs
if (strcmp(currentDeviceStatus, "CHARGING") == 0 || strcmp(currentDeviceStatus, "COMPLETED") == 0 || (ackMessage && strstr(ackMessage, "arrived"))) {
if(currentSpotId.length() > 0) doc["spotId"] = currentSpotId;
}
}
} else { // General status update / heartbeat
doc["actualRobotStatus"] = currentDeviceStatus;
doc["voltage"] = currentVoltage; // Example: Add these if backend expects them with general status
doc["current"] = currentCurrent;
doc["power"] = currentPower;
doc["energyConsumed"] = currentEnergyConsumed;
doc["errorCode"] = currentErrorCode; // General device error code
if (currentSessionId.length() > 0) {
// For general status, if a session is active, it might be relevant as activeTaskId
// This depends on how backend interprets activeTaskId outside of specific task ACKs.
doc["activeTaskId"] = currentSessionId; // Or a more generic field if RobotStatusMessage evolves
// Add fields as per requirements.md
doc["location"] = currentSimulatedLocation;
doc["battery"] = currentSimulatedBattery; // Example value, should be updated by a battery sim function
// doc["errorCode"] = currentErrorCode; // 根据用户要求常规状态下不发送errorCode
// Fields specific to certain statuses
if (strcmp(currentDeviceStatus, "MOVING") == 0) {
if (currentTargetSpot.length() > 0) doc["targetSpot"] = currentTargetSpot;
} else if (strcmp(currentDeviceStatus, "CHARGING") == 0) {
if (currentSpotId.length() > 0) doc["spotId"] = currentSpotId;
if (chargeStartTimeMillis > 0) {
doc["durationSeconds"] = (millis() - chargeStartTimeMillis) / 1000;
} else {
doc["durationSeconds"] = 0;
}
} else if (strcmp(currentDeviceStatus, "COMPLETED") == 0) {
if (currentSpotId.length() > 0) doc["spotId"] = currentSpotId;
// totalDurationSeconds would typically be set upon actual completion event,
// for now, a regular status update might not have final total, or we can omit.
// Let's assume for now that if status is COMPLETED, we send a placeholder or last known duration.
// A more robust solution is needed for totalDurationSeconds.
// For now, publish_ack_message for STOP_CHARGE should probably carry the final duration.
} else if (strcmp(currentDeviceStatus, "ERROR") == 0) {
// errorCode is already included. message for error could be added if available.
// doc["message"] = "Simulated error description"; // if we have one
}
}
// Common fields (timestamp can be added by backend or here if NTP is used)
@@ -244,14 +324,17 @@ void publish_heartbeat() {
}
// Simplified ACK message function
void publish_ack_message(const char* taskId, bool success, const char* message, const char* sessionIdForAckContext) {
void publish_ack_message(const char* taskId, bool success, const char* message, const char* contextInfo) {
if (!taskId || strlen(taskId) == 0) {
Serial.println("无法发送ACK: taskId 为空");
// Potentially send a general error status if appropriate, but usually ACK needs a taskId
return;
}
// Use the main publish_status_update function formatted as an ACK
publish_status_update(true, taskId, success ? "SUCCESS" : "FAILURE", message, success ? "0" : "GENERAL_ERROR_ON_ACK", sessionIdForAckContext);
// 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 ? "SUCCESS" : "FAILURE", message, nullptr, contextInfo);
}
void setup() {
@@ -293,4 +376,52 @@ void loop() {
currentPower = 0.0;
}
delay(100); // 短暂延时避免loop过于频繁给其他任务一点时间 (可选)
// 模拟电量消耗和位置变化 (更符合需求文档)
if (strcmp(currentDeviceStatus, "CHARGING") == 0) {
if (currentSimulatedBattery > 0) { // 充电时电量可以缓慢增加,或保持不变,取决于模拟逻辑
// currentSimulatedBattery = min(100, currentSimulatedBattery + 1); // 简单模拟充电增加
}
// durationSeconds is calculated in publish_status_update
} else if (strcmp(currentDeviceStatus, "MOVING") == 0) {
if (currentSimulatedBattery > 5) { // 移动时消耗电量
// currentSimulatedBattery--; // 简单模拟电量消耗
}
} else { // IDLE, COMPLETED, ERROR
//电量可能不变或缓慢消耗
}
// 简单模拟位置更新 (可以更复杂)
// currentSimulatedLocation = ... ; // 可以在特定事件中更新
// --- 动态模拟数据更新 ---
if (currentTime - lastBatteryUpdateTime > batteryUpdateInterval) {
if (strcmp(currentDeviceStatus, "CHARGING") == 0) {
if (currentSimulatedBattery < 100) {
currentSimulatedBattery += 1; // 每 batteryUpdateInterval 增加 1% 电量
Serial.println("模拟: 电量增加至 " + String(currentSimulatedBattery) + "%");
} else {
currentSimulatedBattery = 100; // 防止超过100
}
} else if (strcmp(currentDeviceStatus, "MOVING") == 0) {
if (currentSimulatedBattery > 2) {
currentSimulatedBattery -= 2; // 每 batteryUpdateInterval 消耗 2% 电量
Serial.println("模拟: 电量消耗至 " + String(currentSimulatedBattery) + "%");
} else {
currentSimulatedBattery = 2; // 防止低于2极端情况
}
} else { // IDLE, COMPLETED, FAULTED
if (currentSimulatedBattery > 1) {
// 非常缓慢的自然消耗,或者不消耗
// currentSimulatedBattery -= 1;
}
}
lastBatteryUpdateTime = currentTime;
}
// 位置模拟: 通常在状态转换时callback中已经更新了主要位置。
// loop中可以添加更细致的移动中的位置更新但目前保持简单依赖callback中的设定。
// 例如: if (strcmp(currentDeviceStatus, "MOVING") == 0) { /* update location based on time/progress */ }
delay(100);
}