Enhance device activation and OTA update process

- Add support for device activation with audio feedback
- Refactor OTA update flow to include activation code handling
- Update asset management for localized sound resources
- Improve error handling and device state management
- Reorganize binary asset includes and CMake configuration
This commit is contained in:
Terrence
2025-02-16 06:59:19 +08:00
parent 14a89cae33
commit 3a71c1e895
39 changed files with 220 additions and 86 deletions

View File

@@ -4,7 +4,7 @@
# CMakeLists in this exact order for cmake to work correctly # CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
set(PROJECT_VER "1.1.2") set(PROJECT_VER "1.1.9")
# Add this line to disable the specific warning # Add this line to disable the specific warning
add_compile_options(-Wno-missing-field-initializers) add_compile_options(-Wno-missing-field-initializers)

View File

@@ -13,8 +13,6 @@ set(SOURCES "audio_codecs/audio_codec.cc"
"display/ssd1306_display.cc" "display/ssd1306_display.cc"
"boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.c" "boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.c"
"protocols/protocol.cc" "protocols/protocol.cc"
"protocols/mqtt_protocol.cc"
"protocols/websocket_protocol.cc"
"iot/thing.cc" "iot/thing.cc"
"iot/thing_manager.cc" "iot/thing_manager.cc"
"system_info.cc" "system_info.cc"
@@ -107,6 +105,12 @@ endif()
file(GLOB BOARD_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.cc) file(GLOB BOARD_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.cc)
list(APPEND SOURCES ${BOARD_SOURCES}) list(APPEND SOURCES ${BOARD_SOURCES})
if(CONFIG_CONNECTION_TYPE_MQTT_UDP)
list(APPEND SOURCES "protocols/mqtt_protocol.cc")
elseif(CONFIG_CONNECTION_TYPE_WEBSOCKET)
list(APPEND SOURCES "protocols/websocket_protocol.cc")
endif()
if(CONFIG_USE_AUDIO_PROCESSING) if(CONFIG_USE_AUDIO_PROCESSING)
list(APPEND SOURCES "audio_processing/audio_processor.cc" "audio_processing/wake_word_detect.cc") list(APPEND SOURCES "audio_processing/audio_processor.cc" "audio_processing/wake_word_detect.cc")
endif() endif()
@@ -119,8 +123,10 @@ if(CONFIG_IDF_TARGET_ESP32)
"audio_codecs/es8388_audio_codec.cc") "audio_codecs/es8388_audio_codec.cc")
endif() endif()
file(GLOB ASSETS ${CMAKE_CURRENT_SOURCE_DIR}/assets/zh/*.p3)
idf_component_register(SRCS ${SOURCES} idf_component_register(SRCS ${SOURCES}
EMBED_FILES "assets/err_reg.p3" "assets/err_pin.p3" "assets/wificonfig.p3" "assets/upgrade.p3" EMBED_FILES ${ASSETS}
INCLUDE_DIRS ${INCLUDE_DIRS} INCLUDE_DIRS ${INCLUDE_DIRS}
WHOLE_ARCHIVE WHOLE_ARCHIVE
) )

View File

@@ -8,6 +8,7 @@
#include "websocket_protocol.h" #include "websocket_protocol.h"
#include "font_awesome_symbols.h" #include "font_awesome_symbols.h"
#include "iot/thing_manager.h" #include "iot/thing_manager.h"
#include "assets/zh/binary.h"
#include <cstring> #include <cstring>
#include <esp_log.h> #include <esp_log.h>
@@ -17,14 +18,6 @@
#define TAG "Application" #define TAG "Application"
extern const char p3_err_reg_start[] asm("_binary_err_reg_p3_start");
extern const char p3_err_reg_end[] asm("_binary_err_reg_p3_end");
extern const char p3_err_pin_start[] asm("_binary_err_pin_p3_start");
extern const char p3_err_pin_end[] asm("_binary_err_pin_p3_end");
extern const char p3_wificonfig_start[] asm("_binary_wificonfig_p3_start");
extern const char p3_wificonfig_end[] asm("_binary_wificonfig_p3_end");
extern const char p3_upgrade_start[] asm("_binary_upgrade_p3_start");
extern const char p3_upgrade_end[] asm("_binary_upgrade_p3_end");
static const char* const STATE_STRINGS[] = { static const char* const STATE_STRINGS[] = {
"unknown", "unknown",
@@ -35,6 +28,7 @@ static const char* const STATE_STRINGS[] = {
"listening", "listening",
"speaking", "speaking",
"upgrading", "upgrading",
"activating",
"fatal_error", "fatal_error",
"invalid_state" "invalid_state"
}; };
@@ -42,9 +36,6 @@ static const char* const STATE_STRINGS[] = {
Application::Application() { Application::Application() {
event_group_ = xEventGroupCreate(); event_group_ = xEventGroupCreate();
background_task_ = new BackgroundTask(4096 * 8); background_task_ = new BackgroundTask(4096 * 8);
ota_.SetCheckVersionUrl(CONFIG_OTA_VERSION_URL);
ota_.SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
} }
Application::~Application() { Application::~Application() {
@@ -61,13 +52,9 @@ void Application::CheckNewVersion() {
ota_.SetPostData(board.GetJson()); ota_.SetPostData(board.GetJson());
while (true) { while (true) {
bool success = ota_.CheckVersion(); if (ota_.CheckVersion()) {
if (ota_.HasActivationCode()) {
DisplayActivationCode();
}
if (success) {
if (ota_.HasNewVersion()) { if (ota_.HasNewVersion()) {
Alert("Info", "正在升级固件"); Alert("OTA 升级", "正在升级系统", "happy", std::string(p3_upgrade_start, p3_upgrade_end - p3_upgrade_start));
// Wait for the chat state to be idle // Wait for the chat state to be idle
do { do {
vTaskDelay(pdMS_TO_TICKS(3000)); vTaskDelay(pdMS_TO_TICKS(3000));
@@ -78,7 +65,7 @@ void Application::CheckNewVersion() {
SetDeviceState(kDeviceStateUpgrading); SetDeviceState(kDeviceStateUpgrading);
display->SetIcon(FONT_AWESOME_DOWNLOAD); display->SetIcon(FONT_AWESOME_DOWNLOAD);
display->SetStatus("新版本 " + ota_.GetFirmwareVersion()); display->SetChatMessage("system", "新版本 " + ota_.GetFirmwareVersion());
board.SetPowerSaveMode(false); board.SetPowerSaveMode(false);
#if CONFIG_USE_AUDIO_PROCESSING #if CONFIG_USE_AUDIO_PROCESSING
@@ -100,52 +87,83 @@ void Application::CheckNewVersion() {
ota_.StartUpgrade([display](int progress, size_t speed) { ota_.StartUpgrade([display](int progress, size_t speed) {
char buffer[64]; char buffer[64];
snprintf(buffer, sizeof(buffer), "%d%% %zuKB/s", progress, speed / 1024); snprintf(buffer, sizeof(buffer), "%d%% %zuKB/s", progress, speed / 1024);
display->SetStatus(buffer); display->SetChatMessage("system", buffer);
}); });
// If upgrade success, the device will reboot and never reach here // If upgrade success, the device will reboot and never reach here
display->SetStatus("更新失败"); display->SetStatus("更新失败");
ESP_LOGI(TAG, "Firmware upgrade failed..."); ESP_LOGI(TAG, "Firmware upgrade failed...");
vTaskDelay(pdMS_TO_TICKS(3000)); vTaskDelay(pdMS_TO_TICKS(3000));
esp_restart(); Reboot();
}); });
} else { } else {
ota_.MarkCurrentVersionValid(); ota_.MarkCurrentVersionValid();
display->ShowNotification("版本 " + ota_.GetCurrentVersion()); display->ShowNotification("版本 " + ota_.GetCurrentVersion());
}
// Check if the activation code is valid
if (ota_.HasActivationCode()) {
SetDeviceState(kDeviceStateActivating);
ShowActivationCode();
} else {
SetDeviceState(kDeviceStateIdle);
display->SetChatMessage("system", "");
return; return;
} }
}
}
// Check again in 60 seconds // Check again in 60 seconds
vTaskDelay(pdMS_TO_TICKS(60000)); vTaskDelay(pdMS_TO_TICKS(60000));
} }
} }
void Application::DisplayActivationCode() { void Application::ShowActivationCode() {
ESP_LOGW(TAG, "Activation Message: %s", ota_.GetActivationMessage().c_str()); auto& message = ota_.GetActivationMessage();
ESP_LOGW(TAG, "Activation Code: %s", ota_.GetActivationCode().c_str()); auto& code = ota_.GetActivationCode();
auto display = Board::GetInstance().GetDisplay();
display->ShowNotification(ota_.GetActivationMessage(), 30000); struct digit_sound {
char digit;
const char* sound_data_start;
const char* sound_data_end;
};
digit_sound digit_sounds[] = {
{'0', p3_0_start, p3_0_end},
{'1', p3_1_start, p3_1_end},
{'2', p3_2_start, p3_2_end},
{'3', p3_3_start, p3_3_end},
{'4', p3_4_start, p3_4_end},
{'5', p3_5_start, p3_5_end},
{'6', p3_6_start, p3_6_end},
{'7', p3_7_start, p3_7_end},
{'8', p3_8_start, p3_8_end},
{'9', p3_9_start, p3_9_end},
};
std::string sound = std::string(p3_activation_start, p3_activation_end - p3_activation_start);
for (const auto& digit : code) {
auto it = std::find_if(digit_sounds, digit_sounds + sizeof(digit_sounds) / sizeof(digit_sound),
[digit](const digit_sound& ds) { return ds.digit == digit; });
if (it != digit_sounds + sizeof(digit_sounds) / sizeof(digit_sound)) {
sound += std::string(it->sound_data_start, it->sound_data_end - it->sound_data_start);
}
}
Alert("激活设备", message, "happy", sound);
} }
void Application::Alert(const std::string& title, const std::string& message) { void Application::Alert(const std::string& status, const std::string& message, const std::string& emotion, const std::string& sound) {
ESP_LOGW(TAG, "Alert: %s, %s", title.c_str(), message.c_str()); ESP_LOGW(TAG, "Alert %s: %s [%s]", status.c_str(), message.c_str(), emotion.c_str());
auto display = Board::GetInstance().GetDisplay(); auto display = Board::GetInstance().GetDisplay();
display->ShowNotification(message); display->SetStatus(status);
display->SetEmotion(emotion);
if (message == "进入配网模式") { display->SetChatMessage("system", message);
PlayLocalFile(p3_wificonfig_start, p3_wificonfig_end - p3_wificonfig_start); if (!sound.empty()) {
} else if (message == "正在升级固件") { PlayLocalFile(sound.data(), sound.size());
PlayLocalFile(p3_upgrade_start, p3_upgrade_end - p3_upgrade_start);
} else if (message == "请插入SIM卡") {
PlayLocalFile(p3_err_pin_start, p3_err_pin_end - p3_err_pin_start);
} else if (message == "无法接入网络,请检查流量卡状态") {
PlayLocalFile(p3_err_reg_start, p3_err_reg_end - p3_err_reg_start);
} }
} }
void Application::PlayLocalFile(const char* data, size_t size) { void Application::PlayLocalFile(const char* data, size_t size) {
ESP_LOGI(TAG, "PlayLocalFile: %zu bytes", size); ESP_LOGI(TAG, "PlayLocalFile: %zu bytes", size);
auto codec = Board::GetInstance().GetAudioCodec();
codec->EnableOutput(true);
SetDecodeSampleRate(16000); SetDecodeSampleRate(16000);
for (const char* p = data; p < data + size; ) { for (const char* p = data; p < data + size; ) {
auto p3 = (BinaryProtocol3*)p; auto p3 = (BinaryProtocol3*)p;
@@ -164,6 +182,11 @@ void Application::PlayLocalFile(const char* data, size_t size) {
void Application::ToggleChatState() { void Application::ToggleChatState() {
Schedule([this]() { Schedule([this]() {
if (device_state_ == kDeviceStateActivating) {
Reboot();
return;
}
if (!protocol_) { if (!protocol_) {
ESP_LOGE(TAG, "Protocol not initialized"); ESP_LOGE(TAG, "Protocol not initialized");
return; return;
@@ -172,7 +195,7 @@ void Application::ToggleChatState() {
if (device_state_ == kDeviceStateIdle) { if (device_state_ == kDeviceStateIdle) {
SetDeviceState(kDeviceStateConnecting); SetDeviceState(kDeviceStateConnecting);
if (!protocol_->OpenAudioChannel()) { if (!protocol_->OpenAudioChannel()) {
Alert("Error", "Failed to open audio channel"); Alert("ERROR", "无法建立音频通道");
SetDeviceState(kDeviceStateIdle); SetDeviceState(kDeviceStateIdle);
return; return;
} }
@@ -190,6 +213,11 @@ void Application::ToggleChatState() {
void Application::StartListening() { void Application::StartListening() {
Schedule([this]() { Schedule([this]() {
if (device_state_ == kDeviceStateActivating) {
Reboot();
return;
}
if (!protocol_) { if (!protocol_) {
ESP_LOGE(TAG, "Protocol not initialized"); ESP_LOGE(TAG, "Protocol not initialized");
return; return;
@@ -201,7 +229,7 @@ void Application::StartListening() {
SetDeviceState(kDeviceStateConnecting); SetDeviceState(kDeviceStateConnecting);
if (!protocol_->OpenAudioChannel()) { if (!protocol_->OpenAudioChannel()) {
SetDeviceState(kDeviceStateIdle); SetDeviceState(kDeviceStateIdle);
Alert("Error", "Failed to open audio channel"); Alert("ERROR", "无法建立音频通道");
return; return;
} }
} }
@@ -275,14 +303,14 @@ void Application::Start() {
board.StartNetwork(); board.StartNetwork();
// Initialize the protocol // Initialize the protocol
display->SetStatus("初始化协议"); display->SetStatus("加载协议...");
#ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET #ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET
protocol_ = std::make_unique<WebsocketProtocol>(); protocol_ = std::make_unique<WebsocketProtocol>();
#else #else
protocol_ = std::make_unique<MqttProtocol>(); protocol_ = std::make_unique<MqttProtocol>();
#endif #endif
protocol_->OnNetworkError([this](const std::string& message) { protocol_->OnNetworkError([this](const std::string& message) {
Alert("Error", std::move(message)); Alert("ERROR", message);
}); });
protocol_->OnIncomingAudio([this](std::vector<uint8_t>&& data) { protocol_->OnIncomingAudio([this](std::vector<uint8_t>&& data) {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
@@ -371,6 +399,9 @@ void Application::Start() {
}); });
// Check for new firmware version or get the MQTT broker address // Check for new firmware version or get the MQTT broker address
ota_.SetCheckVersionUrl(CONFIG_OTA_VERSION_URL);
ota_.SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
ota_.SetHeader("Client-Id", board.GetUuid());
xTaskCreate([](void* arg) { xTaskCreate([](void* arg) {
Application* app = (Application*)arg; Application* app = (Application*)arg;
app->CheckNewVersion(); app->CheckNewVersion();
@@ -664,3 +695,8 @@ void Application::UpdateIotStates() {
protocol_->SendIotStates(states); protocol_->SendIotStates(states);
} }
} }
void Application::Reboot() {
ESP_LOGI(TAG, "Rebooting...");
esp_restart();
}

View File

@@ -35,6 +35,7 @@ enum DeviceState {
kDeviceStateListening, kDeviceStateListening,
kDeviceStateSpeaking, kDeviceStateSpeaking,
kDeviceStateUpgrading, kDeviceStateUpgrading,
kDeviceStateActivating,
kDeviceStateFatalError kDeviceStateFatalError
}; };
@@ -55,12 +56,13 @@ public:
bool IsVoiceDetected() const { return voice_detected_; } bool IsVoiceDetected() const { return voice_detected_; }
void Schedule(std::function<void()> callback); void Schedule(std::function<void()> callback);
void SetDeviceState(DeviceState state); void SetDeviceState(DeviceState state);
void Alert(const std::string& title, const std::string& message); void Alert(const std::string& status, const std::string& message, const std::string& emotion = "", const std::string& sound = "");
void AbortSpeaking(AbortReason reason); void AbortSpeaking(AbortReason reason);
void ToggleChatState(); void ToggleChatState();
void StartListening(); void StartListening();
void StopListening(); void StopListening();
void UpdateIotStates(); void UpdateIotStates();
void Reboot();
private: private:
Application(); Application();
@@ -100,7 +102,7 @@ private:
void ResetDecoder(); void ResetDecoder();
void SetDecodeSampleRate(int sample_rate); void SetDecodeSampleRate(int sample_rate);
void CheckNewVersion(); void CheckNewVersion();
void DisplayActivationCode(); void ShowActivationCode();
void PlayLocalFile(const char* data, size_t size); void PlayLocalFile(const char* data, size_t size);
}; };

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
main/assets/zh/0.p3 Normal file

Binary file not shown.

BIN
main/assets/zh/1.p3 Normal file

Binary file not shown.

BIN
main/assets/zh/2.p3 Normal file

Binary file not shown.

BIN
main/assets/zh/3.p3 Normal file

Binary file not shown.

BIN
main/assets/zh/4.p3 Normal file

Binary file not shown.

BIN
main/assets/zh/5.p3 Normal file

Binary file not shown.

BIN
main/assets/zh/6.p3 Normal file

Binary file not shown.

BIN
main/assets/zh/7.p3 Normal file

Binary file not shown.

BIN
main/assets/zh/8.p3 Normal file

Binary file not shown.

BIN
main/assets/zh/9.p3 Normal file

Binary file not shown.

Binary file not shown.

39
main/assets/zh/binary.h Normal file
View File

@@ -0,0 +1,39 @@
#ifndef BINARY_H
#define BINARY_H
extern const char p3_err_reg_start[] asm("_binary_err_reg_p3_start");
extern const char p3_err_reg_end[] asm("_binary_err_reg_p3_end");
extern const char p3_err_pin_start[] asm("_binary_err_pin_p3_start");
extern const char p3_err_pin_end[] asm("_binary_err_pin_p3_end");
extern const char p3_wificonfig_start[] asm("_binary_wificonfig_p3_start");
extern const char p3_wificonfig_end[] asm("_binary_wificonfig_p3_end");
extern const char p3_upgrade_start[] asm("_binary_upgrade_p3_start");
extern const char p3_upgrade_end[] asm("_binary_upgrade_p3_end");
extern const char p3_activation_start[] asm("_binary_activation_p3_start");
extern const char p3_activation_end[] asm("_binary_activation_p3_end");
extern const char p3_welcome_start[] asm("_binary_welcome_p3_start");
extern const char p3_welcome_end[] asm("_binary_welcome_p3_end");
extern const char p3_0_start[] asm("_binary_0_p3_start");
extern const char p3_0_end[] asm("_binary_0_p3_end");
extern const char p3_1_start[] asm("_binary_1_p3_start");
extern const char p3_1_end[] asm("_binary_1_p3_end");
extern const char p3_2_start[] asm("_binary_2_p3_start");
extern const char p3_2_end[] asm("_binary_2_p3_end");
extern const char p3_3_start[] asm("_binary_3_p3_start");
extern const char p3_3_end[] asm("_binary_3_p3_end");
extern const char p3_4_start[] asm("_binary_4_p3_start");
extern const char p3_4_end[] asm("_binary_4_p3_end");
extern const char p3_5_start[] asm("_binary_5_p3_start");
extern const char p3_5_end[] asm("_binary_5_p3_end");
extern const char p3_6_start[] asm("_binary_6_p3_start");
extern const char p3_6_end[] asm("_binary_6_p3_end");
extern const char p3_7_start[] asm("_binary_7_p3_start");
extern const char p3_7_end[] asm("_binary_7_p3_end");
extern const char p3_8_start[] asm("_binary_8_p3_start");
extern const char p3_8_end[] asm("_binary_8_p3_end");
extern const char p3_9_start[] asm("_binary_9_p3_start");
extern const char p3_9_end[] asm("_binary_9_p3_end");
#endif

BIN
main/assets/zh/err_pin.p3 Normal file

Binary file not shown.

BIN
main/assets/zh/err_reg.p3 Normal file

Binary file not shown.

Binary file not shown.

BIN
main/assets/zh/upgrade.p3 Normal file

Binary file not shown.

BIN
main/assets/zh/welcome.p3 Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -20,7 +20,10 @@ BackgroundTask::~BackgroundTask() {
void BackgroundTask::Schedule(std::function<void()> callback) { void BackgroundTask::Schedule(std::function<void()> callback) {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
if (active_tasks_ >= 30) { if (active_tasks_ >= 30) {
ESP_LOGW(TAG, "active_tasks_ == %u", active_tasks_.load()); int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
if (free_sram < 10000) {
ESP_LOGW(TAG, "active_tasks_ == %u, free_sram == %u", active_tasks_.load(), free_sram);
}
} }
active_tasks_++; active_tasks_++;
main_tasks_.emplace_back([this, cb = std::move(callback)]() { main_tasks_.emplace_back([this, cb = std::move(callback)]() {

View File

@@ -3,6 +3,7 @@
#include "application.h" #include "application.h"
#include "display.h" #include "display.h"
#include "font_awesome_symbols.h" #include "font_awesome_symbols.h"
#include "assets/zh/binary.h"
#include <esp_log.h> #include <esp_log.h>
#include <esp_timer.h> #include <esp_timer.h>
@@ -24,7 +25,7 @@ std::string Ml307Board::GetBoardType() {
void Ml307Board::StartNetwork() { void Ml307Board::StartNetwork() {
auto display = Board::GetInstance().GetDisplay(); auto display = Board::GetInstance().GetDisplay();
display->SetStatus("初始化模块"); display->SetStatus("检测模组...");
modem_.SetDebug(false); modem_.SetDebug(false);
modem_.SetBaudRate(921600); modem_.SetBaudRate(921600);
@@ -47,10 +48,10 @@ void Ml307Board::WaitForNetworkReady() {
display->SetStatus("等待网络..."); display->SetStatus("等待网络...");
int result = modem_.WaitForNetworkReady(); int result = modem_.WaitForNetworkReady();
if (result == -1) { if (result == -1) {
application.Alert("Error", "请插入SIM卡"); application.Alert("PIN_ERROR", "请插入SIM卡", "sad", std::string(p3_err_pin_start, p3_err_pin_end - p3_err_pin_start));
return; return;
} else if (result == -2) { } else if (result == -2) {
application.Alert("Error", "无法接入网络,请检查流量卡状态"); application.Alert("REG_ERROR", "无法接入网络,请检查流量卡状态", "sad", std::string(p3_err_reg_start, p3_err_reg_end - p3_err_reg_start));
return; return;
} }

View File

@@ -5,6 +5,7 @@
#include "system_info.h" #include "system_info.h"
#include "font_awesome_symbols.h" #include "font_awesome_symbols.h"
#include "settings.h" #include "settings.h"
#include "assets/zh/binary.h"
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
@@ -37,7 +38,6 @@ std::string WifiBoard::GetBoardType() {
void WifiBoard::EnterWifiConfigMode() { void WifiBoard::EnterWifiConfigMode() {
auto& application = Application::GetInstance(); auto& application = Application::GetInstance();
auto display = Board::GetInstance().GetDisplay();
application.SetDeviceState(kDeviceStateWifiConfiguring); application.SetDeviceState(kDeviceStateWifiConfiguring);
auto& wifi_ap = WifiConfigurationAp::GetInstance(); auto& wifi_ap = WifiConfigurationAp::GetInstance();
@@ -50,10 +50,8 @@ void WifiBoard::EnterWifiConfigMode() {
hint += ",然后打开浏览器访问 "; hint += ",然后打开浏览器访问 ";
hint += wifi_ap.GetWebServerUrl(); hint += wifi_ap.GetWebServerUrl();
display->SetStatus(hint);
// 播报配置 WiFi 的提示 // 播报配置 WiFi 的提示
application.Alert("Info", "进入配网模式"); application.Alert("配网模式", hint, "", std::string(p3_wificonfig_start, p3_wificonfig_end - p3_wificonfig_start));
// Wait forever until reset after configuration // Wait forever until reset after configuration
while (true) { while (true) {
@@ -83,11 +81,11 @@ void WifiBoard::StartNetwork() {
auto& wifi_station = WifiStation::GetInstance(); auto& wifi_station = WifiStation::GetInstance();
wifi_station.OnScanBegin([this]() { wifi_station.OnScanBegin([this]() {
auto display = Board::GetInstance().GetDisplay(); auto display = Board::GetInstance().GetDisplay();
display->ShowNotification("正在扫描 WiFi 网络", 30000); display->ShowNotification("扫描 WiFi...", 30000);
}); });
wifi_station.OnConnect([this](const std::string& ssid) { wifi_station.OnConnect([this](const std::string& ssid) {
auto display = Board::GetInstance().GetDisplay(); auto display = Board::GetInstance().GetDisplay();
display->ShowNotification(std::string("正在连接 ") + ssid, 30000); display->ShowNotification(std::string("连接 ") + ssid + "...", 30000);
}); });
wifi_station.OnConnected([this](const std::string& ssid) { wifi_station.OnConnected([this](const std::string& ssid) {
auto display = Board::GetInstance().GetDisplay(); auto display = Board::GetInstance().GetDisplay();

View File

@@ -210,6 +210,11 @@ void Display::SetIcon(const char* icon) {
} }
void Display::SetChatMessage(const std::string &role, const std::string &content) { void Display::SetChatMessage(const std::string &role, const std::string &content) {
DisplayLockGuard lock(this);
if (chat_message_label_ == nullptr) {
return;
}
lv_label_set_text(chat_message_label_, content.c_str());
} }
void Display::SetBacklight(uint8_t brightness) { void Display::SetBacklight(uint8_t brightness) {

View File

@@ -42,6 +42,7 @@ protected:
lv_obj_t *notification_label_ = nullptr; lv_obj_t *notification_label_ = nullptr;
lv_obj_t *mute_label_ = nullptr; lv_obj_t *mute_label_ = nullptr;
lv_obj_t *battery_label_ = nullptr; lv_obj_t *battery_label_ = nullptr;
lv_obj_t* chat_message_label_ = nullptr;
const char* battery_icon_ = nullptr; const char* battery_icon_ = nullptr;
const char* network_icon_ = nullptr; const char* network_icon_ = nullptr;
bool muted_ = false; bool muted_ = false;

View File

@@ -274,14 +274,6 @@ void LcdDisplay::SetupUI() {
lv_obj_set_style_text_font(battery_label_, fonts_.icon_font, 0); lv_obj_set_style_text_font(battery_label_, fonts_.icon_font, 0);
} }
void LcdDisplay::SetChatMessage(const std::string &role, const std::string &content) {
DisplayLockGuard lock(this);
if (chat_message_label_ == nullptr) {
return;
}
lv_label_set_text(chat_message_label_, content.c_str());
}
void LcdDisplay::SetEmotion(const std::string &emotion) { void LcdDisplay::SetEmotion(const std::string &emotion) {
struct Emotion { struct Emotion {
const char* icon; const char* icon;

View File

@@ -26,7 +26,6 @@ protected:
lv_obj_t* content_ = nullptr; lv_obj_t* content_ = nullptr;
lv_obj_t* container_ = nullptr; lv_obj_t* container_ = nullptr;
lv_obj_t* side_bar_ = nullptr; lv_obj_t* side_bar_ = nullptr;
lv_obj_t* chat_message_label_ = nullptr;
DisplayFonts fonts_; DisplayFonts fonts_;
@@ -46,7 +45,6 @@ public:
DisplayFonts fonts); DisplayFonts fonts);
~LcdDisplay(); ~LcdDisplay();
virtual void SetChatMessage(const std::string &role, const std::string &content) override;
virtual void SetEmotion(const std::string &emotion) override; virtual void SetEmotion(const std::string &emotion) override;
virtual void SetIcon(const char* icon) override; virtual void SetIcon(const char* icon) override;
virtual void SetBacklight(uint8_t brightness) override; virtual void SetBacklight(uint8_t brightness) override;

View File

@@ -132,6 +132,20 @@ void Ssd1306Display::Unlock() {
lvgl_port_unlock(); lvgl_port_unlock();
} }
void Ssd1306Display::SetChatMessage(const std::string &role, const std::string &content) {
DisplayLockGuard lock(this);
if (content_right_ == nullptr) {
lv_label_set_text(chat_message_label_, content.c_str());
} else {
if (content.empty()) {
lv_obj_add_flag(content_right_, LV_OBJ_FLAG_HIDDEN);
} else {
lv_label_set_text(chat_message_label_, content.c_str());
lv_obj_clear_flag(content_right_, LV_OBJ_FLAG_HIDDEN);
}
}
}
void Ssd1306Display::SetupUI_128x64() { void Ssd1306Display::SetupUI_128x64() {
DisplayLockGuard lock(this); DisplayLockGuard lock(this);
@@ -149,20 +163,46 @@ void Ssd1306Display::SetupUI_128x64() {
/* Status bar */ /* Status bar */
status_bar_ = lv_obj_create(container_); status_bar_ = lv_obj_create(container_);
lv_obj_set_size(status_bar_, LV_HOR_RES, 18); lv_obj_set_size(status_bar_, LV_HOR_RES, 16);
lv_obj_set_style_border_width(status_bar_, 0, 0);
lv_obj_set_style_pad_all(status_bar_, 0, 0);
lv_obj_set_style_radius(status_bar_, 0, 0); lv_obj_set_style_radius(status_bar_, 0, 0);
/* Content */ /* Content */
content_ = lv_obj_create(container_); content_ = lv_obj_create(container_);
lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF); lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_radius(status_bar_, 0, 0); lv_obj_set_style_radius(content_, 0, 0);
lv_obj_set_style_pad_all(content_, 0, 0);
lv_obj_set_width(content_, LV_HOR_RES); lv_obj_set_width(content_, LV_HOR_RES);
lv_obj_set_flex_grow(content_, 1); lv_obj_set_flex_grow(content_, 1);
lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_ROW);
lv_obj_set_style_flex_main_place(content_, LV_FLEX_ALIGN_CENTER, 0);
emotion_label_ = lv_label_create(content_); // 创建左侧固定宽度的容器
content_left_ = lv_obj_create(content_);
lv_obj_set_size(content_left_, 32, LV_SIZE_CONTENT); // 固定宽度32像素
lv_obj_set_style_pad_all(content_left_, 0, 0);
lv_obj_set_style_border_width(content_left_, 0, 0);
emotion_label_ = lv_label_create(content_left_);
lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_1, 0); lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_1, 0);
lv_label_set_text(emotion_label_, FONT_AWESOME_AI_CHIP); lv_label_set_text(emotion_label_, FONT_AWESOME_AI_CHIP);
lv_obj_center(emotion_label_); lv_obj_center(emotion_label_);
lv_obj_set_style_pad_top(emotion_label_, 8, 0);
// 创建右侧可扩展的容器
content_right_ = lv_obj_create(content_);
lv_obj_set_size(content_right_, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(content_right_, 0, 0);
lv_obj_set_style_border_width(content_right_, 0, 0);
lv_obj_set_flex_grow(content_right_, 1);
lv_obj_add_flag(content_right_, LV_OBJ_FLAG_HIDDEN);
chat_message_label_ = lv_label_create(content_right_);
lv_label_set_text(chat_message_label_, "");
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP);
lv_obj_set_width(chat_message_label_, lv_pct(100));
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_LEFT, 0);
/* Status bar */ /* Status bar */
lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW); lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW);
@@ -250,17 +290,19 @@ void Ssd1306Display::SetupUI_128x32() {
lv_label_set_text(battery_label_, ""); lv_label_set_text(battery_label_, "");
lv_obj_set_style_text_font(battery_label_, icon_font_, 0); lv_obj_set_style_text_font(battery_label_, icon_font_, 0);
status_label_ = lv_label_create(side_bar_); status_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(status_label_, 1); lv_obj_set_style_pad_left(status_label_, 2, 0);
lv_obj_set_width(status_label_, width_ - 32);
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_label_set_text(status_label_, "正在初始化"); lv_label_set_text(status_label_, "正在初始化");
notification_label_ = lv_label_create(side_bar_); notification_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(notification_label_, 1);
lv_obj_set_width(notification_label_, width_ - 32);
lv_label_set_long_mode(notification_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_label_set_text(notification_label_, "通知"); lv_label_set_text(notification_label_, "通知");
lv_obj_set_style_pad_left(notification_label_, 2, 0);
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
chat_message_label_ = lv_label_create(side_bar_);
lv_obj_set_flex_grow(chat_message_label_, 1);
lv_obj_set_width(chat_message_label_, width_ - 32);
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_label_set_text(chat_message_label_, "");
} }

View File

@@ -13,6 +13,8 @@ private:
lv_obj_t* status_bar_ = nullptr; lv_obj_t* status_bar_ = nullptr;
lv_obj_t* content_ = nullptr; lv_obj_t* content_ = nullptr;
lv_obj_t* content_left_ = nullptr;
lv_obj_t* content_right_ = nullptr;
lv_obj_t* container_ = nullptr; lv_obj_t* container_ = nullptr;
lv_obj_t* side_bar_ = nullptr; lv_obj_t* side_bar_ = nullptr;
@@ -29,6 +31,8 @@ public:
Ssd1306Display(void* i2c_master_handle, int width, int height, bool mirror_x, bool mirror_y, Ssd1306Display(void* i2c_master_handle, int width, int height, bool mirror_x, bool mirror_y,
const lv_font_t* text_font, const lv_font_t* icon_font); const lv_font_t* text_font, const lv_font_t* icon_font);
~Ssd1306Display(); ~Ssd1306Display();
virtual void SetChatMessage(const std::string &role, const std::string &content);
}; };
#endif // SSD1306_DISPLAY_H #endif // SSD1306_DISPLAY_H

View File

@@ -211,8 +211,13 @@ void CircularStrip::OnStateChanged() {
Blink(color, 100); Blink(color, 100);
break; break;
} }
case kDeviceStateActivating: {
StripColor color = { LOW_BRIGHTNESS, DEFAULT_BRIGHTNESS, LOW_BRIGHTNESS };
Blink(color, 500);
break;
}
default: default:
ESP_LOGE(TAG, "Invalid led strip event: %d", device_state); ESP_LOGW(TAG, "Unknown led strip event: %d", device_state);
return; return;
} }
} }

View File

@@ -151,8 +151,12 @@ void SingleLed::OnStateChanged() {
SetColor(0, DEFAULT_BRIGHTNESS, 0); SetColor(0, DEFAULT_BRIGHTNESS, 0);
StartContinuousBlink(100); StartContinuousBlink(100);
break; break;
case kDeviceStateActivating:
SetColor(0, DEFAULT_BRIGHTNESS, 0);
StartContinuousBlink(500);
break;
default: default:
ESP_LOGE(TAG, "Invalid led strip event: %d", device_state); ESP_LOGW(TAG, "Unknown led strip event: %d", device_state);
return; return;
} }
} }

View File

@@ -71,6 +71,7 @@ bool Ota::CheckVersion() {
return false; return false;
} }
has_activation_code_ = false;
cJSON *activation = cJSON_GetObjectItem(root, "activation"); cJSON *activation = cJSON_GetObjectItem(root, "activation");
if (activation != NULL) { if (activation != NULL) {
cJSON* message = cJSON_GetObjectItem(activation, "message"); cJSON* message = cJSON_GetObjectItem(activation, "message");
@@ -84,6 +85,7 @@ bool Ota::CheckVersion() {
has_activation_code_ = true; has_activation_code_ = true;
} }
has_mqtt_config_ = false;
cJSON *mqtt = cJSON_GetObjectItem(root, "mqtt"); cJSON *mqtt = cJSON_GetObjectItem(root, "mqtt");
if (mqtt != NULL) { if (mqtt != NULL) {
Settings settings("mqtt", true); Settings settings("mqtt", true);

View File

@@ -10,8 +10,6 @@
#define TAG "WS" #define TAG "WS"
#ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET
WebsocketProtocol::WebsocketProtocol() { WebsocketProtocol::WebsocketProtocol() {
event_group_handle_ = xEventGroupCreate(); event_group_handle_ = xEventGroupCreate();
} }
@@ -61,7 +59,7 @@ bool WebsocketProtocol::OpenAudioChannel() {
websocket_->SetHeader("Authorization", token.c_str()); websocket_->SetHeader("Authorization", token.c_str());
websocket_->SetHeader("Protocol-Version", "1"); websocket_->SetHeader("Protocol-Version", "1");
websocket_->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str()); websocket_->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
websocket_->SetHeader("X-Uuid", Board::GetInstance().GetUuid().c_str()); websocket_->SetHeader("Client-Id", Board::GetInstance().GetUuid().c_str());
websocket_->OnData([this](const char* data, size_t len, bool binary) { websocket_->OnData([this](const char* data, size_t len, bool binary) {
if (binary) { if (binary) {
@@ -147,5 +145,3 @@ void WebsocketProtocol::ParseServerHello(const cJSON* root) {
xEventGroupSetBits(event_group_handle_, WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT); xEventGroupSetBits(event_group_handle_, WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT);
} }
#endif