#include #include #include // 用于JSON操作 // ----------- 设备配置 (需要为您自己的设备和环境修改) ----------- // WiFi const char *ssid = "UFI_DB50CD"; // 请输入您的 Wi-Fi 名称 const char *password = "1349534012"; // 请输入您的 Wi-Fi 密码 // MQTT Broker const char *mqtt_broker = "broker.emqx.io"; // 您的 MQTT Broker 地址 const char *mqtt_username = "emqx"; // 您的 MQTT 用户名 (如果需要) const char *mqtt_password = "public"; // 您的 MQTT 密码 (如果需要) const int mqtt_port = 1883; // 您的 MQTT 端口 // 设备唯一标识符 (非常重要, 必须与后端注册的一致) const char *spotUid = "ESP32_SPOT_001"; // 例如: "SPOT001", "P005-A1" 等 // ----------- MQTT 主题定义 ----------- // 基于 application.yml 和后端服务实现 String topic_uplink_to_backend; // 上报给后端: yupi_mqtt_power_project/robot/status/{spotUid} String topic_downlink_from_backend; // 从后端接收指令: yupi_mqtt_power_project/robot/command/{spotUid} // ----------- 全局变量 ----------- WiFiClient espClient; PubSubClient client(espClient); // 模拟硬件状态 (实际项目中需要从传感器或硬件逻辑获取) const char* currentDeviceStatus = "IDLE"; // 设备当前状态: IDLE, CHARGING, FAULTED 等 float currentVoltage = 220.0; float currentCurrent = 0.0; float currentPower = 0.0; float currentEnergyConsumed = 0.0; int currentErrorCode = 0; String currentSessionId = ""; // 当前充电会话ID // 定时发送相关 unsigned long lastStatusUpdateTime = 0; unsigned long lastHeartbeatTime = 0; const long statusUpdateInterval = 30000; // 状态上报间隔 (例如: 30秒) const long heartbeatInterval = 60000; // 心跳间隔 (例如: 60秒) void setup_mqtt_topics() { String backend_status_base = "yupi_mqtt_power_project/robot/status/"; String backend_command_base = "yupi_mqtt_power_project/robot/command/"; topic_uplink_to_backend = backend_status_base + String(spotUid); topic_downlink_from_backend = backend_command_base + String(spotUid); Serial.println("MQTT 主题初始化完成 (匹配后端实现):"); Serial.println(" 上行主题 (状态/心跳/ACK): " + topic_uplink_to_backend); Serial.println(" 下行主题 (接收指令): " + topic_downlink_from_backend); } void connect_wifi() { Serial.println("正在连接 Wi-Fi..."); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWi-Fi 连接成功!"); Serial.print("IP 地址: "); Serial.println(WiFi.localIP()); } void reconnect_mqtt() { while (!client.connected()) { Serial.print("尝试连接 MQTT Broker: "); Serial.println(mqtt_broker); String client_id = "esp32-client-" + String(spotUid) + "-"; client_id += String(WiFi.macAddress()); Serial.printf("客户端 ID: %s \n", client_id.c_str()); if (client.connect(client_id.c_str(), mqtt_username, mqtt_password)) { Serial.println("MQTT Broker 连接成功!"); // 订阅唯一下行指令主题 client.subscribe(topic_downlink_from_backend.c_str()); Serial.println("已订阅指令主题: " + topic_downlink_from_backend); publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr); // 连接成功后发送一次常规状态更新 } else { Serial.print("连接失败, rc="); Serial.print(client.state()); Serial.println(" 2秒后重试..."); delay(2000); } } } void callback(char *topic, byte *payload, unsigned int length) { Serial.println("-----------------------"); Serial.print("消息抵达, 主题: "); Serial.println(topic); char message[length + 1]; memcpy(message, payload, length); message[length] = '\0'; Serial.print("消息内容: "); Serial.println(message); if (String(topic) != topic_downlink_from_backend) { Serial.println("消息非来自预期的指令主题,忽略。"); return; } StaticJsonDocument<256> doc; DeserializationError error = deserializeJson(doc, message); if (error) { Serial.print("JSON 解析失败: "); Serial.println(error.f_str()); return; } const char* cmdType = doc["commandType"]; // 例如: "START_CHARGE", "STOP_CHARGE" const char* taskId = doc["taskId"]; // 用于ACK if (cmdType == nullptr || taskId == nullptr) { Serial.println("指令JSON缺少 commandType 或 taskId 字段。"); publish_ack_message(taskId, false, "Command JSON invalid", nullptr); // 尝试ACK错误 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(); } else if (strcmp(cmdType, "START_CHARGE") == 0) { Serial.println("接收到 [启动充电] 指令"); currentDeviceStatus = "CHARGING"; if (doc.containsKey("sessionId")) { currentSessionId = String(doc["sessionId"].as()); } else { currentSessionId = ""; //确保没有sessionId时清空 } Serial.println("模拟: 充电已启动。会话ID: " + currentSessionId); 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) { Serial.println("接收到 [停止充电] 指令"); currentDeviceStatus = "COMPLETED"; Serial.println("模拟: 充电已停止。"); String previousSessionId = currentSessionId; // 保存一下,以防ACK需要 currentSessionId = ""; publish_ack_message(taskId, true, "Charging stopped successfully", 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) { // Serial.println("接收到 [查询状态] 指令"); // publish_regular_status_update(); // 回复当前状态 // publish_ack_message(taskId, true, "Status reported", nullptr); // } else { Serial.println("未知指令 commandType: " + String(cmdType)); publish_ack_message(taskId, false, ("Unknown commandType: " + String(cmdType)).c_str(), nullptr); } Serial.println("-----------------------"); } void publish_message(const String& topic, const JsonDocument& doc, const char* message_type) { String jsonBuffer; serializeJson(doc, jsonBuffer); Serial.print("发送 "); Serial.print(message_type); Serial.print(" 到主题 ["); Serial.print(topic); Serial.print("]: "); Serial.println(jsonBuffer); if (client.publish(topic.c_str(), jsonBuffer.c_str())) { Serial.println(String(message_type) + " 发送成功"); } else { Serial.println(String(message_type) + " 发送失败"); } } // isAckOrTaskUpdate: true if this is an ACK or a task-specific update, false for general status/heartbeat // ackTaskId: The taskId if this is an ACK for a command. Null otherwise. void publish_status_update(bool isAckOrTaskUpdate, const char* ackTaskId, const char* ackStatus, const char* ackMessage, const char* ackErrorCode, const char* ackSessionId) { StaticJsonDocument<512> doc; doc["robotUid"] = spotUid; if (isAckOrTaskUpdate) { if (ackTaskId) doc["taskId"] = ackTaskId; if (ackStatus) doc["status"] = ackStatus; // e.g., "SUCCESS", "FAILURE" or task-specific status if (ackMessage) doc["message"] = ackMessage; if (ackErrorCode) doc["errorCode"] = ackErrorCode; // actualRobotStatus should still be sent to reflect current state after ACK 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. } 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 } } // Common fields (timestamp can be added by backend or here if NTP is used) // doc["timestamp"] = String(millis()); // Already using millis() publish_message(topic_uplink_to_backend, doc, isAckOrTaskUpdate ? "ACK/TaskUpdate" : "StatusUpdate"); if (!isAckOrTaskUpdate) { // Only update lastStatusUpdateTime for general status updates, not for ACKs triggered by commands lastStatusUpdateTime = millis(); } } void publish_regular_status_update() { // This is a wrapper for general periodic status updates publish_status_update(false, nullptr, nullptr, nullptr, nullptr, nullptr); } void publish_heartbeat() { StaticJsonDocument<256> doc; // Heartbeat can be simpler doc["robotUid"] = spotUid; doc["actualRobotStatus"] = currentDeviceStatus; // Heartbeat includes current status // Optionally, add a specific "messageType": "HEARTBEAT" if backend needs explicit differentiation // beyond just a minimal status update. For now, relying on RobotStatusMessage structure. publish_message(topic_uplink_to_backend, doc, "Heartbeat"); lastHeartbeatTime = millis(); } // Simplified ACK message function void publish_ack_message(const char* taskId, bool success, const char* message, const char* sessionIdForAckContext) { 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); } void setup() { Serial.begin(115200); Serial.println("\nESP32 充电桩模拟客户端启动..."); setup_mqtt_topics(); // 初始化MQTT主题 connect_wifi(); // 连接Wi-Fi client.setServer(mqtt_broker, mqtt_port); client.setCallback(callback); // 设置消息回调函数 } void loop() { if (!client.connected()) { reconnect_mqtt(); } client.loop(); unsigned long currentTime = millis(); if (currentTime - lastStatusUpdateTime > statusUpdateInterval) { publish_regular_status_update(); // Use the new wrapper } if (currentTime - lastHeartbeatTime > heartbeatInterval) { publish_heartbeat(); // Uses the new heartbeat logic } // 模拟充电过程中的电量和功率变化 (仅为演示) if (String(currentDeviceStatus) == "CHARGING") { currentEnergyConsumed += 0.01; // 假设每秒消耗0.01kWh (不精确,仅为演示) currentCurrent = 5.0; // 假设充电电流5A currentPower = (currentVoltage * currentCurrent) / 1000.0; // kW // 注意:实际项目中这些值应来自传感器或充电控制器 // 这里为了演示,每隔一段时间简单更新一下 } else { currentCurrent = 0.0; currentPower = 0.0; } delay(100); // 短暂延时,避免loop过于频繁,给其他任务一点时间 (可选) }