mirror of
https://github.com/78/xiaozhi-esp32.git
synced 2026-07-21 02:05:52 +00:00
* feat: migrate firmware to xiaozhi-fonts 2.0.0 * fix: restart scrolling subtitles on text change * docs: add AGENTS.md for project overview, architecture, rules, and commands - Introduced AGENTS.md to document the XiaoZhi voice-assistant firmware architecture, required rules for development, board configuration process, and validation steps. - Included detailed sections on project structure, commands for building and testing, and authoritative documentation references. * feat: finalize glyph push protocol * fix: remove stale emoji font include * build: use xiaozhi-fonts 2.0.0 * fix: decouple custom fonts from glyph push * refactor: improve wake word invocation handling - Introduced BeginWakeWordInvoke method to streamline the wake word processing flow. - Updated HandleWakeWordDetectedEvent to utilize the new method, ensuring proper state transitions and scheduling. - Enhanced error handling to prevent the device from getting stuck in the connecting state. - Refactored audio channel management to improve responsiveness during wake word detection. --------- Co-authored-by: Xiaoxia <terrence.huang@tenclass.com>
359 lines
12 KiB
C++
359 lines
12 KiB
C++
#include <esp_err.h>
|
|
#include <esp_log.h>
|
|
#include <material_symbols.h>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#include "application.h"
|
|
#include "assets/lang_config.h"
|
|
#include "audio_codec.h"
|
|
#include "board.h"
|
|
#include "dynamic_glyph_cache.h"
|
|
#include "jpg/image_to_jpeg.h"
|
|
#include "lvgl_display.h"
|
|
#include "lvgl_theme.h"
|
|
#include "settings.h"
|
|
|
|
#define TAG "Display"
|
|
|
|
LvglDisplay::LvglDisplay() {
|
|
dynamic_glyph_cache_ = std::make_unique<DynamicGlyphCache>();
|
|
// Notification timer
|
|
esp_timer_create_args_t notification_timer_args = {
|
|
.callback =
|
|
[](void* arg) {
|
|
LvglDisplay* display = static_cast<LvglDisplay*>(arg);
|
|
DisplayLockGuard lock(display);
|
|
lv_obj_add_flag(display->notification_label_, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_remove_flag(display->status_label_, LV_OBJ_FLAG_HIDDEN);
|
|
},
|
|
.arg = this,
|
|
.dispatch_method = ESP_TIMER_TASK,
|
|
.name = "notification_timer",
|
|
.skip_unhandled_events = false,
|
|
};
|
|
ESP_ERROR_CHECK(esp_timer_create(¬ification_timer_args, ¬ification_timer_));
|
|
|
|
// Create a power management lock
|
|
auto ret = esp_pm_lock_create(ESP_PM_APB_FREQ_MAX, 0, "display_update", &pm_lock_);
|
|
if (ret == ESP_ERR_NOT_SUPPORTED) {
|
|
ESP_LOGI(TAG, "Power management not supported");
|
|
} else {
|
|
ESP_ERROR_CHECK(ret);
|
|
}
|
|
}
|
|
|
|
bool LvglDisplay::SetTextFont(std::shared_ptr<LvglFont> text_font) {
|
|
if (text_font == nullptr || text_font->font() == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
DisplayLockGuard lock(this);
|
|
auto& theme_manager = LvglThemeManager::GetInstance();
|
|
auto light_theme = theme_manager.GetTheme("light");
|
|
auto dark_theme = theme_manager.GetTheme("dark");
|
|
if (light_theme == nullptr && dark_theme == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
// LVGL styles keep raw lv_font_t pointers. Keep the previous font owners alive until the
|
|
// current theme has rebound every style to the new font.
|
|
auto previous_light_font = light_theme != nullptr ? light_theme->text_font() : nullptr;
|
|
auto previous_dark_font = dark_theme != nullptr ? dark_theme->text_font() : nullptr;
|
|
if (light_theme != nullptr) {
|
|
light_theme->set_text_font(text_font);
|
|
}
|
|
if (dark_theme != nullptr) {
|
|
dark_theme->set_text_font(text_font);
|
|
}
|
|
if (current_theme_ != nullptr) {
|
|
SetTheme(current_theme_);
|
|
}
|
|
previous_light_font.reset();
|
|
previous_dark_font.reset();
|
|
return true;
|
|
}
|
|
|
|
bool LvglDisplay::AddTextGlyphs(const std::vector<TextGlyph>& glyphs, uint8_t bpp) {
|
|
if (dynamic_glyph_cache_ == nullptr) {
|
|
return false;
|
|
}
|
|
if (glyphs.empty()) {
|
|
if (!TextGlyphStorageUsesPsram()) {
|
|
ClearTextGlyphs();
|
|
}
|
|
return false;
|
|
}
|
|
if (bpp != 1 && bpp != 4) {
|
|
return false;
|
|
}
|
|
|
|
DisplayLockGuard lock(this);
|
|
auto theme = dynamic_cast<LvglTheme*>(current_theme_);
|
|
if (theme == nullptr || theme->text_font() == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
auto fallback = dynamic_glyph_cache_->EnsureFont(theme->text_font()->font(), bpp);
|
|
if (fallback == nullptr) {
|
|
return false;
|
|
}
|
|
theme->text_font()->SetFallback(fallback);
|
|
return dynamic_glyph_cache_->AddGlyphs(glyphs);
|
|
}
|
|
|
|
void LvglDisplay::ClearTextGlyphs() {
|
|
if (dynamic_glyph_cache_ == nullptr) {
|
|
return;
|
|
}
|
|
DisplayLockGuard lock(this);
|
|
dynamic_glyph_cache_->Clear();
|
|
}
|
|
|
|
LvglDisplay::~LvglDisplay() {
|
|
if (notification_timer_ != nullptr) {
|
|
esp_timer_stop(notification_timer_);
|
|
esp_timer_delete(notification_timer_);
|
|
}
|
|
|
|
if (network_label_ != nullptr) {
|
|
lv_obj_del(network_label_);
|
|
}
|
|
if (notification_label_ != nullptr) {
|
|
lv_obj_del(notification_label_);
|
|
}
|
|
if (status_label_ != nullptr) {
|
|
lv_obj_del(status_label_);
|
|
}
|
|
if (mute_label_ != nullptr) {
|
|
lv_obj_del(mute_label_);
|
|
}
|
|
if (battery_label_ != nullptr) {
|
|
lv_obj_del(battery_label_);
|
|
}
|
|
if (low_battery_popup_ != nullptr) {
|
|
lv_obj_del(low_battery_popup_);
|
|
}
|
|
if (pm_lock_ != nullptr) {
|
|
esp_pm_lock_delete(pm_lock_);
|
|
}
|
|
}
|
|
|
|
void LvglDisplay::SetStatus(const char* status) {
|
|
if (!setup_ui_called_) {
|
|
ESP_LOGW(TAG, "SetStatus('%s') called before SetupUI() - message will be lost!", status);
|
|
}
|
|
DisplayLockGuard lock(this);
|
|
if (status_label_ == nullptr) {
|
|
if (setup_ui_called_) {
|
|
ESP_LOGW(TAG,
|
|
"SetStatus('%s') failed: status_label_ is nullptr (SetupUI() was called but "
|
|
"label not created)",
|
|
status);
|
|
}
|
|
return;
|
|
}
|
|
lv_label_set_text(status_label_, status);
|
|
lv_obj_remove_flag(status_label_, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
last_status_update_time_ = std::chrono::system_clock::now();
|
|
}
|
|
|
|
void LvglDisplay::ShowNotification(const std::string& notification, int duration_ms) {
|
|
ShowNotification(notification.c_str(), duration_ms);
|
|
}
|
|
|
|
void LvglDisplay::ShowNotification(const char* notification, int duration_ms) {
|
|
if (!setup_ui_called_) {
|
|
ESP_LOGW(TAG, "ShowNotification('%s') called before SetupUI() - message will be lost!",
|
|
notification);
|
|
}
|
|
DisplayLockGuard lock(this);
|
|
if (notification_label_ == nullptr) {
|
|
if (setup_ui_called_) {
|
|
ESP_LOGW(TAG,
|
|
"ShowNotification('%s') failed: notification_label_ is nullptr (SetupUI() was "
|
|
"called but label not created)",
|
|
notification);
|
|
}
|
|
return;
|
|
}
|
|
lv_label_set_text(notification_label_, notification);
|
|
lv_obj_remove_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_add_flag(status_label_, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
esp_timer_stop(notification_timer_);
|
|
ESP_ERROR_CHECK(esp_timer_start_once(notification_timer_, duration_ms * 1000));
|
|
}
|
|
|
|
void LvglDisplay::UpdateStatusBar(bool update_all) {
|
|
auto& app = Application::GetInstance();
|
|
auto& board = Board::GetInstance();
|
|
auto codec = board.GetAudioCodec();
|
|
|
|
// Update mute icon
|
|
{
|
|
DisplayLockGuard lock(this);
|
|
if (mute_label_ == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// Update icon if mute state changes
|
|
if (codec->output_volume() == 0 && !muted_) {
|
|
muted_ = true;
|
|
lv_label_set_text(mute_label_, MATERIAL_SYMBOLS_VOLUME_OFF);
|
|
} else if (codec->output_volume() > 0 && muted_) {
|
|
muted_ = false;
|
|
lv_label_set_text(mute_label_, "");
|
|
}
|
|
}
|
|
|
|
// Update time
|
|
if (app.GetDeviceState() == kDeviceStateIdle) {
|
|
if (last_status_update_time_ + std::chrono::seconds(10) <
|
|
std::chrono::system_clock::now()) {
|
|
// Set status to clock "HH:MM"
|
|
time_t now = time(NULL);
|
|
struct tm* tm = localtime(&now);
|
|
// Check if the we have already set the time
|
|
if (tm->tm_year >= 2025 - 1900) {
|
|
char time_str[16];
|
|
strftime(time_str, sizeof(time_str), "%H:%M", tm);
|
|
SetStatus(time_str);
|
|
} else {
|
|
ESP_LOGW(TAG, "System time is not set, tm_year: %d", tm->tm_year);
|
|
}
|
|
}
|
|
}
|
|
|
|
esp_pm_lock_acquire(pm_lock_);
|
|
// Update battery icon
|
|
int battery_level;
|
|
bool charging, discharging;
|
|
const char* icon = nullptr;
|
|
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
|
|
if (charging) {
|
|
icon = MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_BOLT;
|
|
} else {
|
|
const char* levels[] = {
|
|
MATERIAL_SYMBOLS_BATTERY_ANDROID_0,
|
|
MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_1,
|
|
MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_2,
|
|
MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_3,
|
|
MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_4,
|
|
MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_5,
|
|
MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_6,
|
|
MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_FULL,
|
|
};
|
|
int level_index = battery_level <= 0
|
|
? 0
|
|
: (battery_level >= 100 ? 7 : 1 + ((battery_level - 1) * 6 / 99));
|
|
icon = levels[level_index];
|
|
}
|
|
DisplayLockGuard lock(this);
|
|
if (battery_label_ != nullptr && battery_icon_ != icon) {
|
|
battery_icon_ = icon;
|
|
lv_label_set_text(battery_label_, battery_icon_);
|
|
}
|
|
|
|
// Check low battery popup only when clock tick event is triggered
|
|
// Because when initializing, the battery level is not ready yet.
|
|
if (low_battery_popup_ != nullptr && !update_all) {
|
|
if (strcmp(icon, MATERIAL_SYMBOLS_BATTERY_ANDROID_0) == 0 && discharging) {
|
|
if (lv_obj_has_flag(low_battery_popup_,
|
|
LV_OBJ_FLAG_HIDDEN)) { // Show if low battery popup is hidden
|
|
lv_obj_remove_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
|
app.Schedule([&app]() { app.PlaySound(Lang::Sounds::OGG_LOW_BATTERY); });
|
|
}
|
|
} else {
|
|
// Hide the low battery popup when the battery is not empty
|
|
if (!lv_obj_has_flag(low_battery_popup_,
|
|
LV_OBJ_FLAG_HIDDEN)) { // Hide if low battery popup is shown
|
|
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update network icon every 10 seconds
|
|
static int seconds_counter = 0;
|
|
if (update_all || seconds_counter++ % 10 == 0) {
|
|
// Don't read 4G network status during firmware upgrade to avoid occupying UART resources
|
|
auto device_state = Application::GetInstance().GetDeviceState();
|
|
static const std::vector<DeviceState> allowed_states = {
|
|
kDeviceStateIdle, kDeviceStateStarting, kDeviceStateWifiConfiguring,
|
|
kDeviceStateListening, kDeviceStateActivating,
|
|
};
|
|
if (std::find(allowed_states.begin(), allowed_states.end(), device_state) !=
|
|
allowed_states.end()) {
|
|
icon = board.GetNetworkStateIcon();
|
|
if (network_label_ != nullptr && icon != nullptr && network_icon_ != icon) {
|
|
DisplayLockGuard lock(this);
|
|
network_icon_ = icon;
|
|
lv_label_set_text(network_label_, network_icon_);
|
|
}
|
|
}
|
|
}
|
|
|
|
esp_pm_lock_release(pm_lock_);
|
|
}
|
|
|
|
void LvglDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {}
|
|
|
|
void LvglDisplay::SetPowerSaveMode(bool on) {
|
|
if (on) {
|
|
SetChatMessage("system", "");
|
|
SetEmotion("sleepy");
|
|
} else {
|
|
SetChatMessage("system", "");
|
|
SetEmotion("neutral");
|
|
}
|
|
}
|
|
|
|
bool LvglDisplay::SnapshotToJpeg(std::string& jpeg_data, int quality) {
|
|
#if CONFIG_LV_USE_SNAPSHOT
|
|
DisplayLockGuard lock(this);
|
|
|
|
lv_obj_t* screen = lv_screen_active();
|
|
lv_draw_buf_t* draw_buffer = lv_snapshot_take(screen, LV_COLOR_FORMAT_RGB565);
|
|
if (draw_buffer == nullptr) {
|
|
ESP_LOGE(TAG, "Failed to take snapshot, draw_buffer is nullptr");
|
|
return false;
|
|
}
|
|
|
|
// swap bytes
|
|
uint16_t* data = (uint16_t*)draw_buffer->data;
|
|
size_t pixel_count = draw_buffer->data_size / 2;
|
|
for (size_t i = 0; i < pixel_count; i++) {
|
|
data[i] = __builtin_bswap16(data[i]);
|
|
}
|
|
|
|
// Clear output string and use callback version to avoid pre-allocating large memory blocks
|
|
jpeg_data.clear();
|
|
|
|
// Use callback-based JPEG encoder to further save memory
|
|
bool ret =
|
|
image_to_jpeg_cb((uint8_t*)draw_buffer->data, draw_buffer->data_size, draw_buffer->header.w,
|
|
draw_buffer->header.h, V4L2_PIX_FMT_RGB565, quality,
|
|
[](void* arg, size_t index, const void* data, size_t len) -> size_t {
|
|
std::string* output = static_cast<std::string*>(arg);
|
|
if (data && len > 0) {
|
|
output->append(static_cast<const char*>(data), len);
|
|
}
|
|
return len;
|
|
},
|
|
&jpeg_data);
|
|
if (!ret) {
|
|
ESP_LOGE(TAG, "Failed to convert image to JPEG");
|
|
}
|
|
|
|
lv_draw_buf_destroy(draw_buffer);
|
|
return ret;
|
|
#else
|
|
ESP_LOGE(TAG, "LV_USE_SNAPSHOT is not enabled");
|
|
return false;
|
|
#endif
|
|
}
|