#include "parent_schedule.h"
|
|
#include "app_task.h"
|
#include "application.h"
|
#include "system.h"
|
|
#include <cJSON.h>
|
#include <esp_log.h>
|
#include <esp_timer.h>
|
#include <stdio.h>
|
#include <stdlib.h>
|
#include <string.h>
|
#include <strings.h>
|
#include <time.h>
|
|
#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();
|
}
|