| | |
| | | #include <cmath> |
| | | #include <cstdio> |
| | | #include <cstring> |
| | | #include <memory> |
| | | #include <string> |
| | | |
| | | #include <esp_lcd_panel_io.h> |
| | |
| | | #include <ds3231.h> |
| | | #include "system.h" |
| | | #include "mqtt.h" |
| | | #include "parent_schedule.h" |
| | | |
| | | #define TAG "sening_xiaozhi_s3" |
| | | #define WIFI_RECONNECT_CONNECTED_BIT BIT0 |
| | |
| | | { |
| | | DeviceState state = Application::GetInstance().GetDeviceState(); |
| | | return state == kDeviceStateConnecting || state == kDeviceStateListening || state == kDeviceStateSpeaking; |
| | | } |
| | | |
| | | extern "C" void Board_HoldIdleForServerPrompt(void) |
| | | { |
| | | Application::GetInstance().Schedule([]() { |
| | | auto& app = Application::GetInstance(); |
| | | app.HoldIdleForServerPrompt(); |
| | | if (app.GetDeviceState() == kDeviceStateSpeaking) { |
| | | app.AbortSpeaking(kAbortReasonNone); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | extern "C" void Board_ReleaseHoldIdle(void) |
| | | { |
| | | Application::GetInstance().Schedule([]() { |
| | | Application::GetInstance().ReleaseHoldIdle(); |
| | | }); |
| | | } |
| | | |
| | | extern "C" void Board_SetLessonDownloadBoost(bool enable) |
| | | { |
| | | Board::GetInstance().SetPowerSaveLevel( |
| | | enable ? PowerSaveLevel::PERFORMANCE : PowerSaveLevel::LOW_POWER); |
| | | } |
| | | |
| | | extern "C" void Board_AnnounceLessonSyncAndWait(void) |
| | | { |
| | | Board_SetLessonDownloadBoost(true); |
| | | Application::GetInstance().Schedule([]() { |
| | | auto& app = Application::GetInstance(); |
| | | app.HoldIdleForServerPrompt(); |
| | | app.Alert("正在同步", "正在同步音频,请稍等", "cloud_arrow_down"); |
| | | if (!app.SendListenDetectNow("[lesson_sync]")) { |
| | | app.SendListenDetect("[lesson_sync]", true); |
| | | } |
| | | }); |
| | | |
| | | int waited_ms = 0; |
| | | bool saw_speaking = false; |
| | | while (waited_ms < 8000) { |
| | | DeviceState state = Application::GetInstance().GetDeviceState(); |
| | | if (state == kDeviceStateSpeaking) { |
| | | saw_speaking = true; |
| | | } else if (saw_speaking && state == kDeviceStateIdle) { |
| | | break; |
| | | } |
| | | vTaskDelay(pdMS_TO_TICKS(100)); |
| | | waited_ms += 100; |
| | | } |
| | | if (!saw_speaking) { |
| | | vTaskDelay(pdMS_TO_TICKS(2500)); |
| | | } |
| | | Application::GetInstance().Schedule([]() { |
| | | auto& app = Application::GetInstance(); |
| | | app.HoldIdleForServerPrompt(); |
| | | app.GetAudioService().EnableVoiceProcessing(false); |
| | | app.GetAudioService().EnableWakeWordDetection(false); |
| | | if (app.GetDeviceState() == kDeviceStateListening) { |
| | | app.SetDeviceState(kDeviceStateIdle); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | extern "C" bool Board_Is_Audio_Busy(void) |
| | |
| | | return ESP_FAIL; |
| | | } |
| | | |
| | | constexpr size_t IO_BUF_SIZE = 16384; |
| | | char *io_buf = (char *)heap_caps_malloc(IO_BUF_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); |
| | | if (io_buf && setvbuf(fp, io_buf, _IOFBF, IO_BUF_SIZE) != 0) { |
| | | heap_caps_free(io_buf); |
| | | io_buf = nullptr; |
| | | } |
| | | |
| | | auto network = Board::GetInstance().GetNetwork(); |
| | | auto http = network->CreateHttp(0); |
| | | http->SetTimeout(30000); |
| | | http->SetTimeout(120000); |
| | | if (!http->Open("GET", url)) { |
| | | fclose(fp); |
| | | if (io_buf) { |
| | | heap_caps_free(io_buf); |
| | | } |
| | | SetDownloadError(error_info, error_info_size, "open http failed"); |
| | | return ESP_FAIL; |
| | | } |
| | |
| | | if (status != 200) { |
| | | fclose(fp); |
| | | http->Close(); |
| | | if (io_buf) { |
| | | heap_caps_free(io_buf); |
| | | } |
| | | char message[64]; |
| | | snprintf(message, sizeof(message), "http status %d", status); |
| | | SetDownloadError(error_info, error_info_size, message); |
| | |
| | | } |
| | | |
| | | size_t content_length = http->GetBodyLength(); |
| | | constexpr size_t BUFFER_SIZE = 1024; |
| | | char *buffer = (char *)heap_caps_malloc(BUFFER_SIZE, MALLOC_CAP_INTERNAL); |
| | | ESP_LOGI(TAG, "download start: %s content_length=%u", url, (unsigned)content_length); |
| | | constexpr size_t BUFFER_SIZE = 16384; |
| | | size_t buffer_size = BUFFER_SIZE; |
| | | char *buffer = (char *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); |
| | | if (!buffer) { |
| | | buffer_size = 4096; |
| | | buffer = (char *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); |
| | | } |
| | | if (!buffer) { |
| | | buffer = (char *)heap_caps_malloc(buffer_size, MALLOC_CAP_INTERNAL); |
| | | } |
| | | if (!buffer) { |
| | | buffer_size = 1024; |
| | | buffer = (char *)heap_caps_malloc(buffer_size, MALLOC_CAP_INTERNAL); |
| | | } |
| | | if (!buffer) { |
| | | fclose(fp); |
| | | http->Close(); |
| | | if (io_buf) { |
| | | heap_caps_free(io_buf); |
| | | } |
| | | SetDownloadError(error_info, error_info_size, "no memory for download buffer"); |
| | | return ESP_ERR_NO_MEM; |
| | | } |
| | |
| | | } |
| | | |
| | | size_t total_read = 0; |
| | | size_t recent_read = 0; |
| | | int last_progress = -1; |
| | | int64_t last_report = 0; |
| | | int64_t last_speed_us = esp_timer_get_time(); |
| | | bool ok = true; |
| | | bool canceled = false; |
| | | while (true) { |
| | | int ret = http->Read(buffer, BUFFER_SIZE); |
| | | int ret = http->Read(buffer, buffer_size); |
| | | if (ret < 0) { |
| | | ESP_LOGE(TAG, "read HTTP failed: %d", ret); |
| | | SetDownloadError(error_info, error_info_size, "read http failed"); |
| | |
| | | } |
| | | |
| | | total_read += ret; |
| | | recent_read += (size_t)ret; |
| | | int64_t now = esp_timer_get_time(); |
| | | if (now - last_speed_us >= 1000000) { |
| | | ESP_LOGI(TAG, "download %u/%u bytes %uB/s", |
| | | (unsigned)total_read, (unsigned)content_length, (unsigned)recent_read); |
| | | last_speed_us = now; |
| | | recent_read = 0; |
| | | } |
| | | if (progress_cb) { |
| | | int progress = content_length > 0 ? (int)(total_read * 100 / content_length) : 0; |
| | | int64_t now = esp_timer_get_time(); |
| | | bool report = content_length <= 0 || progress != last_progress || now - last_report >= 1000000 || progress >= 100; |
| | | if (report) { |
| | | last_progress = progress; |
| | |
| | | fclose(fp); |
| | | http->Close(); |
| | | heap_caps_free(buffer); |
| | | if (io_buf) { |
| | | heap_caps_free(io_buf); |
| | | } |
| | | |
| | | if (!ok) { |
| | | if (check_md5) { |
| | |
| | | task_id.c_str()); |
| | | Board_SetShadowingListenRatio( |
| | | static_cast<float>(properties["listen_ratio"].value<double>())); |
| | | sdcard_mp3_playlist_item_t playlist[SDCARD_MP3_PLAYLIST_MAX]; |
| | | std::unique_ptr<sdcard_mp3_playlist_item_t, void(*)(void*)> playlist( |
| | | nullptr, heap_caps_free); |
| | | int playlist_n = 0; |
| | | int playlist_cap = 0; |
| | | if (!items_json.empty()) { |
| | | cJSON *root = cJSON_Parse(items_json.c_str()); |
| | | cJSON *arr = root; |
| | |
| | | arr = cJSON_GetObjectItem(root, "items"); |
| | | } |
| | | if (cJSON_IsArray(arr)) { |
| | | const int incoming = cJSON_GetArraySize(arr); |
| | | playlist_cap = incoming; |
| | | if (playlist_cap > SDCARD_MP3_PLAYLIST_MAX) { |
| | | ESP_LOGW(TAG, "playlist truncated incoming=%d max=%d", |
| | | incoming, SDCARD_MP3_PLAYLIST_MAX); |
| | | playlist_cap = SDCARD_MP3_PLAYLIST_MAX; |
| | | } |
| | | if (playlist_cap > 0) { |
| | | playlist.reset(static_cast<sdcard_mp3_playlist_item_t *>( |
| | | heap_caps_calloc(playlist_cap, sizeof(sdcard_mp3_playlist_item_t), |
| | | MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT))); |
| | | if (!playlist) { |
| | | cJSON_Delete(root); |
| | | return std::string("{\"accepted\":false,\"error\":\"no_mem\"}"); |
| | | } |
| | | } |
| | | cJSON *it = nullptr; |
| | | cJSON_ArrayForEach(it, arr) { |
| | | if (playlist_n >= SDCARD_MP3_PLAYLIST_MAX) { |
| | | if (playlist_n >= playlist_cap) { |
| | | break; |
| | | } |
| | | const cJSON *name_item = cJSON_GetObjectItem(it, "name"); |
| | |
| | | continue; |
| | | } |
| | | if (!SDCard_Resolve_MP3_Path(name_item->valuestring, |
| | | playlist[playlist_n].path, |
| | | sizeof(playlist[playlist_n].path))) { |
| | | playlist.get()[playlist_n].path, |
| | | sizeof(playlist.get()[playlist_n].path))) { |
| | | ESP_LOGW(TAG, "playlist skip missing %s", name_item->valuestring); |
| | | continue; |
| | | } |
| | | const cJSON *rep = cJSON_GetObjectItem(it, "repeat"); |
| | | playlist[playlist_n].repeat = cJSON_IsNumber(rep) ? rep->valueint : 1; |
| | | if (playlist[playlist_n].repeat < 1) { |
| | | playlist[playlist_n].repeat = 1; |
| | | playlist.get()[playlist_n].repeat = cJSON_IsNumber(rep) ? rep->valueint : 1; |
| | | if (playlist.get()[playlist_n].repeat < 1) { |
| | | playlist.get()[playlist_n].repeat = 1; |
| | | } |
| | | const cJSON *spd = cJSON_GetObjectItem(it, "speed"); |
| | | playlist[playlist_n].speed = cJSON_IsNumber(spd) |
| | | playlist.get()[playlist_n].speed = cJSON_IsNumber(spd) |
| | | ? SDCard_Clamp_MP3_Speed(static_cast<float>(spd->valuedouble)) |
| | | : 1.0f; |
| | | const cJSON *po = cJSON_GetObjectItem(it, "power_off_after"); |
| | | playlist[playlist_n].power_off_after = cJSON_IsTrue(po); |
| | | playlist.get()[playlist_n].power_off_after = cJSON_IsTrue(po); |
| | | playlist_n++; |
| | | } |
| | | } |
| | |
| | | repeat = 1; |
| | | } |
| | | } else { |
| | | name = playlist[0].path; |
| | | speed = playlist[0].speed; |
| | | repeat = playlist[0].repeat; |
| | | power_off_after = playlist[playlist_n - 1].power_off_after; |
| | | name = playlist.get()[0].path; |
| | | speed = playlist.get()[0].speed; |
| | | repeat = playlist.get()[0].repeat; |
| | | power_off_after = playlist.get()[playlist_n - 1].power_off_after; |
| | | } |
| | | int64_t now = esp_timer_get_time(); |
| | | // 跟读/复读会连续播同一文件;不要按路径去重。force 给旧固件重试用。 |
| | | // 跟读/泛听会连续播同一文件;不要按路径去重。force 给旧固件重试用。 |
| | | bool lesson = (task_id == "shadowing" || task_id == "repeater"); |
| | | if (!force && !lesson && playlist_n < 2 && name == last_mp3_play_name && |
| | | std::fabs(speed - last_mp3_play_speed) < 0.001f && |
| | |
| | | audio.EnableWakeWordDetection(false); |
| | | esp_err_t err = ESP_FAIL; |
| | | if (playlist_n > 0) { |
| | | err = SDCard_Play_MP3_File_Playlist(playlist, playlist_n); |
| | | err = SDCard_Play_MP3_File_Playlist(playlist.get(), playlist_n); |
| | | if (err == ESP_OK) { |
| | | SDCard_Show_Matching_JPG_For_MP3_Path(playlist[0].path); |
| | | SDCard_Show_Matching_JPG_For_MP3_Path(playlist.get()[0].path); |
| | | } |
| | | } else { |
| | | err = SDCard_Play_MP3_With_Speed_Repeat(name.data(), speed, repeat, power_off_after); |
| | |
| | | |
| | | extern "C" esp_err_t Board_Enter_Power_Off(void) |
| | | { |
| | | Board_Schedule_OnEnteringPowerOff(); |
| | | auto& board = static_cast<sening_xiaozhi_s3&>(Board::GetInstance()); |
| | | return board.EnterPowerOffStandby(); |
| | | } |