#include "parent_schedule.h" #include "app_task.h" #include "application.h" #include "system.h" #include #include #include #include #include #include #include #include #define TAG "parent_sched" #define SCHEDULE_PATH MOUNT_POINT "/.parent_schedule.json" #define SCHEDULE_MAX_TASKS 100 #define SCHEDULE_ID_MAX 40 #define SCHEDULE_APP_MAX 32 #define SCHEDULE_PARAMS_MAX 384 #define SCHEDULE_TYPE_DAILY 0 #define SCHEDULE_TYPE_HOURLY 1 #define SCHEDULE_TYPE_INTERVAL 2 #define SCHEDULE_END_DEFAULT 0 #define SCHEDULE_END_CHAT 1 #define SCHEDULE_END_POWER_OFF 2 #define SCHEDULE_DUE_WINDOW_SEC 180 typedef struct { char id[SCHEDULE_ID_MAX]; char app_id[SCHEDULE_APP_MAX]; char task_id[SCHEDULE_APP_MAX]; char params_json[SCHEDULE_PARAMS_MAX]; uint8_t enabled; uint8_t type; uint8_t hour; uint8_t min; uint8_t sec; uint8_t weekdays; uint8_t hourly_minute; uint16_t every_minutes; } sched_task_t; static sched_task_t s_tasks[SCHEDULE_MAX_TASKS]; static int s_task_count = 0; static bool s_loaded = false; static char s_next_id[SCHEDULE_ID_MAX]; static char s_next_app[SCHEDULE_APP_MAX]; static char s_next_task[SCHEDULE_APP_MAX]; static char s_next_params[SCHEDULE_PARAMS_MAX]; static time_t s_next_unix = 0; static bool s_next_valid = false; static bool s_occupying = false; static time_t s_busy_until = 0; static int s_on_complete = SCHEDULE_END_DEFAULT; static bool s_deferred_due = false; static bool s_fire_queued = false; static bool s_wait_lesson_files = false; static bool s_files_poll_queued = false; static time_t s_wait_lesson_files_since = 0; #define CLAIMED_SLOT_MAX 16 typedef struct { char id[SCHEDULE_ID_MAX]; time_t unix_time; } claimed_slot_t; static claimed_slot_t s_claimed_slots[CLAIMED_SLOT_MAX]; static int s_claimed_count = 0; static time_t s_last_started_unix = 0; static char s_pending_app[SCHEDULE_APP_MAX]; static char s_pending_task[SCHEDULE_APP_MAX]; static char s_pending_params[SCHEDULE_PARAMS_MAX]; static bool s_duration_fired = false; static esp_timer_handle_t s_duration_timer = NULL; static bool slot_already_claimed(const sched_task_t *task, time_t cand); static uint8_t tm_wday_to_repeat_bit(int tm_wday) { switch (tm_wday) { case 1: return ALARM_REPEAT_MON; case 2: return ALARM_REPEAT_TUE; case 3: return ALARM_REPEAT_WED; case 4: return ALARM_REPEAT_THU; case 5: return ALARM_REPEAT_FRI; case 6: return ALARM_REPEAT_SAT; default: return ALARM_REPEAT_SUN; } } static bool parse_hhmmss(const char *value, uint8_t *hour, uint8_t *min, uint8_t *sec) { int h = 0, m = 0, s = 0; char tail = 0; if (!value) { return false; } if (sscanf(value, "%2d:%2d:%2d%c", &h, &m, &s, &tail) == 3 || sscanf(value, "%2d:%2d%c", &h, &m, &tail) == 2) { if (h < 0 || h > 23 || m < 0 || m > 59 || s < 0 || s > 59) { return false; } *hour = (uint8_t)h; *min = (uint8_t)m; *sec = (uint8_t)s; return true; } return false; } static uint8_t parse_weekdays(const cJSON *repeat) { uint8_t mask = 0; if (!cJSON_IsArray(repeat) || cJSON_GetArraySize(repeat) == 0) { return ALARM_REPEAT_ALL; } const cJSON *day = NULL; cJSON_ArrayForEach(day, repeat) { int value = cJSON_IsNumber(day) ? day->valueint : 0; switch (value) { case 1: mask |= ALARM_REPEAT_MON; break; case 2: mask |= ALARM_REPEAT_TUE; break; case 3: mask |= ALARM_REPEAT_WED; break; case 4: mask |= ALARM_REPEAT_THU; break; case 5: mask |= ALARM_REPEAT_FRI; break; case 6: mask |= ALARM_REPEAT_SAT; break; case 7: mask |= ALARM_REPEAT_SUN; break; default: break; } } return mask ? mask : ALARM_REPEAT_ALL; } static void copy_str(char *dest, size_t dest_size, const char *src) { if (!dest || dest_size == 0) { return; } dest[0] = '\0'; if (src && src[0]) { strlcpy(dest, src, dest_size); } } static time_t wall_now(struct tm *out) { struct tm now = {}; if (!Board_Get_Wall_Clock(&now)) { time_t t = 0; time(&t); localtime_r(&t, &now); } if (out) { *out = now; } now.tm_isdst = -1; return mktime(&now); } static int on_complete_from_params(const cJSON *params) { if (!params) { return SCHEDULE_END_DEFAULT; } const cJSON *item = cJSON_GetObjectItem(params, "on_complete"); if (cJSON_IsString(item) && item->valuestring) { if (strcmp(item->valuestring, "power_off") == 0) { return SCHEDULE_END_POWER_OFF; } if (strcmp(item->valuestring, "chat") == 0) { return SCHEDULE_END_CHAT; } } const cJSON *power = cJSON_GetObjectItem(params, "power_off_after"); if (cJSON_IsTrue(power) || (cJSON_IsNumber(power) && power->valueint != 0)) { return SCHEDULE_END_POWER_OFF; } return SCHEDULE_END_DEFAULT; } static int duration_minutes_from_params(const cJSON *params) { if (!params) { return 0; } const cJSON *item = cJSON_GetObjectItem(params, "duration_minutes"); if (cJSON_IsNumber(item) && item->valuedouble > 0) { int value = (int)item->valuedouble; if (value > 24 * 60) { value = 24 * 60; } return value; } return 0; } static bool parse_one_task(const cJSON *obj, sched_task_t *out) { if (!cJSON_IsObject(obj) || !out) { return false; } memset(out, 0, sizeof(*out)); const cJSON *id = cJSON_GetObjectItem(obj, "id"); const cJSON *app = cJSON_GetObjectItem(obj, "app_id"); const cJSON *task = cJSON_GetObjectItem(obj, "task_id"); const cJSON *enabled = cJSON_GetObjectItem(obj, "enabled"); const cJSON *params = cJSON_GetObjectItem(obj, "params"); const cJSON *schedule = cJSON_GetObjectItem(obj, "schedule"); if (!cJSON_IsString(id) || !id->valuestring[0] || !cJSON_IsString(app) || !app->valuestring[0]) { return false; } copy_str(out->id, sizeof(out->id), id->valuestring); copy_str(out->app_id, sizeof(out->app_id), app->valuestring); copy_str(out->task_id, sizeof(out->task_id), (cJSON_IsString(task) && task->valuestring) ? task->valuestring : ""); out->enabled = enabled ? !cJSON_IsFalse(enabled) : 1; if (params) { char *printed = cJSON_PrintUnformatted(params); if (printed) { copy_str(out->params_json, sizeof(out->params_json), printed); free(printed); } } else { copy_str(out->params_json, sizeof(out->params_json), "{}"); } const cJSON *type = schedule ? cJSON_GetObjectItem(schedule, "type") : NULL; const char *type_str = (cJSON_IsString(type) && type->valuestring) ? type->valuestring : "daily"; if (strcmp(type_str, "hourly") == 0) { out->type = SCHEDULE_TYPE_HOURLY; const cJSON *minute = cJSON_GetObjectItem(schedule, "minute"); int value = cJSON_IsNumber(minute) ? minute->valueint : 0; if (value < 0 || value > 59) { value = 0; } out->hourly_minute = (uint8_t)value; return true; } if (strcmp(type_str, "interval") == 0) { out->type = SCHEDULE_TYPE_INTERVAL; const cJSON *every = cJSON_GetObjectItem(schedule, "every_minutes"); int value = cJSON_IsNumber(every) ? every->valueint : 60; if (value < 1) { value = 1; } if (value > 24 * 60) { value = 24 * 60; } out->every_minutes = (uint16_t)value; return true; } out->type = SCHEDULE_TYPE_DAILY; const cJSON *time_value = schedule ? cJSON_GetObjectItem(schedule, "time") : NULL; if (!cJSON_IsString(time_value) || !parse_hhmmss(time_value->valuestring, &out->hour, &out->min, &out->sec)) { return false; } out->weekdays = parse_weekdays(schedule ? cJSON_GetObjectItem(schedule, "weekdays") : NULL); ESP_LOGI(TAG, "task %s %s/%s daily %02u:%02u:%02u weekdays=0x%02x", out->id, out->app_id, out->task_id, out->hour, out->min, out->sec, out->weekdays); return true; } static bool load_from_file(void) { s_task_count = 0; FILE *fp = fopen(SCHEDULE_PATH, "rb"); if (!fp) { // SD 还没挂载时不要记成“已经加载且没有计划”,否则开机后不再重读。 s_loaded = false; s_task_count = 0; return false; } if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); return false; } long size = ftell(fp); rewind(fp); if (size <= 0 || size > 49152) { fclose(fp); return false; } char *buf = (char *)malloc((size_t)size + 1); if (!buf) { fclose(fp); return false; } size_t n = fread(buf, 1, (size_t)size, fp); fclose(fp); buf[n] = '\0'; cJSON *root = cJSON_Parse(buf); free(buf); if (!root) { return false; } const cJSON *tasks = cJSON_IsArray(root) ? root : cJSON_GetObjectItem(root, "tasks"); if (cJSON_IsArray(tasks)) { const cJSON *item = NULL; cJSON_ArrayForEach(item, tasks) { if (s_task_count >= SCHEDULE_MAX_TASKS) { break; } if (parse_one_task(item, &s_tasks[s_task_count])) { s_task_count++; } } } cJSON_Delete(root); s_loaded = true; ESP_LOGI(TAG, "loaded %d scheduled tasks", s_task_count); return true; } static bool next_occurrence(const sched_task_t *task, time_t not_before, time_t *out) { if (!task || !out || !task->enabled) { return false; } struct tm now_tm = {}; time_t now = wall_now(&now_tm); if (not_before < now) { not_before = now; } if (task->type == SCHEDULE_TYPE_HOURLY) { time_t midnight = now - (now_tm.tm_hour * 3600 + now_tm.tm_min * 60 + now_tm.tm_sec); time_t missed = 0; for (int h = 0; h < 24 * 8; ++h) { time_t cand = midnight + h * 3600 + task->hourly_minute * 60; if (cand > not_before) { if (slot_already_claimed(task, cand)) { continue; } if (missed != 0 && not_before <= now) { *out = missed; return true; } *out = cand; return true; } if (not_before <= now && cand <= now && now - cand <= SCHEDULE_DUE_WINDOW_SEC && !slot_already_claimed(task, cand)) { missed = cand; } } if (missed != 0) { *out = missed; return true; } return false; } if (task->type == SCHEDULE_TYPE_INTERVAL) { int every = task->every_minutes > 0 ? task->every_minutes : 60; time_t step = (time_t)every * 60; if (step < 60) { step = 60; } if (not_before <= now && step > 0) { time_t slot = (now / step) * step; if (slot > 0 && now - slot <= SCHEDULE_DUE_WINDOW_SEC && !slot_already_claimed(task, slot)) { *out = slot; return true; } } time_t cand = ((not_before / step) + 1) * step; if (cand <= not_before) { cand += step; } *out = cand; return true; } uint8_t mask = task->weekdays ? task->weekdays : ALARM_REPEAT_ALL; int now_sec = now_tm.tm_hour * 3600 + now_tm.tm_min * 60 + now_tm.tm_sec; int alarm_sec = task->hour * 3600 + task->min * 60 + task->sec; int min_delta = (int)(not_before - now); if (min_delta < 0) { min_delta = 0; } int best_delta = 8 * 24 * 3600; bool found = false; for (int day_offset = 0; day_offset <= 8; ++day_offset) { int tm_wday = (now_tm.tm_wday + day_offset) % 7; if ((mask & tm_wday_to_repeat_bit(tm_wday)) == 0) { continue; } int delta = day_offset * 24 * 3600 + alarm_sec - now_sec; if (delta <= min_delta) { // 刚过点的几分钟内仍算本次(硬件开机、软关机唤醒),不要直接排到下一天。 if (min_delta <= 0 && delta >= -SCHEDULE_DUE_WINDOW_SEC && !slot_already_claimed(task, now + delta)) { best_delta = delta; found = true; } continue; } if (slot_already_claimed(task, now + (time_t)delta)) { continue; } if (found && best_delta <= 0) { break; } if (delta < best_delta) { best_delta = delta; found = true; } } if (!found) { return false; } *out = now + best_delta; return true; } static void clear_next(void) { s_next_valid = false; s_next_unix = 0; s_next_id[0] = '\0'; s_next_app[0] = '\0'; s_next_task[0] = '\0'; copy_str(s_next_params, sizeof(s_next_params), "{}"); DS2312_Clear_Schedule_Wake(); } static void cancel_duration_timer(void) { if (s_duration_timer) { esp_timer_stop(s_duration_timer); } } static void duration_timer_cb(void *arg) { (void)arg; Application::GetInstance().Schedule([]() { s_duration_fired = true; Board_Schedule_OnNaturalEnd(); }); } static void ensure_duration_timer(void) { if (s_duration_timer) { return; } const esp_timer_create_args_t args = { .callback = duration_timer_cb, .arg = NULL, .dispatch_method = ESP_TIMER_TASK, .name = "sched_dur", .skip_unhandled_events = true, }; if (esp_timer_create(&args, &s_duration_timer) != ESP_OK) { s_duration_timer = NULL; } } static bool is_playlist_task(const char *task_id) { return task_id && (strcmp(task_id, "repeater") == 0 || strcmp(task_id, "shadowing") == 0); } static bool claim_slot_recorded(const char *id, time_t cand) { if (!id || id[0] == '\0' || cand <= 0) { return false; } for (int i = 0; i < s_claimed_count; ++i) { if (s_claimed_slots[i].unix_time == cand && strcmp(s_claimed_slots[i].id, id) == 0) { return true; } } return false; } static void remember_claim_slot(const char *id, time_t unix_time) { if (!id || id[0] == '\0' || unix_time <= 0 || claim_slot_recorded(id, unix_time)) { if (unix_time > s_last_started_unix) { s_last_started_unix = unix_time; } return; } if (s_claimed_count >= CLAIMED_SLOT_MAX) { memmove(&s_claimed_slots[0], &s_claimed_slots[1], sizeof(claimed_slot_t) * (CLAIMED_SLOT_MAX - 1)); s_claimed_count = CLAIMED_SLOT_MAX - 1; } copy_str(s_claimed_slots[s_claimed_count].id, sizeof(s_claimed_slots[s_claimed_count].id), id); s_claimed_slots[s_claimed_count].unix_time = unix_time; s_claimed_count++; if (unix_time > s_last_started_unix) { s_last_started_unix = unix_time; } } static bool slot_already_claimed(const sched_task_t *task, time_t cand) { if (!task) { return false; } // 时间已经走到更晚的计划,更早的那次不再回头启动。 if (s_last_started_unix > 0 && cand < s_last_started_unix) { return true; } return claim_slot_recorded(task->id, cand); } static bool is_chat_task(const char *app_id, const char *task_id) { return app_id && strcmp(app_id, "chat") == 0 && (!task_id || task_id[0] == '\0' || strcmp(task_id, "conversation") == 0); } static void apply_on_complete(bool user_exit) { cancel_duration_timer(); s_occupying = false; s_busy_until = 0; int complete = s_on_complete; s_on_complete = SCHEDULE_END_DEFAULT; bool from_duration = s_duration_fired; s_duration_fired = false; Board_Schedule_Recompute(); if (!user_exit && s_deferred_due && Board_Schedule_IsDueNow(180)) { s_deferred_due = false; Board_Schedule_OnAlarmFired(); return; } s_deferred_due = false; if (!user_exit && complete == SCHEDULE_END_POWER_OFF) { Application::GetInstance().RequestPowerOffStandby(); return; } if (!user_exit && from_duration) { Board_StartAppTask("chat", "conversation", "{}", "default_chat"); } } esp_err_t Board_Schedule_SyncJson(const char *json) { if (!json) { return ESP_ERR_INVALID_ARG; } cJSON *root = cJSON_Parse(json); if (!root) { return ESP_ERR_INVALID_ARG; } const cJSON *tasks = cJSON_IsArray(root) ? root : cJSON_GetObjectItem(root, "tasks"); if (tasks && !cJSON_IsArray(tasks)) { cJSON_Delete(root); return ESP_ERR_INVALID_ARG; } if (tasks && cJSON_GetArraySize(tasks) > SCHEDULE_MAX_TASKS) { cJSON_Delete(root); return ESP_ERR_NO_MEM; } cJSON *store = cJSON_CreateObject(); if (!store) { cJSON_Delete(root); return ESP_ERR_NO_MEM; } cJSON *copy = tasks ? cJSON_Duplicate(tasks, 1) : cJSON_CreateArray(); if (!copy) { cJSON_Delete(store); cJSON_Delete(root); return ESP_ERR_NO_MEM; } cJSON_AddItemToObject(store, "tasks", copy); char *printed = cJSON_PrintUnformatted(store); cJSON_Delete(store); cJSON_Delete(root); if (!printed) { return ESP_ERR_NO_MEM; } FILE *fp = fopen(SCHEDULE_PATH, "wb"); if (!fp) { free(printed); return ESP_FAIL; } size_t len = strlen(printed); size_t wrote = fwrite(printed, 1, len, fp); fclose(fp); free(printed); if (wrote != len) { return ESP_FAIL; } s_loaded = false; if (!load_from_file()) { return ESP_FAIL; } for (int i = 0; i < s_task_count; ++i) { if (!s_tasks[i].enabled) { continue; } Board_RememberLastLessonTask(s_tasks[i].task_id, s_tasks[i].params_json); } return Board_Schedule_Recompute(); } void Board_Schedule_Reload(void) { s_loaded = false; s_task_count = 0; } esp_err_t Board_Schedule_Recompute(void) { if (!s_loaded && !load_from_file()) { ESP_LOGW(TAG, "schedule file not ready"); return ESP_FAIL; } struct tm now_tm = {}; time_t now = wall_now(&now_tm); time_t not_before = now; // 课文计划播完后如果没收到结束回调,不要继续占着下一次闹钟。 if (s_occupying && s_busy_until <= now && !Board_IsLessonPlaybackActive()) { s_occupying = false; s_busy_until = 0; } // 只有设定了持续时长才往后推。正在泛听不能把跟读推迟 12 小时。 if (s_occupying && s_busy_until > now) { not_before = s_busy_until; } int best_index = -1; time_t best_unix = 0; bool best_due = false; for (int i = 0; i < s_task_count; ++i) { time_t cand = 0; if (!next_occurrence(&s_tasks[i], not_before, &cand)) { continue; } bool due = cand <= now; bool take = false; if (best_index < 0) { take = true; } else if (due && !best_due) { take = true; } else if (due && best_due && cand > best_unix) { take = true; } else if (!due && !best_due && cand < best_unix) { take = true; } if (take) { best_index = i; best_unix = cand; best_due = due; } } if (best_index < 0) { ESP_LOGI(TAG, "no upcoming scheduled task"); clear_next(); return ESP_OK; } const sched_task_t *best = &s_tasks[best_index]; copy_str(s_next_id, sizeof(s_next_id), best->id); copy_str(s_next_app, sizeof(s_next_app), best->app_id); copy_str(s_next_task, sizeof(s_next_task), best->task_id); copy_str(s_next_params, sizeof(s_next_params), best->params_json[0] ? best->params_json : "{}"); s_next_unix = best_unix; s_next_valid = true; struct tm fire_tm = {}; localtime_r(&best_unix, &fire_tm); uint8_t bit = tm_wday_to_repeat_bit(fire_tm.tm_wday); DS2312_Set_Schedule_Wake(fire_tm.tm_hour, fire_tm.tm_min, fire_tm.tm_sec, bit); ESP_LOGI(TAG, "next schedule id=%s %s/%s at %02d:%02d:%02d unix=%ld", s_next_id, s_next_app, s_next_task, fire_tm.tm_hour, fire_tm.tm_min, fire_tm.tm_sec, (long)best_unix); return ESP_OK; } bool Board_Schedule_IsDueNow(int window_sec) { if (!s_next_valid) { if (!s_loaded) { load_from_file(); Board_Schedule_Recompute(); } if (!s_next_valid) { return false; } } time_t now = wall_now(NULL); if (window_sec < 1) { window_sec = SCHEDULE_DUE_WINDOW_SEC; } time_t late = now - s_next_unix; // 计划时间未到就不要启动,避免下发后提前打断正在进行的对话。 if (late < 0) { return false; } return late <= window_sec; } bool Board_Schedule_LoadDueTask(char *app_id, size_t app_size, char *task_id, size_t task_size, char *params_json, size_t params_size) { if (!s_next_valid) { return false; } copy_str(app_id, app_size, s_next_app); copy_str(task_id, task_size, s_next_task); copy_str(params_json, params_size, s_next_params[0] ? s_next_params : "{}"); return app_id && app_id[0]; } bool Board_Schedule_ClaimDue(void) { if (!Board_Schedule_IsDueNow(SCHEDULE_DUE_WINDOW_SEC)) { return false; } if (claim_slot_recorded(s_next_id, s_next_unix)) { return false; } remember_claim_slot(s_next_id, s_next_unix); return true; } static void remember_claimed_schedule(const char *task_id) { if (!s_next_valid || !task_id || strcmp(task_id, s_next_task) != 0) { return; } if (!Board_Schedule_IsDueNow(SCHEDULE_DUE_WINDOW_SEC)) { return; } remember_claim_slot(s_next_id, s_next_unix); } static void arm_wait_lesson_files(const char *app_id, const char *task_id, const char *params) { if (!s_wait_lesson_files) { ESP_LOGW(TAG, "schedule %s/%s files not ready, wait for sync", app_id, task_id); Board_Show_Alert("正在同步", "正在同步课文,请稍候", "cloud_arrow_down"); s_wait_lesson_files_since = wall_now(NULL); } s_wait_lesson_files = true; copy_str(s_pending_app, sizeof(s_pending_app), app_id); copy_str(s_pending_task, sizeof(s_pending_task), task_id); copy_str(s_pending_params, sizeof(s_pending_params), (params && params[0]) ? params : "{}"); Board_Schedule_Recompute(); } void Board_Schedule_OnAlarmFired(void) { if (s_fire_queued || s_deferred_due) { return; } s_fire_queued = true; Application::GetInstance().Schedule([]() { s_fire_queued = false; if (!s_next_valid && !Board_Schedule_IsDueNow(SCHEDULE_DUE_WINDOW_SEC)) { Board_Schedule_Recompute(); return; } if (!Board_Schedule_IsDueNow(SCHEDULE_DUE_WINDOW_SEC)) { Board_Schedule_Recompute(); return; } if (Board_AppTask_CurrentPriority() >= Board_AppTask_SourcePriority("mqtt_immediate")) { ESP_LOGI(TAG, "defer schedule, immediate task running"); s_deferred_due = true; return; } char app_id[SCHEDULE_APP_MAX]; char task_id[SCHEDULE_APP_MAX]; char params[SCHEDULE_PARAMS_MAX]; if (!Board_Schedule_LoadDueTask(app_id, sizeof(app_id), task_id, sizeof(task_id), params, sizeof(params))) { Board_Schedule_Recompute(); return; } if (is_playlist_task(task_id) && !Board_LessonFilesReady(task_id, params)) { arm_wait_lesson_files(app_id, task_id, params); return; } ESP_LOGI(TAG, "alarm start schedule %s/%s", app_id, task_id); s_deferred_due = false; s_wait_lesson_files = false; s_wait_lesson_files_since = 0; Board_StartAppTask(app_id, task_id, params, "schedule"); }); } void Board_Schedule_OnLessonFilesUpdated(void) { if (!s_wait_lesson_files || s_pending_task[0] == '\0' || s_files_poll_queued) { return; } time_t now = wall_now(NULL); if (s_wait_lesson_files_since > 0 && now > s_wait_lesson_files_since + 15 * 60) { ESP_LOGW(TAG, "lesson files still missing after 15 min, skip %s/%s", s_pending_app, s_pending_task); s_wait_lesson_files = false; s_wait_lesson_files_since = 0; s_pending_task[0] = '\0'; Application::GetInstance().Schedule([]() { Board_Schedule_Recompute(); }); return; } if (!Board_LessonFilesReady(s_pending_task, s_pending_params)) { return; } s_files_poll_queued = true; Application::GetInstance().Schedule([]() { s_files_poll_queued = false; if (!s_wait_lesson_files || s_pending_task[0] == '\0') { return; } char app_id[SCHEDULE_APP_MAX]; char task_id[SCHEDULE_APP_MAX]; char params[SCHEDULE_PARAMS_MAX]; copy_str(app_id, sizeof(app_id), s_pending_app); copy_str(task_id, sizeof(task_id), s_pending_task); copy_str(params, sizeof(params), s_pending_params[0] ? s_pending_params : "{}"); if (!Board_LessonFilesReady(task_id, params)) { return; } ESP_LOGI(TAG, "lesson files ready, start schedule %s/%s", app_id, task_id); s_wait_lesson_files = false; s_wait_lesson_files_since = 0; s_pending_task[0] = '\0'; Board_StartAppTask(app_id, task_id, params, "schedule"); }); } void Board_Schedule_OnTaskStarted(const char *app_id, const char *task_id, const char *source, const char *params_json) { if (source && strcmp(source, "schedule") == 0) { remember_claimed_schedule(task_id); } cancel_duration_timer(); cJSON *params = params_json ? cJSON_Parse(params_json) : NULL; s_on_complete = on_complete_from_params(params); int duration = duration_minutes_from_params(params); if (params) { cJSON_Delete(params); } bool chat = is_chat_task(app_id, task_id); bool schedule_or_immediate = source && (strcmp(source, "schedule") == 0 || strcmp(source, "mqtt_immediate") == 0); bool parent_end_to_chat = chat && schedule_or_immediate && duration <= 0; if (chat && (!source || strcmp(source, "voice") == 0 || strcmp(source, "default_chat") == 0 || parent_end_to_chat)) { if (source && (strcmp(source, "voice") == 0 || parent_end_to_chat)) { s_deferred_due = false; } s_occupying = false; s_busy_until = 0; s_on_complete = SCHEDULE_END_DEFAULT; Board_Schedule_Recompute(); return; } s_occupying = schedule_or_immediate; time_t now = wall_now(NULL); if (schedule_or_immediate && !is_playlist_task(task_id) && duration > 0) { s_busy_until = now + (time_t)duration * 60; ensure_duration_timer(); if (s_duration_timer) { esp_timer_stop(s_duration_timer); esp_timer_start_once(s_duration_timer, (uint64_t)duration * 60ULL * 1000000ULL); } } else { s_busy_until = 0; } if (source && strcmp(source, "mqtt_immediate") == 0) { return; } Board_Schedule_Recompute(); } void Board_Schedule_OnNaturalEnd(void) { apply_on_complete(false); } void Board_Schedule_OnUserExit(void) { s_deferred_due = false; apply_on_complete(true); } void Board_Schedule_OnEnteringPowerOff(void) { cancel_duration_timer(); s_occupying = false; s_busy_until = 0; s_duration_fired = false; Board_Schedule_Recompute(); }