mirror of
https://github.com/78/xiaozhi-esp32.git
synced 2026-02-13 15:38:08 +00:00
Compare commits
11 Commits
copilot/fi
...
v2.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37110a9d05 | ||
|
|
796312db4c | ||
|
|
9e1724e892 | ||
|
|
0b3b98eca7 | ||
|
|
abd62648cb | ||
|
|
0883a36537 | ||
|
|
b6c61fe390 | ||
|
|
f7284a57df | ||
|
|
96f34ec70f | ||
|
|
aad2f60b87 | ||
|
|
5b874bc3ad |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@ tmp/
|
||||
components/
|
||||
managed_components/
|
||||
build/
|
||||
dist/
|
||||
.vscode/
|
||||
.devcontainer/
|
||||
sdkconfig.old
|
||||
|
||||
@@ -9,5 +9,5 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
|
||||
set(PROJECT_VER "2.2.1")
|
||||
set(PROJECT_VER "2.2.2")
|
||||
project(xiaozhi)
|
||||
|
||||
1
_codeql_detected_source_root
Symbolic link
1
_codeql_detected_source_root
Symbolic link
@@ -0,0 +1 @@
|
||||
.
|
||||
@@ -104,28 +104,28 @@ elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32_LCD)
|
||||
set(BUILTIN_ICON_FONT font_awesome_14_1)
|
||||
elseif(CONFIG_BOARD_TYPE_DF_K10)
|
||||
set(BOARD_TYPE "df-k10")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_DF_S3_AI_CAM)
|
||||
set(BOARD_TYPE "df-s3-ai-cam")
|
||||
elseif(CONFIG_BOARD_TYPE_ESP_BOX_3)
|
||||
set(BOARD_TYPE "esp-box-3")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
set(EMOTE_RESOLUTION "320_240")
|
||||
elseif(CONFIG_BOARD_TYPE_ESP_BOX)
|
||||
set(BOARD_TYPE "esp-box")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
set(EMOTE_RESOLUTION "320_240")
|
||||
elseif(CONFIG_BOARD_TYPE_ESP_BOX_LITE)
|
||||
set(BOARD_TYPE "esp-box-lite")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_2)
|
||||
set(BOARD_TYPE "kevin-box-2")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_14_1)
|
||||
@@ -134,14 +134,14 @@ elseif(CONFIG_BOARD_TYPE_KEVIN_C3)
|
||||
set(BOARD_TYPE "kevin-c3")
|
||||
elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V3_DEV)
|
||||
set(BOARD_TYPE "kevin-sp-v3-dev")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V4_DEV)
|
||||
set(BOARD_TYPE "kevin-sp-v4-dev")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_KEVIN_YUYING_313LCD)
|
||||
set(BOARD_TYPE "kevin-yuying-313lcd")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
@@ -149,9 +149,9 @@ elseif(CONFIG_BOARD_TYPE_KEVIN_YUYING_313LCD)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV_S3)
|
||||
set(BOARD_TYPE "lichuang-dev")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV_C3)
|
||||
set(BOARD_TYPE "lichuang-c3-dev")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
@@ -201,6 +201,11 @@ elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_S3R_CAM_M12_ECHO_BASE)
|
||||
set(BOARD_TYPE "atoms3r-cam-m12-echo-base")
|
||||
elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_ECHOS3R)
|
||||
set(BOARD_TYPE "atom-echos3r")
|
||||
elseif(CONFIG_BOARD_TYPE_M5STACK_CARDPUTER_ADV)
|
||||
set(BOARD_TYPE "m5stack-cardputer-adv")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
|
||||
elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_MATRIX_ECHO_BASE)
|
||||
set(BOARD_TYPE "atommatrix-echo-base")
|
||||
elseif(CONFIG_BOARD_TYPE_XMINI_C3_V3)
|
||||
@@ -436,29 +441,29 @@ elseif(CONFIG_BOARD_TYPE_MOVECALL_CUICAN_ESP32S3)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3)
|
||||
set(BOARD_TYPE "atk-dnesp32s3")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX)
|
||||
set(BOARD_TYPE "atk-dnesp32s3-box")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX0)
|
||||
set(BOARD_TYPE "atk-dnesp32s3-box0")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_WIFI)
|
||||
set(BOARD_TYPE "atk-dnesp32s3-box2-wifi")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_4G)
|
||||
set(BOARD_TYPE "atk-dnesp32s3-box2-4g")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_WIFI)
|
||||
set(BOARD_TYPE "atk-dnesp32s3m-wifi")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
|
||||
@@ -499,24 +504,24 @@ elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_0_96OLED_ML307)
|
||||
set(BUILTIN_ICON_FONT font_awesome_14_1)
|
||||
elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_1_54TFT_WIFI)
|
||||
set(BOARD_TYPE "xingzhi-cube-1.54tft-wifi")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_1_54TFT_ML307)
|
||||
set(BOARD_TYPE "xingzhi-cube-1.54tft-ml307")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_XINGZHI_METAL_1_54_WIFI)
|
||||
set(BOARD_TYPE "xingzhi-metal-1.54-wifi")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_SEEED_STUDIO_SENSECAP_WATCHER)
|
||||
set(BOARD_TYPE "sensecap-watcher")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
set(BUILTIN_TEXT_FONT font_noto_basic_30_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(DEFAULT_EMOJI_COLLECTION noto-emoji_128)
|
||||
elseif(CONFIG_BOARD_TYPE_DOIT_S3_AIBOX)
|
||||
set(BOARD_TYPE "doit-s3-aibox")
|
||||
elseif(CONFIG_BOARD_TYPE_MIXGO_NOVA)
|
||||
@@ -586,10 +591,12 @@ elseif(CONFIG_BOARD_TYPE_OTTO_ROBOT)
|
||||
set(BOARD_TYPE "otto-robot")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_16_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_16_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION otto-gif)
|
||||
elseif(CONFIG_BOARD_TYPE_ELECTRON_BOT)
|
||||
set(BOARD_TYPE "electron-bot")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION otto-gif)
|
||||
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_CAM)
|
||||
set(BOARD_TYPE "bread-compact-wifi-s3cam")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
|
||||
@@ -777,6 +784,8 @@ if(CONFIG_IDF_TARGET_ESP32)
|
||||
"display/lvgl_display/jpg/image_to_jpeg.cpp"
|
||||
"display/lvgl_display/jpg/jpeg_to_image.c"
|
||||
"boards/common/nt26_board.cc"
|
||||
"boards/common/ml307_board.cc"
|
||||
"boards/common/dual_network_board.cc"
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -251,6 +251,9 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_M5STACK_ATOM_ECHOS3R
|
||||
bool "M5Stack AtomEchoS3R"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_M5STACK_CARDPUTER_ADV
|
||||
bool "M5Stack Cardputer Adv"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_M5STACK_ATOM_MATRIX_ECHO_BASE
|
||||
bool "M5Stack AtomMatrix + Echo Base"
|
||||
depends on IDF_TARGET_ESP32
|
||||
|
||||
@@ -368,12 +368,12 @@ void Application::CheckAssetsVersion() {
|
||||
board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE);
|
||||
display->SetChatMessage("system", Lang::Strings::PLEASE_WAIT);
|
||||
|
||||
bool success = assets.Download(download_url, [display](int progress, size_t speed) -> void {
|
||||
std::thread([display, progress, speed]() {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
|
||||
display->SetChatMessage("system", buffer);
|
||||
}).detach();
|
||||
bool success = assets.Download(download_url, [this, display](int progress, size_t speed) -> void {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
|
||||
Schedule([display, message = std::string(buffer)]() {
|
||||
display->SetChatMessage("system", message.c_str());
|
||||
});
|
||||
});
|
||||
|
||||
board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER);
|
||||
@@ -691,14 +691,16 @@ void Application::HandleToggleChatEvent() {
|
||||
}
|
||||
|
||||
if (state == kDeviceStateIdle) {
|
||||
ListeningMode mode = aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime;
|
||||
if (!protocol_->IsAudioChannelOpened()) {
|
||||
SetDeviceState(kDeviceStateConnecting);
|
||||
if (!protocol_->OpenAudioChannel()) {
|
||||
return;
|
||||
}
|
||||
// Schedule to let the state change be processed first (UI update)
|
||||
Schedule([this, mode]() {
|
||||
ContinueOpenAudioChannel(mode);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
SetListeningMode(mode);
|
||||
} else if (state == kDeviceStateSpeaking) {
|
||||
AbortSpeaking(kAbortReasonNone);
|
||||
} else if (state == kDeviceStateListening) {
|
||||
@@ -706,6 +708,21 @@ void Application::HandleToggleChatEvent() {
|
||||
}
|
||||
}
|
||||
|
||||
void Application::ContinueOpenAudioChannel(ListeningMode mode) {
|
||||
// Check state again in case it was changed during scheduling
|
||||
if (GetDeviceState() != kDeviceStateConnecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!protocol_->IsAudioChannelOpened()) {
|
||||
if (!protocol_->OpenAudioChannel()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SetListeningMode(mode);
|
||||
}
|
||||
|
||||
void Application::HandleStartListeningEvent() {
|
||||
auto state = GetDeviceState();
|
||||
|
||||
@@ -726,11 +743,12 @@ void Application::HandleStartListeningEvent() {
|
||||
if (state == kDeviceStateIdle) {
|
||||
if (!protocol_->IsAudioChannelOpened()) {
|
||||
SetDeviceState(kDeviceStateConnecting);
|
||||
if (!protocol_->OpenAudioChannel()) {
|
||||
return;
|
||||
}
|
||||
// Schedule to let the state change be processed first (UI update)
|
||||
Schedule([this]() {
|
||||
ContinueOpenAudioChannel(kListeningModeManualStop);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
SetListeningMode(kListeningModeManualStop);
|
||||
} else if (state == kDeviceStateSpeaking) {
|
||||
AbortSpeaking(kAbortReasonNone);
|
||||
@@ -762,31 +780,19 @@ void Application::HandleWakeWordDetectedEvent() {
|
||||
|
||||
if (state == kDeviceStateIdle) {
|
||||
audio_service_.EncodeWakeWord();
|
||||
auto wake_word = audio_service_.GetLastWakeWord();
|
||||
|
||||
if (!protocol_->IsAudioChannelOpened()) {
|
||||
SetDeviceState(kDeviceStateConnecting);
|
||||
if (!protocol_->OpenAudioChannel()) {
|
||||
audio_service_.EnableWakeWordDetection(true);
|
||||
return;
|
||||
}
|
||||
// Schedule to let the state change be processed first (UI update),
|
||||
// then continue with OpenAudioChannel which may block for ~1 second
|
||||
Schedule([this, wake_word]() {
|
||||
ContinueWakeWordInvoke(wake_word);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
auto wake_word = audio_service_.GetLastWakeWord();
|
||||
ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
|
||||
#if CONFIG_SEND_WAKE_WORD_DATA
|
||||
// Encode and send the wake word data to the server
|
||||
while (auto packet = audio_service_.PopWakeWordPacket()) {
|
||||
protocol_->SendAudio(std::move(packet));
|
||||
}
|
||||
// Set the chat state to wake word detected
|
||||
protocol_->SendWakeWordDetected(wake_word);
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
#else
|
||||
// Set flag to play popup sound after state changes to listening
|
||||
// (PlaySound here would be cleared by ResetDecoder in EnableVoiceProcessing)
|
||||
play_popup_on_listening_ = true;
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
#endif
|
||||
// Channel already opened, continue directly
|
||||
ContinueWakeWordInvoke(wake_word);
|
||||
} else if (state == kDeviceStateSpeaking) {
|
||||
AbortSpeaking(kAbortReasonWakeWordDetected);
|
||||
} else if (state == kDeviceStateActivating) {
|
||||
@@ -795,6 +801,36 @@ void Application::HandleWakeWordDetectedEvent() {
|
||||
}
|
||||
}
|
||||
|
||||
void Application::ContinueWakeWordInvoke(const std::string& wake_word) {
|
||||
// Check state again in case it was changed during scheduling
|
||||
if (GetDeviceState() != kDeviceStateConnecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!protocol_->IsAudioChannelOpened()) {
|
||||
if (!protocol_->OpenAudioChannel()) {
|
||||
audio_service_.EnableWakeWordDetection(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
|
||||
#if CONFIG_SEND_WAKE_WORD_DATA
|
||||
// Encode and send the wake word data to the server
|
||||
while (auto packet = audio_service_.PopWakeWordPacket()) {
|
||||
protocol_->SendAudio(std::move(packet));
|
||||
}
|
||||
// Set the chat state to wake word detected
|
||||
protocol_->SendWakeWordDetected(wake_word);
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
#else
|
||||
// Set flag to play popup sound after state changes to listening
|
||||
// (PlaySound here would be cleared by ResetDecoder in EnableVoiceProcessing)
|
||||
play_popup_on_listening_ = true;
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Application::HandleStateChangedEvent() {
|
||||
DeviceState new_state = state_machine_.GetState();
|
||||
clock_ticks_ = 0;
|
||||
@@ -808,7 +844,8 @@ void Application::HandleStateChangedEvent() {
|
||||
case kDeviceStateUnknown:
|
||||
case kDeviceStateIdle:
|
||||
display->SetStatus(Lang::Strings::STANDBY);
|
||||
display->SetEmotion("neutral");
|
||||
display->ClearChatMessages(); // Clear messages first
|
||||
display->SetEmotion("neutral"); // Then set emotion (wechat mode checks child count)
|
||||
audio_service_.EnableVoiceProcessing(false);
|
||||
audio_service_.EnableWakeWordDetection(true);
|
||||
break;
|
||||
@@ -921,12 +958,12 @@ bool Application::UpgradeFirmware(const std::string& url, const std::string& ver
|
||||
audio_service_.Stop();
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
|
||||
bool upgrade_success = Ota::Upgrade(upgrade_url, [display](int progress, size_t speed) {
|
||||
std::thread([display, progress, speed]() {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
|
||||
display->SetChatMessage("system", buffer);
|
||||
}).detach();
|
||||
bool upgrade_success = Ota::Upgrade(upgrade_url, [this, display](int progress, size_t speed) {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
|
||||
Schedule([display, message = std::string(buffer)]() {
|
||||
display->SetChatMessage("system", message.c_str());
|
||||
});
|
||||
});
|
||||
|
||||
if (!upgrade_success) {
|
||||
@@ -959,27 +996,14 @@ void Application::WakeWordInvoke(const std::string& wake_word) {
|
||||
|
||||
if (!protocol_->IsAudioChannelOpened()) {
|
||||
SetDeviceState(kDeviceStateConnecting);
|
||||
if (!protocol_->OpenAudioChannel()) {
|
||||
audio_service_.EnableWakeWordDetection(true);
|
||||
return;
|
||||
}
|
||||
// Schedule to let the state change be processed first (UI update)
|
||||
Schedule([this, wake_word]() {
|
||||
ContinueWakeWordInvoke(wake_word);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
|
||||
#if CONFIG_USE_AFE_WAKE_WORD || CONFIG_USE_CUSTOM_WAKE_WORD
|
||||
// Encode and send the wake word data to the server
|
||||
while (auto packet = audio_service_.PopWakeWordPacket()) {
|
||||
protocol_->SendAudio(std::move(packet));
|
||||
}
|
||||
// Set the chat state to wake word detected
|
||||
protocol_->SendWakeWordDetected(wake_word);
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
#else
|
||||
// Set flag to play popup sound after state changes to listening
|
||||
// (PlaySound here would be cleared by ResetDecoder in EnableVoiceProcessing)
|
||||
play_popup_on_listening_ = true;
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
#endif
|
||||
// Channel already opened, continue directly
|
||||
ContinueWakeWordInvoke(wake_word);
|
||||
} else if (state == kDeviceStateSpeaking) {
|
||||
Schedule([this]() {
|
||||
AbortSpeaking(kAbortReasonNone);
|
||||
|
||||
@@ -153,6 +153,8 @@ private:
|
||||
void HandleNetworkDisconnectedEvent();
|
||||
void HandleActivationDoneEvent();
|
||||
void HandleWakeWordDetectedEvent();
|
||||
void ContinueOpenAudioChannel(ListeningMode mode);
|
||||
void ContinueWakeWordInvoke(const std::string& wake_word);
|
||||
|
||||
// Activation task (runs in background)
|
||||
void ActivationTask();
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_timer.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <cbin_font.h>
|
||||
|
||||
|
||||
@@ -464,16 +465,21 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
|
||||
SECTOR_SIZE, content_length, sectors_to_erase, total_erase_size);
|
||||
|
||||
// 写入新的资源文件到分区,一边erase一边写入
|
||||
char buffer[512];
|
||||
char* buffer = (char*)heap_caps_malloc(SECTOR_SIZE, MALLOC_CAP_INTERNAL);
|
||||
if (buffer == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate buffer");
|
||||
return false;
|
||||
}
|
||||
size_t total_written = 0;
|
||||
size_t recent_written = 0;
|
||||
size_t current_sector = 0;
|
||||
auto last_calc_time = esp_timer_get_time();
|
||||
|
||||
while (true) {
|
||||
int ret = http->Read(buffer, sizeof(buffer));
|
||||
int ret = http->Read(buffer, SECTOR_SIZE);
|
||||
if (ret < 0) {
|
||||
ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret));
|
||||
heap_caps_free(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -493,6 +499,7 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
|
||||
// 确保擦除范围不超过分区大小
|
||||
if (sector_end > partition_->size) {
|
||||
ESP_LOGE(TAG, "Sector end (%u) exceeds partition size (%lu)", sector_end, partition_->size);
|
||||
heap_caps_free(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -500,6 +507,7 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
|
||||
esp_err_t err = esp_partition_erase_range(partition_, sector_start, SECTOR_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to erase sector %u at offset %u: %s", current_sector, sector_start, esp_err_to_name(err));
|
||||
heap_caps_free(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -510,6 +518,7 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
|
||||
esp_err_t err = esp_partition_write(partition_, total_written, buffer, ret);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write to assets partition at offset %u: %s", total_written, esp_err_to_name(err));
|
||||
heap_caps_free(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -531,6 +540,7 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
|
||||
}
|
||||
|
||||
http->Close();
|
||||
heap_caps_free(buffer);
|
||||
|
||||
if (total_written != content_length) {
|
||||
ESP_LOGE(TAG, "Downloaded size (%u) does not match expected size (%u)", total_written, content_length);
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "جاري تحميل الموارد...",
|
||||
"PLEASE_WAIT": "يرجى الانتظار...",
|
||||
"FOUND_NEW_ASSETS": "تم العثور على موارد جديدة: %s",
|
||||
"HELLO_MY_FRIEND": "مرحباً، صديقي!"
|
||||
"HELLO_MY_FRIEND": "مرحباً، صديقي!",
|
||||
"CONNECTION_SUCCESSFUL": "تم الاتصال بنجاح",
|
||||
"FLIGHT_MODE_OFF": "وضع الطيران معطل",
|
||||
"FLIGHT_MODE_ON": "وضع الطيران قيد التشغيل",
|
||||
"MODEM_INIT_ERROR": "فشل تهيئة المودم"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Намерени нови ресурси: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Неуспешно изтегляне на ресурси",
|
||||
"LOADING_ASSETS": "Зареждане на ресурси...",
|
||||
"HELLO_MY_FRIEND": "Здравей, мой приятел!"
|
||||
"HELLO_MY_FRIEND": "Здравей, мой приятел!",
|
||||
"FLIGHT_MODE_OFF": "Режим на самолет е изключен",
|
||||
"FLIGHT_MODE_ON": "Режим на самолет е включен",
|
||||
"MODEM_INIT_ERROR": "Неуспешна инициализация на модема"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "S'han trobat nous recursos: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "No s'han pogut descarregar els recursos",
|
||||
"LOADING_ASSETS": "Carregant recursos...",
|
||||
"HELLO_MY_FRIEND": "Hola, amic meu!"
|
||||
"HELLO_MY_FRIEND": "Hola, amic meu!",
|
||||
"FLIGHT_MODE_OFF": "El mode avió està desactivat",
|
||||
"FLIGHT_MODE_ON": "El mode avió està activat",
|
||||
"MODEM_INIT_ERROR": "Error d'inicialització del mòdem"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Načítání prostředků...",
|
||||
"PLEASE_WAIT": "Prosím čekejte...",
|
||||
"FOUND_NEW_ASSETS": "Nalezeny nové prostředky: %s",
|
||||
"HELLO_MY_FRIEND": "Ahoj, můj příteli!"
|
||||
"HELLO_MY_FRIEND": "Ahoj, můj příteli!",
|
||||
"CONNECTION_SUCCESSFUL": "Připojení úspěšné",
|
||||
"FLIGHT_MODE_OFF": "Letecký režim je vypnutý",
|
||||
"FLIGHT_MODE_ON": "Letecký režim je zapnutý",
|
||||
"MODEM_INIT_ERROR": "Chyba inicializace modemu"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Fandt nye ressourcer: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Download af ressourcer mislykkedes",
|
||||
"LOADING_ASSETS": "Indlæser ressourcer...",
|
||||
"HELLO_MY_FRIEND": "Hej, min ven!"
|
||||
"HELLO_MY_FRIEND": "Hej, min ven!",
|
||||
"FLIGHT_MODE_OFF": "Flytilstand er slukket",
|
||||
"FLIGHT_MODE_ON": "Flytilstand er tændt",
|
||||
"MODEM_INIT_ERROR": "Modeminitialisering mislykkedes"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Ressourcen werden geladen...",
|
||||
"PLEASE_WAIT": "Bitte warten...",
|
||||
"FOUND_NEW_ASSETS": "Neue Ressourcen gefunden: %s",
|
||||
"HELLO_MY_FRIEND": "Hallo, mein Freund!"
|
||||
"HELLO_MY_FRIEND": "Hallo, mein Freund!",
|
||||
"CONNECTION_SUCCESSFUL": "Verbindung erfolgreich",
|
||||
"FLIGHT_MODE_OFF": "Flugmodus ist deaktiviert",
|
||||
"FLIGHT_MODE_ON": "Flugmodus ist aktiviert",
|
||||
"MODEM_INIT_ERROR": "Modem-Initialisierung fehlgeschlagen"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Βρέθηκαν νέοι πόροι: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Αποτυχία λήψης πόρων",
|
||||
"LOADING_ASSETS": "Φόρτωση πόρων...",
|
||||
"HELLO_MY_FRIEND": "Γεια σου, φίλε μου!"
|
||||
"HELLO_MY_FRIEND": "Γεια σου, φίλε μου!",
|
||||
"FLIGHT_MODE_OFF": "Η λειτουργία πτήσης είναι απενεργοποιημένη",
|
||||
"FLIGHT_MODE_ON": "Η λειτουργία πτήσης είναι ενεργή",
|
||||
"MODEM_INIT_ERROR": "Αποτυχία αρχικοποίησης modem"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,6 +13,8 @@
|
||||
"REG_ERROR": "Unable to access network, please check SIM card status",
|
||||
"MODEM_INIT_ERROR": "Modem initialization failed",
|
||||
"DETECTING_MODULE": "Detecting module...",
|
||||
"FLIGHT_MODE_ON": "Flight mode is on",
|
||||
"FLIGHT_MODE_OFF": "Flight mode is off",
|
||||
"REGISTERING_NETWORK": "Waiting for network...",
|
||||
"CHECKING_NEW_VERSION": "Checking for new version...",
|
||||
"CHECK_NEW_VERSION_FAILED": "Check for new version failed, will retry in %d seconds: %s",
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Cargando recursos...",
|
||||
"PLEASE_WAIT": "Por favor espere...",
|
||||
"FOUND_NEW_ASSETS": "Encontrados nuevos recursos: %s",
|
||||
"HELLO_MY_FRIEND": "¡Hola, mi amigo!"
|
||||
"HELLO_MY_FRIEND": "¡Hola, mi amigo!",
|
||||
"CONNECTION_SUCCESSFUL": "Conexión exitosa",
|
||||
"FLIGHT_MODE_OFF": "El modo avión está desactivado",
|
||||
"FLIGHT_MODE_ON": "El modo avión está activado",
|
||||
"MODEM_INIT_ERROR": "Error de inicialización del módem"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "منابع جدید یافت شد: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "دانلود منابع ناموفق بود",
|
||||
"LOADING_ASSETS": "بارگذاری منابع...",
|
||||
"HELLO_MY_FRIEND": "سلام، دوست من!"
|
||||
"HELLO_MY_FRIEND": "سلام، دوست من!",
|
||||
"FLIGHT_MODE_OFF": "حالت پرواز خاموش است",
|
||||
"FLIGHT_MODE_ON": "حالت پرواز روشن است",
|
||||
"MODEM_INIT_ERROR": "خطا در راهاندازی مودم"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Ladataan resursseja...",
|
||||
"PLEASE_WAIT": "Odota hetki...",
|
||||
"FOUND_NEW_ASSETS": "Löydetty uusia resursseja: %s",
|
||||
"HELLO_MY_FRIEND": "Hei, ystäväni!"
|
||||
"HELLO_MY_FRIEND": "Hei, ystäväni!",
|
||||
"CONNECTION_SUCCESSFUL": "Yhteys onnistui",
|
||||
"FLIGHT_MODE_OFF": "Lentotila on pois päältä",
|
||||
"FLIGHT_MODE_ON": "Lentotila on päällä",
|
||||
"MODEM_INIT_ERROR": "Modeemin alustus epäonnistui"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Nakahanap ng mga bagong assets: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Nabigo ang pag-download ng mga assets",
|
||||
"LOADING_ASSETS": "Nilo-load ang mga assets...",
|
||||
"HELLO_MY_FRIEND": "Kumusta, kaibigan ko!"
|
||||
"HELLO_MY_FRIEND": "Kumusta, kaibigan ko!",
|
||||
"FLIGHT_MODE_OFF": "Naka-off ang flight mode",
|
||||
"FLIGHT_MODE_ON": "Naka-on ang flight mode",
|
||||
"MODEM_INIT_ERROR": "Nabigo ang pag-initialize ng modem"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Chargement des ressources...",
|
||||
"PLEASE_WAIT": "Veuillez patienter...",
|
||||
"FOUND_NEW_ASSETS": "Nouvelles ressources trouvées: %s",
|
||||
"HELLO_MY_FRIEND": "Bonjour, mon ami !"
|
||||
"HELLO_MY_FRIEND": "Bonjour, mon ami !",
|
||||
"CONNECTION_SUCCESSFUL": "Connexion réussie",
|
||||
"FLIGHT_MODE_OFF": "Le mode avion est désactivé",
|
||||
"FLIGHT_MODE_ON": "Le mode avion est activé",
|
||||
"MODEM_INIT_ERROR": "Échec de l'initialisation du modem"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "נמצאו משאבים חדשים: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "הורדת משאבים נכשלה",
|
||||
"LOADING_ASSETS": "טוען משאבים...",
|
||||
"HELLO_MY_FRIEND": "שלום, ידידי!"
|
||||
"HELLO_MY_FRIEND": "שלום, ידידי!",
|
||||
"FLIGHT_MODE_OFF": "מצב טיסה כבוי",
|
||||
"FLIGHT_MODE_ON": "מצב טיסה מופעל",
|
||||
"MODEM_INIT_ERROR": "אתחול המודם נכשל"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "संसाधन लोड हो रहे हैं...",
|
||||
"PLEASE_WAIT": "कृपया प्रतीक्षा करें...",
|
||||
"FOUND_NEW_ASSETS": "नए संसाधन मिले: %s",
|
||||
"HELLO_MY_FRIEND": "नमस्ते, मेरे दोस्त!"
|
||||
"HELLO_MY_FRIEND": "नमस्ते, मेरे दोस्त!",
|
||||
"CONNECTION_SUCCESSFUL": "कनेक्शन सफल",
|
||||
"FLIGHT_MODE_OFF": "फ़्लाइट मोड बंद है",
|
||||
"FLIGHT_MODE_ON": "फ़्लाइट मोड चालू है",
|
||||
"MODEM_INIT_ERROR": "मॉडेम आरंभीकरण विफल"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Pronađeni novi resursi: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Preuzimanje resursa nije uspjelo",
|
||||
"LOADING_ASSETS": "Učitavanje resursa...",
|
||||
"HELLO_MY_FRIEND": "Bok, moj prijatelju!"
|
||||
"HELLO_MY_FRIEND": "Bok, moj prijatelju!",
|
||||
"FLIGHT_MODE_OFF": "Način rada u zrakoplovu je isključen",
|
||||
"FLIGHT_MODE_ON": "Način rada u zrakoplovu je uključen",
|
||||
"MODEM_INIT_ERROR": "Neuspjela inicijalizacija modema"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Új erőforrások találva: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Az erőforrások letöltése sikertelen",
|
||||
"LOADING_ASSETS": "Erőforrások betöltése...",
|
||||
"HELLO_MY_FRIEND": "Helló, barátom!"
|
||||
"HELLO_MY_FRIEND": "Helló, barátom!",
|
||||
"FLIGHT_MODE_OFF": "A repülési mód ki van kapcsolva",
|
||||
"FLIGHT_MODE_ON": "A repülési mód be van kapcsolva",
|
||||
"MODEM_INIT_ERROR": "A modem inicializálása sikertelen"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Memuat aset...",
|
||||
"PLEASE_WAIT": "Mohon tunggu...",
|
||||
"FOUND_NEW_ASSETS": "Ditemukan aset baru: %s",
|
||||
"HELLO_MY_FRIEND": "Halo, teman saya!"
|
||||
"HELLO_MY_FRIEND": "Halo, teman saya!",
|
||||
"CONNECTION_SUCCESSFUL": "Koneksi berhasil",
|
||||
"FLIGHT_MODE_OFF": "Mode pesawat nonaktif",
|
||||
"FLIGHT_MODE_ON": "Mode pesawat aktif",
|
||||
"MODEM_INIT_ERROR": "Gagal menginisialisasi modem"
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Caricamento risorse...",
|
||||
"PLEASE_WAIT": "Attendere prego...",
|
||||
"FOUND_NEW_ASSETS": "Trovate nuove risorse: %s",
|
||||
"HELLO_MY_FRIEND": "Ciao, amico mio!"
|
||||
"HELLO_MY_FRIEND": "Ciao, amico mio!",
|
||||
"CONNECTION_SUCCESSFUL": "Connessione riuscita",
|
||||
"FLIGHT_MODE_OFF": "La modalità aereo è disattivata",
|
||||
"FLIGHT_MODE_ON": "La modalità aereo è attiva",
|
||||
"MODEM_INIT_ERROR": "Inizializzazione modem non riuscita"
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "アセットを読み込み中...",
|
||||
"PLEASE_WAIT": "お待ちください...",
|
||||
"FOUND_NEW_ASSETS": "新しいアセットが見つかりました: %s",
|
||||
"HELLO_MY_FRIEND": "こんにちは、友達!"
|
||||
"HELLO_MY_FRIEND": "こんにちは、友達!",
|
||||
"CONNECTION_SUCCESSFUL": "接続成功",
|
||||
"FLIGHT_MODE_OFF": "機内モードがオフです",
|
||||
"FLIGHT_MODE_ON": "機内モードがオンです",
|
||||
"MODEM_INIT_ERROR": "モデムの初期化に失敗しました"
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,9 @@
|
||||
"LOADING_ASSETS": "에셋 로딩 중...",
|
||||
"PLEASE_WAIT": "잠시 기다려 주세요...",
|
||||
"FOUND_NEW_ASSETS": "새로운 에셋을 발견했습니다: %s",
|
||||
"HELLO_MY_FRIEND": "안녕하세요, 친구!"
|
||||
"HELLO_MY_FRIEND": "안녕하세요, 친구!",
|
||||
"FLIGHT_MODE_OFF": "비행기 모드가 꺼져 있습니다",
|
||||
"FLIGHT_MODE_ON": "비행기 모드가 켜져 있습니다",
|
||||
"MODEM_INIT_ERROR": "모뎀 초기화 실패"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Menemui aset baharu: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Gagal memuat turun aset",
|
||||
"LOADING_ASSETS": "Memuatkan aset...",
|
||||
"HELLO_MY_FRIEND": "Hai, kawan saya!"
|
||||
"HELLO_MY_FRIEND": "Hai, kawan saya!",
|
||||
"FLIGHT_MODE_OFF": "Mod penerbangan dimatikan",
|
||||
"FLIGHT_MODE_ON": "Mod penerbangan dihidupkan",
|
||||
"MODEM_INIT_ERROR": "Modem gagal dimulakan"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Fant nye ressurser: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Nedlasting av ressurser mislyktes",
|
||||
"LOADING_ASSETS": "Laster ressurser...",
|
||||
"HELLO_MY_FRIEND": "Hei, min venn!"
|
||||
"HELLO_MY_FRIEND": "Hei, min venn!",
|
||||
"FLIGHT_MODE_OFF": "Flymodus er av",
|
||||
"FLIGHT_MODE_ON": "Flymodus er på",
|
||||
"MODEM_INIT_ERROR": "Modeminitialisering mislyktes"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Nieuwe bronnen gevonden: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Downloaden van bronnen mislukt",
|
||||
"LOADING_ASSETS": "Bronnen laden...",
|
||||
"HELLO_MY_FRIEND": "Hallo, mijn vriend!"
|
||||
"HELLO_MY_FRIEND": "Hallo, mijn vriend!",
|
||||
"FLIGHT_MODE_OFF": "Vliegtuigmodus is uitgeschakeld",
|
||||
"FLIGHT_MODE_ON": "Vliegtuigmodus is ingeschakeld",
|
||||
"MODEM_INIT_ERROR": "Modeminitialisatie mislukt"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Ładowanie zasobów...",
|
||||
"PLEASE_WAIT": "Proszę czekać...",
|
||||
"FOUND_NEW_ASSETS": "Znaleziono nowe zasoby: %s",
|
||||
"HELLO_MY_FRIEND": "Cześć, mój przyjacielu!"
|
||||
"HELLO_MY_FRIEND": "Cześć, mój przyjacielu!",
|
||||
"CONNECTION_SUCCESSFUL": "Połączenie udane",
|
||||
"FLIGHT_MODE_OFF": "Tryb samolotowy jest wyłączony",
|
||||
"FLIGHT_MODE_ON": "Tryb samolotowy jest włączony",
|
||||
"MODEM_INIT_ERROR": "Inicjalizacja modemu nie powiodła się"
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "A carregar recursos...",
|
||||
"PLEASE_WAIT": "Por favor aguarde...",
|
||||
"FOUND_NEW_ASSETS": "Encontrados novos recursos: %s",
|
||||
"HELLO_MY_FRIEND": "Olá, meu amigo!"
|
||||
"HELLO_MY_FRIEND": "Olá, meu amigo!",
|
||||
"CONNECTION_SUCCESSFUL": "Ligação bem-sucedida",
|
||||
"FLIGHT_MODE_OFF": "O modo avião está desativado",
|
||||
"FLIGHT_MODE_ON": "O modo avião está ativado",
|
||||
"MODEM_INIT_ERROR": "Falha na inicialização do modem"
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Se încarcă resursele...",
|
||||
"PLEASE_WAIT": "Vă rugăm să așteptați...",
|
||||
"FOUND_NEW_ASSETS": "S-au găsit resurse noi: %s",
|
||||
"HELLO_MY_FRIEND": "Salut, prietenul meu!"
|
||||
"HELLO_MY_FRIEND": "Salut, prietenul meu!",
|
||||
"CONNECTION_SUCCESSFUL": "Conexiune reușită",
|
||||
"FLIGHT_MODE_OFF": "Modul avion este dezactivat",
|
||||
"FLIGHT_MODE_ON": "Modul avion este activat",
|
||||
"MODEM_INIT_ERROR": "Inițializarea modemului a eșuat"
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Загрузка ресурсов...",
|
||||
"PLEASE_WAIT": "Пожалуйста, подождите...",
|
||||
"FOUND_NEW_ASSETS": "Найдены новые ресурсы: %s",
|
||||
"HELLO_MY_FRIEND": "Привет, мой друг!"
|
||||
"HELLO_MY_FRIEND": "Привет, мой друг!",
|
||||
"CONNECTION_SUCCESSFUL": "Подключение успешно",
|
||||
"FLIGHT_MODE_OFF": "Режим полета выключен",
|
||||
"FLIGHT_MODE_ON": "Режим полета включен",
|
||||
"MODEM_INIT_ERROR": "Ошибка инициализации модема"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Nájdené nové zdroje: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Sťahovanie zdrojov zlyhalo",
|
||||
"LOADING_ASSETS": "Načítavanie zdrojov...",
|
||||
"HELLO_MY_FRIEND": "Ahoj, môj priateľ!"
|
||||
"HELLO_MY_FRIEND": "Ahoj, môj priateľ!",
|
||||
"FLIGHT_MODE_OFF": "Letecký režim je vypnutý",
|
||||
"FLIGHT_MODE_ON": "Letecký režim je zapnutý",
|
||||
"MODEM_INIT_ERROR": "Chyba inicializácie modemu"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Najdeni novi viri: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Prenos virov ni uspel",
|
||||
"LOADING_ASSETS": "Nalaganje virov...",
|
||||
"HELLO_MY_FRIEND": "Pozdravljeni, moj prijatelj!"
|
||||
"HELLO_MY_FRIEND": "Pozdravljeni, moj prijatelj!",
|
||||
"FLIGHT_MODE_OFF": "Način leta je izklopljen",
|
||||
"FLIGHT_MODE_ON": "Način leta je vklopljen",
|
||||
"MODEM_INIT_ERROR": "Inicializacija modema ni uspela"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Пронађени нови ресурси: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Преузимање ресурса није успело",
|
||||
"LOADING_ASSETS": "Учитавање ресурса...",
|
||||
"HELLO_MY_FRIEND": "Здраво, пријатељу!"
|
||||
"HELLO_MY_FRIEND": "Здраво, пријатељу!",
|
||||
"FLIGHT_MODE_OFF": "Режим лета је искључен",
|
||||
"FLIGHT_MODE_ON": "Режим лета је укључен",
|
||||
"MODEM_INIT_ERROR": "Иницијализација модема није успела"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
"FOUND_NEW_ASSETS": "Hittade nya resurser: %s",
|
||||
"DOWNLOAD_ASSETS_FAILED": "Nedladdning av resurser misslyckades",
|
||||
"LOADING_ASSETS": "Laddar resurser...",
|
||||
"HELLO_MY_FRIEND": "Hej, min vän!"
|
||||
"HELLO_MY_FRIEND": "Hej, min vän!",
|
||||
"FLIGHT_MODE_OFF": "Flygläge är av",
|
||||
"FLIGHT_MODE_ON": "Flygläge är på",
|
||||
"MODEM_INIT_ERROR": "Modeminitiering misslyckades"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,6 +51,9 @@
|
||||
"LOADING_ASSETS": "กำลังโหลดทรัพยากร...",
|
||||
"PLEASE_WAIT": "กรุณารอสักครู่...",
|
||||
"FOUND_NEW_ASSETS": "พบทรัพยากรใหม่: %s",
|
||||
"HELLO_MY_FRIEND": "สวัสดี เพื่อนของฉัน!"
|
||||
"HELLO_MY_FRIEND": "สวัสดี เพื่อนของฉัน!",
|
||||
"FLIGHT_MODE_OFF": "โหมดเครื่องบินปิดอยู่",
|
||||
"FLIGHT_MODE_ON": "โหมดเครื่องบินเปิดอยู่",
|
||||
"MODEM_INIT_ERROR": "การเริ่มต้นโมเด็มล้มเหลว"
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Varlıklar yükleniyor...",
|
||||
"PLEASE_WAIT": "Lütfen bekleyin...",
|
||||
"FOUND_NEW_ASSETS": "Yeni varlıklar bulundu: %s",
|
||||
"HELLO_MY_FRIEND": "Merhaba, arkadaşım!"
|
||||
"HELLO_MY_FRIEND": "Merhaba, arkadaşım!",
|
||||
"CONNECTION_SUCCESSFUL": "Bağlantı başarılı",
|
||||
"FLIGHT_MODE_OFF": "Uçak modu kapalı",
|
||||
"FLIGHT_MODE_ON": "Uçak modu açık",
|
||||
"MODEM_INIT_ERROR": "Modem başlatma hatası"
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "Завантаження ресурсів...",
|
||||
"PLEASE_WAIT": "Будь ласка, зачекайте...",
|
||||
"FOUND_NEW_ASSETS": "Знайдено нові ресурси: %s",
|
||||
"HELLO_MY_FRIEND": "Привіт, мій друже!"
|
||||
"HELLO_MY_FRIEND": "Привіт, мій друже!",
|
||||
"CONNECTION_SUCCESSFUL": "Підключення успішне",
|
||||
"FLIGHT_MODE_OFF": "Режим польоту вимкнено",
|
||||
"FLIGHT_MODE_ON": "Режим польоту увімкнено",
|
||||
"MODEM_INIT_ERROR": "Помилка ініціалізації модему"
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,9 @@
|
||||
"LOADING_ASSETS": "Đang tải tài nguyên...",
|
||||
"PLEASE_WAIT": "Vui lòng đợi...",
|
||||
"FOUND_NEW_ASSETS": "Tìm thấy tài nguyên mới: %s",
|
||||
"HELLO_MY_FRIEND": "Xin chào, bạn của tôi!"
|
||||
"HELLO_MY_FRIEND": "Xin chào, bạn của tôi!",
|
||||
"FLIGHT_MODE_OFF": "Chế độ máy bay đang tắt",
|
||||
"FLIGHT_MODE_ON": "Chế độ máy bay đang bật",
|
||||
"MODEM_INIT_ERROR": "Khởi tạo modem thất bại"
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,9 @@
|
||||
"LOADING_ASSETS": "加载资源...",
|
||||
"PLEASE_WAIT": "请稍候...",
|
||||
"FOUND_NEW_ASSETS": "发现新资源: %s",
|
||||
"HELLO_MY_FRIEND": "你好,我的朋友!"
|
||||
"HELLO_MY_FRIEND": "你好,我的朋友!",
|
||||
"CONNECTION_SUCCESSFUL": "连接成功",
|
||||
"FLIGHT_MODE_OFF": "飞行模式已关闭",
|
||||
"FLIGHT_MODE_ON": "飞行模式已开启"
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,10 @@
|
||||
"LOADING_ASSETS": "載入資源...",
|
||||
"PLEASE_WAIT": "請稍候...",
|
||||
"FOUND_NEW_ASSETS": "發現新資源: %s",
|
||||
"HELLO_MY_FRIEND": "你好,我的朋友!"
|
||||
"HELLO_MY_FRIEND": "你好,我的朋友!",
|
||||
"CONNECTION_SUCCESSFUL": "連線成功",
|
||||
"FLIGHT_MODE_OFF": "飛航模式已關閉",
|
||||
"FLIGHT_MODE_ON": "飛航模式已開啟",
|
||||
"MODEM_INIT_ERROR": "模組初始化失敗"
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,9 @@ void AfeWakeWord::Feed(const std::vector<int16_t>& data) {
|
||||
if (afe_data_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!(xEventGroupGetBits(event_group_) & DETECTION_RUNNING_EVENT)) {
|
||||
return;
|
||||
}
|
||||
afe_iface_->feed(afe_data_, data.data());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "dual_network_board.h"
|
||||
#include "wifi_board.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
@@ -57,7 +57,7 @@ static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = {
|
||||
|
||||
#define TAG "ESP32-LCD-MarsbearSupport"
|
||||
|
||||
class CompactWifiBoardLCD : public DualNetworkBoard {
|
||||
class CompactWifiBoardLCD : public WifiBoard {
|
||||
private:
|
||||
Button boot_button_;
|
||||
Button touch_button_;
|
||||
@@ -136,26 +136,14 @@ private:
|
||||
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (GetNetworkType() == NetworkType::WIFI) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
// cast to WifiBoard
|
||||
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
|
||||
wifi_board.EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
gpio_set_level(BUILTIN_LED_GPIO, 1);
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
|
||||
boot_button_.OnDoubleClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
|
||||
SwitchNetworkType();
|
||||
}
|
||||
});
|
||||
|
||||
asr_button_.OnClick([this]() {
|
||||
std::string wake_word="你好小智";
|
||||
Application::GetInstance().WakeWordInvoke(wake_word);
|
||||
@@ -174,8 +162,7 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
CompactWifiBoardLCD() :
|
||||
DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN),
|
||||
CompactWifiBoardLCD() : WifiBoard(),
|
||||
boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO) {
|
||||
InitializeSpi();
|
||||
InitializeLcdDisplay();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "dual_network_board.h"
|
||||
#include "wifi_board.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
#define TAG "ESP32-MarsbearSupport"
|
||||
|
||||
class CompactWifiBoard : public DualNetworkBoard {
|
||||
class CompactWifiBoard : public WifiBoard {
|
||||
private:
|
||||
Button boot_button_;
|
||||
Button touch_button_;
|
||||
@@ -104,26 +104,14 @@ private:
|
||||
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (GetNetworkType() == NetworkType::WIFI) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
// cast to WifiBoard
|
||||
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
|
||||
wifi_board.EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
gpio_set_level(BUILTIN_LED_GPIO, 1);
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
|
||||
boot_button_.OnDoubleClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
|
||||
SwitchNetworkType();
|
||||
}
|
||||
});
|
||||
|
||||
asr_button_.OnClick([this]() {
|
||||
std::string wake_word="你好小智";
|
||||
Application::GetInstance().WakeWordInvoke(wake_word);
|
||||
@@ -145,7 +133,7 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
CompactWifiBoard() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO)
|
||||
CompactWifiBoard() : WifiBoard(), boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO)
|
||||
{
|
||||
InitializeDisplayI2c();
|
||||
InitializeSsd1306Display();
|
||||
|
||||
@@ -9,6 +9,7 @@ public:
|
||||
virtual bool Capture() = 0;
|
||||
virtual bool SetHMirror(bool enabled) = 0;
|
||||
virtual bool SetVFlip(bool enabled) = 0;
|
||||
virtual bool SetSwapBytes(bool enabled) { return false; } // Optional, default no-op
|
||||
virtual std::string Explain(const std::string& question) = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -41,6 +41,11 @@ Esp32Camera::~Esp32Camera() {
|
||||
esp_camera_fb_return(current_fb_);
|
||||
current_fb_ = nullptr;
|
||||
}
|
||||
if (encode_buf_) {
|
||||
heap_caps_free(encode_buf_);
|
||||
encode_buf_ = nullptr;
|
||||
encode_buf_size_ = 0;
|
||||
}
|
||||
esp_camera_deinit();
|
||||
streaming_on_ = false;
|
||||
}
|
||||
@@ -72,30 +77,46 @@ bool Esp32Camera::Capture() {
|
||||
}
|
||||
}
|
||||
|
||||
// Perform byte swapping for RGB565 format and prepare preview image
|
||||
// Prepare encode buffer for RGB565 format (with optional byte swapping)
|
||||
if (current_fb_->format == PIXFORMAT_RGB565) {
|
||||
size_t pixel_count = current_fb_->width * current_fb_->height;
|
||||
size_t data_size = pixel_count * 2;
|
||||
|
||||
uint8_t *preview_data = (uint8_t *)heap_caps_malloc(data_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (preview_data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for preview image");
|
||||
return false;
|
||||
// Allocate or reallocate encode buffer if needed
|
||||
if (encode_buf_size_ < data_size) {
|
||||
if (encode_buf_) {
|
||||
heap_caps_free(encode_buf_);
|
||||
}
|
||||
encode_buf_ = (uint8_t *)heap_caps_malloc(data_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (encode_buf_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for encode buffer");
|
||||
encode_buf_size_ = 0;
|
||||
return false;
|
||||
}
|
||||
encode_buf_size_ = data_size;
|
||||
}
|
||||
|
||||
// Copy data to encode buffer with optional byte swapping
|
||||
uint16_t *src = (uint16_t *)current_fb_->buf;
|
||||
uint16_t *dst = (uint16_t *)preview_data;
|
||||
for (size_t i = 0; i < pixel_count; i++) {
|
||||
// Copy data from driver buffer to preview buffer with byte swapping
|
||||
dst[i] = __builtin_bswap16(src[i]);
|
||||
uint16_t *dst = (uint16_t *)encode_buf_;
|
||||
if (swap_bytes_enabled_) {
|
||||
for (size_t i = 0; i < pixel_count; i++) {
|
||||
dst[i] = __builtin_bswap16(src[i]);
|
||||
}
|
||||
} else {
|
||||
memcpy(encode_buf_, current_fb_->buf, data_size);
|
||||
}
|
||||
|
||||
// Display preview image
|
||||
auto display = dynamic_cast<LvglDisplay *>(Board::GetInstance().GetDisplay());
|
||||
if (display != nullptr) {
|
||||
display->SetPreviewImage(std::make_unique<LvglAllocatedImage>(preview_data, data_size, current_fb_->width, current_fb_->height, current_fb_->width * 2, LV_COLOR_FORMAT_RGB565));
|
||||
} else {
|
||||
heap_caps_free(preview_data);
|
||||
// Allocate separate buffer for preview display
|
||||
uint8_t *preview_data = (uint8_t *)heap_caps_malloc(data_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (preview_data != nullptr) {
|
||||
memcpy(preview_data, encode_buf_, data_size);
|
||||
auto display = dynamic_cast<LvglDisplay *>(Board::GetInstance().GetDisplay());
|
||||
if (display != nullptr) {
|
||||
display->SetPreviewImage(std::make_unique<LvglAllocatedImage>(preview_data, data_size, current_fb_->width, current_fb_->height, current_fb_->width * 2, LV_COLOR_FORMAT_RGB565));
|
||||
} else {
|
||||
heap_caps_free(preview_data);
|
||||
}
|
||||
}
|
||||
} else if (current_fb_->format == PIXFORMAT_JPEG) {
|
||||
// JPEG format preview usually requires decoding, skip preview display for now, just log
|
||||
@@ -126,6 +147,11 @@ bool Esp32Camera::SetVFlip(bool enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Esp32Camera::SetSwapBytes(bool enabled) {
|
||||
swap_bytes_enabled_ = enabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Esp32Camera::Explain(const std::string &question) {
|
||||
if (explain_url_.empty()) {
|
||||
throw std::runtime_error("Image explain URL or token is not set");
|
||||
@@ -172,7 +198,15 @@ std::string Esp32Camera::Explain(const std::string &question) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = image_to_jpeg_cb(current_fb_->buf, current_fb_->len, w, h, enc_fmt, 80,
|
||||
// Use encode buffer for RGB565, otherwise use original frame buffer
|
||||
uint8_t *jpeg_src_buf = current_fb_->buf;
|
||||
size_t jpeg_src_len = current_fb_->len;
|
||||
if (current_fb_->format == PIXFORMAT_RGB565 && encode_buf_ != nullptr) {
|
||||
jpeg_src_buf = encode_buf_;
|
||||
jpeg_src_len = encode_buf_size_;
|
||||
}
|
||||
|
||||
bool ok = image_to_jpeg_cb(jpeg_src_buf, jpeg_src_len, w, h, enc_fmt, 80,
|
||||
[](void* arg, size_t index, const void* data, size_t len) -> size_t {
|
||||
auto jpeg_queue = static_cast<QueueHandle_t>(arg);
|
||||
JpegChunk chunk = {.data = nullptr, .len = len};
|
||||
|
||||
@@ -23,10 +23,13 @@ class Esp32Camera : public Camera
|
||||
{
|
||||
private:
|
||||
bool streaming_on_ = false;
|
||||
bool swap_bytes_enabled_ = true; // Swap pixel byte order for RGB565, enabled by default
|
||||
std::string explain_url_;
|
||||
std::string explain_token_;
|
||||
std::thread encoder_thread_;
|
||||
camera_fb_t *current_fb_ = nullptr;
|
||||
uint8_t *encode_buf_ = nullptr; // Buffer for JPEG encoding (with optional byte swap)
|
||||
size_t encode_buf_size_ = 0;
|
||||
|
||||
public:
|
||||
Esp32Camera(const camera_config_t &config);
|
||||
@@ -36,5 +39,6 @@ public:
|
||||
virtual bool Capture() override;
|
||||
virtual bool SetHMirror(bool enabled) override;
|
||||
virtual bool SetVFlip(bool enabled) override;
|
||||
virtual bool SetSwapBytes(bool enabled) override;
|
||||
virtual std::string Explain(const std::string &question) override;
|
||||
};
|
||||
|
||||
@@ -107,6 +107,9 @@ void Nt26Board::StartNetwork() {
|
||||
ScheduleAsyncStop();
|
||||
OnNetworkEvent(NetworkEvent::ModemErrorInitFailed);
|
||||
break;
|
||||
case UartEthModem::UartEthModemEvent::InFlightMode:
|
||||
ESP_LOGW(TAG, "Modem in flight mode");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
#include <esp_log.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "assets.h"
|
||||
#include "assets/lang_config.h"
|
||||
#include "display/lvgl_display/emoji_collection.h"
|
||||
#include "display/lvgl_display/lvgl_image.h"
|
||||
#include "display/lvgl_display/lvgl_theme.h"
|
||||
#include "otto_emoji_gif.h"
|
||||
|
||||
#define TAG "ElectronEmojiDisplay"
|
||||
ElectronEmojiDisplay::ElectronEmojiDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y,
|
||||
@@ -19,64 +20,12 @@ ElectronEmojiDisplay::ElectronEmojiDisplay(esp_lcd_panel_io_handle_t panel_io, e
|
||||
}
|
||||
|
||||
void ElectronEmojiDisplay::InitializeElectronEmojis() {
|
||||
ESP_LOGI(TAG, "初始化Electron GIF表情");
|
||||
|
||||
auto otto_emoji_collection = std::make_shared<EmojiCollection>();
|
||||
|
||||
// 中性/平静类表情 -> staticstate
|
||||
otto_emoji_collection->AddEmoji("staticstate", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("neutral", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("relaxed", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("sleepy", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("idle", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
|
||||
// 积极/开心类表情 -> happy
|
||||
otto_emoji_collection->AddEmoji("happy", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("laughing", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("funny", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("loving", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("confident", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("winking", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("cool", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("delicious", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("kissy", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("silly", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
|
||||
// 悲伤类表情 -> sad
|
||||
otto_emoji_collection->AddEmoji("sad", new LvglRawImage((void*)sad.data, sad.data_size));
|
||||
otto_emoji_collection->AddEmoji("crying", new LvglRawImage((void*)sad.data, sad.data_size));
|
||||
|
||||
// 愤怒类表情 -> anger
|
||||
otto_emoji_collection->AddEmoji("anger", new LvglRawImage((void*)anger.data, anger.data_size));
|
||||
otto_emoji_collection->AddEmoji("angry", new LvglRawImage((void*)anger.data, anger.data_size));
|
||||
|
||||
// 惊讶类表情 -> scare
|
||||
otto_emoji_collection->AddEmoji("scare", new LvglRawImage((void*)scare.data, scare.data_size));
|
||||
otto_emoji_collection->AddEmoji("surprised", new LvglRawImage((void*)scare.data, scare.data_size));
|
||||
otto_emoji_collection->AddEmoji("shocked", new LvglRawImage((void*)scare.data, scare.data_size));
|
||||
|
||||
// 思考/困惑类表情 -> buxue
|
||||
otto_emoji_collection->AddEmoji("buxue", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
otto_emoji_collection->AddEmoji("thinking", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
otto_emoji_collection->AddEmoji("confused", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
otto_emoji_collection->AddEmoji("embarrassed", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
|
||||
// 将表情集合添加到主题中
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto light_theme = theme_manager.GetTheme("light");
|
||||
auto dark_theme = theme_manager.GetTheme("dark");
|
||||
|
||||
if (light_theme != nullptr) {
|
||||
light_theme->set_emoji_collection(otto_emoji_collection);
|
||||
}
|
||||
if (dark_theme != nullptr) {
|
||||
dark_theme->set_emoji_collection(otto_emoji_collection);
|
||||
}
|
||||
ESP_LOGI(TAG, "Electron表情初始化将由Assets系统处理");
|
||||
// 表情初始化已移至assets系统,通过DEFAULT_EMOJI_COLLECTION=otto-gif配置
|
||||
// assets.cc会从assets分区加载GIF表情并设置到theme
|
||||
|
||||
// 设置默认表情为staticstate
|
||||
SetEmotion("staticstate");
|
||||
|
||||
ESP_LOGI(TAG, "Electron GIF表情初始化完成");
|
||||
}
|
||||
|
||||
void ElectronEmojiDisplay::SetupChatLabel() {
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
{
|
||||
"name": "esp-box-3",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_USE_DEVICE_AEC=y",
|
||||
"CONFIG_USE_EMOTE_MESSAGE_STYLE=y",
|
||||
"CONFIG_FLASH_CUSTOM_ASSETS=y",
|
||||
"CONFIG_CUSTOM_ASSETS_FILE=\"https://dl.espressif.com/AE/wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-esp-box-3.bin\""
|
||||
"CONFIG_USE_DEVICE_AEC=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
48
main/boards/m5stack-cardputer-adv/README.md
Normal file
48
main/boards/m5stack-cardputer-adv/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# M5Stack Cardputer Adv
|
||||
|
||||
M5Stack Cardputer Adv 是一款基于 ESP32-S3FN8 (Stamp-S3A) 的卡片式电脑。
|
||||
|
||||
## 硬件规格
|
||||
|
||||
| 组件 | 规格 |
|
||||
|------|------|
|
||||
| MCU | ESP32-S3FN8 @ 240MHz |
|
||||
| Flash | 8MB |
|
||||
| 显示屏 | ST7789V2 1.14" 240x135 |
|
||||
| 音频编解码 | ES8311 |
|
||||
| 功放 | NS4150B |
|
||||
| 麦克风 | MEMS |
|
||||
| 键盘 | 56键 (TCA8418) |
|
||||
| IMU | BMI270 |
|
||||
| 电池 | 1750mAh |
|
||||
|
||||
## 引脚定义
|
||||
|
||||
### 显示屏 (ST7789V2)
|
||||
| 功能 | GPIO |
|
||||
|------|------|
|
||||
| MOSI | GPIO35 |
|
||||
| SCLK | GPIO36 |
|
||||
| CS | GPIO37 |
|
||||
| DC | GPIO34 |
|
||||
| RST | GPIO33 |
|
||||
| BL | GPIO38 |
|
||||
|
||||
### 音频 (ES8311)
|
||||
| 功能 | GPIO |
|
||||
|------|------|
|
||||
| I2C SDA | GPIO8 |
|
||||
| I2C SCL | GPIO9 |
|
||||
| I2S BCLK | GPIO41 |
|
||||
| I2S LRCK | GPIO43 |
|
||||
| I2S DOUT | GPIO46 |
|
||||
| I2S DIN | GPIO42 |
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 按下 BOOT 按钮进入配网模式
|
||||
2. 连接 WiFi 后即可使用语音助手功能
|
||||
|
||||
## 参考链接
|
||||
|
||||
- [M5Stack Cardputer Adv 官方文档](https://docs.m5stack.com/en/core/Cardputer-Adv)
|
||||
58
main/boards/m5stack-cardputer-adv/config.h
Normal file
58
main/boards/m5stack-cardputer-adv/config.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#ifndef _BOARD_CONFIG_H_
|
||||
#define _BOARD_CONFIG_H_
|
||||
|
||||
// M5Stack Cardputer Adv Board configuration
|
||||
// MCU: ESP32-S3FN8 (Stamp-S3A)
|
||||
// Display: ST7789V2 1.14" 240x135
|
||||
// Audio: ES8311 + NS4150B
|
||||
|
||||
#include <driver/gpio.h>
|
||||
|
||||
// Audio settings
|
||||
#define AUDIO_INPUT_SAMPLE_RATE 24000
|
||||
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
|
||||
|
||||
// I2S Audio pins (ES8311)
|
||||
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_NC
|
||||
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_41 // SCLK
|
||||
#define AUDIO_I2S_GPIO_WS GPIO_NUM_43 // LRCK
|
||||
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_42 // DSDIN (MCU -> ES8311)
|
||||
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_46 // ASDOUT (ES8311 -> MCU)
|
||||
|
||||
// I2C pins (shared for ES8311, TCA8418, BMI270)
|
||||
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_8
|
||||
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_9
|
||||
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
|
||||
#define AUDIO_CODEC_PA_PIN GPIO_NUM_NC // NS4150B is always on
|
||||
|
||||
// Button
|
||||
#define BOOT_BUTTON_GPIO GPIO_NUM_0
|
||||
#define BUILTIN_LED_GPIO GPIO_NUM_NC
|
||||
#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC
|
||||
#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC
|
||||
|
||||
// Display ST7789V2 (SPI)
|
||||
#define DISPLAY_WIDTH 240
|
||||
#define DISPLAY_HEIGHT 135
|
||||
#define DISPLAY_MIRROR_X true
|
||||
#define DISPLAY_MIRROR_Y false
|
||||
#define DISPLAY_SWAP_XY true
|
||||
|
||||
#define DISPLAY_OFFSET_X 40
|
||||
#define DISPLAY_OFFSET_Y 52
|
||||
|
||||
#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_35
|
||||
#define DISPLAY_SPI_SCLK_PIN GPIO_NUM_36
|
||||
#define DISPLAY_SPI_CS_PIN GPIO_NUM_37
|
||||
#define DISPLAY_DC_PIN GPIO_NUM_34
|
||||
#define DISPLAY_RST_PIN GPIO_NUM_33
|
||||
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_38
|
||||
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
|
||||
|
||||
// Keyboard TCA8418 I2C address
|
||||
#define KEYBOARD_TCA8418_ADDR 0x34
|
||||
|
||||
// IMU BMI270 I2C address
|
||||
#define IMU_BMI270_ADDR 0x68
|
||||
|
||||
#endif // _BOARD_CONFIG_H_
|
||||
13
main/boards/m5stack-cardputer-adv/config.json
Normal file
13
main/boards/m5stack-cardputer-adv/config.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"target": "esp32s3",
|
||||
"builds": [
|
||||
{
|
||||
"name": "m5stack-cardputer-adv",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_SPIRAM=n",
|
||||
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
158
main/boards/m5stack-cardputer-adv/m5stack_cardputer_adv.cc
Normal file
158
main/boards/m5stack-cardputer-adv/m5stack_cardputer_adv.cc
Normal file
@@ -0,0 +1,158 @@
|
||||
#include "wifi_board.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "i2c_device.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/spi_common.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
|
||||
#define TAG "CardputerAdv"
|
||||
|
||||
class M5StackCardputerAdvBoard : public WifiBoard {
|
||||
private:
|
||||
i2c_master_bus_handle_t i2c_bus_;
|
||||
LcdDisplay* display_;
|
||||
Button boot_button_;
|
||||
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
|
||||
esp_lcd_panel_handle_t panel_ = nullptr;
|
||||
|
||||
void InitializeI2c() {
|
||||
ESP_LOGI(TAG, "Initialize I2C bus");
|
||||
i2c_master_bus_config_t i2c_bus_cfg = {
|
||||
.i2c_port = I2C_NUM_0,
|
||||
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
|
||||
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.intr_priority = 0,
|
||||
.trans_queue_depth = 0,
|
||||
.flags = {
|
||||
.enable_internal_pullup = 1,
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
|
||||
}
|
||||
|
||||
void I2cDetect() {
|
||||
uint8_t address;
|
||||
ESP_LOGI(TAG, "I2C device scan:");
|
||||
printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n");
|
||||
for (int i = 0; i < 128; i += 16) {
|
||||
printf("%02x: ", i);
|
||||
for (int j = 0; j < 16; j++) {
|
||||
fflush(stdout);
|
||||
address = i + j;
|
||||
esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200));
|
||||
if (ret == ESP_OK) {
|
||||
printf("%02x ", address);
|
||||
} else if (ret == ESP_ERR_TIMEOUT) {
|
||||
printf("UU ");
|
||||
} else {
|
||||
printf("-- ");
|
||||
}
|
||||
}
|
||||
printf("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeSpi() {
|
||||
ESP_LOGI(TAG, "Initialize SPI bus");
|
||||
spi_bus_config_t buscfg = {};
|
||||
buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN;
|
||||
buscfg.miso_io_num = GPIO_NUM_NC;
|
||||
buscfg.sclk_io_num = DISPLAY_SPI_SCLK_PIN;
|
||||
buscfg.quadwp_io_num = GPIO_NUM_NC;
|
||||
buscfg.quadhd_io_num = GPIO_NUM_NC;
|
||||
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
|
||||
ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
|
||||
}
|
||||
|
||||
void InitializeSt7789Display() {
|
||||
ESP_LOGI(TAG, "Initialize ST7789V2 display");
|
||||
|
||||
esp_lcd_panel_io_spi_config_t io_config = {};
|
||||
io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN;
|
||||
io_config.dc_gpio_num = DISPLAY_DC_PIN;
|
||||
io_config.spi_mode = 0;
|
||||
io_config.pclk_hz = 40 * 1000 * 1000;
|
||||
io_config.trans_queue_depth = 10;
|
||||
io_config.lcd_cmd_bits = 8;
|
||||
io_config.lcd_param_bits = 8;
|
||||
io_config.flags.sio_mode = 1; // 3-wire SPI mode (M5GFX uses spi_3wire = true)
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io_));
|
||||
|
||||
ESP_LOGI(TAG, "Install ST7789 panel driver");
|
||||
esp_lcd_panel_dev_config_t panel_config = {};
|
||||
panel_config.reset_gpio_num = DISPLAY_RST_PIN;
|
||||
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
|
||||
panel_config.bits_per_pixel = 16;
|
||||
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io_, &panel_config, &panel_));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_, true));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel_, DISPLAY_SWAP_XY));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y));
|
||||
|
||||
display_ = new SpiLcdDisplay(panel_io_, panel_,
|
||||
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y,
|
||||
DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
}
|
||||
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
}
|
||||
|
||||
public:
|
||||
M5StackCardputerAdvBoard() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeI2c();
|
||||
I2cDetect();
|
||||
InitializeSpi();
|
||||
InitializeSt7789Display();
|
||||
InitializeButtons();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
static Es8311AudioCodec audio_codec(
|
||||
i2c_bus_,
|
||||
I2C_NUM_0,
|
||||
AUDIO_INPUT_SAMPLE_RATE,
|
||||
AUDIO_OUTPUT_SAMPLE_RATE,
|
||||
AUDIO_I2S_GPIO_MCLK,
|
||||
AUDIO_I2S_GPIO_BCLK,
|
||||
AUDIO_I2S_GPIO_WS,
|
||||
AUDIO_I2S_GPIO_DOUT,
|
||||
AUDIO_I2S_GPIO_DIN,
|
||||
AUDIO_CODEC_PA_PIN,
|
||||
AUDIO_CODEC_ES8311_ADDR,
|
||||
false); // use_mclk = false, Cardputer Adv has no MCLK pin
|
||||
return &audio_codec;
|
||||
}
|
||||
|
||||
virtual Display* GetDisplay() override {
|
||||
return display_;
|
||||
}
|
||||
|
||||
virtual Backlight* GetBacklight() override {
|
||||
// M5GFX uses 256Hz PWM frequency for Cardputer backlight
|
||||
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT, 256);
|
||||
return &backlight;
|
||||
}
|
||||
};
|
||||
|
||||
DECLARE_BOARD(M5StackCardputerAdvBoard);
|
||||
@@ -1,44 +1,63 @@
|
||||
#ifndef _BOARD_CONFIG_H_
|
||||
#define _BOARD_CONFIG_H_
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/adc.h>
|
||||
#include <driver/gpio.h>
|
||||
|
||||
#define OTTO_VERSION_AUTO 0
|
||||
#define OTTO_VERSION_CAMERA 1
|
||||
#define OTTO_VERSION_NO_CAMERA 2
|
||||
|
||||
#ifndef OTTO_HARDWARE_VERSION
|
||||
#define OTTO_HARDWARE_VERSION OTTO_VERSION_AUTO
|
||||
#endif
|
||||
|
||||
enum OttoCameraType {
|
||||
OTTO_CAMERA_NONE = 0,
|
||||
OTTO_CAMERA_OV2640 = 1,
|
||||
OTTO_CAMERA_OV3660 = 2,
|
||||
OTTO_CAMERA_UNKNOWN = 99,
|
||||
};
|
||||
|
||||
#define OV2640_PID_1 0x2640
|
||||
#define OV2640_PID_2 0x2626
|
||||
#define OV3660_PID 0x3660
|
||||
|
||||
struct HardwareConfig {
|
||||
gpio_num_t power_charge_detect_pin;
|
||||
adc_unit_t power_adc_unit;
|
||||
adc_channel_t power_adc_channel;
|
||||
|
||||
|
||||
gpio_num_t right_leg_pin;
|
||||
gpio_num_t right_foot_pin;
|
||||
gpio_num_t left_leg_pin;
|
||||
gpio_num_t left_foot_pin;
|
||||
gpio_num_t left_hand_pin;
|
||||
gpio_num_t right_hand_pin;
|
||||
|
||||
|
||||
int audio_input_sample_rate;
|
||||
int audio_output_sample_rate;
|
||||
bool audio_use_simplex;
|
||||
|
||||
|
||||
gpio_num_t audio_i2s_gpio_ws;
|
||||
gpio_num_t audio_i2s_gpio_bclk;
|
||||
gpio_num_t audio_i2s_gpio_din;
|
||||
gpio_num_t audio_i2s_gpio_dout;
|
||||
|
||||
|
||||
gpio_num_t audio_i2s_mic_gpio_ws;
|
||||
gpio_num_t audio_i2s_mic_gpio_sck;
|
||||
gpio_num_t audio_i2s_mic_gpio_din;
|
||||
gpio_num_t audio_i2s_spk_gpio_dout;
|
||||
gpio_num_t audio_i2s_spk_gpio_bclk;
|
||||
gpio_num_t audio_i2s_spk_gpio_lrck;
|
||||
|
||||
|
||||
gpio_num_t display_backlight_pin;
|
||||
gpio_num_t display_mosi_pin;
|
||||
gpio_num_t display_clk_pin;
|
||||
gpio_num_t display_dc_pin;
|
||||
gpio_num_t display_rst_pin;
|
||||
gpio_num_t display_cs_pin;
|
||||
|
||||
|
||||
gpio_num_t i2c_sda_pin;
|
||||
gpio_num_t i2c_scl_pin;
|
||||
};
|
||||
@@ -47,37 +66,37 @@ constexpr HardwareConfig CAMERA_VERSION_CONFIG = {
|
||||
.power_charge_detect_pin = GPIO_NUM_NC,
|
||||
.power_adc_unit = ADC_UNIT_1,
|
||||
.power_adc_channel = ADC_CHANNEL_1,
|
||||
|
||||
|
||||
.right_leg_pin = GPIO_NUM_43,
|
||||
.right_foot_pin = GPIO_NUM_44,
|
||||
.left_leg_pin = GPIO_NUM_5,
|
||||
.left_foot_pin = GPIO_NUM_6,
|
||||
.left_hand_pin = GPIO_NUM_4,
|
||||
.right_hand_pin = GPIO_NUM_7,
|
||||
|
||||
|
||||
.audio_input_sample_rate = 16000,
|
||||
.audio_output_sample_rate = 16000,
|
||||
.audio_use_simplex = false,
|
||||
|
||||
|
||||
.audio_i2s_gpio_ws = GPIO_NUM_40,
|
||||
.audio_i2s_gpio_bclk = GPIO_NUM_42,
|
||||
.audio_i2s_gpio_din = GPIO_NUM_41,
|
||||
.audio_i2s_gpio_dout = GPIO_NUM_39,
|
||||
|
||||
|
||||
.audio_i2s_mic_gpio_ws = GPIO_NUM_NC,
|
||||
.audio_i2s_mic_gpio_sck = GPIO_NUM_NC,
|
||||
.audio_i2s_mic_gpio_din = GPIO_NUM_NC,
|
||||
.audio_i2s_spk_gpio_dout = GPIO_NUM_NC,
|
||||
.audio_i2s_spk_gpio_bclk = GPIO_NUM_NC,
|
||||
.audio_i2s_spk_gpio_lrck = GPIO_NUM_NC,
|
||||
|
||||
|
||||
.display_backlight_pin = GPIO_NUM_38,
|
||||
.display_mosi_pin = GPIO_NUM_45,
|
||||
.display_clk_pin = GPIO_NUM_48,
|
||||
.display_dc_pin = GPIO_NUM_47,
|
||||
.display_rst_pin = GPIO_NUM_1,
|
||||
.display_cs_pin = GPIO_NUM_NC,
|
||||
|
||||
|
||||
.i2c_sda_pin = GPIO_NUM_15,
|
||||
.i2c_scl_pin = GPIO_NUM_16,
|
||||
};
|
||||
@@ -86,37 +105,37 @@ constexpr HardwareConfig NON_CAMERA_VERSION_CONFIG = {
|
||||
.power_charge_detect_pin = GPIO_NUM_21,
|
||||
.power_adc_unit = ADC_UNIT_2,
|
||||
.power_adc_channel = ADC_CHANNEL_3,
|
||||
|
||||
|
||||
.right_leg_pin = GPIO_NUM_39,
|
||||
.right_foot_pin = GPIO_NUM_38,
|
||||
.left_leg_pin = GPIO_NUM_17,
|
||||
.left_foot_pin = GPIO_NUM_18,
|
||||
.left_hand_pin = GPIO_NUM_8,
|
||||
.right_hand_pin = GPIO_NUM_12,
|
||||
|
||||
|
||||
.audio_input_sample_rate = 16000,
|
||||
.audio_output_sample_rate = 24000,
|
||||
.audio_use_simplex = true,
|
||||
|
||||
|
||||
.audio_i2s_gpio_ws = GPIO_NUM_NC,
|
||||
.audio_i2s_gpio_bclk = GPIO_NUM_NC,
|
||||
.audio_i2s_gpio_din = GPIO_NUM_NC,
|
||||
.audio_i2s_gpio_dout = GPIO_NUM_NC,
|
||||
|
||||
|
||||
.audio_i2s_mic_gpio_ws = GPIO_NUM_4,
|
||||
.audio_i2s_mic_gpio_sck = GPIO_NUM_5,
|
||||
.audio_i2s_mic_gpio_din = GPIO_NUM_6,
|
||||
.audio_i2s_spk_gpio_dout = GPIO_NUM_7,
|
||||
.audio_i2s_spk_gpio_bclk = GPIO_NUM_15,
|
||||
.audio_i2s_spk_gpio_lrck = GPIO_NUM_16,
|
||||
|
||||
|
||||
.display_backlight_pin = GPIO_NUM_3,
|
||||
.display_mosi_pin = GPIO_NUM_10,
|
||||
.display_clk_pin = GPIO_NUM_9,
|
||||
.display_dc_pin = GPIO_NUM_46,
|
||||
.display_rst_pin = GPIO_NUM_11,
|
||||
.display_cs_pin = GPIO_NUM_12,
|
||||
|
||||
|
||||
.i2c_sda_pin = GPIO_NUM_NC,
|
||||
.i2c_scl_pin = GPIO_NUM_NC,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
"CONFIG_HTTPD_WS_SUPPORT=y",
|
||||
"CONFIG_CAMERA_OV2640=y",
|
||||
"CONFIG_CAMERA_OV2640_AUTO_DETECT_DVP_INTERFACE_SENSOR=y",
|
||||
"CONFIG_CAMERA_OV2640_DVP_YUV422_240X240_25FPS=y"
|
||||
"CONFIG_CAMERA_OV2640_DVP_YUV422_240X240_25FPS=y",
|
||||
"CONFIG_CAMERA_OV3660=y",
|
||||
"CONFIG_CAMERA_OV3660_AUTO_DETECT_DVP_INTERFACE_SENSOR=y",
|
||||
"CONFIG_CAMERA_OV3660_DVP_YUV422_240X240_24FPS=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
#include <esp_log.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "assets.h"
|
||||
#include "assets/lang_config.h"
|
||||
#include "display/lvgl_display/emoji_collection.h"
|
||||
#include "display/lvgl_display/lvgl_image.h"
|
||||
#include "display/lvgl_display/lvgl_theme.h"
|
||||
#include "otto_emoji_gif.h"
|
||||
|
||||
#define TAG "OttoEmojiDisplay"
|
||||
OttoEmojiDisplay::OttoEmojiDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy)
|
||||
@@ -24,64 +25,12 @@ void OttoEmojiDisplay::SetupPreviewImage() {
|
||||
}
|
||||
|
||||
void OttoEmojiDisplay::InitializeOttoEmojis() {
|
||||
ESP_LOGI(TAG, "初始化Otto GIF表情");
|
||||
|
||||
auto otto_emoji_collection = std::make_shared<EmojiCollection>();
|
||||
|
||||
// 中性/平静类表情 -> staticstate
|
||||
otto_emoji_collection->AddEmoji("staticstate", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("neutral", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("relaxed", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("sleepy", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("idle", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
|
||||
// 积极/开心类表情 -> happy
|
||||
otto_emoji_collection->AddEmoji("happy", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("laughing", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("funny", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("loving", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("confident", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("winking", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("cool", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("delicious", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("kissy", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("silly", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
|
||||
// 悲伤类表情 -> sad
|
||||
otto_emoji_collection->AddEmoji("sad", new LvglRawImage((void*)sad.data, sad.data_size));
|
||||
otto_emoji_collection->AddEmoji("crying", new LvglRawImage((void*)sad.data, sad.data_size));
|
||||
|
||||
// 愤怒类表情 -> anger
|
||||
otto_emoji_collection->AddEmoji("anger", new LvglRawImage((void*)anger.data, anger.data_size));
|
||||
otto_emoji_collection->AddEmoji("angry", new LvglRawImage((void*)anger.data, anger.data_size));
|
||||
|
||||
// 惊讶类表情 -> scare
|
||||
otto_emoji_collection->AddEmoji("scare", new LvglRawImage((void*)scare.data, scare.data_size));
|
||||
otto_emoji_collection->AddEmoji("surprised", new LvglRawImage((void*)scare.data, scare.data_size));
|
||||
otto_emoji_collection->AddEmoji("shocked", new LvglRawImage((void*)scare.data, scare.data_size));
|
||||
|
||||
// 思考/困惑类表情 -> buxue
|
||||
otto_emoji_collection->AddEmoji("buxue", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
otto_emoji_collection->AddEmoji("thinking", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
otto_emoji_collection->AddEmoji("confused", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
otto_emoji_collection->AddEmoji("embarrassed", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
|
||||
// 将表情集合添加到主题中
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto light_theme = theme_manager.GetTheme("light");
|
||||
auto dark_theme = theme_manager.GetTheme("dark");
|
||||
|
||||
if (light_theme != nullptr) {
|
||||
light_theme->set_emoji_collection(otto_emoji_collection);
|
||||
}
|
||||
if (dark_theme != nullptr) {
|
||||
dark_theme->set_emoji_collection(otto_emoji_collection);
|
||||
}
|
||||
ESP_LOGI(TAG, "Otto表情初始化将由Assets系统处理");
|
||||
// 表情初始化已移至assets系统,通过DEFAULT_EMOJI_COLLECTION=otto-gif配置
|
||||
// assets.cc会从assets分区加载GIF表情并设置到theme
|
||||
|
||||
// 设置默认表情为staticstate
|
||||
SetEmotion("staticstate");
|
||||
|
||||
ESP_LOGI(TAG, "Otto GIF表情初始化完成");
|
||||
}
|
||||
|
||||
LV_FONT_DECLARE(OTTO_ICON_FONT);
|
||||
@@ -148,7 +97,7 @@ void OttoEmojiDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
|
||||
auto img_dsc = preview_image_cached_->image_dsc();
|
||||
// 设置图片源并显示预览图片
|
||||
lv_image_set_src(preview_image_, img_dsc);
|
||||
lv_image_set_rotation(preview_image_, -900);
|
||||
lv_image_set_rotation(preview_image_, 900);
|
||||
if (img_dsc->header.w > 0 && img_dsc->header.h > 0) {
|
||||
// zoom factor 1.0
|
||||
lv_image_set_scale(preview_image_, 256 * width_ / img_dsc->header.w);
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/spi_common.h>
|
||||
#include <driver/ledc.h>
|
||||
#include <driver/spi_common.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
#include "application.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "button.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "config.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "esp_video.h"
|
||||
#include "lamp_controller.h"
|
||||
#include "led/single_led.h"
|
||||
#include "mcp_server.h"
|
||||
#include "otto_emoji_display.h"
|
||||
#include "power_manager.h"
|
||||
#include "system_reset.h"
|
||||
#include "wifi_board.h"
|
||||
#include "esp_video.h"
|
||||
#include "websocket_control_server.h"
|
||||
#include "wifi_board.h"
|
||||
|
||||
#define TAG "OttoRobot"
|
||||
|
||||
@@ -34,9 +34,10 @@ private:
|
||||
HardwareConfig hw_config_;
|
||||
AudioCodec* audio_codec_;
|
||||
i2c_master_bus_handle_t i2c_bus_;
|
||||
EspVideo *camera_;
|
||||
EspVideo* camera_;
|
||||
bool has_camera_;
|
||||
|
||||
OttoCameraType camera_type_;
|
||||
|
||||
bool DetectHardwareVersion() {
|
||||
ledc_timer_config_t ledc_timer = {
|
||||
.speed_mode = LEDC_LOW_SPEED_MODE,
|
||||
@@ -49,7 +50,7 @@ private:
|
||||
if (ret != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
ledc_channel_config_t ledc_channel = {
|
||||
.gpio_num = CAMERA_XCLK,
|
||||
.speed_mode = LEDC_LOW_SPEED_MODE,
|
||||
@@ -63,7 +64,7 @@ private:
|
||||
if (ret != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
i2c_master_bus_config_t i2c_bus_cfg = {
|
||||
.i2c_port = I2C_NUM_0,
|
||||
@@ -73,11 +74,12 @@ private:
|
||||
.glitch_ignore_cnt = 7,
|
||||
.intr_priority = 0,
|
||||
.trans_queue_depth = 0,
|
||||
.flags = {
|
||||
.enable_internal_pullup = 1,
|
||||
},
|
||||
.flags =
|
||||
{
|
||||
.enable_internal_pullup = 1,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
ret = i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_);
|
||||
if (ret != ESP_OK) {
|
||||
ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL, 0);
|
||||
@@ -85,7 +87,8 @@ private:
|
||||
}
|
||||
const uint8_t camera_addresses[] = {0x30, 0x3C, 0x21, 0x60};
|
||||
bool camera_found = false;
|
||||
|
||||
uint16_t detected_pid = 0;
|
||||
|
||||
for (size_t i = 0; i < sizeof(camera_addresses); i++) {
|
||||
uint8_t addr = camera_addresses[i];
|
||||
i2c_device_config_t dev_cfg = {
|
||||
@@ -93,36 +96,71 @@ private:
|
||||
.device_address = addr,
|
||||
.scl_speed_hz = 100000,
|
||||
};
|
||||
|
||||
|
||||
i2c_master_dev_handle_t dev_handle;
|
||||
ret = i2c_master_bus_add_device(i2c_bus_, &dev_cfg, &dev_handle);
|
||||
if (ret == ESP_OK) {
|
||||
uint8_t reg_addr = 0x0A;
|
||||
uint8_t data[2];
|
||||
ret = i2c_master_transmit_receive(dev_handle, ®_addr, 1, data, 2, 200);
|
||||
if (ret == ESP_OK) {
|
||||
uint8_t data[2] = {0, 0};
|
||||
|
||||
uint8_t reg_addr_8bit = 0x0A;
|
||||
ret = i2c_master_transmit_receive(dev_handle, ®_addr_8bit, 1, data, 2, 200);
|
||||
if (ret == ESP_OK && (data[0] != 0 || data[1] != 0)) {
|
||||
detected_pid = (data[0] << 8) | data[1];
|
||||
ESP_LOGI(TAG, "检测到摄像头 (OV2640方式) PID=0x%04X (地址=0x%02X)",
|
||||
detected_pid, addr);
|
||||
camera_found = true;
|
||||
i2c_master_bus_rm_device(dev_handle);
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t reg_addr_high[2] = {0x30, 0x0A};
|
||||
uint8_t reg_addr_low[2] = {0x30, 0x0B};
|
||||
uint8_t pid_high = 0, pid_low = 0;
|
||||
|
||||
ret = i2c_master_transmit_receive(dev_handle, reg_addr_high, 2, &pid_high, 1, 200);
|
||||
if (ret == ESP_OK) {
|
||||
ret =
|
||||
i2c_master_transmit_receive(dev_handle, reg_addr_low, 2, &pid_low, 1, 200);
|
||||
if (ret == ESP_OK) {
|
||||
detected_pid = (pid_high << 8) | pid_low;
|
||||
if (detected_pid != 0) {
|
||||
ESP_LOGI(TAG, "检测到摄像头 (OV3660方式) PID=0x%04X (地址=0x%02X)",
|
||||
detected_pid, addr);
|
||||
camera_found = true;
|
||||
i2c_master_bus_rm_device(dev_handle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i2c_master_bus_rm_device(dev_handle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!camera_found) {
|
||||
i2c_del_master_bus(i2c_bus_);
|
||||
i2c_bus_ = nullptr;
|
||||
ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL, 0);
|
||||
camera_type_ = OTTO_CAMERA_NONE;
|
||||
} else {
|
||||
// 根据 PID 判断摄像头类型
|
||||
if (detected_pid == OV2640_PID_1 || detected_pid == OV2640_PID_2) {
|
||||
camera_type_ = OTTO_CAMERA_OV2640;
|
||||
ESP_LOGI(TAG, "摄像头类型: OV2640 (PID=0x%04X)", detected_pid);
|
||||
} else if (detected_pid == OV3660_PID) {
|
||||
camera_type_ = OTTO_CAMERA_OV3660;
|
||||
ESP_LOGI(TAG, "摄像头类型: OV3660 (PID=0x%04X)", detected_pid);
|
||||
} else {
|
||||
camera_type_ = OTTO_CAMERA_UNKNOWN;
|
||||
ESP_LOGW(TAG, "未知摄像头类型,PID=0x%04X", detected_pid);
|
||||
}
|
||||
}
|
||||
return camera_found;
|
||||
}
|
||||
|
||||
|
||||
void InitializePowerManager() {
|
||||
power_manager_ = new PowerManager(
|
||||
hw_config_.power_charge_detect_pin,
|
||||
hw_config_.power_adc_unit,
|
||||
hw_config_.power_adc_channel
|
||||
);
|
||||
power_manager_ = new PowerManager(hw_config_.power_charge_detect_pin,
|
||||
hw_config_.power_adc_unit, hw_config_.power_adc_channel);
|
||||
}
|
||||
|
||||
void InitializeSpi() {
|
||||
@@ -163,9 +201,9 @@ private:
|
||||
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
|
||||
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
|
||||
|
||||
display_ = new OttoEmojiDisplay(
|
||||
panel_io, panel, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y,
|
||||
DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
display_ = new OttoEmojiDisplay(panel_io, panel, DISPLAY_WIDTH, DISPLAY_HEIGHT,
|
||||
DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X,
|
||||
DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
}
|
||||
|
||||
void InitializeButtons() {
|
||||
@@ -179,17 +217,14 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
void InitializeOttoController() {
|
||||
::InitializeOttoController(hw_config_);
|
||||
}
|
||||
|
||||
public:
|
||||
const HardwareConfig& GetHardwareConfig() const {
|
||||
return hw_config_;
|
||||
}
|
||||
|
||||
private:
|
||||
void InitializeOttoController() { ::InitializeOttoController(hw_config_); }
|
||||
|
||||
public:
|
||||
const HardwareConfig& GetHardwareConfig() const { return hw_config_; }
|
||||
|
||||
OttoCameraType GetCameraType() const { return camera_type_; }
|
||||
|
||||
private:
|
||||
void InitializeWebSocketControlServer() {
|
||||
ws_control_server_ = new WebSocketControlServer();
|
||||
if (!ws_control_server_->Start(8080)) {
|
||||
@@ -201,7 +236,7 @@ private:
|
||||
void StartNetwork() override {
|
||||
WifiBoard::StartNetwork();
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
|
||||
|
||||
InitializeWebSocketControlServer();
|
||||
}
|
||||
|
||||
@@ -209,20 +244,21 @@ private:
|
||||
if (!has_camera_ || i2c_bus_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
static esp_cam_ctlr_dvp_pin_config_t dvp_pin_config = {
|
||||
.data_width = CAM_CTLR_DATA_WIDTH_8,
|
||||
.data_io = {
|
||||
[0] = CAMERA_D0,
|
||||
[1] = CAMERA_D1,
|
||||
[2] = CAMERA_D2,
|
||||
[3] = CAMERA_D3,
|
||||
[4] = CAMERA_D4,
|
||||
[5] = CAMERA_D5,
|
||||
[6] = CAMERA_D6,
|
||||
[7] = CAMERA_D7,
|
||||
},
|
||||
.data_io =
|
||||
{
|
||||
[0] = CAMERA_D0,
|
||||
[1] = CAMERA_D1,
|
||||
[2] = CAMERA_D2,
|
||||
[3] = CAMERA_D3,
|
||||
[4] = CAMERA_D4,
|
||||
[5] = CAMERA_D5,
|
||||
[6] = CAMERA_D6,
|
||||
[7] = CAMERA_D7,
|
||||
},
|
||||
.vsync_io = CAMERA_VSYNC,
|
||||
.de_io = CAMERA_HSYNC,
|
||||
.pclk_io = CAMERA_PCLK,
|
||||
@@ -248,86 +284,126 @@ private:
|
||||
};
|
||||
|
||||
camera_ = new EspVideo(video_config);
|
||||
camera_->SetVFlip(true);
|
||||
|
||||
// 根据摄像头类型设置不同的翻转参数
|
||||
switch (camera_type_) {
|
||||
case OTTO_CAMERA_OV3660:
|
||||
camera_->SetVFlip(true);
|
||||
camera_->SetHMirror(true);
|
||||
ESP_LOGI(TAG, "OV3660: 设置 VFlip=true, HMirror=true");
|
||||
break;
|
||||
case OTTO_CAMERA_OV2640:
|
||||
default:
|
||||
camera_->SetVFlip(true);
|
||||
camera_->SetHMirror(false);
|
||||
ESP_LOGI(TAG, "OV2640: 设置 VFlip=true, HMirror=false");
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
} catch (...) {
|
||||
camera_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void InitializeAudioCodec() {
|
||||
if (hw_config_.audio_use_simplex) {
|
||||
audio_codec_ = new NoAudioCodecSimplex(
|
||||
hw_config_.audio_input_sample_rate,
|
||||
hw_config_.audio_output_sample_rate,
|
||||
hw_config_.audio_i2s_spk_gpio_bclk,
|
||||
hw_config_.audio_i2s_spk_gpio_lrck,
|
||||
hw_config_.audio_i2s_spk_gpio_dout,
|
||||
hw_config_.audio_i2s_mic_gpio_sck,
|
||||
hw_config_.audio_i2s_mic_gpio_ws,
|
||||
hw_config_.audio_i2s_mic_gpio_din
|
||||
);
|
||||
hw_config_.audio_input_sample_rate, hw_config_.audio_output_sample_rate,
|
||||
hw_config_.audio_i2s_spk_gpio_bclk, hw_config_.audio_i2s_spk_gpio_lrck,
|
||||
hw_config_.audio_i2s_spk_gpio_dout, hw_config_.audio_i2s_mic_gpio_sck,
|
||||
hw_config_.audio_i2s_mic_gpio_ws, hw_config_.audio_i2s_mic_gpio_din);
|
||||
} else {
|
||||
audio_codec_ = new NoAudioCodecDuplex(
|
||||
hw_config_.audio_input_sample_rate,
|
||||
hw_config_.audio_output_sample_rate,
|
||||
hw_config_.audio_i2s_gpio_bclk,
|
||||
hw_config_.audio_i2s_gpio_ws,
|
||||
hw_config_.audio_i2s_gpio_dout,
|
||||
hw_config_.audio_i2s_gpio_din
|
||||
);
|
||||
hw_config_.audio_input_sample_rate, hw_config_.audio_output_sample_rate,
|
||||
hw_config_.audio_i2s_gpio_bclk, hw_config_.audio_i2s_gpio_ws,
|
||||
hw_config_.audio_i2s_gpio_dout, hw_config_.audio_i2s_gpio_din);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
OttoRobot() : boot_button_(BOOT_BUTTON_GPIO),
|
||||
audio_codec_(nullptr),
|
||||
i2c_bus_(nullptr),
|
||||
camera_(nullptr),
|
||||
has_camera_(false) {
|
||||
|
||||
OttoRobot()
|
||||
: boot_button_(BOOT_BUTTON_GPIO),
|
||||
audio_codec_(nullptr),
|
||||
i2c_bus_(nullptr),
|
||||
camera_(nullptr),
|
||||
has_camera_(false),
|
||||
camera_type_(OTTO_CAMERA_NONE) {
|
||||
#if OTTO_HARDWARE_VERSION == OTTO_VERSION_AUTO
|
||||
// 自动检测硬件版本(同时检测摄像头类型)
|
||||
has_camera_ = DetectHardwareVersion();
|
||||
|
||||
if (has_camera_)
|
||||
ESP_LOGI(TAG, "自动检测硬件版本: %s", has_camera_ ? "摄像头版" : "无摄像头版");
|
||||
#elif OTTO_HARDWARE_VERSION == OTTO_VERSION_CAMERA
|
||||
// 强制使用摄像头版本,但仍检测具体摄像头类型
|
||||
has_camera_ = DetectHardwareVersion();
|
||||
if (!has_camera_) {
|
||||
// 检测失败时仍使用摄像头配置,但不知道具体类型
|
||||
has_camera_ = true;
|
||||
camera_type_ = OTTO_CAMERA_UNKNOWN;
|
||||
ESP_LOGW(TAG, "强制使用摄像头版本配置,但未能检测到摄像头类型");
|
||||
// 初始化 I2C 总线用于摄像头
|
||||
i2c_master_bus_config_t i2c_bus_cfg = {
|
||||
.i2c_port = I2C_NUM_0,
|
||||
.sda_io_num = CAMERA_VERSION_CONFIG.i2c_sda_pin,
|
||||
.scl_io_num = CAMERA_VERSION_CONFIG.i2c_scl_pin,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.intr_priority = 0,
|
||||
.trans_queue_depth = 0,
|
||||
.flags =
|
||||
{
|
||||
.enable_internal_pullup = 1,
|
||||
},
|
||||
};
|
||||
i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "强制使用摄像头版本配置");
|
||||
}
|
||||
#elif OTTO_HARDWARE_VERSION == OTTO_VERSION_NO_CAMERA
|
||||
// 强制使用无摄像头版本
|
||||
has_camera_ = false;
|
||||
camera_type_ = OTTO_CAMERA_NONE;
|
||||
ESP_LOGI(TAG, "强制使用无摄像头版本配置");
|
||||
#else
|
||||
#error \
|
||||
"OTTO_HARDWARE_VERSION 设置无效,请使用 OTTO_VERSION_AUTO, OTTO_VERSION_CAMERA 或 OTTO_VERSION_NO_CAMERA"
|
||||
#endif
|
||||
|
||||
if (has_camera_)
|
||||
hw_config_ = CAMERA_VERSION_CONFIG;
|
||||
else
|
||||
else
|
||||
hw_config_ = NON_CAMERA_VERSION_CONFIG;
|
||||
|
||||
|
||||
|
||||
InitializeSpi();
|
||||
InitializeLcdDisplay();
|
||||
InitializeButtons();
|
||||
InitializePowerManager();
|
||||
InitializeAudioCodec();
|
||||
|
||||
|
||||
if (has_camera_) {
|
||||
if (!InitializeCamera()) {
|
||||
has_camera_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
InitializeOttoController();
|
||||
ws_control_server_ = nullptr;
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
virtual AudioCodec *GetAudioCodec() override {
|
||||
return audio_codec_;
|
||||
}
|
||||
virtual AudioCodec* GetAudioCodec() override { return audio_codec_; }
|
||||
|
||||
virtual Display* GetDisplay() override {
|
||||
return display_;
|
||||
}
|
||||
virtual Display* GetDisplay() override { return display_; }
|
||||
|
||||
virtual Backlight* GetBacklight() override {
|
||||
static PwmBacklight* backlight = nullptr;
|
||||
if (backlight == nullptr) {
|
||||
backlight = new PwmBacklight(hw_config_.display_backlight_pin, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
|
||||
backlight =
|
||||
new PwmBacklight(hw_config_.display_backlight_pin, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
|
||||
}
|
||||
return backlight;
|
||||
}
|
||||
|
||||
|
||||
virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override {
|
||||
charging = power_manager_->IsCharging();
|
||||
discharging = !charging;
|
||||
@@ -335,9 +411,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual Camera *GetCamera() override {
|
||||
return has_camera_ ? camera_ : nullptr;
|
||||
}
|
||||
virtual Camera* GetCamera() override { return has_camera_ ? camera_ : nullptr; }
|
||||
};
|
||||
|
||||
DECLARE_BOARD(OttoRobot);
|
||||
|
||||
@@ -45,6 +45,10 @@ void Display::SetChatMessage(const char* role, const char* content) {
|
||||
ESP_LOGW(TAG, " %s", content);
|
||||
}
|
||||
|
||||
void Display::ClearChatMessages() {
|
||||
// Default empty implementation, override in subclasses if needed
|
||||
}
|
||||
|
||||
void Display::SetTheme(Theme* theme) {
|
||||
current_theme_ = theme;
|
||||
Settings settings("display", true);
|
||||
|
||||
@@ -35,6 +35,7 @@ public:
|
||||
virtual void ShowNotification(const std::string ¬ification, int duration_ms = 3000);
|
||||
virtual void SetEmotion(const char* emotion);
|
||||
virtual void SetChatMessage(const char* role, const char* content);
|
||||
virtual void ClearChatMessages();
|
||||
virtual void SetTheme(Theme* theme);
|
||||
virtual Theme* GetTheme() { return current_theme_; }
|
||||
virtual void UpdateStatusBar(bool update_all = false);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <esp_lvgl_port.h>
|
||||
#include <esp_psram.h>
|
||||
#include <cstring>
|
||||
#include <src/misc/cache/lv_cache.h>
|
||||
|
||||
#include "board.h"
|
||||
|
||||
@@ -510,25 +511,31 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
if (child_count >= MAX_MESSAGES) {
|
||||
// Delete the oldest message (first child object)
|
||||
lv_obj_t* first_child = lv_obj_get_child(content_, 0);
|
||||
lv_obj_t* last_child = lv_obj_get_child(content_, child_count - 1);
|
||||
if (first_child != nullptr) {
|
||||
lv_obj_del(first_child);
|
||||
// Refresh child count after deletion
|
||||
child_count = lv_obj_get_child_cnt(content_);
|
||||
}
|
||||
// Scroll to the last message immediately
|
||||
if (last_child != nullptr) {
|
||||
lv_obj_scroll_to_view_recursive(last_child, LV_ANIM_OFF);
|
||||
// Scroll to the last message immediately (get last_child after deletion)
|
||||
if (child_count > 0) {
|
||||
lv_obj_t* last_child = lv_obj_get_child(content_, child_count - 1);
|
||||
if (last_child != nullptr && lv_obj_is_valid(last_child)) {
|
||||
lv_obj_scroll_to_view_recursive(last_child, LV_ANIM_OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collapse system messages (if it's a system message, check if the last message is also a system message)
|
||||
if (strcmp(role, "system") == 0) {
|
||||
// Refresh child count to get accurate count after potential deletion above
|
||||
child_count = lv_obj_get_child_cnt(content_);
|
||||
if (child_count > 0) {
|
||||
// Get the last message container
|
||||
lv_obj_t* last_container = lv_obj_get_child(content_, child_count - 1);
|
||||
if (last_container != nullptr && lv_obj_get_child_cnt(last_container) > 0) {
|
||||
if (last_container != nullptr && lv_obj_is_valid(last_container) && lv_obj_get_child_cnt(last_container) > 0) {
|
||||
// Get the bubble inside the container
|
||||
lv_obj_t* last_bubble = lv_obj_get_child(last_container, 0);
|
||||
if (last_bubble != nullptr) {
|
||||
if (last_bubble != nullptr && lv_obj_is_valid(last_bubble)) {
|
||||
// Check if bubble type is system message
|
||||
void* bubble_type_ptr = lv_obj_get_user_data(last_bubble);
|
||||
if (bubble_type_ptr != nullptr && strcmp((const char*)bubble_type_ptr, "system") == 0) {
|
||||
@@ -549,7 +556,6 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
}
|
||||
|
||||
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
||||
auto text_font = lvgl_theme->text_font()->font();
|
||||
|
||||
// Create a message bubble
|
||||
lv_obj_t* msg_bubble = lv_obj_create(content_);
|
||||
@@ -562,28 +568,25 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
lv_obj_t* msg_text = lv_label_create(msg_bubble);
|
||||
lv_label_set_text(msg_text, content);
|
||||
|
||||
// Calculate actual text width
|
||||
lv_coord_t text_width = lv_txt_get_width(content, strlen(content), text_font, 0);
|
||||
|
||||
// Calculate bubble width
|
||||
// Calculate bubble width constraints
|
||||
lv_coord_t max_width = LV_HOR_RES * 85 / 100 - 16; // 85% of screen width
|
||||
lv_coord_t min_width = 20;
|
||||
lv_coord_t bubble_width;
|
||||
|
||||
// Let LVGL calculate the natural text width first
|
||||
lv_obj_set_width(msg_text, LV_SIZE_CONTENT);
|
||||
lv_obj_update_layout(msg_text);
|
||||
lv_coord_t text_width = lv_obj_get_width(msg_text);
|
||||
|
||||
// Ensure text width is not less than minimum width
|
||||
if (text_width < min_width) {
|
||||
text_width = min_width;
|
||||
}
|
||||
|
||||
// If text width is less than max width, use text width
|
||||
if (text_width < max_width) {
|
||||
bubble_width = text_width;
|
||||
} else {
|
||||
bubble_width = max_width;
|
||||
}
|
||||
// Constrain to max width
|
||||
lv_coord_t bubble_width = (text_width < max_width) ? text_width : max_width;
|
||||
|
||||
// Set message text width
|
||||
lv_obj_set_width(msg_text, bubble_width); // Subtract padding
|
||||
lv_obj_set_width(msg_text, bubble_width);
|
||||
lv_label_set_long_mode(msg_text, LV_LABEL_LONG_WRAP);
|
||||
|
||||
// Set bubble width
|
||||
@@ -770,6 +773,26 @@ void LcdDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
|
||||
// Auto-scroll to the image bubble
|
||||
lv_obj_scroll_to_view_recursive(img_bubble, LV_ANIM_ON);
|
||||
}
|
||||
|
||||
void LcdDisplay::ClearChatMessages() {
|
||||
DisplayLockGuard lock(this);
|
||||
if (content_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use lv_obj_clean to delete all children of content_ (chat message bubbles)
|
||||
lv_obj_clean(content_);
|
||||
|
||||
// Reset chat_message_label_ as it has been deleted
|
||||
chat_message_label_ = nullptr;
|
||||
|
||||
// Show the centered AI logo (emoji_label_) again
|
||||
if (emoji_label_ != nullptr) {
|
||||
lv_obj_remove_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Chat messages cleared");
|
||||
}
|
||||
#else
|
||||
void LcdDisplay::SetupUI() {
|
||||
DisplayLockGuard lock(this);
|
||||
@@ -887,29 +910,35 @@ void LcdDisplay::SetupUI() {
|
||||
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
|
||||
lv_obj_align(status_label_, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
/* Top layer: Bottom bar - fixed at bottom, minimum height 48, height can be adaptive */
|
||||
/* Top layer: Bottom bar - fixed height at bottom */
|
||||
bottom_bar_ = lv_obj_create(screen);
|
||||
lv_obj_set_width(bottom_bar_, LV_HOR_RES);
|
||||
lv_obj_set_height(bottom_bar_, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_min_height(bottom_bar_, 48, 0); // Set minimum height 48
|
||||
lv_obj_set_size(bottom_bar_, LV_HOR_RES, text_font->line_height + lvgl_theme->spacing(12));
|
||||
lv_obj_set_style_radius(bottom_bar_, 0, 0);
|
||||
lv_obj_set_style_bg_color(bottom_bar_, lvgl_theme->background_color(), 0);
|
||||
lv_obj_set_style_text_color(bottom_bar_, lvgl_theme->text_color(), 0);
|
||||
lv_obj_set_style_pad_top(bottom_bar_, lvgl_theme->spacing(2), 0);
|
||||
lv_obj_set_style_pad_bottom(bottom_bar_, lvgl_theme->spacing(2), 0);
|
||||
lv_obj_set_style_pad_all(bottom_bar_, 0, 0);
|
||||
lv_obj_set_style_pad_left(bottom_bar_, lvgl_theme->spacing(4), 0);
|
||||
lv_obj_set_style_pad_right(bottom_bar_, lvgl_theme->spacing(4), 0);
|
||||
lv_obj_set_style_border_width(bottom_bar_, 0, 0);
|
||||
lv_obj_set_scrollbar_mode(bottom_bar_, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_align(bottom_bar_, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
|
||||
/* chat_message_label_ placed in bottom_bar_ and vertically centered */
|
||||
/* chat_message_label_ placed in bottom_bar_, single-line horizontal scroll */
|
||||
chat_message_label_ = lv_label_create(bottom_bar_);
|
||||
lv_label_set_text(chat_message_label_, "");
|
||||
lv_obj_set_width(chat_message_label_, LV_HOR_RES - lvgl_theme->spacing(8)); // Subtract left and right padding
|
||||
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP); // Auto wrap mode
|
||||
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // Center text alignment
|
||||
lv_obj_set_width(chat_message_label_, LV_HOR_RES - lvgl_theme->spacing(8));
|
||||
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_color(chat_message_label_, lvgl_theme->text_color(), 0);
|
||||
lv_obj_align(chat_message_label_, LV_ALIGN_CENTER, 0, 0); // Vertically and horizontally centered in bottom_bar_
|
||||
lv_obj_align(chat_message_label_, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
// Start scrolling after a delay (short text won't scroll)
|
||||
static lv_anim_t a;
|
||||
lv_anim_init(&a);
|
||||
lv_anim_set_delay(&a, 1000);
|
||||
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);
|
||||
lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN);
|
||||
lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN);
|
||||
|
||||
low_battery_popup_ = lv_obj_create(screen);
|
||||
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
|
||||
@@ -968,6 +997,14 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
}
|
||||
lv_label_set_text(chat_message_label_, content);
|
||||
}
|
||||
|
||||
void LcdDisplay::ClearChatMessages() {
|
||||
DisplayLockGuard lock(this);
|
||||
// In non-wechat mode, just clear the chat message label
|
||||
if (chat_message_label_ != nullptr) {
|
||||
lv_label_set_text(chat_message_label_, "");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void LcdDisplay::SetEmotion(const char* emotion) {
|
||||
@@ -1001,6 +1038,8 @@ void LcdDisplay::SetEmotion(const char* emotion) {
|
||||
gif_controller_ = std::make_unique<LvglGif>(image->image_dsc());
|
||||
|
||||
if (gif_controller_->IsLoaded()) {
|
||||
// Set loop delay to 1000ms
|
||||
gif_controller_->SetLoopDelay(3000);
|
||||
// Set up frame update callback
|
||||
gif_controller_->SetFrameCallback([this]() {
|
||||
lv_image_set_src(emoji_image_, gif_controller_->image_dsc());
|
||||
@@ -1107,7 +1146,7 @@ void LcdDisplay::SetTheme(Theme* theme) {
|
||||
if (lv_obj_get_child_cnt(obj) > 0) {
|
||||
// Might be a container, check if it's a user or system message container
|
||||
// User and system message containers are transparent
|
||||
lv_opa_t bg_opa = lv_obj_get_style_bg_opa(obj, 0);
|
||||
lv_opa_t bg_opa = lv_obj_get_style_bg_opa(obj, LV_PART_MAIN);
|
||||
if (bg_opa == LV_OPA_TRANSP) {
|
||||
// This is a user or system message container
|
||||
bubble = lv_obj_get_child(obj, 0);
|
||||
|
||||
@@ -48,7 +48,8 @@ protected:
|
||||
public:
|
||||
~LcdDisplay();
|
||||
virtual void SetEmotion(const char* emotion) override;
|
||||
virtual void SetChatMessage(const char* role, const char* content) override;
|
||||
virtual void SetChatMessage(const char* role, const char* content) override;
|
||||
virtual void ClearChatMessages() override;
|
||||
virtual void SetPreviewImage(std::unique_ptr<LvglImage> image) override;
|
||||
|
||||
// Add theme switching function
|
||||
|
||||
@@ -30,8 +30,8 @@ typedef struct Table {
|
||||
|
||||
static gd_GIF * gif_open(gd_GIF * gif);
|
||||
static bool f_gif_open(gd_GIF * gif, const void * path, bool is_file);
|
||||
static void f_gif_read(gd_GIF * gif, void * buf, size_t len);
|
||||
static int f_gif_seek(gd_GIF * gif, size_t pos, int k);
|
||||
static inline void f_gif_read(gd_GIF * gif, void * buf, size_t len);
|
||||
static inline int f_gif_seek(gd_GIF * gif, size_t pos, int k);
|
||||
static void f_gif_close(gd_GIF * gif);
|
||||
|
||||
#if LV_USE_DRAW_SW_ASM == LV_DRAW_SW_ASM_HELIUM
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
#define TAG "LvglGif"
|
||||
|
||||
LvglGif::LvglGif(const lv_img_dsc_t* img_dsc)
|
||||
: gif_(nullptr), timer_(nullptr), last_call_(0), playing_(false), loaded_(false) {
|
||||
: gif_(nullptr), timer_(nullptr), last_call_(0), playing_(false), loaded_(false),
|
||||
loop_delay_ms_(0), loop_waiting_(false), loop_wait_start_(0) {
|
||||
if (!img_dsc || !img_dsc->data) {
|
||||
ESP_LOGE(TAG, "Invalid image descriptor");
|
||||
return;
|
||||
@@ -66,6 +67,7 @@ void LvglGif::Start() {
|
||||
|
||||
if (timer_) {
|
||||
playing_ = true;
|
||||
loop_waiting_ = false; // Reset loop waiting state
|
||||
last_call_ = lv_tick_get();
|
||||
lv_timer_resume(timer_);
|
||||
lv_timer_reset(timer_);
|
||||
@@ -104,9 +106,15 @@ void LvglGif::Stop() {
|
||||
lv_timer_pause(timer_);
|
||||
}
|
||||
|
||||
// Reset loop waiting state
|
||||
loop_waiting_ = false;
|
||||
|
||||
if (gif_) {
|
||||
gd_rewind(gif_);
|
||||
NextFrame();
|
||||
// Render first frame without advancing
|
||||
if (gif_->canvas) {
|
||||
gd_render_frame(gif_, gif_->canvas);
|
||||
}
|
||||
ESP_LOGD(TAG, "GIF animation stopped and rewound");
|
||||
}
|
||||
}
|
||||
@@ -134,6 +142,15 @@ void LvglGif::SetLoopCount(int32_t count) {
|
||||
gif_->loop_count = count;
|
||||
}
|
||||
|
||||
uint32_t LvglGif::GetLoopDelay() const {
|
||||
return loop_delay_ms_;
|
||||
}
|
||||
|
||||
void LvglGif::SetLoopDelay(uint32_t delay_ms) {
|
||||
loop_delay_ms_ = delay_ms;
|
||||
ESP_LOGD(TAG, "Loop delay set to %lu ms", delay_ms);
|
||||
}
|
||||
|
||||
uint16_t LvglGif::width() const {
|
||||
if (!loaded_ || !gif_) {
|
||||
return 0;
|
||||
@@ -157,6 +174,18 @@ void LvglGif::NextFrame() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're in loop wait state (only for infinite loop GIFs with delay)
|
||||
if (loop_waiting_) {
|
||||
uint32_t wait_elapsed = lv_tick_elaps(loop_wait_start_);
|
||||
if (wait_elapsed < loop_delay_ms_) {
|
||||
// Still waiting for loop delay
|
||||
return;
|
||||
}
|
||||
// Loop delay completed, continue playing
|
||||
loop_waiting_ = false;
|
||||
ESP_LOGD(TAG, "Loop delay completed, continuing GIF");
|
||||
}
|
||||
|
||||
// Check if enough time has passed for the next frame
|
||||
uint32_t elapsed = lv_tick_elaps(last_call_);
|
||||
if (elapsed < gif_->gce.delay * 10) {
|
||||
@@ -165,15 +194,30 @@ void LvglGif::NextFrame() {
|
||||
|
||||
last_call_ = lv_tick_get();
|
||||
|
||||
// Save file position before getting next frame to detect loop
|
||||
uint32_t pos_before = gif_->f_rw_p;
|
||||
|
||||
// Get next frame
|
||||
int has_next = gd_get_frame(gif_);
|
||||
if (has_next == 0) {
|
||||
// Animation finished, pause timer
|
||||
// Animation truly finished (non-infinite loop)
|
||||
playing_ = false;
|
||||
if (timer_) {
|
||||
lv_timer_pause(timer_);
|
||||
}
|
||||
ESP_LOGD(TAG, "GIF animation completed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect loop by checking if file position jumped back (rewound to start)
|
||||
// This works for looping GIFs regardless of when loop_count is set
|
||||
if (loop_delay_ms_ > 0 && gif_->f_rw_p < pos_before) {
|
||||
// File position decreased, meaning GIF looped back to beginning
|
||||
// Start waiting before rendering this frame
|
||||
loop_waiting_ = true;
|
||||
loop_wait_start_ = lv_tick_get();
|
||||
ESP_LOGD(TAG, "GIF completed one cycle, waiting %lu ms before next loop", loop_delay_ms_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render current frame
|
||||
|
||||
@@ -58,6 +58,17 @@ public:
|
||||
*/
|
||||
void SetLoopCount(int32_t count);
|
||||
|
||||
/**
|
||||
* Get loop delay in milliseconds (delay between loops)
|
||||
*/
|
||||
uint32_t GetLoopDelay() const;
|
||||
|
||||
/**
|
||||
* Set loop delay in milliseconds (delay between loops)
|
||||
* @param delay_ms Delay in milliseconds before starting next loop. 0 means no delay.
|
||||
*/
|
||||
void SetLoopDelay(uint32_t delay_ms);
|
||||
|
||||
/**
|
||||
* Get GIF dimensions
|
||||
*/
|
||||
@@ -86,6 +97,11 @@ private:
|
||||
bool playing_;
|
||||
bool loaded_;
|
||||
|
||||
// Loop delay configuration
|
||||
uint32_t loop_delay_ms_; // Delay between loops in milliseconds
|
||||
bool loop_waiting_; // Whether we're waiting for the next loop
|
||||
uint32_t loop_wait_start_; // Timestamp when loop wait started
|
||||
|
||||
// Frame update callback
|
||||
std::function<void()> frame_callback_;
|
||||
|
||||
|
||||
@@ -20,21 +20,21 @@ dependencies:
|
||||
espressif/esp_lcd_panel_io_additions: ^1.0.1
|
||||
78/esp_lcd_nv3023: ~1.0.0
|
||||
78/esp-wifi-connect: ~3.0.2
|
||||
espressif/esp_audio_effects: ~1.2.0
|
||||
espressif/esp_audio_codec: ~2.4.0
|
||||
78/esp-ml307: ~3.5.3
|
||||
espressif/esp_audio_effects: ~1.2.1
|
||||
espressif/esp_audio_codec: ~2.4.1
|
||||
78/esp-ml307: ~3.6.3
|
||||
78/uart-eth-modem:
|
||||
version: ~0.1.3
|
||||
version: ~0.3.1
|
||||
rules:
|
||||
- if: target not in [esp32]
|
||||
78/xiaozhi-fonts: ~1.5.5
|
||||
espressif/led_strip: ~3.0.1
|
||||
espressif/esp_codec_dev: ~1.5
|
||||
espressif/esp-sr: ~2.2.0
|
||||
espressif/button: ~4.1.3
|
||||
78/xiaozhi-fonts: ~1.6.0
|
||||
espressif/led_strip: ~3.0.2
|
||||
espressif/esp_codec_dev: ~1.5.4
|
||||
espressif/esp-sr: ~2.3.0
|
||||
espressif/button: ~4.1.5
|
||||
espressif/knob: ^1.0.0
|
||||
espressif/esp32-camera:
|
||||
version: ^2.0.15
|
||||
version: ^2.1.4
|
||||
rules:
|
||||
- if: target in [esp32s3]
|
||||
espressif/esp_video:
|
||||
@@ -51,15 +51,15 @@ dependencies:
|
||||
espressif/esp_lcd_touch_gt1151: ^1
|
||||
waveshare/esp_lcd_touch_cst9217: ^1.0.3
|
||||
espressif/esp_lcd_touch_cst816s: ^1.0.6
|
||||
lvgl/lvgl: ~9.3.0
|
||||
esp_lvgl_port: ~2.6.0
|
||||
lvgl/lvgl: ~9.4.0
|
||||
esp_lvgl_port: ~2.7.0
|
||||
espressif/esp_io_expander_tca95xx_16bit: ^2.0.0
|
||||
espressif2022/image_player: ^1.1.1
|
||||
espressif2022/esp_emote_expression: ^0.1.0
|
||||
espressif/adc_mic: ^0.2.1
|
||||
espressif/esp_mmap_assets: '>=1.2'
|
||||
txp666/otto-emoji-gif-component:
|
||||
version: ^1.0.3
|
||||
version: ^1.1.1
|
||||
rules:
|
||||
- if: target in [esp32s3]
|
||||
espressif/adc_battery_estimation: ^0.2.0
|
||||
|
||||
49
main/ota.cc
49
main/ota.cc
@@ -3,6 +3,8 @@
|
||||
#include "settings.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <cJSON.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_partition.h>
|
||||
@@ -10,6 +12,7 @@
|
||||
#include <esp_app_format.h>
|
||||
#include <esp_efuse.h>
|
||||
#include <esp_efuse_table.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#ifdef SOC_HMAC_SUPPORTED
|
||||
#include <esp_hmac.h>
|
||||
#endif
|
||||
@@ -292,19 +295,28 @@ bool Ota::Upgrade(const std::string& firmware_url, std::function<void(int progre
|
||||
return false;
|
||||
}
|
||||
|
||||
char buffer[512];
|
||||
constexpr size_t PAGE_SIZE = 4096;
|
||||
char* buffer = (char*)heap_caps_malloc(PAGE_SIZE, MALLOC_CAP_INTERNAL);
|
||||
if (buffer == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t buffer_offset = 0; // Current data size in buffer
|
||||
size_t total_read = 0, recent_read = 0;
|
||||
auto last_calc_time = esp_timer_get_time();
|
||||
while (true) {
|
||||
int ret = http->Read(buffer, sizeof(buffer));
|
||||
int ret = http->Read(buffer + buffer_offset, PAGE_SIZE - buffer_offset);
|
||||
if (ret < 0) {
|
||||
ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret));
|
||||
heap_caps_free(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate speed and progress every second
|
||||
recent_read += ret;
|
||||
total_read += ret;
|
||||
buffer_offset += ret;
|
||||
if (esp_timer_get_time() - last_calc_time >= 1000000 || ret == 0) {
|
||||
size_t progress = total_read * 100 / content_length;
|
||||
ESP_LOGI(TAG, "Progress: %u%% (%u/%u), Speed: %uB/s", progress, total_read, content_length, recent_read);
|
||||
@@ -315,22 +327,16 @@ bool Ota::Upgrade(const std::string& firmware_url, std::function<void(int progre
|
||||
recent_read = 0;
|
||||
}
|
||||
|
||||
if (ret == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!image_header_checked) {
|
||||
image_header.append(buffer, ret);
|
||||
image_header.append(buffer, buffer_offset);
|
||||
if (image_header.size() >= sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) {
|
||||
esp_app_desc_t new_app_info;
|
||||
memcpy(&new_app_info, image_header.data() + sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t), sizeof(esp_app_desc_t));
|
||||
|
||||
auto current_version = esp_app_get_description()->version;
|
||||
ESP_LOGI(TAG, "Current version: %s, New version: %s", current_version, new_app_info.version);
|
||||
|
||||
if (esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle)) {
|
||||
esp_ota_abort(update_handle);
|
||||
ESP_LOGE(TAG, "Failed to begin OTA");
|
||||
heap_caps_free(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -338,14 +344,27 @@ bool Ota::Upgrade(const std::string& firmware_url, std::function<void(int progre
|
||||
std::string().swap(image_header);
|
||||
}
|
||||
}
|
||||
auto err = esp_ota_write(update_handle, buffer, ret);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_ota_abort(update_handle);
|
||||
return false;
|
||||
|
||||
// Write to flash when buffer is full (4KB) or it's the last chunk
|
||||
bool is_last_chunk = (ret == 0);
|
||||
if (buffer_offset == PAGE_SIZE || (is_last_chunk && buffer_offset > 0)) {
|
||||
auto err = esp_ota_write(update_handle, buffer, buffer_offset);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_ota_abort(update_handle);
|
||||
heap_caps_free(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer_offset = 0;
|
||||
}
|
||||
|
||||
if (is_last_chunk) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
http->Close();
|
||||
heap_caps_free(buffer);
|
||||
|
||||
esp_err_t err = esp_ota_end(update_handle);
|
||||
if (err != ESP_OK) {
|
||||
|
||||
@@ -119,7 +119,8 @@ bool MqttProtocol::StartMqttClient(bool report_error) {
|
||||
auto alive = alive_; // Capture alive flag
|
||||
Application::GetInstance().Schedule([this, alive]() {
|
||||
if (*alive) {
|
||||
CloseAudioChannel();
|
||||
// Server initiated goodbye, don't send goodbye back to avoid ping-pong
|
||||
CloseAudioChannel(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -188,17 +189,23 @@ bool MqttProtocol::SendAudio(std::unique_ptr<AudioStreamPacket> packet) {
|
||||
return udp_->Send(encrypted) > 0;
|
||||
}
|
||||
|
||||
void MqttProtocol::CloseAudioChannel() {
|
||||
void MqttProtocol::CloseAudioChannel(bool send_goodbye) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(channel_mutex_);
|
||||
udp_.reset();
|
||||
}
|
||||
|
||||
std::string message = "{";
|
||||
message += "\"session_id\":\"" + session_id_ + "\",";
|
||||
message += "\"type\":\"goodbye\"";
|
||||
message += "}";
|
||||
SendText(message);
|
||||
ESP_LOGI(TAG, "Closing audio channel, send_goodbye: %d", send_goodbye);
|
||||
|
||||
// Only send goodbye when client initiates the close
|
||||
// Don't send if server already sent goodbye (to avoid ping-pong)
|
||||
if (send_goodbye) {
|
||||
std::string message = "{";
|
||||
message += "\"session_id\":\"" + session_id_ + "\",";
|
||||
message += "\"type\":\"goodbye\"";
|
||||
message += "}";
|
||||
SendText(message);
|
||||
}
|
||||
|
||||
if (on_audio_channel_closed_ != nullptr) {
|
||||
on_audio_channel_closed_();
|
||||
|
||||
@@ -31,7 +31,7 @@ public:
|
||||
bool Start() override;
|
||||
bool SendAudio(std::unique_ptr<AudioStreamPacket> packet) override;
|
||||
bool OpenAudioChannel() override;
|
||||
void CloseAudioChannel() override;
|
||||
void CloseAudioChannel(bool send_goodbye = true) override;
|
||||
bool IsAudioChannelOpened() const override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -65,7 +65,7 @@ public:
|
||||
|
||||
virtual bool Start() = 0;
|
||||
virtual bool OpenAudioChannel() = 0;
|
||||
virtual void CloseAudioChannel() = 0;
|
||||
virtual void CloseAudioChannel(bool send_goodbye = true) = 0;
|
||||
virtual bool IsAudioChannelOpened() const = 0;
|
||||
virtual bool SendAudio(std::unique_ptr<AudioStreamPacket> packet) = 0;
|
||||
virtual void SendWakeWordDetected(const std::string& wake_word);
|
||||
|
||||
@@ -75,7 +75,8 @@ bool WebsocketProtocol::IsAudioChannelOpened() const {
|
||||
return websocket_ != nullptr && websocket_->IsConnected() && !error_occurred_ && !IsTimeout();
|
||||
}
|
||||
|
||||
void WebsocketProtocol::CloseAudioChannel() {
|
||||
void WebsocketProtocol::CloseAudioChannel(bool send_goodbye) {
|
||||
(void)send_goodbye; // Websocket doesn't need to send goodbye message
|
||||
websocket_.reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public:
|
||||
bool Start() override;
|
||||
bool SendAudio(std::unique_ptr<AudioStreamPacket> packet) override;
|
||||
bool OpenAudioChannel() override;
|
||||
void CloseAudioChannel() override;
|
||||
void CloseAudioChannel(bool send_goodbye = true) override;
|
||||
bool IsAudioChannelOpened() const override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -222,6 +222,19 @@ def process_emoji_collection(emoji_collection_dir, assets_dir):
|
||||
|
||||
emoji_list = []
|
||||
|
||||
# Check if this is otto-gif collection
|
||||
is_otto_gif = 'otto-emoji-gif-component' in emoji_collection_dir or emoji_collection_dir.endswith('otto-gif')
|
||||
|
||||
# Otto GIF emoji aliases mapping
|
||||
otto_gif_aliases = {
|
||||
"staticstate": ["neutral", "relaxed", "sleepy", "idle"],
|
||||
"happy": ["laughing", "funny", "loving", "confident", "winking", "cool", "delicious", "kissy", "silly"],
|
||||
"sad": ["crying"],
|
||||
"anger": ["angry"],
|
||||
"scare": ["surprised", "shocked"],
|
||||
"buxue": ["thinking", "confused", "embarrassed"]
|
||||
}
|
||||
|
||||
# Copy each image from input directory to build/assets directory
|
||||
for root, dirs, files in os.walk(emoji_collection_dir):
|
||||
for file in files:
|
||||
@@ -233,11 +246,19 @@ def process_emoji_collection(emoji_collection_dir, assets_dir):
|
||||
# Get filename without extension
|
||||
filename_without_ext = os.path.splitext(file)[0]
|
||||
|
||||
# Add to emoji list
|
||||
# Add main emoji entry
|
||||
emoji_list.append({
|
||||
"name": filename_without_ext,
|
||||
"file": file
|
||||
})
|
||||
|
||||
# Add aliases for otto-gif emojis
|
||||
if is_otto_gif and filename_without_ext in otto_gif_aliases:
|
||||
for alias in otto_gif_aliases[filename_without_ext]:
|
||||
emoji_list.append({
|
||||
"name": alias,
|
||||
"file": file
|
||||
})
|
||||
|
||||
return emoji_list
|
||||
|
||||
@@ -672,7 +693,10 @@ def get_text_font_path(builtin_text_font, xiaozhi_fonts_path):
|
||||
|
||||
# Convert from basic to common font name
|
||||
# e.g., font_puhui_basic_16_4 -> font_puhui_common_16_4.bin
|
||||
font_name = builtin_text_font.replace('basic', 'common') + '.bin'
|
||||
if builtin_text_font.startswith('font_noto_'):
|
||||
font_name = builtin_text_font.replace('basic', 'qwen') + '.bin'
|
||||
else:
|
||||
font_name = builtin_text_font.replace('basic', 'common') + '.bin'
|
||||
font_path = os.path.join(xiaozhi_fonts_path, 'cbin', font_name)
|
||||
|
||||
if os.path.exists(font_path):
|
||||
@@ -682,20 +706,45 @@ def get_text_font_path(builtin_text_font, xiaozhi_fonts_path):
|
||||
return None
|
||||
|
||||
|
||||
def get_emoji_collection_path(default_emoji_collection, xiaozhi_fonts_path):
|
||||
def get_emoji_collection_path(default_emoji_collection, xiaozhi_fonts_path, project_root=None):
|
||||
"""
|
||||
Get the emoji collection path if needed
|
||||
Returns the emoji directory path or None if no emoji collection is needed
|
||||
|
||||
Supports:
|
||||
- PNG emoji collections from xiaozhi-fonts (e.g., emojis_32, twemoji_64)
|
||||
- GIF emoji collections from xiaozhi-fonts (e.g., noto-emoji_128, noto-emoji_64)
|
||||
- Otto GIF emoji collection (otto-gif)
|
||||
"""
|
||||
if not default_emoji_collection:
|
||||
return None
|
||||
|
||||
# Special handling for otto-gif collection
|
||||
if default_emoji_collection == 'otto-gif':
|
||||
if project_root:
|
||||
otto_gif_path = os.path.join(project_root, 'managed_components',
|
||||
'txp666__otto-emoji-gif-component', 'gifs')
|
||||
if os.path.exists(otto_gif_path):
|
||||
return otto_gif_path
|
||||
else:
|
||||
print(f"Warning: Otto GIF emoji collection directory not found: {otto_gif_path}")
|
||||
return None
|
||||
else:
|
||||
print("Warning: project_root not provided, cannot locate otto-gif collection")
|
||||
return None
|
||||
|
||||
# Try PNG emoji collections first (e.g., emojis_32, twemoji_64)
|
||||
emoji_path = os.path.join(xiaozhi_fonts_path, 'png', default_emoji_collection)
|
||||
if os.path.exists(emoji_path):
|
||||
return emoji_path
|
||||
else:
|
||||
print(f"Warning: Emoji collection directory not found: {emoji_path}")
|
||||
return None
|
||||
|
||||
# Try GIF emoji collections (e.g., noto-emoji_128, noto-emoji_64, noto-emoji_32)
|
||||
emoji_path = os.path.join(xiaozhi_fonts_path, 'gif', default_emoji_collection)
|
||||
if os.path.exists(emoji_path):
|
||||
return emoji_path
|
||||
|
||||
print(f"Warning: Emoji collection directory not found in png/ or gif/: {default_emoji_collection}")
|
||||
return None
|
||||
|
||||
|
||||
def build_assets_integrated(wakenet_model_paths, multinet_model_paths, text_font_path, emoji_collection_path, extra_files_path, output_path, multinet_model_info=None):
|
||||
@@ -828,7 +877,10 @@ def main():
|
||||
text_font_path = get_text_font_path(args.builtin_text_font, args.xiaozhi_fonts_path)
|
||||
|
||||
# Get emoji collection path if needed
|
||||
emoji_collection_path = get_emoji_collection_path(args.emoji_collection, args.xiaozhi_fonts_path)
|
||||
# Calculate project root from script location for otto-gif support
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(script_dir)
|
||||
emoji_collection_path = get_emoji_collection_path(args.emoji_collection, args.xiaozhi_fonts_path, project_root)
|
||||
|
||||
# Get extra files path if provided
|
||||
extra_files_path = args.extra_files
|
||||
|
||||
@@ -7,8 +7,8 @@ CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
CONFIG_SPIRAM_SPEED_80M=y
|
||||
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=512
|
||||
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=65536
|
||||
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=2048
|
||||
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=98304
|
||||
CONFIG_SPIRAM_MEMTEST=n
|
||||
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y
|
||||
|
||||
|
||||
Reference in New Issue
Block a user