#ifndef __SYSTEM_H__ #define __SYSTEM_H__ #define APP_FIRMWARE_VERSION "1.0.0" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include #include #include #include #include "esp_sntp.h" #include "driver/gpio.h" #include "driver/i2c_master.h" #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif #define ALARM_MAX_NUM 10 #define ALARM_ID_MAX_LEN 32 #define ALARM_REPEAT_MON 0x01 #define ALARM_REPEAT_TUE 0x02 #define ALARM_REPEAT_WED 0x04 #define ALARM_REPEAT_THU 0x08 #define ALARM_REPEAT_FRI 0x10 #define ALARM_REPEAT_SAT 0x20 #define ALARM_REPEAT_SUN 0x40 #define ALARM_REPEAT_ALL 0x7F #define MOUNT_POINT "/sdcard" #define SDCARD_PATH_MAX_LEN 256 /* 跟读/泛听没有「课文只能 N 句」的业务上限。256 只是一次 MCP 下发的安全顶。 */ #define SDCARD_MP3_PLAYLIST_MAX 256 typedef struct { char path[SDCARD_PATH_MAX_LEN]; float speed; int repeat; bool power_off_after; } sdcard_mp3_playlist_item_t; #define SCREEN_MIRROR_X false #define SCREEN_MIRROR_Y false #define SCREEN_SWAP_XY false #define WIFI_HISTORY_MAX_NUM 5 #define BOARD_MQTT_BROKER_HOST "124.222.109.197" #define BOARD_MQTT_BROKER_PORT 2100 #define BOARD_MQTT_USERNAME "AI_XIAOZHI" #define BOARD_MQTT_PASSWORD "V9qL7!mX2@rT8$kN5^wP1&c" #define BOARD_MQTT_CMD_CTL_TOPIC_FORMAT "snlp/v1/%s/cmd/ctl" #define BOARD_MQTT_CMD_TASK_TOPIC_FORMAT "snlp/v1/%s/cmd/task" #define BOARD_MQTT_RPT_LOG_TOPIC_FORMAT "snlp/v1/%s/rpt/log" #define BOARD_MQTT_RPT_ACK_TOPIC_FORMAT "snlp/v1/%s/rpt/ack" #define BOARD_MQTT_RPT_PROGRESS_TOPIC_FORMAT "snlp/v1/%s/rpt/progress" #define BOARD_MQTT_RPT_STATE_TOPIC_FORMAT "snlp/v1/%s/rpt/state" #define BOARD_MQTT_RPT_EVENT_TOPIC_FORMAT "snlp/v1/%s/rpt/event" #define BOARD_MQTT_EXECUTE_COMMANDS 1 /** * @brief Wi-Fi 待机低功耗超时时间,单位为秒。 * * 当设备持续处于空闲待命状态超过该时间后,板级逻辑会断开 Wi-Fi station 以降低功耗, * 但不会关闭本地离线唤醒词检测。设置为 0 或负数可禁用该待机模式。 */ #define WIFI_STANDBY_TIMEOUT_SEC 300 #define POWER_OFF_WAKE_RECONNECT_TIMEOUT_MS 30000 /** * @brief 板级 Custom/MultiNet 唤醒词配置。 * * 如果需要修改智能体的名,要在控制台中修改小智的记忆。 */ #define BOARD_CUSTOM_WAKE_WORD_PINYIN "ni hao xiao zhi" #define BOARD_CUSTOM_WAKE_WORD_DISPLAY "你好小智" #define BOARD_CUSTOM_WAKE_WORD_THRESHOLD_PERCENT 20 /** * @brief 板级 MultiNet 离线命令词。只留没网也必须能用的入口: * 「切换模式」「模式切换」「泛听」「开始泛听」「退出泛听」「退出跟读」「我们聊天吧」「我们来聊天吧」。 * 泛听/跟读播音频时不上行 ASR,退出口令必须能离线识别。其它 App 的进入词仍只在 Server apps.json。 */ #define OFFLINE_COMMAND_WORD "切换模式" #define OFFLINE_COMMAND_WORD_PINYIN "qie huan mo shi" #define OFFLINE_COMMAND_WORD_ALT "模式切换" #define OFFLINE_COMMAND_WORD_ALT_PINYIN "mo shi qie huan" #define OFFLINE_REPEATER_WORD "泛听" #define OFFLINE_REPEATER_WORD_PINYIN "fan ting" #define OFFLINE_START_REPEATER_WORD "开始泛听" #define OFFLINE_START_REPEATER_WORD_PINYIN "kai shi fan ting" #define OFFLINE_EXIT_REPEATER_WORD "退出泛听" #define OFFLINE_EXIT_REPEATER_WORD_PINYIN "tui chu fan ting" #define OFFLINE_EXIT_SHADOWING_WORD "退出跟读" #define OFFLINE_EXIT_SHADOWING_WORD_PINYIN "tui chu gen du" #define OFFLINE_CHAT_WORD "我们聊天吧" #define OFFLINE_CHAT_WORD_PINYIN "wo men liao tian ba" #define OFFLINE_CHAT_WORD_FULL "我们来聊天吧" #define OFFLINE_CHAT_WORD_FULL_PINYIN "wo men lai liao tian ba" void Board_Offline_Command_Callback(void); bool Board_Handle_Offline_Command(const char *command); bool Board_Is_Mode_Switch_Command(const char *command); bool Board_Is_Chat_Return_Command(const char *command); bool Board_Is_App_Launch_Command(const char *command); /** * @brief 使用 SNTP 网络时间同步 DS3231 RTC。 * * 该函数会启动 SNTP 客户端并在时间同步完成后写入 DS3231,适用于设备连接 Wi-Fi * 之后校准 RTC 时间。函数会保存传入的 DS3231 I2C 设备描述符,供 SNTP 同步完成 * 回调继续使用。 * * @param i2c_dev 已初始化的 DS3231 I2C 设备描述符,不能为空。 */ void SNTP_To_DS2312(i2c_dev_t *i2c_dev); /** * @brief 根据已保存的每日闹钟列表更新 DS3231 Alarm1。 * * 该函数会读取持久化闹钟列表,选择相对于当前时间最近的未来闹钟,并写入 * DS3231 Alarm1。若列表为空或读取失败,则会关闭 Alarm1。传入 time 时使用该 * 时间作为当前时间基准;传入 NULL 时会优先从 DS3231 读取当前时间,失败后回退 * 到系统时间。 * * @param i2c_dev 已初始化的 DS3231 I2C 设备描述符,不能为空。 * @param time 可选的当前时间基准;为 NULL 时由函数自行获取当前时间。 */ void DS2312_Update_Alarm(i2c_dev_t *i2c_dev, struct tm *time); /** * @brief 启动 DS3231 闹钟后台监控任务。 * * 该函数会创建一个后台任务,每秒检查闹钟列表、当前时间和 DS3231 Alarm1 触发标志。 * 如果检测到当前写入的 Alarm1 已不是最近的未来闹钟,或 Alarm1 已触发但没有外部 * IO 通知,任务会主动清除标志并重新调度后续闹钟。重复调用不会创建多个监控任务。 * * @param i2c_dev 已初始化的 DS3231 I2C 设备描述符,不能为空。 */ void DS2312_Start_Alarm_Monitor(i2c_dev_t *i2c_dev); /** * @brief 添加一个每日重复的 RTC 闹钟。 * * 该函数会将指定时分秒写入持久化闹钟列表,并在保存成功后立即重新调度 * DS3231 Alarm1。若时间参数非法、闹钟已存在或列表已满,函数会忽略本次添加。 * * @param i2c_dev 已初始化的 DS3231 I2C 设备描述符,不能为空。 * @param hour 小时,取值范围 0-23。 * @param min 分钟,取值范围 0-59。 * @param sec 秒,取值范围 0-59。 */ void DS2312_Add_Alarm_Time(i2c_dev_t *i2c_dev, int hour, int min, int sec); /** * @brief 获取当前保存的 RTC 闹钟列表。 * * 返回值为 JSON 字符串,供 MCP 或其他上层逻辑直接解析。字符串由模块内部静态缓冲区 * 保存,调用者不应释放或长期保存该指针;后续再次调用本函数可能覆盖原内容。 * * @param i2c_dev 保留参数,用于保持接口与其他 DS2312 操作一致,当前实现不依赖该参数。 * @return 指向内部静态缓冲区的 JSON 字符串。 */ const char *DS2312_Get_Alarm_List(i2c_dev_t *i2c_dev); /** * @brief 按索引删除一个已保存的 RTC 闹钟。 * * index 对应 DS2312_Get_Alarm_List() 返回列表中的 index 字段。删除成功后会立即重新 * 调度 DS3231 Alarm1;若索引非法或列表读取失败,函数不会修改现有闹钟配置。 * * @param i2c_dev 已初始化的 DS3231 I2C 设备描述符,不能为空。 * @param index 待删除闹钟的列表索引。 */ void DS2312_Delete_Alarm_Index(i2c_dev_t *i2c_dev, uint8_t index); /** * @brief 清空所有已保存的 RTC 闹钟。 * * 该函数会清空持久化闹钟列表,并关闭 DS3231 Alarm1 中断配置,防止旧闹钟继续触发。 * * @param i2c_dev 已初始化的 DS3231 I2C 设备描述符,不能为空。 */ void DS2312_Delete_Alarm_All(i2c_dev_t *i2c_dev); esp_err_t DS2312_Set_Unix_Time(i2c_dev_t *i2c_dev, time_t timestamp); esp_err_t DS2312_Set_Unix_Time_Saved(time_t 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); esp_err_t DS2312_Delete_Alarm_Id(i2c_dev_t *i2c_dev, const char *id); esp_err_t DS2312_Set_Alarm_Id_Saved(const char *id, int hour, int min, int sec, uint8_t repeat_mask); esp_err_t DS2312_Delete_Alarm_Id_Saved(const char *id); /** * @brief 获取 SD 卡中的 MP3 文件列表。 * * 函数会递归遍历 SD 卡挂载目录,仅返回后缀为 .mp3 或 .MP3 的文件。子目录文件 * 以 / 开头的 SD 卡相对路径返回。返回值为 JSON 字符串,字符串存放在模块内部静态缓冲区中,调用者不应释放该指针。 * * @return 指向内部静态缓冲区的 MP3 文件列表 JSON 字符串。 */ const char *SDCard_Get_MP3_List(void); /** * @brief 按文件名启动 SD 卡 MP3 播放。 * * mp3_name 可以是完整文件名,也可以是能够唯一匹配 MP3 文件名的关键字。若当前正在播放 * 其他 MP3,会先停止旧任务再启动新播放;若当前处于小智播报暂停音乐的状态,新音乐会 * 保持暂停,等待上层恢复。 * * @param mp3_name MP3 文件名或匹配关键字,不能为空。 * @return ESP_OK 表示播放任务已成功启动;其他值表示文件不存在、参数非法或播放启动失败。 */ esp_err_t SDCard_Play_MP3(char *mp3_name); #define SDCARD_MP3_SPEED_MIN 0.5f #define SDCARD_MP3_SPEED_MAX 2.0f #define SDCARD_MP3_SPEED_DEFAULT 1.0f /** * @brief 将播放倍率夹紧到 [0.5, 2.0];NaN/非法则回退 1.0。 * * 实现是先按 speed 做线性时域拉伸,再用文件原始采样率转到喇叭采样率。 * 0.5~2.0 内任意浮点均可;非 1.0 会改变音调。不要把 sample_rate*speed 交给 AE 变采样。 */ float SDCard_Clamp_MP3_Speed(float speed); /** * @brief 按文件名和速度启动 SD 卡 MP3 播放。 * * speed 取值 [0.5, 2.0],缺省 1.0;超出范围会被夹紧。非 1.0 通过重采样实现,会改变音调。 * * @param mp3_name MP3 文件名或匹配关键字,不能为空。 * @param speed 播放速度倍率。 * @return ESP_OK 表示播放任务已成功启动;ESP_ERR_INVALID_ARG 表示文件名非法。 */ esp_err_t SDCard_Play_MP3_With_Speed(char *mp3_name, float speed); esp_err_t SDCard_Play_MP3_With_Speed_Repeat(char *mp3_name, float speed, int repeat, bool power_off_after); void SDCard_Show_Matching_JPG_For_MP3_Path(const char *mp3_path); /** * @brief 停止当前 SD 卡 MP3 播放。 * * 该函数会取消当前播放任务,并清除暂停状态,确保后续不会因为小智播报结束而自动恢复 * 已被用户停止的音乐。 */ void SDCard_Stop_Play(void); /** * @brief 暂停当前或即将启动的 SD 卡 MP3 播放。 * * 若 MP3 正在播放,播放任务会在安全检查点阻塞输出;若当前没有播放任务,也会记录暂停 * 状态,使随后启动的 MP3 在上层调用恢复接口前保持静音暂停。 */ void SDCard_Pause_Play(void); /** * @brief 恢复被暂停的 SD 卡 MP3 播放。 * * 该函数会清除暂停标志,使正在等待的 MP3 播放任务继续读取、解码并输出音频。 */ void SDCard_Resume_Play(void); /** * @brief 查询 SD 卡 MP3 播放任务是否存在。 * * @return true 表示当前存在 MP3 播放任务;false 表示没有正在运行的播放任务。 */ bool SDCard_Is_Playing(void); /** * @brief 课文开播前释放 AFE、退出聆听并等到内部 SRAM 够 SD DMA。 * * 必须在创建 MP3 播放任务之前调用。AFE/MultiNet 和 SDMMC DMA 争用连续内部 SRAM * 时,fopen/sdmmc_read_blocks 会失败,严重时 newlib 锁初始化 abort 重启。 */ void SDCard_PreparePlaybackAndWait(void); /** * @brief 课文 MP3 已进入 PSRAM 后才允许离线口令 AFE。 */ bool SDCard_Mp3OfflineWakeAllowed(void); /** * @brief 查询 SD 卡 MP3 播放是否处于暂停状态。 * * @return true 表示播放被暂停或后续新播放应从暂停状态开始;false 表示未暂停。 */ bool SDCard_Is_Paused(void); /** * @brief 删除 SD 卡中的 MP3 文件。 * * 函数会根据文件名或匹配关键字解析目标 MP3 文件并执行删除。若文件不存在或名称非法, * 函数不会修改 SD 卡内容。 * * @param mp3_name MP3 文件名或匹配关键字,不能为空。 */ void SDCard_Delete_MP3(char *mp3_name); /** * @brief 按完整路径启动 MP3 文件播放。 * * 该接口面向模块内部或高级调用者,要求传入已经解析完成的 SD 卡文件路径。普通 MCP * 调用应优先使用 SDCard_Play_MP3(),由其负责文件名安全检查和路径解析。 * * @param path MP3 文件完整路径,不能为空。 * @return ESP_OK 表示播放任务已成功启动;其他值表示路径非法、播放器忙或任务创建失败。 */ esp_err_t SDCard_Play_MP3_File(const char *path); /** * @brief 按完整路径和速度启动 MP3 文件播放。 * * speed 取值 [0.5, 2.0],缺省 1.0;超出范围会被夹紧。非 1.0 通过重采样实现,会改变音调。 * * @param path MP3 文件完整路径,不能为空。 * @param speed 播放速度倍率。 * @return ESP_OK 表示播放任务已成功启动;ESP_ERR_INVALID_ARG 表示路径非法。 */ esp_err_t SDCard_Play_MP3_File_With_Speed(const char *path, float speed); esp_err_t SDCard_Play_MP3_File_With_Speed_Repeat(const char *path, float speed, int repeat, bool power_off_after); bool SDCard_Resolve_MP3_Path(const char *mp3_name, char *path, size_t path_size); esp_err_t SDCard_Play_MP3_File_Playlist(const sdcard_mp3_playlist_item_t *items, int count); int SDCard_Get_Repeat_Left(void); esp_err_t SDCard_Map_Business_Path(const char *business_path, char *sd_path, size_t sd_path_size); esp_err_t SDCard_Ensure_Business_Dir(const char *business_path); 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 SDCard_Commit_Part_File(const char *part_path, const char *final_path); void SDCard_Remove_File_If_Exists(const char *path); esp_err_t SDCard_Delete_File_By_Business_Path(const char *business_path); esp_err_t SDCard_Save_Manifest(const char *course_id, const char *manifest_json); typedef bool (*board_download_progress_cb_t)(int progress, const char *info, void *ctx); esp_err_t Board_Download_File_To_Path(const char *url, const char *part_path, const char *expected_md5, board_download_progress_cb_t progress_cb, void *ctx, char *error_info, size_t error_info_size); typedef void (*board_ota_progress_cb_t)(int progress, size_t speed, void *ctx); typedef void (*board_ota_complete_cb_t)(bool success, void *ctx); esp_err_t Board_Start_Firmware_Upgrade(const char *url, const char *version, board_ota_progress_cb_t progress_cb, board_ota_complete_cb_t complete_cb, void *ctx); /** * @brief 获取 SD 卡中的 JPG 图片列表。 * * 函数会遍历 SD 卡挂载目录,仅返回后缀为 .jpg 或 .JPG 的图片文件名。尺寸校验在显示图片时执行, * 避免列表查询阶段逐个解码图片导致 MCP 响应变慢。返回值为 JSON 字符串,存放在模块内部静态缓冲区中,调用者不应释放该指针。 * * @return 指向内部静态缓冲区的 JPG 图片列表 JSON 字符串。 */ const char *SDCard_Get_JPG_List(void); /** * @brief 在 LCD 面板上显示 SD 卡中的 JPG 图片。 * * 函数会根据文件名解析图片路径,读取并解码 160x160 JPG,完成必要的 RGB565/BGR565 * 像素格式转换后绘制到指定 LCD 面板。显示成功后会保存最后一次显示的图片名称,用于 * 下次开机恢复显示。 * * @param panel 已初始化的 LCD panel 句柄,不能为空。 * @param jpg_name JPG 文件名,不能为空。 */ void SDCard_Play_JPG(esp_lcd_panel_handle_t panel, char *jpg_name); /** * @brief 删除 SD 卡中的 JPG 图片文件。 * * 函数会根据文件名解析目标 JPG 文件并执行删除。若文件不存在、名称非法或删除失败, * 函数不会影响其他图片文件。 * * @param jpg_name JPG 文件名,不能为空。 */ void SDCard_Delete_JPG(char *jpg_name); /** * @brief 恢复显示上一次成功显示的 JPG 图片。 * * 函数会从 NVS 中读取最后一次显示的 JPG 文件名,并在确认文件仍然存在后重新显示。 * 适合在 LCD 初始化和 SD 卡挂载完成后调用。 * * @param panel 已初始化的 LCD panel 句柄,不能为空。 */ void SDCard_Restore_Last_JPG(esp_lcd_panel_handle_t panel); /** * @brief 获取当前电池电量百分比。 * * 函数会读取电池 ADC 引脚电压,结合板级分压参数换算为单节锂电池电压,并映射到 * 0-100 的百分比范围。返回值用于 MCP 电池状态和小智设备状态上报。 * * @return 电池电量百分比,范围 0-100。 */ uint8_t Battery_Get_Value(void); /** * @brief 获取当前充电状态。 * * 函数会读取充电检测 GPIO,并按照板级硬件定义判断是否正在充电。 * * @return true 表示正在充电;false 表示未处于充电状态。 */ bool Battery_Is_Charging(void); /** * @brief 获取已保存的 Wi-Fi 历史列表。 * * 返回值为 JSON 字符串,仅包含 SSID 和索引,不包含密码等敏感信息。字符串由模块内部 * 静态对象保存,调用者不应释放该指针;后续再次调用本函数可能覆盖原内容。 * * @return 指向内部静态缓冲区的 Wi-Fi 历史列表 JSON 字符串。 */ const char *WIFI_Get_History_List(void); /** * @brief 清空已保存的 Wi-Fi 历史列表。 * * 该函数会删除持久化保存的 Wi-Fi SSID 和密码,并清空内存中的历史列表。函数不会主动 * 断开当前 Wi-Fi 连接;清空后通常需要重启设备,设备会因无历史 Wi-Fi 而重新进入配网流程。 */ void WIFI_Delete_All(void); uint8_t Board_Get_CPU_Usage(void); esp_err_t Board_Set_Output_Volume(int level); int Board_Get_Output_Volume(void); esp_err_t Board_Enter_Standby(void); esp_err_t Board_Request_Wake(int timeout_ms); esp_err_t Board_Enter_Power_Off(void); esp_err_t Board_Restore_From_Power_Off(int timeout_ms); bool Board_Is_Power_Off(void); bool Board_Get_Wall_Clock(struct tm *out); esp_err_t DS2312_Set_Schedule_Wake(int hour, int min, int sec, uint8_t weekday_bit); void DS2312_Clear_Schedule_Wake(void); esp_err_t Board_Reboot_Delayed(int delay_seconds); void Board_Acquire_Sleep_Inhibit(void); void Board_Release_Sleep_Inhibit(void); bool Board_Is_Voice_Session_Active(void); void Board_HoldIdleForServerPrompt(void); void Board_ReleaseHoldIdle(void); void Board_AnnounceLessonSyncAndWait(void); void Board_SetLessonDownloadBoost(bool enable); bool Board_Is_Audio_Busy(void); void Board_Show_JPG(const char *jpg_name); void Board_Show_Black_Screen(void); void Board_Show_Alert(const char *status, const char *message, const char *emotion); #ifdef __cplusplus } #endif #endif