From 220e4fab27b7f419b3e89e2f2d28ae789aac09d7 Mon Sep 17 00:00:00 2001
From: wangzhibo <wangzhibo@shsening.com>
Date: 星期四, 08 十月 2026 16:29:55 +0800
Subject: [PATCH] 5种场景模式家长端ok
---
xiaozhi-esp32-2.2.6/main/boards/sening-xiaozhi-s3/mqtt.c | 665 ++++++++++++++++++++++++++++++++++++++++++++-----------
1 files changed, 528 insertions(+), 137 deletions(-)
diff --git a/xiaozhi-esp32-2.2.6/main/boards/sening-xiaozhi-s3/mqtt.c b/xiaozhi-esp32-2.2.6/main/boards/sening-xiaozhi-s3/mqtt.c
index f0a4cb4..bf7f4d2 100644
--- a/xiaozhi-esp32-2.2.6/main/boards/sening-xiaozhi-s3/mqtt.c
+++ b/xiaozhi-esp32-2.2.6/main/boards/sening-xiaozhi-s3/mqtt.c
@@ -4,27 +4,32 @@
#include <stdlib.h>
#include <string.h>
#include <strings.h>
+#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <cJSON.h>
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_timer.h"
+#include "esp_mac.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mqtt_client.h"
#include "system.h"
#include "app_task.h"
+#include "parent_schedule.h"
#define BOARD_MQTT_KEEPALIVE_SEC 120
-#define BOARD_MQTT_STATE_REPORT_INTERVAL_MS 30000
-#define BOARD_MQTT_RECONNECT_DELAY_MS 30000
+#define BOARD_MQTT_STATE_REPORT_INTERVAL_MS 60000
+#define BOARD_MQTT_RECONNECT_RETRY_MS 5000
+#define BOARD_MQTT_RECONNECT_MIN_INTERNAL_HEAP 49152
#define BOARD_MQTT_NETWORK_TIMEOUT_MS 15000
#define BOARD_MQTT_FINAL_REPORT_WAIT_MS 10000
#define BOARD_MQTT_VOICE_YIELD_WAIT_MS 120000
+#define BOARD_MQTT_PROGRESS_PUBLISH_INTERVAL_US 15000000
+#define BOARD_MQTT_SYNC_DISPLAY_INTERVAL_US 2000000
#define BOARD_MQTT_TASK_PRIORITY 5
-#define BOARD_MQTT_RECONNECT_TASK_PRIORITY 4
#define BOARD_MQTT_TOPIC_MAX_LEN 128
#define BOARD_MQTT_PAYLOAD_MAX_LEN 8192
#define BOARD_MQTT_DOWNLOAD_FIELD_MAX_LEN 256
@@ -47,13 +52,19 @@
static esp_mqtt_client_handle_t s_mqtt_client = NULL;
static bool s_mqtt_connected = false;
static char s_device_id[BOARD_MQTT_TOPIC_MAX_LEN];
+static char s_mac_id[BOARD_MQTT_TOPIC_MAX_LEN];
static char s_cmd_ctl_topic[BOARD_MQTT_TOPIC_MAX_LEN];
static char s_cmd_task_topic[BOARD_MQTT_TOPIC_MAX_LEN];
+static char s_cmd_ctl_mac_topic[BOARD_MQTT_TOPIC_MAX_LEN];
+static char s_cmd_task_mac_topic[BOARD_MQTT_TOPIC_MAX_LEN];
static char s_rpt_log_topic[BOARD_MQTT_TOPIC_MAX_LEN];
static char s_rpt_ack_topic[BOARD_MQTT_TOPIC_MAX_LEN];
static char s_rpt_progress_topic[BOARD_MQTT_TOPIC_MAX_LEN];
static char s_rpt_state_topic[BOARD_MQTT_TOPIC_MAX_LEN];
+static char s_rpt_state_mac_topic[BOARD_MQTT_TOPIC_MAX_LEN];
static char s_rpt_event_topic[BOARD_MQTT_TOPIC_MAX_LEN];
+static const char BOARD_MQTT_LWT_PAYLOAD[] =
+ "{\"msg_type\":\"state\",\"payload\":{\"online\":false,\"net\":\"offline\"}}";
typedef enum {
BOARD_MQTT_TASK_NONE = 0,
BOARD_MQTT_TASK_DOWNLOAD,
@@ -68,11 +79,28 @@
static volatile bool s_download_voice_yield_requested = false;
static volatile bool s_download_cancel_requested = false;
static portMUX_TYPE s_task_lock = portMUX_INITIALIZER_UNLOCKED;
-static TaskHandle_t s_reconnect_task_handle = NULL;
static TaskHandle_t s_state_task_handle = NULL;
static TaskHandle_t s_playlist_task_handle = NULL;
static volatile bool s_playlist_stop_requested = false;
static uint32_t s_state_sequence = 0;
+
+typedef struct {
+ bool active;
+ uint32_t gen;
+ char app_id[32];
+ char task_id[32];
+ char source[24];
+ char *params_json;
+} board_mqtt_queued_app_task_t;
+
+static board_mqtt_queued_app_task_t s_queued_app_task = {0};
+static uint32_t s_queued_app_gen = 0;
+
+typedef struct {
+ bool success;
+ int error_code;
+ const char *info;
+} board_mqtt_action_result_t;
static void BoardMqtt_HandleCommand(const char *topic, int topic_len, const char *data, int data_len);
static bool BoardMqtt_CopyString(char *dest, size_t dest_size, const char *src);
@@ -82,6 +110,10 @@
static void BoardMqtt_FinishTask(board_mqtt_task_type_t type);
static void BoardMqtt_StopPlaylistTask(void);
static esp_err_t BoardMqtt_StartPlaylistTask(void);
+static board_mqtt_action_result_t BoardMqtt_HandleUpdateManifest(const char *req_id, cJSON *payload);
+static void BoardMqtt_ClearQueuedAppTask(void);
+static void BoardMqtt_StartQueuedAppTaskIfReady(bool preload_ok, uint32_t queued_gen);
+static bool s_bind_manifest_to_queue = false;
typedef struct {
char topic[BOARD_MQTT_TOPIC_MAX_LEN];
@@ -98,12 +130,6 @@
BOARD_MQTT_TOPIC_CTL,
BOARD_MQTT_TOPIC_TASK,
} board_mqtt_topic_type_t;
-
-typedef struct {
- bool success;
- int error_code;
- const char *info;
-} board_mqtt_action_result_t;
static board_mqtt_action_result_t BoardMqtt_HandleUnsetAlarm(cJSON *payload);
@@ -126,6 +152,8 @@
char *manifest_json;
size_t asset_count;
bool replace_existing_download;
+ bool start_queued_app;
+ uint32_t queued_gen;
board_mqtt_manifest_asset_t assets[];
} board_mqtt_manifest_job_t;
@@ -160,7 +188,9 @@
size_t asset_index;
size_t asset_count;
int64_t last_publish_us;
+ int64_t last_display_us;
int last_progress;
+ bool ignore_voice_yield;
} board_mqtt_manifest_progress_ctx_t;
static bool BoardMqtt_IsDownloadTask(board_mqtt_task_type_t type)
@@ -175,6 +205,11 @@
type = s_task_type;
portEXIT_CRITICAL(&s_task_lock);
return type;
+}
+
+bool Board_IsLessonSyncing(void)
+{
+ return BoardMqtt_IsDownloadTask(BoardMqtt_GetTaskType());
}
static bool BoardMqtt_BeginTask(board_mqtt_task_type_t type)
@@ -300,9 +335,9 @@
return (long)now;
}
-static void BoardMqtt_PublishJson(const char *topic, cJSON *json, int qos)
+static void BoardMqtt_PublishJsonTo(const char *topic, cJSON *json, int qos, int retain)
{
- if (!s_mqtt_client || !s_mqtt_connected || !topic || !json) {
+ if (!s_mqtt_client || !s_mqtt_connected || !topic || !topic[0] || !json) {
return;
}
@@ -312,7 +347,7 @@
return;
}
- int msg_id = esp_mqtt_client_publish(s_mqtt_client, topic, payload, 0, qos, 0);
+ int msg_id = esp_mqtt_client_publish(s_mqtt_client, topic, payload, 0, qos, retain);
if (msg_id >= 0) {
ESP_LOGI(TAG, "Published MQTT message, topic=%s, msg_id=%d", topic, msg_id);
} else {
@@ -321,8 +356,17 @@
cJSON_free(payload);
}
+static void BoardMqtt_PublishJson(const char *topic, cJSON *json, int qos)
+{
+ BoardMqtt_PublishJsonTo(topic, json, qos, 0);
+}
+
static void BoardMqtt_WaitConnectedForFinalReport(void)
{
+ if (heap_caps_get_free_size(MALLOC_CAP_INTERNAL) < BOARD_MQTT_RECONNECT_MIN_INTERNAL_HEAP) {
+ ESP_LOGW(TAG, "Skip MQTT final report wait, internal heap low");
+ return;
+ }
int waited_ms = 0;
while (s_mqtt_client && !s_mqtt_connected && waited_ms < BOARD_MQTT_FINAL_REPORT_WAIT_MS) {
ESP_LOGW(TAG, "Waiting MQTT reconnect before final report, waited=%dms", waited_ms);
@@ -359,11 +403,18 @@
cJSON_AddNumberToObject(payload, "cpu", Board_Get_CPU_Usage());
cJSON_AddNumberToObject(payload, "mem", mem_percent);
cJSON_AddNumberToObject(payload, "battery", Battery_Get_Value());
+ cJSON_AddTrueToObject(payload, "online");
cJSON_AddStringToObject(payload, "net", wifi_connected ? "wifi" : "offline");
cJSON_AddStringToObject(payload, "mode", task_type != BOARD_MQTT_TASK_NONE ? "task" : (Board_Is_Voice_Session_Active() ? "voice" : "idle"));
+ if (s_mac_id[0]) {
+ cJSON_AddStringToObject(payload, "mac", s_mac_id);
+ }
cJSON_AddItemToObject(root, "payload", payload);
- BoardMqtt_PublishJson(s_rpt_state_topic, root, BOARD_MQTT_QOS);
+ BoardMqtt_PublishJsonTo(s_rpt_state_topic, root, BOARD_MQTT_QOS, 1);
+ if (s_rpt_state_mac_topic[0] && strcmp(s_rpt_state_mac_topic, s_rpt_state_topic) != 0) {
+ BoardMqtt_PublishJsonTo(s_rpt_state_mac_topic, root, BOARD_MQTT_QOS, 1);
+ }
cJSON_Delete(root);
}
@@ -485,6 +536,23 @@
cJSON_Delete(root);
}
+static void BoardMqtt_ShowLessonSyncProgress(int percent, size_t asset_index, size_t asset_count)
+{
+ char msg[96];
+ if (percent < 0) {
+ percent = 0;
+ } else if (percent > 100) {
+ percent = 100;
+ }
+ if (asset_count == 0) {
+ snprintf(msg, sizeof(msg), "姝e湪鍚屾璇炬枃锛岃绋嶅��");
+ } else {
+ snprintf(msg, sizeof(msg), "姝e湪涓嬭浇 %u/%u锛屽凡瀹屾垚 %d%%",
+ (unsigned)(asset_index + 1), (unsigned)asset_count, percent);
+ }
+ Board_Show_Alert("姝e湪鍚屾", msg, "cloud_arrow_down");
+}
+
static char *BoardMqtt_CopySegment(const char *data, int data_len)
{
if (!data || data_len <= 0) {
@@ -583,6 +651,43 @@
return written > 0 && (size_t)written < dest_size;
}
+static bool BoardMqtt_SdRegularFileReady(const char *business_path)
+{
+ char sd_path[SDCARD_PATH_MAX_LEN];
+ if (!business_path || business_path[0] == '\0') {
+ return false;
+ }
+ if (SDCard_Map_Business_Path(business_path, sd_path, sizeof(sd_path)) != ESP_OK) {
+ return false;
+ }
+ struct stat st;
+ if (stat(sd_path, &st) != 0) {
+ return false;
+ }
+ return S_ISREG(st.st_mode) && st.st_size > 0;
+}
+
+static bool BoardMqtt_JoinDownloadUrl(char *dest, size_t dest_size, const char *base_url, const char *rel)
+{
+ if (!dest || dest_size == 0 || !base_url || !rel || rel[0] == '\0') {
+ return false;
+ }
+ size_t base_len = strlen(base_url);
+ while (base_len > 0 && base_url[base_len - 1] == '/') {
+ --base_len;
+ }
+ if (base_len == 0) {
+ return false;
+ }
+ int written;
+ if (rel[0] == '/') {
+ written = snprintf(dest, dest_size, "%.*s%s", (int)base_len, base_url, rel);
+ } else {
+ written = snprintf(dest, dest_size, "%.*s/%s", (int)base_len, base_url, rel);
+ }
+ return written > 0 && (size_t)written < dest_size;
+}
+
static bool BoardMqtt_IsSupportedResourcePath(const char *sd_path)
{
const char *dot = sd_path ? strrchr(sd_path, '.') : NULL;
@@ -610,6 +715,88 @@
return BoardMqtt_CopyString(dest, dest_size, md5->valuestring);
}
+static bool BoardMqtt_IsLessonAppTask(const char *app_id, const char *task_id)
+{
+ return app_id && task_id && strcmp(app_id, "english_learning") == 0 &&
+ (strcmp(task_id, "repeater") == 0 || strcmp(task_id, "shadowing") == 0);
+}
+
+static void BoardMqtt_ClearQueuedAppTask(void)
+{
+ free(s_queued_app_task.params_json);
+ memset(&s_queued_app_task, 0, sizeof(s_queued_app_task));
+ s_queued_app_gen++;
+}
+
+static bool BoardMqtt_QueueAppTask(const char *app_id, const char *task_id, const char *source, const char *params_json)
+{
+ BoardMqtt_ClearQueuedAppTask();
+ if (!BoardMqtt_CopyString(s_queued_app_task.app_id, sizeof(s_queued_app_task.app_id), app_id) ||
+ !BoardMqtt_CopyString(s_queued_app_task.task_id, sizeof(s_queued_app_task.task_id), task_id) ||
+ !BoardMqtt_CopyString(s_queued_app_task.source, sizeof(s_queued_app_task.source), source && source[0] ? source : "mqtt_immediate")) {
+ BoardMqtt_ClearQueuedAppTask();
+ return false;
+ }
+ const char *params = (params_json && params_json[0]) ? params_json : "{}";
+ size_t params_len = strlen(params) + 1;
+ s_queued_app_task.params_json = (char *)malloc(params_len);
+ if (!s_queued_app_task.params_json) {
+ BoardMqtt_ClearQueuedAppTask();
+ return false;
+ }
+ memcpy(s_queued_app_task.params_json, params, params_len);
+ s_queued_app_task.active = true;
+ s_queued_app_task.gen = s_queued_app_gen;
+ return true;
+}
+
+static bool BoardMqtt_PackageCached(cJSON *files)
+{
+ if (!cJSON_IsArray(files) || cJSON_GetArraySize(files) <= 0) {
+ return false;
+ }
+ cJSON *file = NULL;
+ cJSON_ArrayForEach(file, files) {
+ if (!cJSON_IsString(file) || !file->valuestring || file->valuestring[0] == '\0') {
+ return false;
+ }
+ char business_path[SDCARD_PATH_MAX_LEN];
+ if (!BoardMqtt_CopyBusinessPath(business_path, sizeof(business_path), file->valuestring)) {
+ return false;
+ }
+ if (!BoardMqtt_SdRegularFileReady(business_path)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static void BoardMqtt_StartQueuedAppTaskIfReady(bool preload_ok, uint32_t queued_gen)
+{
+ if (!s_queued_app_task.active || s_queued_app_task.gen != queued_gen) {
+ return;
+ }
+ if (!preload_ok) {
+ Board_ReleaseHoldIdle();
+ ESP_LOGW(TAG, "preload failed, drop queued %s/%s", s_queued_app_task.app_id, s_queued_app_task.task_id);
+ Board_Show_Alert("鍚屾澶辫触", "璇炬枃涓嬭浇澶辫触锛岃绋嶅悗閲嶈瘯", "sad");
+ BoardMqtt_ClearQueuedAppTask();
+ return;
+ }
+ ESP_LOGI(TAG, "package READY, start AI task %s/%s", s_queued_app_task.app_id, s_queued_app_task.task_id);
+ char app_id[32];
+ char task_id[32];
+ char source[24];
+ char *params = s_queued_app_task.params_json;
+ s_queued_app_task.params_json = NULL;
+ BoardMqtt_CopyString(app_id, sizeof(app_id), s_queued_app_task.app_id);
+ BoardMqtt_CopyString(task_id, sizeof(task_id), s_queued_app_task.task_id);
+ BoardMqtt_CopyString(source, sizeof(source), s_queued_app_task.source);
+ BoardMqtt_ClearQueuedAppTask();
+ Board_StartAppTask(app_id, task_id, params, source);
+ free(params);
+}
+
static bool BoardMqtt_ActionIsCtl(const char *action)
{
return strcmp(action, "SET_VOLUME") == 0 || strcmp(action, "SET_POWER") == 0 ||
@@ -617,6 +804,7 @@
strcmp(action, "STOP_MEDIA") == 0 || strcmp(action, "DELETE_FILE") == 0 ||
strcmp(action, "SET_ALARM") == 0 || strcmp(action, "UNSET_ALARM") == 0 ||
strcmp(action, "REBOOT") == 0 || strcmp(action, "START_APP_TASK") == 0 ||
+ strcmp(action, "SYNC_SCHEDULE") == 0 ||
strcmp(action, "ENSURE_APP_DIRS") == 0;
}
@@ -1229,16 +1417,6 @@
if (err != ESP_OK) {
return BoardMqtt_ResultFail(BOARD_MQTT_ERR_EXEC_FAILED, esp_err_to_name(err));
}
-
- cJSON *app_id = cJSON_GetObjectItem(payload, "app_id");
- cJSON *task_id = cJSON_GetObjectItem(payload, "task_id");
- if (cJSON_IsString(app_id) && app_id->valuestring && app_id->valuestring[0]) {
- const char *tid = (cJSON_IsString(task_id) && task_id->valuestring) ? task_id->valuestring : "";
- cJSON *params = cJSON_GetObjectItem(payload, "params");
- char *params_json = params ? cJSON_PrintUnformatted(params) : NULL;
- Board_SavePendingTask(app_id->valuestring, tid, params_json, "schedule", id->valuestring);
- free(params_json);
- }
return BoardMqtt_ResultOk("alarm updated");
}
@@ -1331,7 +1509,8 @@
if (!force && progress == progress_ctx->last_progress) {
return true;
}
- if (!force && progress_ctx->last_publish_us > 0 && now - progress_ctx->last_publish_us < 2000000) {
+ if (!force && progress_ctx->last_publish_us > 0
+ && now - progress_ctx->last_publish_us < BOARD_MQTT_PROGRESS_PUBLISH_INTERVAL_US) {
return true;
}
@@ -1352,7 +1531,7 @@
ESP_LOGI(TAG, "Cancel manifest download by request");
return false;
}
- if (Board_Is_Voice_Session_Active()) {
+ if (!progress_ctx->ignore_voice_yield && Board_Is_Voice_Session_Active()) {
s_download_voice_yield_requested = true;
ESP_LOGI(TAG, "Cancel manifest download for voice session");
return false;
@@ -1366,11 +1545,21 @@
int aggregate_progress = (int)(((progress_ctx->asset_index * 100) + (size_t)progress) / progress_ctx->asset_count);
int64_t now = esp_timer_get_time();
- bool force = progress <= 0 || progress >= 100 || aggregate_progress >= 100;
+ bool show = progress_ctx->last_display_us == 0
+ || now - progress_ctx->last_display_us >= BOARD_MQTT_SYNC_DISPLAY_INTERVAL_US
+ || aggregate_progress >= 100
+ || progress <= 0;
+ if (show) {
+ progress_ctx->last_display_us = now;
+ BoardMqtt_ShowLessonSyncProgress(aggregate_progress, progress_ctx->asset_index, progress_ctx->asset_count);
+ }
+
+ bool force = progress >= 100 || progress_ctx->last_progress < 0 || aggregate_progress >= 100;
if (!force && aggregate_progress == progress_ctx->last_progress) {
return true;
}
- if (!force && progress_ctx->last_publish_us > 0 && now - progress_ctx->last_publish_us < 2000000) {
+ if (!force && progress_ctx->last_publish_us > 0
+ && now - progress_ctx->last_publish_us < BOARD_MQTT_PROGRESS_PUBLISH_INTERVAL_US) {
return true;
}
@@ -1395,7 +1584,8 @@
if (!force && progress == job->last_progress) {
return;
}
- if (!force && job->last_publish_us > 0 && now - job->last_publish_us < 2000000) {
+ if (!force && job->last_publish_us > 0
+ && now - job->last_publish_us < BOARD_MQTT_PROGRESS_PUBLISH_INTERVAL_US) {
return;
}
@@ -1467,6 +1657,24 @@
Board_Acquire_Sleep_Inhibit();
s_download_voice_yield_requested = false;
+ if (job->start_queued_app) {
+ ESP_LOGI(TAG, "lesson preload keeps downloading during listen/speaking");
+ }
+
+ bool need_download = false;
+ for (size_t i = 0; i < job->asset_count; ++i) {
+ if (!BoardMqtt_SdRegularFileReady(job->assets[i].target_path)) {
+ need_download = true;
+ break;
+ }
+ }
+ if (need_download) {
+ Board_SetLessonDownloadBoost(true);
+ vTaskDelay(pdMS_TO_TICKS(80));
+ if (job->start_queued_app) {
+ Board_AnnounceLessonSyncAndWait();
+ }
+ }
char final_path[SDCARD_PATH_MAX_LEN] = {0};
char part_path[SDCARD_PATH_MAX_LEN] = {0};
@@ -1475,6 +1683,7 @@
vTaskDelay(pdMS_TO_TICKS(100));
BoardMqtt_PublishProgress(job->req_id, 0, "saving manifest");
+ BoardMqtt_ShowLessonSyncProgress(0, 0, job->asset_count);
err = SDCard_Save_Manifest(job->course_id, job->manifest_json);
if (err != ESP_OK) {
snprintf(error_info, sizeof(error_info), "%s", esp_err_to_name(err));
@@ -1485,6 +1694,11 @@
memset(final_path, 0, sizeof(final_path));
memset(part_path, 0, sizeof(part_path));
memset(error_info, 0, sizeof(error_info));
+
+ if (BoardMqtt_SdRegularFileReady(asset->target_path)) {
+ ESP_LOGI(TAG, "skip manifest asset, already on sd: %s", asset->target_path);
+ continue;
+ }
err = SDCard_Prepare_Business_File(asset->target_path, final_path, sizeof(final_path), part_path, sizeof(part_path));
if (err != ESP_OK) {
@@ -1497,11 +1711,17 @@
.asset_index = i,
.asset_count = job->asset_count,
.last_publish_us = 0,
+ .last_display_us = 0,
.last_progress = -1,
+ .ignore_voice_yield = job->start_queued_app,
};
err = Board_Download_File_To_Path(asset->url, part_path, asset->md5, BoardMqtt_ManifestProgressCallback,
&progress_ctx, error_info, sizeof(error_info));
if (err != ESP_OK) {
+ ESP_LOGW(TAG, "manifest asset failed %u/%u path=%s err=%s info=%s",
+ (unsigned)(i + 1), (unsigned)job->asset_count,
+ asset->target_path, esp_err_to_name(err),
+ error_info[0] ? error_info : "");
SDCard_Remove_File_If_Exists(part_path);
if (s_download_voice_yield_requested || s_download_cancel_requested) {
int canceled_progress = progress_ctx.last_progress >= 0 ? progress_ctx.last_progress : 0;
@@ -1534,13 +1754,26 @@
if (err == ESP_OK) {
BoardMqtt_PublishProgress(job->req_id, 100, "manifest assets downloaded");
BoardMqtt_PublishAck(job->req_id, true, 0, "manifest saved and assets downloaded");
+ if (job->start_queued_app) {
+ Board_Show_Alert("鍚屾瀹屾垚", "寮�濮嬫挱鏀�", "happy");
+ BoardMqtt_StartQueuedAppTaskIfReady(true, job->queued_gen);
+ }
+ Board_Schedule_OnLessonFilesUpdated();
} else {
BoardMqtt_PublishAck(job->req_id, false, BOARD_MQTT_ERR_EXEC_FAILED,
error_info[0] ? error_info : esp_err_to_name(err));
+ if (job->start_queued_app) {
+ BoardMqtt_StartQueuedAppTaskIfReady(false, job->queued_gen);
+ } else {
+ Board_Show_Alert("鍚屾澶辫触", error_info[0] ? error_info : "璇炬枃涓嬭浇澶辫触", "sad");
+ }
}
}
s_download_voice_yield_requested = false;
+ if (need_download) {
+ Board_SetLessonDownloadBoost(false);
+ }
Board_Release_Sleep_Inhibit();
BoardMqtt_FinishTask(BOARD_MQTT_TASK_MANIFEST);
cJSON_free(job->manifest_json);
@@ -1558,6 +1791,8 @@
Board_Acquire_Sleep_Inhibit();
s_download_voice_yield_requested = false;
+ Board_SetLessonDownloadBoost(true);
+ vTaskDelay(pdMS_TO_TICKS(80));
char final_path[SDCARD_PATH_MAX_LEN] = {0};
char part_path[SDCARD_PATH_MAX_LEN] = {0};
@@ -1608,6 +1843,7 @@
}
s_download_voice_yield_requested = false;
+ Board_SetLessonDownloadBoost(false);
BoardMqtt_FinishTask(BOARD_MQTT_TASK_DOWNLOAD);
Board_Release_Sleep_Inhibit();
free(job);
@@ -1641,6 +1877,10 @@
}
if (!BoardMqtt_IsSupportedResourcePath(sd_path)) {
return BoardMqtt_ResultFail(BOARD_MQTT_ERR_UNSUPPORTED, "only mp3 and jpg download is supported");
+ }
+ if (BoardMqtt_SdRegularFileReady(business_path)) {
+ ESP_LOGI(TAG, "skip download, already on sd: %s", business_path);
+ return BoardMqtt_ResultOk("already on sd");
}
BoardMqtt_StopPlaylistTask();
if (!BoardMqtt_BeginTask(BOARD_MQTT_TASK_DOWNLOAD)) {
@@ -1724,11 +1964,25 @@
{
cJSON *course_id = cJSON_GetObjectItem(payload, "course_id");
cJSON *assets = cJSON_GetObjectItem(payload, "assets");
+ cJSON *files = cJSON_GetObjectItem(payload, "files");
+ cJSON *base_url = cJSON_GetObjectItem(payload, "base_url");
+ bool has_assets = cJSON_IsArray(assets) && cJSON_GetArraySize(assets) > 0;
+ bool has_files = cJSON_IsString(base_url) && base_url->valuestring && base_url->valuestring[0] != '\0' &&
+ cJSON_IsArray(files) && cJSON_GetArraySize(files) > 0;
if (!cJSON_IsString(course_id) || !BoardMqtt_IsSafeCourseId(course_id->valuestring) ||
- !cJSON_IsArray(assets) || cJSON_GetArraySize(assets) <= 0) {
+ (!has_assets && !has_files)) {
return BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest payload");
}
- BoardMqtt_StopPlaylistTask();
+ if (has_files && !BoardMqtt_HasPrefix(base_url->valuestring, "http://") &&
+ !BoardMqtt_HasPrefix(base_url->valuestring, "https://")) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest base_url");
+ }
+ if (has_files && !BoardMqtt_PackageCached(files)) {
+ Board_Show_Alert("姝e湪鍚屾", "姝e湪鍚屾璇炬枃锛岃绋嶅��", "cloud_arrow_down");
+ }
+ if (s_bind_manifest_to_queue) {
+ BoardMqtt_StopPlaylistTask();
+ }
bool replace_existing_download = false;
board_mqtt_task_type_t current_type = BoardMqtt_GetTaskType();
@@ -1751,13 +2005,14 @@
return BoardMqtt_ResultFail((code), (message)); \
} while (0)
- int asset_count_int = cJSON_GetArraySize(assets);
+ int asset_count_int = has_assets ? cJSON_GetArraySize(assets) : cJSON_GetArraySize(files);
size_t source_asset_count = (size_t)asset_count_int;
- if (source_asset_count > SIZE_MAX / 2) {
+ if (has_assets && source_asset_count > SIZE_MAX / 2) {
BOARD_MQTT_RETURN_MANIFEST_FAIL(BOARD_MQTT_ERR_EXEC_FAILED, "manifest asset count too large");
}
- size_t max_asset_count = source_asset_count * 2;
- if (max_asset_count > (SIZE_MAX - sizeof(board_mqtt_manifest_job_t)) / sizeof(board_mqtt_manifest_asset_t)) {
+ size_t max_asset_count = has_assets ? source_asset_count * 2 : source_asset_count;
+ if (max_asset_count == 0 ||
+ max_asset_count > (SIZE_MAX - sizeof(board_mqtt_manifest_job_t)) / sizeof(board_mqtt_manifest_asset_t)) {
BOARD_MQTT_RETURN_MANIFEST_FAIL(BOARD_MQTT_ERR_EXEC_FAILED, "manifest asset count too large");
}
@@ -1774,6 +2029,8 @@
}
job->manifest_json = manifest_json;
job->replace_existing_download = replace_existing_download;
+ job->start_queued_app = s_bind_manifest_to_queue && s_queued_app_task.active;
+ job->queued_gen = s_queued_app_task.gen;
#define BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(code, message) do { \
cJSON_free(job->manifest_json); \
@@ -1787,73 +2044,103 @@
}
size_t index = 0;
- cJSON *asset = NULL;
- cJSON_ArrayForEach(asset, assets) {
- cJSON *url = cJSON_IsObject(asset) ? cJSON_GetObjectItem(asset, "url") : NULL;
- cJSON *target_path = cJSON_IsObject(asset) ? cJSON_GetObjectItem(asset, "target_path") : NULL;
- cJSON *md5 = cJSON_IsObject(asset) ? cJSON_GetObjectItem(asset, "md5") : NULL;
- if (!cJSON_IsString(url) || !cJSON_IsString(target_path)) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest asset");
+ if (has_files && !has_assets) {
+ cJSON *file = NULL;
+ cJSON_ArrayForEach(file, files) {
+ if (!cJSON_IsString(file) || !file->valuestring || file->valuestring[0] == '\0') {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest file path");
+ }
+ char business_path[SDCARD_PATH_MAX_LEN];
+ if (!BoardMqtt_CopyBusinessPath(business_path, sizeof(business_path), file->valuestring)) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest target path");
+ }
+ char sd_path[SDCARD_PATH_MAX_LEN];
+ if (SDCard_Map_Business_Path(business_path, sd_path, sizeof(sd_path)) != ESP_OK) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest target path");
+ }
+ if (!BoardMqtt_IsSupportedResourcePath(sd_path)) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_UNSUPPORTED, "only mp3 and jpg manifest assets are supported");
+ }
+ char joined_url[BOARD_MQTT_DOWNLOAD_FIELD_MAX_LEN];
+ if (!BoardMqtt_JoinDownloadUrl(joined_url, sizeof(joined_url), base_url->valuestring, business_path)) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "manifest asset url too long");
+ }
+ board_mqtt_manifest_asset_t *job_asset = &job->assets[index];
+ if (!BoardMqtt_CopyString(job_asset->url, sizeof(job_asset->url), joined_url) ||
+ !BoardMqtt_CopyString(job_asset->target_path, sizeof(job_asset->target_path), business_path)) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "manifest asset field too long");
+ }
+ ++index;
}
- if (!BoardMqtt_HasPrefix(url->valuestring, "http://") && !BoardMqtt_HasPrefix(url->valuestring, "https://")) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest asset url");
- }
- if (!BoardMqtt_ValidateOptionalMd5(md5)) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest asset md5");
- }
+ } else {
+ cJSON *asset = NULL;
+ cJSON_ArrayForEach(asset, assets) {
+ cJSON *url = cJSON_IsObject(asset) ? cJSON_GetObjectItem(asset, "url") : NULL;
+ cJSON *target_path = cJSON_IsObject(asset) ? cJSON_GetObjectItem(asset, "target_path") : NULL;
+ cJSON *md5 = cJSON_IsObject(asset) ? cJSON_GetObjectItem(asset, "md5") : NULL;
+ if (!cJSON_IsString(url) || !cJSON_IsString(target_path)) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest asset");
+ }
+ if (!BoardMqtt_HasPrefix(url->valuestring, "http://") && !BoardMqtt_HasPrefix(url->valuestring, "https://")) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest asset url");
+ }
+ if (!BoardMqtt_ValidateOptionalMd5(md5)) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest asset md5");
+ }
- char business_path[SDCARD_PATH_MAX_LEN];
- if (!BoardMqtt_CopyBusinessPath(business_path, sizeof(business_path), target_path->valuestring)) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest target path");
- }
+ char business_path[SDCARD_PATH_MAX_LEN];
+ if (!BoardMqtt_CopyBusinessPath(business_path, sizeof(business_path), target_path->valuestring)) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest target path");
+ }
- char sd_path[SDCARD_PATH_MAX_LEN];
- if (SDCard_Map_Business_Path(business_path, sd_path, sizeof(sd_path)) != ESP_OK) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest target path");
- }
- if (!BoardMqtt_IsSupportedResourcePath(sd_path)) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_UNSUPPORTED, "only mp3 and jpg manifest assets are supported");
- }
+ char sd_path[SDCARD_PATH_MAX_LEN];
+ if (SDCard_Map_Business_Path(business_path, sd_path, sizeof(sd_path)) != ESP_OK) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest target path");
+ }
+ if (!BoardMqtt_IsSupportedResourcePath(sd_path)) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_UNSUPPORTED, "only mp3 and jpg manifest assets are supported");
+ }
- board_mqtt_manifest_asset_t *job_asset = &job->assets[index];
- if (!BoardMqtt_CopyString(job_asset->url, sizeof(job_asset->url), url->valuestring) ||
- !BoardMqtt_CopyString(job_asset->target_path, sizeof(job_asset->target_path), business_path) ||
- !BoardMqtt_CopyOptionalMd5(job_asset->md5, sizeof(job_asset->md5), md5)) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "manifest asset field too long");
- }
- ++index;
+ board_mqtt_manifest_asset_t *job_asset = &job->assets[index];
+ if (!BoardMqtt_CopyString(job_asset->url, sizeof(job_asset->url), url->valuestring) ||
+ !BoardMqtt_CopyString(job_asset->target_path, sizeof(job_asset->target_path), business_path) ||
+ !BoardMqtt_CopyOptionalMd5(job_asset->md5, sizeof(job_asset->md5), md5)) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "manifest asset field too long");
+ }
+ ++index;
- cJSON *cover_url = cJSON_IsObject(asset) ? cJSON_GetObjectItem(asset, "cover_url") : NULL;
- cJSON *cover_target_path = cJSON_IsObject(asset) ? cJSON_GetObjectItem(asset, "cover_target_path") : NULL;
- if (!cJSON_IsString(cover_url) || !cJSON_IsString(cover_target_path) ||
- cover_url->valuestring[0] == '\0' || cover_target_path->valuestring[0] == '\0') {
- continue;
- }
- if (!BoardMqtt_HasPrefix(cover_url->valuestring, "http://") && !BoardMqtt_HasPrefix(cover_url->valuestring, "https://")) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest cover url");
- }
- if (!BoardMqtt_CopyBusinessPath(business_path, sizeof(business_path), cover_target_path->valuestring)) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest cover path");
- }
- if (SDCard_Map_Business_Path(business_path, sd_path, sizeof(sd_path)) != ESP_OK) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest cover path");
- }
- const char *cover_dot = strrchr(sd_path, '.');
- if (!cover_dot || strcasecmp(cover_dot, ".jpg") != 0) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_UNSUPPORTED, "only jpg cover assets are supported");
- }
+ cJSON *cover_url = cJSON_IsObject(asset) ? cJSON_GetObjectItem(asset, "cover_url") : NULL;
+ cJSON *cover_target_path = cJSON_IsObject(asset) ? cJSON_GetObjectItem(asset, "cover_target_path") : NULL;
+ if (!cJSON_IsString(cover_url) || !cJSON_IsString(cover_target_path) ||
+ cover_url->valuestring[0] == '\0' || cover_target_path->valuestring[0] == '\0') {
+ continue;
+ }
+ if (!BoardMqtt_HasPrefix(cover_url->valuestring, "http://") && !BoardMqtt_HasPrefix(cover_url->valuestring, "https://")) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest cover url");
+ }
+ if (!BoardMqtt_CopyBusinessPath(business_path, sizeof(business_path), cover_target_path->valuestring)) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest cover path");
+ }
+ if (SDCard_Map_Business_Path(business_path, sd_path, sizeof(sd_path)) != ESP_OK) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "invalid manifest cover path");
+ }
+ const char *cover_dot = strrchr(sd_path, '.');
+ if (!cover_dot || strcasecmp(cover_dot, ".jpg") != 0) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_UNSUPPORTED, "only jpg cover assets are supported");
+ }
- job_asset = &job->assets[index];
- if (!BoardMqtt_CopyString(job_asset->url, sizeof(job_asset->url), cover_url->valuestring) ||
- !BoardMqtt_CopyString(job_asset->target_path, sizeof(job_asset->target_path), business_path)) {
- BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "manifest cover field too long");
+ job_asset = &job->assets[index];
+ if (!BoardMqtt_CopyString(job_asset->url, sizeof(job_asset->url), cover_url->valuestring) ||
+ !BoardMqtt_CopyString(job_asset->target_path, sizeof(job_asset->target_path), business_path)) {
+ BOARD_MQTT_FREE_MANIFEST_JOB_AND_FAIL(BOARD_MQTT_ERR_INVALID_PARAM, "manifest cover field too long");
+ }
+ ++index;
}
- ++index;
}
job->asset_count = index;
- if (SDCard_Is_Playing()) {
- ESP_LOGI(TAG, "Stop MP3 playback before MQTT manifest download");
+ if (job->start_queued_app && SDCard_Is_Playing()) {
+ ESP_LOGI(TAG, "Stop MP3 playback before PRELOAD for queued AI task");
SDCard_Stop_Play();
}
@@ -1868,7 +2155,7 @@
return BoardMqtt_ResultOk("manifest accepted");
}
-static board_mqtt_action_result_t BoardMqtt_HandleStartAppTask(cJSON *payload)
+static board_mqtt_action_result_t BoardMqtt_HandleStartAppTask(const char *req_id, cJSON *payload)
{
if (!payload) {
return BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "missing payload");
@@ -1877,24 +2164,77 @@
cJSON *task_id = cJSON_GetObjectItem(payload, "task_id");
cJSON *source = cJSON_GetObjectItem(payload, "source");
cJSON *params = cJSON_GetObjectItem(payload, "params");
+ cJSON *package = cJSON_GetObjectItem(payload, "package");
if (!cJSON_IsString(app_id) || !app_id->valuestring || app_id->valuestring[0] == '\0') {
return BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "missing app_id");
}
const char *tid = (cJSON_IsString(task_id) && task_id->valuestring) ? task_id->valuestring : "";
const char *src = (cJSON_IsString(source) && source->valuestring) ? source->valuestring : "mqtt_immediate";
char *params_json = params ? cJSON_PrintUnformatted(params) : NULL;
- esp_err_t err = Board_StartAppTask(app_id->valuestring, tid, params_json, src);
+ const bool lesson = BoardMqtt_IsLessonAppTask(app_id->valuestring, tid);
+
+ if (!lesson || !cJSON_IsObject(package)) {
+ if (lesson && !Board_LessonFilesReady(tid, params_json)) {
+ ESP_LOGW(TAG, "lesson files missing on SD, start after sync %s/%s",
+ app_id->valuestring, tid);
+ Board_Show_Alert("姝e湪鍚屾", "姝e湪鍚屾闊抽锛岃绋嶇瓑", "cloud_arrow_down");
+ }
+ BoardMqtt_ClearQueuedAppTask();
+ esp_err_t err = Board_StartAppTask(app_id->valuestring, tid, params_json, src);
+ free(params_json);
+ if (err == ESP_ERR_NOT_FOUND) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_NOT_FOUND, "unknown app or task");
+ }
+ if (err == ESP_ERR_INVALID_ARG) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "invalid app or task id");
+ }
+ if (err == ESP_ERR_INVALID_STATE) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_BUSY, "preempted by higher priority task");
+ }
+ if (err != ESP_OK) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_EXEC_FAILED, esp_err_to_name(err));
+ }
+ return BoardMqtt_ResultOk("app task started");
+ }
+
+ cJSON *files = cJSON_GetObjectItem(package, "files");
+ if (BoardMqtt_PackageCached(files)) {
+ ESP_LOGI(TAG, "package cached, READY %s/%s", app_id->valuestring, tid);
+ BoardMqtt_ClearQueuedAppTask();
+ esp_err_t err = Board_StartAppTask(app_id->valuestring, tid, params_json, src);
+ free(params_json);
+ if (err == ESP_ERR_NOT_FOUND) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_NOT_FOUND, "unknown app or task");
+ }
+ if (err == ESP_ERR_INVALID_ARG) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "invalid app or task id");
+ }
+ if (err == ESP_ERR_INVALID_STATE) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_BUSY, "preempted by higher priority task");
+ }
+ if (err != ESP_OK) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_EXEC_FAILED, esp_err_to_name(err));
+ }
+ return BoardMqtt_ResultOk("app task started");
+ }
+
+ if (!BoardMqtt_QueueAppTask(app_id->valuestring, tid, src, params_json)) {
+ free(params_json);
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_EXEC_FAILED, "failed to queue app task");
+ }
free(params_json);
- if (err == ESP_ERR_NOT_FOUND) {
- return BoardMqtt_ResultFail(BOARD_MQTT_ERR_NOT_FOUND, "unknown app or task");
+ ESP_LOGI(TAG, "PRELOAD package then run %s/%s", app_id->valuestring, tid);
+ Board_HoldIdleForServerPrompt();
+ Board_Show_Alert("姝e湪鍚屾", "姝e湪鍚屾璇炬枃锛岃绋嶅��", "cloud_arrow_down");
+ s_bind_manifest_to_queue = true;
+ board_mqtt_action_result_t preload = BoardMqtt_HandleUpdateManifest(req_id, package);
+ s_bind_manifest_to_queue = false;
+ if (!preload.success) {
+ Board_ReleaseHoldIdle();
+ BoardMqtt_ClearQueuedAppTask();
+ Board_Show_Alert("鍚屾澶辫触", preload.info ? preload.info : "璇炬枃涓嬭浇澶辫触", "sad");
}
- if (err == ESP_ERR_INVALID_STATE) {
- return BoardMqtt_ResultFail(BOARD_MQTT_ERR_BUSY, "preempted by higher priority task");
- }
- if (err != ESP_OK) {
- return BoardMqtt_ResultFail(BOARD_MQTT_ERR_EXEC_FAILED, esp_err_to_name(err));
- }
- return BoardMqtt_ResultOk("app task started");
+ return preload.success ? BoardMqtt_ResultOk("preload queued") : preload;
}
static board_mqtt_action_result_t BoardMqtt_HandleEnsureAppDirs(cJSON *payload)
@@ -1929,6 +2269,29 @@
return BoardMqtt_ResultOk("app dirs ensured");
}
+static board_mqtt_action_result_t BoardMqtt_HandleSyncSchedule(cJSON *payload)
+{
+ if (!payload) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "missing payload");
+ }
+ char *json = cJSON_PrintUnformatted(payload);
+ if (!json) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_EXEC_FAILED, "schedule json alloc failed");
+ }
+ esp_err_t err = Board_Schedule_SyncJson(json);
+ free(json);
+ if (err == ESP_ERR_INVALID_ARG) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "invalid schedule");
+ }
+ if (err == ESP_ERR_NO_MEM) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "too many scheduled tasks");
+ }
+ if (err != ESP_OK) {
+ return BoardMqtt_ResultFail(BOARD_MQTT_ERR_EXEC_FAILED, esp_err_to_name(err));
+ }
+ return BoardMqtt_ResultOk("schedule synced");
+}
+
static board_mqtt_action_result_t BoardMqtt_DispatchAction(const char *req_id, const char *action, cJSON *payload)
{
if (strcmp(action, "SET_VOLUME") == 0) return BoardMqtt_HandleSetVolume(payload);
@@ -1943,17 +2306,20 @@
if (strcmp(action, "UPDATE_MANIFEST") == 0 || strcmp(action, "SYNC_COURSE_TASK") == 0) return BoardMqtt_HandleUpdateManifest(req_id, payload);
if (strcmp(action, "DOWNLOAD") == 0) return BoardMqtt_HandleDownload(req_id, payload);
if (strcmp(action, "OTA_UPDATE") == 0) return BoardMqtt_HandleOtaUpdate(req_id, payload);
- if (strcmp(action, "START_APP_TASK") == 0) return BoardMqtt_HandleStartAppTask(payload);
+ if (strcmp(action, "START_APP_TASK") == 0) return BoardMqtt_HandleStartAppTask(req_id, payload);
+ if (strcmp(action, "SYNC_SCHEDULE") == 0) return BoardMqtt_HandleSyncSchedule(payload);
if (strcmp(action, "ENSURE_APP_DIRS") == 0) return BoardMqtt_HandleEnsureAppDirs(payload);
return BoardMqtt_ResultFail(BOARD_MQTT_ERR_UNSUPPORTED, "unsupported action");
}
static board_mqtt_topic_type_t BoardMqtt_GetTopicType(const char *topic)
{
- if (strcmp(topic, s_cmd_ctl_topic) == 0) {
+ if (strcmp(topic, s_cmd_ctl_topic) == 0 ||
+ (s_cmd_ctl_mac_topic[0] && strcmp(topic, s_cmd_ctl_mac_topic) == 0)) {
return BOARD_MQTT_TOPIC_CTL;
}
- if (strcmp(topic, s_cmd_task_topic) == 0) {
+ if (strcmp(topic, s_cmd_task_topic) == 0 ||
+ (s_cmd_task_mac_topic[0] && strcmp(topic, s_cmd_task_mac_topic) == 0)) {
return BOARD_MQTT_TOPIC_TASK;
}
return BOARD_MQTT_TOPIC_UNKNOWN;
@@ -2074,7 +2440,9 @@
result = BoardMqtt_ResultFail(BOARD_MQTT_ERR_TOPIC_MISMATCH, "invalid command topic");
} else if (!cJSON_IsString(version) || strcmp(version->valuestring, "1.0") != 0) {
result = BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "invalid header.version");
- } else if (!cJSON_IsString(device_id) || strcmp(device_id->valuestring, s_device_id) != 0) {
+ } else if (!cJSON_IsString(device_id) ||
+ (strcmp(device_id->valuestring, s_device_id) != 0 &&
+ (s_mac_id[0] == '\0' || strcasecmp(device_id->valuestring, s_mac_id) != 0))) {
result = BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "invalid header.device_id");
} else if (!cJSON_IsNumber(timestamp)) {
result = BoardMqtt_ResultFail(BOARD_MQTT_ERR_INVALID_PARAM, "invalid header.timestamp");
@@ -2095,49 +2463,45 @@
free(payload_text);
}
-static void BoardMqtt_ReconnectTask(void *arg)
+static void BoardMqtt_TryReconnect(void)
{
- (void)arg;
- while (s_mqtt_client && !s_mqtt_connected) {
- if (Board_Is_Voice_Session_Active()) {
- ESP_LOGI(TAG, "Delay local MQTT reconnect while voice session is active");
- vTaskDelay(pdMS_TO_TICKS(BOARD_MQTT_RECONNECT_DELAY_MS));
- continue;
- }
- ESP_LOGI(TAG, "Reconnecting local MQTT client");
- esp_err_t err = esp_mqtt_client_reconnect(s_mqtt_client);
- if (err != ESP_OK) {
- ESP_LOGW(TAG, "Local MQTT reconnect failed: %s", esp_err_to_name(err));
- vTaskDelay(pdMS_TO_TICKS(BOARD_MQTT_RECONNECT_DELAY_MS));
- continue;
- }
- break;
+ if (!s_mqtt_client || s_mqtt_connected) {
+ return;
}
- s_reconnect_task_handle = NULL;
- vTaskDelete(NULL);
+ size_t internal_free = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
+ if (internal_free < BOARD_MQTT_RECONNECT_MIN_INTERNAL_HEAP) {
+ ESP_LOGW(TAG, "Skip MQTT reconnect, internal heap=%u", (unsigned)internal_free);
+ return;
+ }
+ ESP_LOGI(TAG, "Reconnecting local MQTT client");
+ esp_err_t err = esp_mqtt_client_reconnect(s_mqtt_client);
+ if (err != ESP_OK) {
+ ESP_LOGW(TAG, "Local MQTT reconnect failed: %s", esp_err_to_name(err));
+ }
}
static void BoardMqtt_ScheduleReconnect(void)
{
- if (!s_mqtt_client || s_reconnect_task_handle) {
- return;
- }
- BaseType_t ret = xTaskCreate(BoardMqtt_ReconnectTask, "mqtt_reconnect", 4096, NULL, BOARD_MQTT_RECONNECT_TASK_PRIORITY, &s_reconnect_task_handle);
- if (ret != pdPASS) {
- ESP_LOGE(TAG, "Failed to create local MQTT reconnect task");
- s_reconnect_task_handle = NULL;
- }
+ // 鏂繛鍥炶皟閲屼笉瑕佸啀寮� 4KB 浠诲姟锛氬啀瑙佸悗鍔犺浇鍞ら啋璇嶆椂鍐呴儴 SRAM 寰堢揣锛�
+ // xTaskCreate 澶辫触鍚� WiFi MODEM sleep 杩樹細鍦� phy_track_pll_init 閲� abort銆�
+ ESP_LOGW(TAG, "MQTT disconnected, retry from state task");
}
static void BoardMqtt_StateTask(void *arg)
{
(void)arg;
while (s_mqtt_client) {
- vTaskDelay(pdMS_TO_TICKS(BOARD_MQTT_STATE_REPORT_INTERVAL_MS));
- if (Board_Is_Audio_Busy()) {
+ if (s_mqtt_connected) {
+ if (!BoardMqtt_IsDownloadTask(BoardMqtt_GetTaskType())) {
+ BoardMqtt_PublishState();
+ }
+ vTaskDelay(pdMS_TO_TICKS(BOARD_MQTT_STATE_REPORT_INTERVAL_MS));
continue;
}
- BoardMqtt_PublishState();
+ vTaskDelay(pdMS_TO_TICKS(BOARD_MQTT_RECONNECT_RETRY_MS));
+ if (!s_mqtt_connected) {
+ BoardMqtt_TryReconnect();
+ }
}
s_state_task_handle = NULL;
vTaskDelete(NULL);
@@ -2175,9 +2539,14 @@
case MQTT_EVENT_CONNECTED:
s_mqtt_connected = true;
ESP_LOGI(TAG, "MQTT business server connected, host=%s, port=%d, username=%s", BOARD_MQTT_BROKER_HOST, BOARD_MQTT_BROKER_PORT, BOARD_MQTT_USERNAME);
- ESP_LOGI(TAG, "MQTT command topics ready, ctl=%s, task=%s", s_cmd_ctl_topic, s_cmd_task_topic);
+ ESP_LOGI(TAG, "MQTT command topics ready, ctl=%s, task=%s, mac_ctl=%s",
+ s_cmd_ctl_topic, s_cmd_task_topic, s_cmd_ctl_mac_topic);
BoardMqtt_Subscribe(s_cmd_ctl_topic);
BoardMqtt_Subscribe(s_cmd_task_topic);
+ if (s_cmd_ctl_mac_topic[0] && strcmp(s_cmd_ctl_mac_topic, s_cmd_ctl_topic) != 0) {
+ BoardMqtt_Subscribe(s_cmd_ctl_mac_topic);
+ BoardMqtt_Subscribe(s_cmd_task_mac_topic);
+ }
BoardMqtt_PublishLog("MQTT connected");
BoardMqtt_PublishState();
break;
@@ -2226,6 +2595,23 @@
return ESP_ERR_INVALID_SIZE;
}
+ uint8_t mac[6] = {0};
+ if (esp_read_mac(mac, ESP_MAC_WIFI_STA) == ESP_OK) {
+ snprintf(s_mac_id, sizeof(s_mac_id), "%02x:%02x:%02x:%02x:%02x:%02x",
+ mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
+ BoardMqtt_FormatTopic(s_cmd_ctl_mac_topic, sizeof(s_cmd_ctl_mac_topic),
+ BOARD_MQTT_CMD_CTL_TOPIC_FORMAT, s_mac_id);
+ BoardMqtt_FormatTopic(s_cmd_task_mac_topic, sizeof(s_cmd_task_mac_topic),
+ BOARD_MQTT_CMD_TASK_TOPIC_FORMAT, s_mac_id);
+ BoardMqtt_FormatTopic(s_rpt_state_mac_topic, sizeof(s_rpt_state_mac_topic),
+ BOARD_MQTT_RPT_STATE_TOPIC_FORMAT, s_mac_id);
+ } else {
+ s_mac_id[0] = '\0';
+ s_cmd_ctl_mac_topic[0] = '\0';
+ s_cmd_task_mac_topic[0] = '\0';
+ s_rpt_state_mac_topic[0] = '\0';
+ }
+
esp_err_t ret = BoardMqtt_FormatTopic(s_cmd_ctl_topic, sizeof(s_cmd_ctl_topic), BOARD_MQTT_CMD_CTL_TOPIC_FORMAT, client_id);
if (ret != ESP_OK) {
return ret;
@@ -2269,6 +2655,11 @@
mqtt_config.credentials.authentication.password = BOARD_MQTT_PASSWORD;
}
mqtt_config.session.keepalive = BOARD_MQTT_KEEPALIVE_SEC;
+ mqtt_config.session.last_will.topic = s_rpt_state_mac_topic[0] ? s_rpt_state_mac_topic : s_rpt_state_topic;
+ mqtt_config.session.last_will.msg = BOARD_MQTT_LWT_PAYLOAD;
+ mqtt_config.session.last_will.msg_len = sizeof(BOARD_MQTT_LWT_PAYLOAD) - 1;
+ mqtt_config.session.last_will.qos = 1;
+ mqtt_config.session.last_will.retain = true;
mqtt_config.network.disable_auto_reconnect = true;
mqtt_config.network.timeout_ms = BOARD_MQTT_NETWORK_TIMEOUT_MS;
--
Gitblit v1.9.1