#include "system.h" #include "config.h" #include "app_task.h" #include "parent_schedule.h" #include "display/lvgl_display/jpg/jpeg_to_image.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define TAG "system" #define ALARM_STORE_MAGIC 0x314d5241U #define ALARM_STORE_VERSION 2 #define ALARM_ENTRY_ENABLED 1 #define SECONDS_PER_DAY (24 * 60 * 60) #define ALARM_LIST_BUFFER_SIZE 1024 #define MP3_LIST_BUFFER_SIZE 2048 #define JPG_LIST_BUFFER_SIZE 2048 #define JPG_REQUIRED_WIDTH 160 #define JPG_REQUIRED_HEIGHT 160 #define JPG_SETTINGS_NAMESPACE "sdcard_jpg" #define JPG_SETTINGS_LAST_NAME "last_name" #define BATTERY_MIN_MV 3300 #define BATTERY_MAX_MV 4200 #define BATTERY_ADC_SAMPLES 10 #define BATTERY_ADC_DIVIDER_NUMERATOR 35 #define BATTERY_ADC_DIVIDER_DENOMINATOR 17 typedef struct __attribute__((packed)) { char id[ALARM_ID_MAX_LEN]; uint8_t hour; uint8_t min; uint8_t sec; uint8_t flags; uint8_t repeat_mask; uint8_t reserved[2]; } alarm_entry_t; typedef struct __attribute__((packed)) { uint32_t magic; uint8_t version; uint8_t count; uint16_t reserved; uint32_t checksum; alarm_entry_t entries[ALARM_MAX_NUM]; } alarm_store_t; static i2c_dev_t *ds2321_dev; static char alarm_list_buffer[ALARM_LIST_BUFFER_SIZE]; static char mp3_list_buffer[MP3_LIST_BUFFER_SIZE]; static char jpg_list_buffer[JPG_LIST_BUFFER_SIZE]; static adc_oneshot_unit_handle_t battery_adc_handle; static adc_channel_t battery_adc_channel; static adc_unit_t battery_adc_unit; static adc_cali_handle_t battery_adc_cali_handle; static bool battery_adc_initialized; static bool battery_adc_cali_initialized; static bool battery_charging_gpio_initialized; static alarm_entry_t current_alarm1_entry; static bool current_alarm1_valid; static alarm_entry_t schedule_wake_entry; static bool schedule_wake_valid; static TaskHandle_t alarm_monitor_task_handle; static TaskHandle_t alarm_power_off_restore_task_handle; static void sntp_finish_callback(struct timeval *tv); static uint32_t alarm_store_checksum(const alarm_store_t *store); static void alarm_store_init_empty(alarm_store_t *store); static bool alarm_entry_is_valid(const alarm_entry_t *entry); static bool alarm_id_is_safe(const char *id); static int find_alarm_by_id(const alarm_store_t *store, const char *id); static int alarm_entry_seconds(const alarm_entry_t *entry); static int tm_seconds(const struct tm *time); static void alarm_store_sort(alarm_store_t *store); static const esp_partition_t *get_config_partition(void); static esp_err_t load_alarm_store(alarm_store_t *store); static esp_err_t save_alarm_store(alarm_store_t *store); static bool get_current_time(i2c_dev_t *i2c_dev, struct tm *timeinfo); static int select_next_alarm_index(const alarm_store_t *store, const struct tm *now); static int alarm_delta_seconds(const alarm_entry_t *entry, const struct tm *now); static const alarm_entry_t *select_soonest_alarm(const alarm_store_t *store, const struct tm *now); static void program_soonest_alarm(i2c_dev_t *i2c_dev, const struct tm *now); static int mqtt_weekday_to_tm_wday(int day); static uint8_t tm_wday_to_repeat_bit(int tm_wday); static bool alarm_entry_equals(const alarm_entry_t *left, const alarm_entry_t *right); static void alarm_power_off_restore_task(void *arg); static void schedule_power_off_restore_from_alarm(void); static void alarm_monitor_task(void *arg); static void disable_alarm1(i2c_dev_t *i2c_dev); static void program_alarm1(i2c_dev_t *i2c_dev, const alarm_entry_t *entry); static bool media_name_is_safe(const char *name); static bool course_id_is_safe(const char *course_id); static esp_err_t mkdir_recursive(const char *path); static bool mp3_name_has_extension(const char *mp3_name); static bool jpg_name_has_extension(const char *jpg_name); static bool build_media_path(const char *name, char *path, size_t path_size); static bool build_mp3_path(const char *mp3_name, char *path, size_t path_size); static bool build_jpg_path(const char *jpg_name, char *path, size_t path_size); static bool resolve_mp3_path(const char *mp3_name, char *path, size_t path_size); static bool build_matching_jpg_name(const char *mp3_path, char *jpg_name, size_t jpg_name_size); static void append_media_list_dir_json(const char *dir_path, const char *relative_dir, char *buffer, size_t buffer_size, bool (*extension_filter)(const char *), size_t *used, int *index, bool *truncated); static const char *build_media_list_json(char *buffer, size_t buffer_size, bool (*extension_filter)(const char *)); static void convert_rgb565_to_bgr565(uint8_t *image, size_t width, size_t height, size_t stride); static void save_last_jpg_name(const char *jpg_name); static bool load_last_jpg_name(char *jpg_name, size_t jpg_name_size); static esp_err_t initialize_battery_adc(void); static void initialize_battery_charging_gpio(void); static bool initialize_battery_adc_calibration(void); static uint8_t battery_mv_to_percent(uint32_t mv); static size_t append_json_escaped_string(char *buffer, size_t buffer_size, size_t used, const char *value); /* ======================== Public API ======================== */ void Board_Offline_Command_Callback(void) { ESP_LOGI(TAG, "offline command triggered: %s", OFFLINE_COMMAND_WORD); } bool Board_Is_Mode_Switch_Command(const char *command) { return command && (strcmp(command, OFFLINE_COMMAND_WORD) == 0 || strcmp(command, OFFLINE_COMMAND_WORD_PINYIN) == 0 || strcmp(command, OFFLINE_COMMAND_WORD_ALT) == 0 || strcmp(command, OFFLINE_COMMAND_WORD_ALT_PINYIN) == 0); } bool Board_Is_Chat_Return_Command(const char *command) { if (!command || command[0] == '\0') { return false; } return strcmp(command, OFFLINE_CHAT_WORD) == 0 || strcmp(command, OFFLINE_CHAT_WORD_PINYIN) == 0 || strcmp(command, OFFLINE_CHAT_WORD_FULL) == 0 || strcmp(command, OFFLINE_CHAT_WORD_FULL_PINYIN) == 0 || strcmp(command, OFFLINE_EXIT_REPEATER_WORD) == 0 || strcmp(command, OFFLINE_EXIT_REPEATER_WORD_PINYIN) == 0 || strcmp(command, OFFLINE_EXIT_SHADOWING_WORD) == 0 || strcmp(command, OFFLINE_EXIT_SHADOWING_WORD_PINYIN) == 0 || strstr(command, OFFLINE_CHAT_WORD_FULL) || strstr(command, OFFLINE_CHAT_WORD) || strstr(command, OFFLINE_EXIT_REPEATER_WORD) || strstr(command, OFFLINE_EXIT_SHADOWING_WORD); } bool Board_Is_App_Launch_Command(const char *command) { if (!command || command[0] == '\0') { return false; } if (Board_Is_Chat_Return_Command(command)) { return false; } return strcmp(command, OFFLINE_REPEATER_WORD) == 0 || strcmp(command, OFFLINE_START_REPEATER_WORD) == 0 || strstr(command, OFFLINE_START_REPEATER_WORD) || strstr(command, "打开泛听"); } bool Board_Handle_Offline_Command(const char *command) { if (!command || Board_Is_Power_Off()) { return false; } if (Board_Handle_Voice_Command(command)) { return true; } if (!Board_Is_Mode_Switch_Command(command)) { return false; } Board_Offline_Command_Callback(); Board_EnterVoiceEntry(); return true; } void SNTP_To_DS2312(i2c_dev_t *i2c_dev) { ds2321_dev = i2c_dev; if (esp_sntp_enabled()) { esp_sntp_stop(); } esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); esp_sntp_setservername(0, "ntp.aliyun.com"); esp_sntp_setservername(1, "time.asia.apple.com"); esp_sntp_setservername(2, "pool.ntp.org"); esp_sntp_set_time_sync_notification_cb(sntp_finish_callback); esp_sntp_init(); } static const alarm_entry_t *select_soonest_alarm(const alarm_store_t *store, const struct tm *now) { const alarm_entry_t *best = NULL; int best_delta = SECONDS_PER_DAY * 8 + 1; if (store && store->count > 0) { int index = select_next_alarm_index(store, now); if (index >= 0) { best = &store->entries[index]; best_delta = alarm_delta_seconds(best, now); } } if (schedule_wake_valid) { int delta = alarm_delta_seconds(&schedule_wake_entry, now); if (delta < best_delta) { best = &schedule_wake_entry; } } return best; } static void program_soonest_alarm(i2c_dev_t *i2c_dev, const struct tm *now) { alarm_store_t store; if (load_alarm_store(&store) != ESP_OK) { memset(&store, 0, sizeof(store)); } const alarm_entry_t *best = select_soonest_alarm(&store, now); if (!best) { if (current_alarm1_valid && !Board_Is_Power_Off()) { disable_alarm1(i2c_dev); } return; } if (!current_alarm1_valid) { program_alarm1(i2c_dev, best); return; } int next_delta = alarm_delta_seconds(best, now); int current_delta = alarm_delta_seconds(¤t_alarm1_entry, now); if (next_delta < current_delta || !alarm_entry_equals(best, ¤t_alarm1_entry)) { program_alarm1(i2c_dev, best); } } void DS2312_Update_Alarm(i2c_dev_t *i2c_dev, struct tm *time) { struct tm now; if (time) { now = *time; } else if (!get_current_time(i2c_dev, &now)) { ESP_LOGW(TAG, "current time unavailable, skip alarm update"); return; } program_soonest_alarm(i2c_dev, &now); } void DS2312_Start_Alarm_Monitor(i2c_dev_t *i2c_dev) { ds2321_dev = i2c_dev; if (alarm_monitor_task_handle) { return; } BaseType_t ret = xTaskCreate(alarm_monitor_task, "alarm_monitor", 4096, i2c_dev, 5, &alarm_monitor_task_handle); if (ret != pdPASS) { alarm_monitor_task_handle = NULL; ESP_LOGE(TAG, "create alarm monitor task failed"); } } bool Board_Get_Wall_Clock(struct tm *out) { if (!out) { return false; } return get_current_time(ds2321_dev, out); } esp_err_t DS2312_Set_Schedule_Wake(int hour, int min, int sec, uint8_t weekday_bit) { if (hour < 0 || hour > 23 || min < 0 || min > 59 || sec < 0 || sec > 59 || (weekday_bit & ALARM_REPEAT_ALL) == 0) { return ESP_ERR_INVALID_ARG; } memset(&schedule_wake_entry, 0, sizeof(schedule_wake_entry)); strlcpy(schedule_wake_entry.id, "_sched", sizeof(schedule_wake_entry.id)); schedule_wake_entry.hour = (uint8_t)hour; schedule_wake_entry.min = (uint8_t)min; schedule_wake_entry.sec = (uint8_t)sec; schedule_wake_entry.flags = ALARM_ENTRY_ENABLED; schedule_wake_entry.repeat_mask = (uint8_t)(weekday_bit & ALARM_REPEAT_ALL); schedule_wake_valid = true; DS2312_Update_Alarm(ds2321_dev, NULL); ESP_LOGI(TAG, "schedule wake %02d:%02d:%02d mask=0x%02x", hour, min, sec, schedule_wake_entry.repeat_mask); return ESP_OK; } void DS2312_Clear_Schedule_Wake(void) { if (!schedule_wake_valid) { return; } schedule_wake_valid = false; memset(&schedule_wake_entry, 0, sizeof(schedule_wake_entry)); DS2312_Update_Alarm(ds2321_dev, NULL); } void DS2312_Add_Alarm_Time(i2c_dev_t *i2c_dev, int hour, int min, int sec) { char id[ALARM_ID_MAX_LEN]; snprintf(id, sizeof(id), "daily_%02d%02d%02d", hour, min, sec); esp_err_t err = DS2312_Set_Alarm_Id(i2c_dev, id, hour, min, sec, ALARM_REPEAT_ALL); if (err != ESP_OK) { ESP_LOGW(TAG, "add alarm failed: %02d:%02d:%02d, %s", hour, min, sec, esp_err_to_name(err)); } } const char *DS2312_Get_Alarm_List(i2c_dev_t *i2c_dev) { (void)i2c_dev; alarm_store_t store; esp_err_t err = load_alarm_store(&store); if (err != ESP_OK) { snprintf(alarm_list_buffer, sizeof(alarm_list_buffer), "{\"error\":\"failed to load alarm list: %s\"}", esp_err_to_name(err)); return alarm_list_buffer; } size_t used = 0; int written = snprintf(alarm_list_buffer, sizeof(alarm_list_buffer), "{\"count\":%u,\"alarms\":[", store.count); if (written < 0) { alarm_list_buffer[0] = '\0'; return alarm_list_buffer; } used = (size_t)written; for (uint8_t i = 0; i < store.count && used < sizeof(alarm_list_buffer); ++i) { alarm_entry_t *entry = &store.entries[i]; written = snprintf(alarm_list_buffer + used, sizeof(alarm_list_buffer) - used, "%s{\"index\":%u,\"id\":", i == 0 ? "" : ",", i); if (written < 0 || used + (size_t)written >= sizeof(alarm_list_buffer)) { break; } used += (size_t)written; used = append_json_escaped_string(alarm_list_buffer, sizeof(alarm_list_buffer), used, entry->id); written = snprintf(alarm_list_buffer + used, sizeof(alarm_list_buffer) - used, ",\"time\":\"%02u:%02u:%02u\",\"hour\":%u,\"min\":%u,\"sec\":%u,\"repeat\":[", entry->hour, entry->min, entry->sec, entry->hour, entry->min, entry->sec); if (written < 0 || used + (size_t)written >= sizeof(alarm_list_buffer)) { break; } used += (size_t)written; bool first = true; for (int day = 1; day <= 7; ++day) { int tm_wday = mqtt_weekday_to_tm_wday(day); if ((entry->repeat_mask & tm_wday_to_repeat_bit(tm_wday)) == 0) { continue; } written = snprintf(alarm_list_buffer + used, sizeof(alarm_list_buffer) - used, "%s%d", first ? "" : ",", day); if (written < 0 || used + (size_t)written >= sizeof(alarm_list_buffer)) { break; } used += (size_t)written; first = false; } written = snprintf(alarm_list_buffer + used, sizeof(alarm_list_buffer) - used, "]}"); if (written < 0 || used + (size_t)written >= sizeof(alarm_list_buffer)) { break; } used += (size_t)written; } if (used < sizeof(alarm_list_buffer)) { snprintf(alarm_list_buffer + used, sizeof(alarm_list_buffer) - used, "]}"); } else { alarm_list_buffer[sizeof(alarm_list_buffer) - 1] = '\0'; } return alarm_list_buffer; } void DS2312_Delete_Alarm_Index(i2c_dev_t *i2c_dev, uint8_t index) { alarm_store_t store; if (load_alarm_store(&store) != ESP_OK) { return; } if (index >= store.count) { ESP_LOGW(TAG, "alarm index out of range: %u", index); return; } alarm_entry_t deleted = store.entries[index]; for (uint8_t i = index; i + 1 < store.count; ++i) { store.entries[i] = store.entries[i + 1]; } store.count--; memset(&store.entries[store.count], 0, sizeof(store.entries[store.count])); if (save_alarm_store(&store) == ESP_OK) { DS2312_Update_Alarm(i2c_dev, NULL); ESP_LOGI(TAG, "delete alarm: %02d:%02d:%02d", deleted.hour, deleted.min, deleted.sec); } } void DS2312_Delete_Alarm_All(i2c_dev_t *i2c_dev) { alarm_store_t store; alarm_store_init_empty(&store); if (save_alarm_store(&store) == ESP_OK) { disable_alarm1(i2c_dev); ESP_LOGI(TAG, "delete all alarms"); } } esp_err_t DS2312_Set_Unix_Time(i2c_dev_t *i2c_dev, time_t timestamp) { if (!i2c_dev || timestamp <= 0) { return ESP_ERR_INVALID_ARG; } struct timeval tv = { .tv_sec = timestamp, .tv_usec = 0, }; esp_err_t err = settimeofday(&tv, NULL) == 0 ? ESP_OK : ESP_FAIL; if (err != ESP_OK) { ESP_LOGE(TAG, "settimeofday failed"); return err; } setenv("TZ", "CST-8", 1); tzset(); struct tm timeinfo = {0}; localtime_r(×tamp, &timeinfo); err = ds3231_set_time(i2c_dev, &timeinfo); if (err != ESP_OK) { ESP_LOGE(TAG, "set DS3231 time failed: %s", esp_err_to_name(err)); return err; } DS2312_Update_Alarm(i2c_dev, &timeinfo); ESP_LOGI(TAG, "set clock from MQTT: %04d-%02d-%02d %02d:%02d:%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); return ESP_OK; } esp_err_t DS2312_Set_Unix_Time_Saved(time_t timestamp) { return DS2312_Set_Unix_Time(ds2321_dev, timestamp); } esp_err_t DS2312_Set_Alarm_Id(i2c_dev_t *i2c_dev, const char *id, int hour, int min, int sec, uint8_t repeat_mask) { if (!alarm_id_is_safe(id) || hour < 0 || hour > 23 || min < 0 || min > 59 || sec < 0 || sec > 59 || (repeat_mask & ALARM_REPEAT_ALL) == 0) { return ESP_ERR_INVALID_ARG; } repeat_mask &= ALARM_REPEAT_ALL; alarm_store_t store; esp_err_t err = load_alarm_store(&store); if (err != ESP_OK) { return err; } int index = find_alarm_by_id(&store, id); if (index < 0) { if (store.count >= ALARM_MAX_NUM) { return ESP_ERR_NO_MEM; } index = store.count++; } alarm_entry_t *entry = &store.entries[index]; memset(entry, 0, sizeof(*entry)); strlcpy(entry->id, id, sizeof(entry->id)); entry->hour = (uint8_t)hour; entry->min = (uint8_t)min; entry->sec = (uint8_t)sec; entry->flags = ALARM_ENTRY_ENABLED; entry->repeat_mask = repeat_mask; err = save_alarm_store(&store); if (err == ESP_OK) { DS2312_Update_Alarm(i2c_dev, NULL); ESP_LOGI(TAG, "set alarm id=%s time=%02d:%02d:%02d repeat=0x%02x", id, hour, min, sec, repeat_mask); } return err; } esp_err_t DS2312_Delete_Alarm_Id(i2c_dev_t *i2c_dev, const char *id) { if (!alarm_id_is_safe(id)) { return ESP_ERR_INVALID_ARG; } alarm_store_t store; esp_err_t err = load_alarm_store(&store); if (err != ESP_OK) { return err; } int index = find_alarm_by_id(&store, id); if (index < 0) { return ESP_ERR_NOT_FOUND; } for (uint8_t i = (uint8_t)index; i + 1 < store.count; ++i) { store.entries[i] = store.entries[i + 1]; } store.count--; memset(&store.entries[store.count], 0, sizeof(store.entries[store.count])); err = save_alarm_store(&store); if (err == ESP_OK) { DS2312_Update_Alarm(i2c_dev, NULL); ESP_LOGI(TAG, "delete alarm id=%s", id); } return err; } esp_err_t DS2312_Set_Alarm_Id_Saved(const char *id, int hour, int min, int sec, uint8_t repeat_mask) { return DS2312_Set_Alarm_Id(ds2321_dev, id, hour, min, sec, repeat_mask); } esp_err_t DS2312_Delete_Alarm_Id_Saved(const char *id) { return DS2312_Delete_Alarm_Id(ds2321_dev, id); } /* ======================== Battery API ======================== */ uint8_t Battery_Get_Value(void) { if (initialize_battery_adc() != ESP_OK) { return 0; } int raw = 0; uint32_t raw_sum = 0; uint32_t adc_mv_sum = 0; uint32_t valid_samples = 0; bool use_calibration = initialize_battery_adc_calibration(); for (int i = 0; i < BATTERY_ADC_SAMPLES; ++i) { if (adc_oneshot_read(battery_adc_handle, battery_adc_channel, &raw) == ESP_OK) { raw_sum += raw; if (use_calibration) { int calibrated_mv = 0; if (adc_cali_raw_to_voltage(battery_adc_cali_handle, raw, &calibrated_mv) == ESP_OK) { adc_mv_sum += calibrated_mv; valid_samples++; } } else { adc_mv_sum += raw * 3300 / 4095; valid_samples++; } } } if (valid_samples == 0) { return 0; } uint32_t raw_avg = raw_sum / valid_samples; uint32_t adc_mv = adc_mv_sum / valid_samples; uint32_t voltage_mv = adc_mv * BATTERY_ADC_DIVIDER_NUMERATOR / BATTERY_ADC_DIVIDER_DENOMINATOR; ESP_LOGI(TAG, "battery ADC raw=%lu adc=%lumV battery=%lumV", raw_avg, adc_mv, voltage_mv); return battery_mv_to_percent(voltage_mv); } bool Battery_Is_Charging(void) { initialize_battery_charging_gpio(); return gpio_get_level(ESP_BAT_CHRG) == 1; } uint8_t Board_Get_CPU_Usage(void) { #if configGENERATE_RUN_TIME_STATS static uint32_t last_total_runtime; static uint32_t last_idle_runtime; UBaseType_t task_count = uxTaskGetNumberOfTasks(); TaskStatus_t *tasks = calloc(task_count, sizeof(TaskStatus_t)); if (!tasks) { return 0; } uint32_t total_runtime = 0; task_count = uxTaskGetSystemState(tasks, task_count, &total_runtime); uint32_t idle_runtime = 0; for (UBaseType_t i = 0; i < task_count; ++i) { if (strncmp(tasks[i].pcTaskName, "IDLE", 4) == 0) { idle_runtime += tasks[i].ulRunTimeCounter; } } free(tasks); uint32_t total_delta = total_runtime - last_total_runtime; uint32_t idle_delta = idle_runtime - last_idle_runtime; last_total_runtime = total_runtime; last_idle_runtime = idle_runtime; if (total_delta == 0) { return 0; } uint32_t capacity_delta = total_delta * CONFIG_FREERTOS_NUMBER_OF_CORES; if (idle_delta >= capacity_delta) { return 0; } uint32_t usage = ((capacity_delta - idle_delta) * 100) / capacity_delta; return usage > 100 ? 100 : (uint8_t)usage; #else return 0; #endif } /* ======================= SD Card MP3 API ====================== */ const char *SDCard_Get_MP3_List(void) { return build_media_list_json(mp3_list_buffer, sizeof(mp3_list_buffer), mp3_name_has_extension); } float SDCard_Clamp_MP3_Speed(float speed) { if (speed != speed) { return SDCARD_MP3_SPEED_DEFAULT; } if (speed < SDCARD_MP3_SPEED_MIN) { return SDCARD_MP3_SPEED_MIN; } if (speed > SDCARD_MP3_SPEED_MAX) { return SDCARD_MP3_SPEED_MAX; } return speed; } bool SDCard_Resolve_MP3_Path(const char *mp3_name, char *path, size_t path_size) { return resolve_mp3_path(mp3_name, path, path_size); } esp_err_t SDCard_Play_MP3(char *mp3_name) { return SDCard_Play_MP3_With_Speed(mp3_name, 1.0f); } esp_err_t SDCard_Play_MP3_With_Speed(char *mp3_name, float speed) { return SDCard_Play_MP3_With_Speed_Repeat(mp3_name, speed, 1, false); } esp_err_t SDCard_Play_MP3_With_Speed_Repeat(char *mp3_name, float speed, int repeat, bool power_off_after) { speed = SDCard_Clamp_MP3_Speed(speed); char path[SDCARD_PATH_MAX_LEN]; if (!resolve_mp3_path(mp3_name, path, sizeof(path))) { ESP_LOGW(TAG, "MP3 not found or invalid: %s", mp3_name ? mp3_name : "null"); SDCard_Resume_Play(); return ESP_ERR_NOT_FOUND; } esp_err_t err = SDCard_Play_MP3_File_With_Speed_Repeat(path, speed, repeat, power_off_after); if (err != ESP_OK) { SDCard_Resume_Play(); ESP_LOGE(TAG, "start MP3 playback failed: %s", esp_err_to_name(err)); return err; } SDCard_Show_Matching_JPG_For_MP3_Path(path); return ESP_OK; } void SDCard_Show_Matching_JPG_For_MP3_Path(const char *mp3_path) { char jpg_name[SDCARD_PATH_MAX_LEN]; char jpg_path[SDCARD_PATH_MAX_LEN]; if (build_matching_jpg_name(mp3_path, jpg_name, sizeof(jpg_name)) && build_jpg_path(jpg_name, jpg_path, sizeof(jpg_path)) && access(jpg_path, F_OK) == 0) { Board_Show_JPG(jpg_name); } } esp_err_t SDCard_Map_Business_Path(const char *business_path, char *sd_path, size_t sd_path_size) { if (!business_path || !sd_path || sd_path_size == 0 || business_path[0] != '/') { return ESP_ERR_INVALID_ARG; } if (strstr(business_path, "..") || strchr(business_path, '\\')) { return ESP_ERR_INVALID_ARG; } if (strcmp(business_path, "/") == 0) { return ESP_ERR_INVALID_ARG; } int written = snprintf(sd_path, sd_path_size, "%s%s", MOUNT_POINT, business_path); if (written <= 0 || (size_t)written >= sd_path_size) { return ESP_ERR_INVALID_SIZE; } return ESP_OK; } esp_err_t SDCard_Ensure_Business_Dir(const char *business_path) { char sd_path[SDCARD_PATH_MAX_LEN]; esp_err_t err = SDCard_Map_Business_Path(business_path, sd_path, sizeof(sd_path)); if (err != ESP_OK) { return err; } err = mkdir_recursive(sd_path); if (err != ESP_OK) { ESP_LOGE(TAG, "ensure dir failed: %s", sd_path); return err; } ESP_LOGI(TAG, "ensure dir: %s", sd_path); return ESP_OK; } esp_err_t SDCard_Prepare_Business_File(const char *business_path, char *final_path, size_t final_path_size, char *part_path, size_t part_path_size) { esp_err_t err = SDCard_Map_Business_Path(business_path, final_path, final_path_size); if (err != ESP_OK) { return err; } char parent[SDCARD_PATH_MAX_LEN]; if (strlen(final_path) >= sizeof(parent)) { return ESP_ERR_INVALID_SIZE; } strcpy(parent, final_path); char *slash = strrchr(parent, '/'); if (!slash || slash == parent) { return ESP_ERR_INVALID_ARG; } *slash = '\0'; err = mkdir_recursive(parent); if (err != ESP_OK) { return err; } int written = snprintf(part_path, part_path_size, "%s.part", final_path); if (written <= 0 || (size_t)written >= part_path_size) { return ESP_ERR_INVALID_SIZE; } SDCard_Remove_File_If_Exists(part_path); return ESP_OK; } esp_err_t SDCard_Commit_Part_File(const char *part_path, const char *final_path) { if (!part_path || !final_path) { return ESP_ERR_INVALID_ARG; } unlink(final_path); if (rename(part_path, final_path) != 0) { ESP_LOGE(TAG, "commit part file failed: %s -> %s", part_path, final_path); return ESP_FAIL; } return ESP_OK; } void SDCard_Remove_File_If_Exists(const char *path) { if (path && path[0] != '\0') { unlink(path); } } esp_err_t SDCard_Delete_File_By_Business_Path(const char *business_path) { char path[SDCARD_PATH_MAX_LEN]; esp_err_t err = SDCard_Map_Business_Path(business_path, path, sizeof(path)); if (err != ESP_OK) { return err; } struct stat st; if (stat(path, &st) != 0) { return errno == ENOENT ? ESP_ERR_NOT_FOUND : ESP_FAIL; } if (!S_ISREG(st.st_mode)) { return ESP_ERR_INVALID_ARG; } if (unlink(path) != 0) { ESP_LOGE(TAG, "delete file failed: %s", path); return ESP_FAIL; } ESP_LOGI(TAG, "delete file: %s", path); return ESP_OK; } esp_err_t SDCard_Save_Manifest(const char *course_id, const char *manifest_json) { if (!course_id_is_safe(course_id) || !manifest_json) { return ESP_ERR_INVALID_ARG; } char manifest_dir[SDCARD_PATH_MAX_LEN]; int written = snprintf(manifest_dir, sizeof(manifest_dir), "%s/manifest", MOUNT_POINT); if (written <= 0 || (size_t)written >= sizeof(manifest_dir)) { return ESP_ERR_INVALID_SIZE; } esp_err_t err = mkdir_recursive(manifest_dir); if (err != ESP_OK) { return err; } char path[SDCARD_PATH_MAX_LEN]; written = snprintf(path, sizeof(path), "%s/%s.json", manifest_dir, course_id); if (written <= 0 || (size_t)written >= sizeof(path)) { return ESP_ERR_INVALID_SIZE; } FILE *fp = fopen(path, "w"); if (!fp) { ESP_LOGE(TAG, "open manifest for write failed: %s", path); return ESP_FAIL; } size_t len = strlen(manifest_json); bool ok = fwrite(manifest_json, 1, len, fp) == len; fclose(fp); if (!ok) { ESP_LOGE(TAG, "write manifest failed: %s", path); return ESP_FAIL; } ESP_LOGI(TAG, "save manifest: %s", path); return ESP_OK; } void SDCard_Delete_MP3(char *mp3_name) { char path[SDCARD_PATH_MAX_LEN]; if (!build_mp3_path(mp3_name, path, sizeof(path))) { ESP_LOGW(TAG, "invalid MP3 name: %s", mp3_name ? mp3_name : "null"); return; } if (unlink(path) != 0) { ESP_LOGE(TAG, "delete MP3 failed: %s", path); return; } ESP_LOGI(TAG, "delete MP3: %s", path); } /* ======================= SD Card JPG API ====================== */ const char *SDCard_Get_JPG_List(void) { return build_media_list_json(jpg_list_buffer, sizeof(jpg_list_buffer), jpg_name_has_extension); } void SDCard_Play_JPG(esp_lcd_panel_handle_t panel, char *jpg_name) { if (!panel) { ESP_LOGW(TAG, "LCD panel is not ready"); return; } char path[SDCARD_PATH_MAX_LEN]; if (!build_jpg_path(jpg_name, path, sizeof(path))) { ESP_LOGW(TAG, "invalid JPG name: %s", jpg_name ? jpg_name : "null"); return; } FILE *fp = fopen(path, "rb"); if (!fp) { ESP_LOGW(TAG, "JPG not found: %s", path); return; } if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); ESP_LOGE(TAG, "seek JPG failed: %s", path); return; } long file_size = ftell(fp); if (file_size <= 0 || fseek(fp, 0, SEEK_SET) != 0) { fclose(fp); ESP_LOGE(TAG, "invalid JPG size: %s", path); return; } uint8_t *jpg_data = (uint8_t *)malloc((size_t)file_size); if (!jpg_data) { fclose(fp); ESP_LOGE(TAG, "no memory for JPG: %s", path); return; } size_t read_size = fread(jpg_data, 1, (size_t)file_size, fp); fclose(fp); if (read_size != (size_t)file_size) { free(jpg_data); ESP_LOGE(TAG, "read JPG failed: %s", path); return; } uint8_t *image = NULL; size_t image_len = 0; size_t width = 0; size_t height = 0; size_t stride = 0; esp_err_t err = jpeg_to_image(jpg_data, (size_t)file_size, &image, &image_len, &width, &height, &stride); free(jpg_data); if (err != ESP_OK || !image) { ESP_LOGE(TAG, "decode JPG failed: %s, %s", path, esp_err_to_name(err)); return; } if (width != JPG_REQUIRED_WIDTH || height != JPG_REQUIRED_HEIGHT) { heap_caps_free(image); ESP_LOGW(TAG, "JPG size unsupported: %s, %ux%u", path, (unsigned)width, (unsigned)height); return; } convert_rgb565_to_bgr565(image, width, height, stride); for (size_t y = 0; y < height; ++y) { esp_err_t draw_err = esp_lcd_panel_draw_bitmap(panel, 0, y, width, y + 1, image + y * stride); if (draw_err != ESP_OK) { heap_caps_free(image); ESP_LOGE(TAG, "draw JPG failed: %s, line=%u, %s", path, (unsigned)y, esp_err_to_name(draw_err)); return; } } heap_caps_free(image); save_last_jpg_name(jpg_name); ESP_LOGI(TAG, "show JPG: %s", path); } void SDCard_Restore_Last_JPG(esp_lcd_panel_handle_t panel) { char jpg_name[SDCARD_PATH_MAX_LEN]; if (!load_last_jpg_name(jpg_name, sizeof(jpg_name))) { return; } char path[SDCARD_PATH_MAX_LEN]; if (build_jpg_path(jpg_name, path, sizeof(path)) && access(path, F_OK) == 0) { SDCard_Play_JPG(panel, jpg_name); } else { ESP_LOGW(TAG, "last JPG no longer exists: %s", jpg_name); } } void SDCard_Delete_JPG(char *jpg_name) { char path[SDCARD_PATH_MAX_LEN]; if (!build_jpg_path(jpg_name, path, sizeof(path))) { ESP_LOGW(TAG, "invalid JPG name: %s", jpg_name ? jpg_name : "null"); return; } if (unlink(path) != 0) { ESP_LOGE(TAG, "delete JPG failed: %s", path); return; } ESP_LOGI(TAG, "delete JPG: %s", path); } /* ===================== Internal helpers ===================== */ static bool course_id_is_safe(const char *course_id) { if (!course_id || course_id[0] == '\0' || strlen(course_id) > 64) { return false; } for (const char *p = course_id; *p; ++p) { if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_' || *p == '-')) { return false; } } return true; } static esp_err_t mkdir_recursive(const char *path) { if (!path || path[0] == '\0') { return ESP_ERR_INVALID_ARG; } char tmp[SDCARD_PATH_MAX_LEN]; if (strlen(path) >= sizeof(tmp)) { return ESP_ERR_INVALID_SIZE; } strcpy(tmp, path); for (char *p = tmp + 1; *p; ++p) { if (*p != '/') { continue; } *p = '\0'; if (mkdir(tmp, 0775) != 0 && errno != EEXIST) { return ESP_FAIL; } *p = '/'; } if (mkdir(tmp, 0775) != 0 && errno != EEXIST) { return ESP_FAIL; } return ESP_OK; } static bool media_name_is_safe(const char *name) { if (!name || name[0] == '\0' || strlen(name) >= SDCARD_PATH_MAX_LEN - strlen(MOUNT_POINT) - 2) { return false; } if (strchr(name, '\\') || strstr(name, "..") || strcmp(name, ".") == 0) { return false; } if (name[0] == '/') { return name[1] != '\0'; } return strchr(name, '/') == NULL && strcmp(name, "..") != 0; } static bool mp3_name_has_extension(const char *mp3_name) { if (!mp3_name) { return false; } const char *dot = strrchr(mp3_name, '.'); return dot && strcasecmp(dot, ".mp3") == 0; } static bool jpg_name_has_extension(const char *jpg_name) { if (!jpg_name) { return false; } const char *dot = strrchr(jpg_name, '.'); return dot && strcasecmp(dot, ".jpg") == 0; } static bool build_media_path(const char *name, char *path, size_t path_size) { if (!media_name_is_safe(name)) { return false; } const char *relative_name = name[0] == '/' ? name + 1 : name; int written = snprintf(path, path_size, "%s/%s", MOUNT_POINT, relative_name); return written > 0 && (size_t)written < path_size; } static bool build_mp3_path(const char *mp3_name, char *path, size_t path_size) { return mp3_name_has_extension(mp3_name) && build_media_path(mp3_name, path, path_size); } static bool build_jpg_path(const char *jpg_name, char *path, size_t path_size) { return jpg_name_has_extension(jpg_name) && build_media_path(jpg_name, path, path_size); } static bool build_matching_jpg_name(const char *mp3_path, char *jpg_name, size_t jpg_name_size) { if (!mp3_path || !jpg_name || jpg_name_size == 0) { return false; } size_t mount_len = strlen(MOUNT_POINT); if (strncmp(mp3_path, MOUNT_POINT, mount_len) != 0 || mp3_path[mount_len] != '/') { return false; } const char *relative_path = mp3_path + mount_len; size_t len = strlen(relative_path); if (len < 4 || strcasecmp(relative_path + len - 4, ".mp3") != 0 || len >= jpg_name_size) { return false; } memcpy(jpg_name, relative_path, len + 1); memcpy(jpg_name + len - 4, ".jpg", 5); return true; } static void append_media_list_dir_json(const char *dir_path, const char *relative_dir, char *buffer, size_t buffer_size, bool (*extension_filter)(const char *), size_t *used, int *index, bool *truncated) { DIR *dir = opendir(dir_path); if (!dir) { return; } struct dirent *entry; while (!*truncated && (entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } char entry_path[SDCARD_PATH_MAX_LEN]; int written = snprintf(entry_path, sizeof(entry_path), "%s/%s", dir_path, entry->d_name); if (written <= 0 || (size_t)written >= sizeof(entry_path)) { *truncated = true; break; } struct stat st; if (stat(entry_path, &st) != 0) { continue; } if (S_ISDIR(st.st_mode)) { char child_relative_dir[SDCARD_PATH_MAX_LEN]; written = relative_dir[0] == '\0' ? snprintf(child_relative_dir, sizeof(child_relative_dir), "%s", entry->d_name) : snprintf(child_relative_dir, sizeof(child_relative_dir), "%s/%s", relative_dir, entry->d_name); if (written <= 0 || (size_t)written >= sizeof(child_relative_dir)) { *truncated = true; break; } append_media_list_dir_json(entry_path, child_relative_dir, buffer, buffer_size, extension_filter, used, index, truncated); continue; } if (!S_ISREG(st.st_mode) || !extension_filter(entry->d_name)) { continue; } char display_name[SDCARD_PATH_MAX_LEN]; written = relative_dir[0] == '\0' ? snprintf(display_name, sizeof(display_name), "%s", entry->d_name) : snprintf(display_name, sizeof(display_name), "/%s/%s", relative_dir, entry->d_name); if (written <= 0 || (size_t)written >= sizeof(display_name)) { *truncated = true; break; } written = snprintf(buffer + *used, buffer_size - *used, "%s{\"index\":%d,\"name\":", *index == 0 ? "" : ",", *index); if (written < 0 || *used + (size_t)written >= buffer_size) { *truncated = true; break; } *used += (size_t)written; *used = append_json_escaped_string(buffer, buffer_size, *used, display_name); if (*used >= buffer_size) { *truncated = true; break; } written = snprintf(buffer + *used, buffer_size - *used, "}"); if (written < 0 || *used + (size_t)written >= buffer_size) { *truncated = true; break; } *used += (size_t)written; (*index)++; } closedir(dir); } static const char *build_media_list_json(char *buffer, size_t buffer_size, bool (*extension_filter)(const char *)) { DIR *dir = opendir(MOUNT_POINT); if (!dir) { snprintf(buffer, buffer_size, "{\"files\":[],\"count\":0,\"truncated\":false,\"error\":\"failed to open %s\"}", MOUNT_POINT); return buffer; } closedir(dir); size_t used = 0; int index = 0; bool truncated = false; int written = snprintf(buffer, buffer_size, "{\"files\":["); if (written < 0 || (size_t)written >= buffer_size) { snprintf(buffer, buffer_size, "{\"files\":[],\"count\":0,\"truncated\":true}"); return buffer; } used = (size_t)written; append_media_list_dir_json(MOUNT_POINT, "", buffer, buffer_size, extension_filter, &used, &index, &truncated); if (used < buffer_size) { snprintf(buffer + used, buffer_size - used, "],\"count\":%d,\"truncated\":%s}", index, truncated ? "true" : "false"); } else { snprintf(buffer, buffer_size, "{\"files\":[],\"count\":0,\"truncated\":true}"); } return buffer; } static bool resolve_mp3_path(const char *mp3_name, char *path, size_t path_size) { if (build_mp3_path(mp3_name, path, path_size) && access(path, F_OK) == 0) { return true; } if (!media_name_is_safe(mp3_name) || mp3_name_has_extension(mp3_name)) { return false; } DIR *dir = opendir(MOUNT_POINT); if (!dir) { return false; } bool found = false; struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (!mp3_name_has_extension(entry->d_name) || !strstr(entry->d_name, mp3_name)) { continue; } int written = snprintf(path, path_size, "%s/%s", MOUNT_POINT, entry->d_name); found = written > 0 && (size_t)written < path_size; break; } closedir(dir); return found; } static void convert_rgb565_to_bgr565(uint8_t *image, size_t width, size_t height, size_t stride) { for (size_t y = 0; y < height; ++y) { uint16_t *line = (uint16_t *)(image + y * stride); for (size_t x = 0; x < width; ++x) { uint16_t pixel = line[x]; uint16_t r = (pixel >> 11) & 0x1F; uint16_t g = pixel & 0x07E0; uint16_t b = pixel & 0x1F; line[x] = (b << 11) | g | r; } } } static void save_last_jpg_name(const char *jpg_name) { nvs_handle_t handle; if (nvs_open(JPG_SETTINGS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) { return; } nvs_set_str(handle, JPG_SETTINGS_LAST_NAME, jpg_name); nvs_commit(handle); nvs_close(handle); } static bool load_last_jpg_name(char *jpg_name, size_t jpg_name_size) { nvs_handle_t handle; if (nvs_open(JPG_SETTINGS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK) { return false; } size_t length = jpg_name_size; esp_err_t err = nvs_get_str(handle, JPG_SETTINGS_LAST_NAME, jpg_name, &length); nvs_close(handle); return err == ESP_OK && jpg_name[0] != '\0'; } static esp_err_t initialize_battery_adc(void) { if (battery_adc_initialized) { return ESP_OK; } esp_err_t err = adc_oneshot_io_to_channel(ESP_BAT_ADC, &battery_adc_unit, &battery_adc_channel); if (err != ESP_OK) { ESP_LOGE(TAG, "battery ADC pin mapping failed: %s", esp_err_to_name(err)); return err; } adc_oneshot_unit_init_cfg_t init_config = { .unit_id = battery_adc_unit, .ulp_mode = ADC_ULP_MODE_DISABLE, }; err = adc_oneshot_new_unit(&init_config, &battery_adc_handle); if (err != ESP_OK) { ESP_LOGE(TAG, "battery ADC unit init failed: %s", esp_err_to_name(err)); return err; } adc_oneshot_chan_cfg_t channel_config = { .atten = ADC_ATTEN_DB_12, .bitwidth = ADC_BITWIDTH_12, }; err = adc_oneshot_config_channel(battery_adc_handle, battery_adc_channel, &channel_config); if (err != ESP_OK) { ESP_LOGE(TAG, "battery ADC channel config failed: %s", esp_err_to_name(err)); return err; } battery_adc_initialized = true; initialize_battery_adc_calibration(); return ESP_OK; } static bool initialize_battery_adc_calibration(void) { if (battery_adc_cali_initialized) { return battery_adc_cali_handle != NULL; } #if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED adc_cali_curve_fitting_config_t cali_config = { .unit_id = battery_adc_unit, .chan = battery_adc_channel, .atten = ADC_ATTEN_DB_12, .bitwidth = ADC_BITWIDTH_12, }; esp_err_t err = adc_cali_create_scheme_curve_fitting(&cali_config, &battery_adc_cali_handle); #elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED adc_cali_line_fitting_config_t cali_config = { .unit_id = battery_adc_unit, .atten = ADC_ATTEN_DB_12, .bitwidth = ADC_BITWIDTH_12, }; esp_err_t err = adc_cali_create_scheme_line_fitting(&cali_config, &battery_adc_cali_handle); #else esp_err_t err = ESP_ERR_NOT_SUPPORTED; #endif battery_adc_cali_initialized = true; if (err != ESP_OK) { ESP_LOGW(TAG, "battery ADC calibration unavailable: %s", esp_err_to_name(err)); battery_adc_cali_handle = NULL; return false; } return true; } static void initialize_battery_charging_gpio(void) { if (battery_charging_gpio_initialized) { return; } gpio_config_t gpio_cfg = { .pin_bit_mask = 1ULL << ESP_BAT_CHRG, .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; ESP_ERROR_CHECK_WITHOUT_ABORT(gpio_config(&gpio_cfg)); battery_charging_gpio_initialized = true; } static uint8_t battery_mv_to_percent(uint32_t mv) { if (mv <= BATTERY_MIN_MV) { return 0; } if (mv >= BATTERY_MAX_MV) { return 100; } return (uint8_t)((mv - BATTERY_MIN_MV) * 100 / (BATTERY_MAX_MV - BATTERY_MIN_MV)); } static size_t append_json_escaped_string(char *buffer, size_t buffer_size, size_t used, const char *value) { if (used >= buffer_size) { return used; } buffer[used++] = '"'; for (const char *p = value; *p && used + 2 < buffer_size; ++p) { if (*p == '"' || *p == '\\') { buffer[used++] = '\\'; } buffer[used++] = *p; } if (used < buffer_size) { buffer[used++] = '"'; } if (used < buffer_size) { buffer[used] = '\0'; } else { buffer[buffer_size - 1] = '\0'; } return used; } static void sntp_finish_callback(struct timeval *tv) { setenv("TZ", "CST-8", 1); tzset(); time_t now = 0; struct tm timeinfo = {0}; time(&now); localtime_r(&now, &timeinfo); ESP_LOGI(TAG, "本地时间: %d-%02d-%02d %02d:%02d:%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); if (!ds2321_dev) { ESP_LOGW(TAG, "DS3231 device is not ready"); return; } struct tm rtc_time = timeinfo; ESP_ERROR_CHECK_WITHOUT_ABORT(ds3231_set_time(ds2321_dev, &rtc_time)); ESP_ERROR_CHECK_WITHOUT_ABORT(ds3231_get_time(ds2321_dev, &rtc_time)); ESP_LOGI(TAG, "DS3231 当前时间: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.tm_year + 1900, rtc_time.tm_mon + 1, rtc_time.tm_mday, rtc_time.tm_hour, rtc_time.tm_min, rtc_time.tm_sec); DS2312_Update_Alarm(ds2321_dev, &rtc_time); ESP_LOGI(TAG, "SNTP 同步完成"); } static uint32_t alarm_store_checksum(const alarm_store_t *store) { alarm_store_t copy = *store; copy.checksum = 0; uint32_t hash = 2166136261U; const uint8_t *data = (const uint8_t *)© for (size_t i = 0; i < sizeof(copy); ++i) { hash ^= data[i]; hash *= 16777619U; } return hash; } static void alarm_store_init_empty(alarm_store_t *store) { memset(store, 0, sizeof(*store)); store->magic = ALARM_STORE_MAGIC; store->version = ALARM_STORE_VERSION; store->checksum = alarm_store_checksum(store); } static bool alarm_entry_is_valid(const alarm_entry_t *entry) { return alarm_id_is_safe(entry->id) && entry->hour <= 23 && entry->min <= 59 && entry->sec <= 59 && (entry->repeat_mask & ALARM_REPEAT_ALL) != 0; } static bool alarm_id_is_safe(const char *id) { if (!id || id[0] == '\0' || strlen(id) >= ALARM_ID_MAX_LEN) { return false; } for (const char *p = id; *p; ++p) { if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_' || *p == '-')) { return false; } } return true; } static int find_alarm_by_id(const alarm_store_t *store, const char *id) { for (uint8_t i = 0; i < store->count; ++i) { if (strcmp(store->entries[i].id, id) == 0) { return i; } } return -1; } static int alarm_entry_seconds(const alarm_entry_t *entry) { return entry->hour * 3600 + entry->min * 60 + entry->sec; } static int tm_seconds(const struct tm *time) { return time->tm_hour * 3600 + time->tm_min * 60 + time->tm_sec; } static void alarm_store_sort(alarm_store_t *store) { for (uint8_t i = 0; i < store->count; ++i) { for (uint8_t j = i + 1; j < store->count; ++j) { if (alarm_entry_seconds(&store->entries[j]) < alarm_entry_seconds(&store->entries[i])) { alarm_entry_t tmp = store->entries[i]; store->entries[i] = store->entries[j]; store->entries[j] = tmp; } } } } static const esp_partition_t *get_config_partition(void) { const esp_partition_t *partition = esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "config"); if (!partition) { ESP_LOGE(TAG, "config partition not found"); } return partition; } static esp_err_t load_alarm_store(alarm_store_t *store) { const esp_partition_t *partition = get_config_partition(); if (!partition) { alarm_store_init_empty(store); return ESP_ERR_NOT_FOUND; } esp_err_t err = esp_partition_read(partition, 0, store, sizeof(*store)); if (err != ESP_OK) { ESP_LOGE(TAG, "read alarm store failed: %s", esp_err_to_name(err)); alarm_store_init_empty(store); return err; } if (store->magic != ALARM_STORE_MAGIC || store->version != ALARM_STORE_VERSION || store->count > ALARM_MAX_NUM || store->checksum != alarm_store_checksum(store)) { alarm_store_init_empty(store); return ESP_OK; } for (uint8_t i = 0; i < store->count; ++i) { if (!alarm_entry_is_valid(&store->entries[i])) { ESP_LOGW(TAG, "invalid alarm entry in flash, reset alarm store"); alarm_store_init_empty(store); return ESP_OK; } } alarm_store_sort(store); return ESP_OK; } static esp_err_t save_alarm_store(alarm_store_t *store) { const esp_partition_t *partition = get_config_partition(); if (!partition) { return ESP_ERR_NOT_FOUND; } store->magic = ALARM_STORE_MAGIC; store->version = ALARM_STORE_VERSION; alarm_store_sort(store); store->checksum = alarm_store_checksum(store); esp_err_t err = esp_partition_erase_range(partition, 0, partition->size); if (err != ESP_OK) { ESP_LOGE(TAG, "erase alarm store failed: %s", esp_err_to_name(err)); return err; } err = esp_partition_write(partition, 0, store, sizeof(*store)); if (err != ESP_OK) { ESP_LOGE(TAG, "write alarm store failed: %s", esp_err_to_name(err)); } return err; } static bool get_current_time(i2c_dev_t *i2c_dev, struct tm *timeinfo) { memset(timeinfo, 0, sizeof(*timeinfo)); if (i2c_dev && ds3231_get_time(i2c_dev, timeinfo) == ESP_OK) { return true; } time_t now = 0; time(&now); localtime_r(&now, timeinfo); return now > 0; } static int select_next_alarm_index(const alarm_store_t *store, const struct tm *now) { if (store->count == 0) { return -1; } int best_index = -1; int best_delta = SECONDS_PER_DAY * 8 + 1; for (uint8_t i = 0; i < store->count; ++i) { int delta = alarm_delta_seconds(&store->entries[i], now); if (delta < best_delta) { best_delta = delta; best_index = i; } } return best_index; } static int alarm_delta_seconds(const alarm_entry_t *entry, const struct tm *now) { int now_sec = tm_seconds(now); int alarm_sec = alarm_entry_seconds(entry); int best_delta = SECONDS_PER_DAY * 8; for (int day_offset = 0; day_offset <= 7; ++day_offset) { int tm_wday = (now->tm_wday + day_offset) % 7; if ((entry->repeat_mask & tm_wday_to_repeat_bit(tm_wday)) == 0) { continue; } int delta = day_offset * SECONDS_PER_DAY + alarm_sec - now_sec; if (delta <= 0) { continue; } if (delta < best_delta) { best_delta = delta; } } return best_delta; } static int mqtt_weekday_to_tm_wday(int day) { return day == 7 ? 0 : day; } 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 alarm_entry_equals(const alarm_entry_t *left, const alarm_entry_t *right) { return strcmp(left->id, right->id) == 0 && left->hour == right->hour && left->min == right->min && left->sec == right->sec && left->repeat_mask == right->repeat_mask; } static void alarm_power_off_restore_task(void *arg) { int timeout_ms = (int)(intptr_t)arg; esp_err_t err = Board_Restore_From_Power_Off(timeout_ms); ESP_LOGI(TAG, "power-off restore from alarm finished: %s", esp_err_to_name(err)); alarm_power_off_restore_task_handle = NULL; vTaskDelete(NULL); } static void schedule_power_off_restore_from_alarm(void) { if (!Board_Is_Power_Off()) { return; } if (alarm_power_off_restore_task_handle) { ESP_LOGI(TAG, "power-off restore already pending"); return; } BaseType_t ret = xTaskCreate(alarm_power_off_restore_task, "alarm_pwr_restore", 4096, (void *)(intptr_t)POWER_OFF_WAKE_RECONNECT_TIMEOUT_MS, 5, &alarm_power_off_restore_task_handle); if (ret != pdPASS) { alarm_power_off_restore_task_handle = NULL; ESP_LOGE(TAG, "create alarm power-off restore task failed"); } } static void alarm_monitor_task(void *arg) { i2c_dev_t *i2c_dev = (i2c_dev_t *)arg; while (true) { vTaskDelay(pdMS_TO_TICKS(1000)); struct tm now; if (!get_current_time(i2c_dev, &now)) { continue; } ds3231_alarm_t alarm_flags = DS3231_ALARM_NONE; if (ds3231_get_alarm_flags(i2c_dev, &alarm_flags) == ESP_OK && (alarm_flags & DS3231_ALARM_1)) { ESP_LOGI(TAG, "DS3231 alarm1 triggered"); ds3231_clear_alarm_flags(i2c_dev, DS3231_ALARM_1); current_alarm1_valid = false; if (Board_Schedule_ClaimDue()) { if (Board_Is_Power_Off()) { schedule_power_off_restore_from_alarm(); } else { Board_Schedule_OnAlarmFired(); } } } else if (Board_Schedule_ClaimDue()) { ESP_LOGI(TAG, "schedule wall clock due"); if (Board_Is_Power_Off()) { schedule_power_off_restore_from_alarm(); } else { Board_Schedule_OnAlarmFired(); } } Board_Schedule_OnLessonFilesUpdated(); program_soonest_alarm(i2c_dev, &now); } } static void disable_alarm1(i2c_dev_t *i2c_dev) { if (!i2c_dev) { return; } ESP_ERROR_CHECK_WITHOUT_ABORT(ds3231_disable_alarm_ints(i2c_dev, DS3231_ALARM_1)); ESP_ERROR_CHECK_WITHOUT_ABORT(ds3231_clear_alarm_flags(i2c_dev, DS3231_ALARM_1)); current_alarm1_valid = false; } static void program_alarm1(i2c_dev_t *i2c_dev, const alarm_entry_t *entry) { if (!i2c_dev || !entry) { return; } struct tm now; if (!get_current_time(i2c_dev, &now)) { return; } int now_sec = tm_seconds(&now); int alarm_sec = alarm_entry_seconds(entry); int best_delta = SECONDS_PER_DAY * 8; struct tm best_time = now; for (int day_offset = 0; day_offset <= 7; ++day_offset) { int tm_wday = (now.tm_wday + day_offset) % 7; if ((entry->repeat_mask & tm_wday_to_repeat_bit(tm_wday)) == 0) { continue; } int delta = day_offset * SECONDS_PER_DAY + alarm_sec - now_sec; if (delta <= 0) { continue; } if (delta < best_delta) { best_delta = delta; time_t target = mktime(&now) + delta; localtime_r(&target, &best_time); } } if (best_delta >= SECONDS_PER_DAY * 8) { disable_alarm1(i2c_dev); return; } struct tm alarm_time = {0}; alarm_time.tm_hour = entry->hour; alarm_time.tm_min = entry->min; alarm_time.tm_sec = entry->sec; alarm_time.tm_mday = best_time.tm_mday; ESP_ERROR_CHECK_WITHOUT_ABORT(ds3231_clear_alarm_flags(i2c_dev, DS3231_ALARM_1)); ESP_ERROR_CHECK_WITHOUT_ABORT(ds3231_set_alarm(i2c_dev, DS3231_ALARM_1, &alarm_time, DS3231_ALARM1_MATCH_SECMINHOURDATE, NULL, 0)); ESP_ERROR_CHECK_WITHOUT_ABORT(ds3231_enable_alarm_ints(i2c_dev, DS3231_ALARM_1)); current_alarm1_entry = *entry; current_alarm1_valid = true; ESP_LOGI(TAG, "program DS3231 alarm1: id=%s %02d:%02d:%02d mday=%d", entry->id, entry->hour, entry->min, entry->sec, best_time.tm_mday); }