#include "system.h"
|
#include "config.h"
|
#include "app_task.h"
|
#include "parent_schedule.h"
|
#include "display/lvgl_display/jpg/jpeg_to_image.h"
|
|
#include <esp_heap_caps.h>
|
#include <esp_partition.h>
|
#include <nvs.h>
|
#include <dirent.h>
|
#include <driver/gpio.h>
|
#include <esp_adc/adc_oneshot.h>
|
#include <esp_adc/adc_cali.h>
|
#include <esp_adc/adc_cali_scheme.h>
|
#include <stdbool.h>
|
#include <stdint.h>
|
#include <stdio.h>
|
#include <stdlib.h>
|
#include <string.h>
|
#include <strings.h>
|
#include <unistd.h>
|
#include <errno.h>
|
#include <sys/stat.h>
|
|
#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);
|
}
|