mirror of
https://github.com/78/xiaozhi-esp32.git
synced 2026-01-14 01:07:30 +08:00
feat: Add lvgl display theme control (#1180)
* feat: Add lvgl display theme control * fix: compiling errors * move light/dark themes to lcd display * fix compile errors --------- Co-authored-by: Xiaoxia <terrence.huang@tenclass.com>
This commit is contained in:
parent
bce662d135
commit
4048647ef8
@ -50,7 +50,11 @@ set(SOURCES "audio/audio_codec.cc"
|
||||
"display/display.cc"
|
||||
"display/lcd_display.cc"
|
||||
"display/oled_display.cc"
|
||||
"display/emoji_collection.cc"
|
||||
"display/lvgl_display/lvgl_display.cc"
|
||||
"display/lvgl_display/emoji_collection.cc"
|
||||
"display/lvgl_display/lvgl_theme.cc"
|
||||
"display/lvgl_display/lvgl_font.cc"
|
||||
"display/lvgl_display/lvgl_image.cc"
|
||||
"protocols/protocol.cc"
|
||||
"protocols/mqtt_protocol.cc"
|
||||
"protocols/websocket_protocol.cc"
|
||||
@ -64,7 +68,7 @@ set(SOURCES "audio/audio_codec.cc"
|
||||
"main.cc"
|
||||
)
|
||||
|
||||
set(INCLUDE_DIRS "." "display" "audio" "protocols")
|
||||
set(INCLUDE_DIRS "." "display" "display/lvgl_display" "audio" "protocols")
|
||||
|
||||
# Add board common files
|
||||
file(GLOB BOARD_COMMON_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/common/*.cc)
|
||||
|
||||
112
main/assets.cc
112
main/assets.cc
@ -2,6 +2,7 @@
|
||||
#include "board.h"
|
||||
#include "display.h"
|
||||
#include "application.h"
|
||||
#include "lvgl_theme.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <spi_flash_mmap.h>
|
||||
@ -32,12 +33,6 @@ Assets::Assets(std::string default_assets_url) {
|
||||
}
|
||||
|
||||
Assets::~Assets() {
|
||||
if (custom_emoji_collection_ != nullptr) {
|
||||
delete custom_emoji_collection_;
|
||||
}
|
||||
if (text_font_) {
|
||||
cbin_font_delete(text_font_);
|
||||
}
|
||||
if (mmap_handle_ != 0) {
|
||||
esp_partition_munmap(mmap_handle_);
|
||||
}
|
||||
@ -111,6 +106,17 @@ bool Assets::InitializePartition() {
|
||||
return checksum_valid_;
|
||||
}
|
||||
|
||||
lv_color_t Assets::ParseColor(const std::string& color) {
|
||||
if (color.find("#") == 0) {
|
||||
// Convert #112233 to lv_color_t
|
||||
uint8_t r = strtol(color.substr(1, 2).c_str(), nullptr, 16);
|
||||
uint8_t g = strtol(color.substr(3, 2).c_str(), nullptr, 16);
|
||||
uint8_t b = strtol(color.substr(5, 2).c_str(), nullptr, 16);
|
||||
return lv_color_make(r, g, b);
|
||||
}
|
||||
return lv_color_black();
|
||||
}
|
||||
|
||||
bool Assets::Apply() {
|
||||
void* ptr = nullptr;
|
||||
size_t size = 0;
|
||||
@ -123,6 +129,14 @@ bool Assets::Apply() {
|
||||
ESP_LOGE(TAG, "The index.json file is not valid");
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON* version = cJSON_GetObjectItem(root, "version");
|
||||
if (cJSON_IsNumber(version)) {
|
||||
if (version->valuedouble > 1) {
|
||||
ESP_LOGE(TAG, "The assets version %d is not supported, please upgrade the firmware", version->valueint);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* srmodels = cJSON_GetObjectItem(root, "srmodels");
|
||||
if (cJSON_IsString(srmodels)) {
|
||||
@ -144,17 +158,21 @@ bool Assets::Apply() {
|
||||
}
|
||||
}
|
||||
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto light_theme = theme_manager.GetTheme("light");
|
||||
auto dark_theme = theme_manager.GetTheme("dark");
|
||||
|
||||
cJSON* font = cJSON_GetObjectItem(root, "text_font");
|
||||
if (cJSON_IsString(font)) {
|
||||
std::string fonts_text_file = font->valuestring;
|
||||
if (GetAssetData(fonts_text_file, ptr, size)) {
|
||||
if (text_font_ != nullptr) {
|
||||
cbin_font_delete(text_font_);
|
||||
}
|
||||
text_font_ = cbin_font_create(static_cast<uint8_t*>(ptr));
|
||||
if (text_font_ == nullptr) {
|
||||
auto text_font = std::make_shared<LvglCBinFont>(ptr);
|
||||
if (text_font->font() == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to load fonts.bin");
|
||||
return false;
|
||||
}
|
||||
light_theme->set_text_font(text_font);
|
||||
dark_theme->set_text_font(text_font);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str());
|
||||
}
|
||||
@ -162,10 +180,7 @@ bool Assets::Apply() {
|
||||
|
||||
cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection");
|
||||
if (cJSON_IsArray(emoji_collection)) {
|
||||
if (custom_emoji_collection_ != nullptr) {
|
||||
delete custom_emoji_collection_;
|
||||
}
|
||||
custom_emoji_collection_ = new CustomEmojiCollection();
|
||||
auto custom_emoji_collection = std::make_shared<CustomEmojiCollection>();
|
||||
int emoji_count = cJSON_GetArraySize(emoji_collection);
|
||||
for (int i = 0; i < emoji_count; i++) {
|
||||
cJSON* emoji = cJSON_GetArrayItem(emoji_collection, i);
|
||||
@ -177,28 +192,63 @@ bool Assets::Apply() {
|
||||
ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring);
|
||||
continue;
|
||||
}
|
||||
auto img = new lv_img_dsc_t {
|
||||
.header = {
|
||||
.magic = LV_IMAGE_HEADER_MAGIC,
|
||||
.cf = LV_COLOR_FORMAT_RAW_ALPHA,
|
||||
},
|
||||
.data_size = size,
|
||||
.data = static_cast<uint8_t*>(ptr),
|
||||
};
|
||||
custom_emoji_collection_->AddEmoji(name->valuestring, img);
|
||||
custom_emoji_collection->AddEmoji(name->valuestring, new LvglRawImage(ptr, size));
|
||||
}
|
||||
}
|
||||
}
|
||||
light_theme->set_emoji_collection(custom_emoji_collection);
|
||||
dark_theme->set_emoji_collection(custom_emoji_collection);
|
||||
}
|
||||
|
||||
cJSON* skin = cJSON_GetObjectItem(root, "skin");
|
||||
if (cJSON_IsObject(skin)) {
|
||||
cJSON* light_skin = cJSON_GetObjectItem(skin, "light");
|
||||
if (cJSON_IsObject(light_skin)) {
|
||||
cJSON* text_color = cJSON_GetObjectItem(light_skin, "text_color");
|
||||
cJSON* background_color = cJSON_GetObjectItem(light_skin, "background_color");
|
||||
cJSON* background_image = cJSON_GetObjectItem(light_skin, "background_image");
|
||||
if (cJSON_IsString(text_color)) {
|
||||
light_theme->set_text_color(ParseColor(text_color->valuestring));
|
||||
}
|
||||
if (cJSON_IsString(background_color)) {
|
||||
light_theme->set_background_color(ParseColor(background_color->valuestring));
|
||||
light_theme->set_chat_background_color(ParseColor(background_color->valuestring));
|
||||
}
|
||||
if (cJSON_IsString(background_image)) {
|
||||
if (!GetAssetData(background_image->valuestring, ptr, size)) {
|
||||
ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
|
||||
return false;
|
||||
}
|
||||
auto background_image = std::make_shared<LvglCBinImage>(ptr);
|
||||
light_theme->set_background_image(background_image);
|
||||
}
|
||||
}
|
||||
cJSON* dark_skin = cJSON_GetObjectItem(skin, "dark");
|
||||
if (cJSON_IsObject(dark_skin)) {
|
||||
cJSON* text_color = cJSON_GetObjectItem(dark_skin, "text_color");
|
||||
cJSON* background_color = cJSON_GetObjectItem(dark_skin, "background_color");
|
||||
cJSON* background_image = cJSON_GetObjectItem(dark_skin, "background_image");
|
||||
if (cJSON_IsString(text_color)) {
|
||||
dark_theme->set_text_color(ParseColor(text_color->valuestring));
|
||||
}
|
||||
if (cJSON_IsString(background_color)) {
|
||||
dark_theme->set_background_color(ParseColor(background_color->valuestring));
|
||||
dark_theme->set_chat_background_color(ParseColor(background_color->valuestring));
|
||||
}
|
||||
if (cJSON_IsString(background_image)) {
|
||||
if (!GetAssetData(background_image->valuestring, ptr, size)) {
|
||||
ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
|
||||
return false;
|
||||
}
|
||||
auto background_image = std::make_shared<LvglCBinImage>(ptr);
|
||||
dark_theme->set_background_image(background_image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
ESP_LOGI(TAG, "Applying new assets to display");
|
||||
display->UpdateStyle({
|
||||
.text_font = text_font_,
|
||||
.icon_font = nullptr,
|
||||
.emoji_collection = custom_emoji_collection_,
|
||||
});
|
||||
|
||||
ESP_LOGI(TAG, "Refreshing display theme...");
|
||||
display->SetTheme(display->GetTheme());
|
||||
cJSON_Delete(root);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ private:
|
||||
bool InitializePartition();
|
||||
uint32_t CalculateChecksum(const char* data, uint32_t length);
|
||||
bool GetAssetData(const std::string& name, void*& ptr, size_t& size);
|
||||
lv_color_t ParseColor(const std::string& color);
|
||||
|
||||
const esp_partition_t* partition_ = nullptr;
|
||||
esp_partition_mmap_handle_t mmap_handle_ = 0;
|
||||
@ -61,9 +62,7 @@ private:
|
||||
bool partition_valid_ = false;
|
||||
bool checksum_valid_ = false;
|
||||
std::string default_assets_url_;
|
||||
lv_font_t* text_font_ = nullptr;
|
||||
srmodel_list_t* models_list_ = nullptr;
|
||||
CustomEmojiCollection* custom_emoji_collection_ = nullptr;
|
||||
std::map<std::string, Asset> assets_;
|
||||
};
|
||||
|
||||
|
||||
@ -153,7 +153,10 @@ std::string Ml307Board::GetDeviceStatusJson() {
|
||||
}
|
||||
auto display = board.GetDisplay();
|
||||
if (display && display->height() > 64) { // For LCD display only
|
||||
cJSON_AddStringToObject(screen, "theme", display->GetTheme().c_str());
|
||||
auto theme = display->GetTheme();
|
||||
if (theme != nullptr) {
|
||||
cJSON_AddStringToObject(screen, "theme", theme->name().c_str());
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(root, "screen", screen);
|
||||
|
||||
|
||||
@ -216,7 +216,10 @@ std::string WifiBoard::GetDeviceStatusJson() {
|
||||
}
|
||||
auto display = board.GetDisplay();
|
||||
if (display && display->height() > 64) { // For LCD display only
|
||||
cJSON_AddStringToObject(screen, "theme", display->GetTheme().c_str());
|
||||
auto theme = display->GetTheme();
|
||||
if (theme != nullptr) {
|
||||
cJSON_AddStringToObject(screen, "theme", theme->name().c_str());
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(root, "screen", screen);
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "electron_emoji_display.h"
|
||||
#include "lvgl_theme.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <font_awesome.h>
|
||||
@ -105,7 +106,11 @@ void ElectronEmojiDisplay::SetupGifContainer() {
|
||||
|
||||
lv_obj_align(chat_message_label_, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
|
||||
LcdDisplay::SetTheme("dark");
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto theme = theme_manager.GetTheme("dark");
|
||||
if (theme != nullptr) {
|
||||
LcdDisplay::SetTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
void ElectronEmojiDisplay::SetEmotion(const char* emotion) {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "otto_emoji_display.h"
|
||||
#include "lvgl_theme.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <font_awesome.h>
|
||||
@ -107,7 +108,11 @@ void OttoEmojiDisplay::SetupGifContainer() {
|
||||
|
||||
lv_obj_align(chat_message_label_, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
|
||||
LcdDisplay::SetTheme("dark");
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto theme = theme_manager.GetTheme("dark");
|
||||
if (theme != nullptr) {
|
||||
LcdDisplay::SetTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
void OttoEmojiDisplay::SetEmotion(const char* emotion) {
|
||||
|
||||
@ -46,7 +46,11 @@ class CustomLcdDisplay : public SpiLcdDisplay {
|
||||
: SpiLcdDisplay(io_handle, panel_handle, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy) {
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
lv_obj_set_size(status_bar_, LV_HOR_RES, style_.text_font->line_height * 2 + 10);
|
||||
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
||||
auto text_font = lvgl_theme->text_font()->font();
|
||||
auto icon_font = lvgl_theme->icon_font()->font();
|
||||
|
||||
lv_obj_set_size(status_bar_, LV_HOR_RES, text_font->line_height * 2 + 10);
|
||||
lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0);
|
||||
lv_obj_set_style_pad_top(status_bar_, 10, 0);
|
||||
lv_obj_set_style_pad_bottom(status_bar_, 1, 0);
|
||||
@ -54,9 +58,9 @@ class CustomLcdDisplay : public SpiLcdDisplay {
|
||||
// 针对圆形屏幕调整位置
|
||||
// network battery mute //
|
||||
// status //
|
||||
lv_obj_align(battery_label_, LV_ALIGN_TOP_MID, -2.5 * style_.icon_font->line_height, 0);
|
||||
lv_obj_align(network_label_, LV_ALIGN_TOP_MID, -0.5 * style_.icon_font->line_height, 0);
|
||||
lv_obj_align(mute_label_, LV_ALIGN_TOP_MID, 1.5 * style_.icon_font->line_height, 0);
|
||||
lv_obj_align(battery_label_, LV_ALIGN_TOP_MID, -2.5 * icon_font->line_height, 0);
|
||||
lv_obj_align(network_label_, LV_ALIGN_TOP_MID, -0.5 * icon_font->line_height, 0);
|
||||
lv_obj_align(mute_label_, LV_ALIGN_TOP_MID, 1.5 * icon_font->line_height, 0);
|
||||
|
||||
lv_obj_align(status_label_, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(status_label_, 0);
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#define ZHENGCHEN_LCD_DISPLAY_H
|
||||
|
||||
#include "display/lcd_display.h"
|
||||
#include "lvgl_theme.h"
|
||||
#include <esp_lvgl_port.h>
|
||||
|
||||
class ZHENGCHEN_LcdDisplay : public SpiLcdDisplay {
|
||||
@ -14,10 +15,12 @@ public:
|
||||
using SpiLcdDisplay::SpiLcdDisplay;
|
||||
|
||||
void SetupHighTempWarningPopup() {
|
||||
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
||||
auto text_font = lvgl_theme->text_font()->font();
|
||||
// 创建高温警告弹窗
|
||||
high_temp_popup_ = lv_obj_create(lv_screen_active()); // 使用当前屏幕
|
||||
lv_obj_set_scrollbar_mode(high_temp_popup_, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_size(high_temp_popup_, LV_HOR_RES * 0.9, style_.text_font->line_height * 2);
|
||||
lv_obj_set_size(high_temp_popup_, LV_HOR_RES * 0.9, text_font->line_height * 2);
|
||||
lv_obj_align(high_temp_popup_, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_obj_set_style_bg_color(high_temp_popup_, lv_palette_main(LV_PALETTE_RED), 0);
|
||||
lv_obj_set_style_radius(high_temp_popup_, 10, 0);
|
||||
|
||||
@ -15,72 +15,13 @@
|
||||
#define TAG "Display"
|
||||
|
||||
Display::Display() {
|
||||
// Notification timer
|
||||
esp_timer_create_args_t notification_timer_args = {
|
||||
.callback = [](void *arg) {
|
||||
Display *display = static_cast<Display*>(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);
|
||||
}
|
||||
}
|
||||
|
||||
Display::~Display() {
|
||||
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 (emotion_label_ != nullptr) {
|
||||
lv_obj_del(emotion_label_);
|
||||
}
|
||||
if( low_battery_popup_ != nullptr ) {
|
||||
lv_obj_del(low_battery_popup_);
|
||||
}
|
||||
if (pm_lock_ != nullptr) {
|
||||
esp_pm_lock_delete(pm_lock_);
|
||||
}
|
||||
}
|
||||
|
||||
void Display::SetStatus(const char* status) {
|
||||
DisplayLockGuard lock(this);
|
||||
if (status_label_ == nullptr) {
|
||||
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();
|
||||
ESP_LOGW(TAG, "SetStatus: %s", status);
|
||||
}
|
||||
|
||||
void Display::ShowNotification(const std::string ¬ification, int duration_ms) {
|
||||
@ -88,134 +29,15 @@ void Display::ShowNotification(const std::string ¬ification, int duration_ms)
|
||||
}
|
||||
|
||||
void Display::ShowNotification(const char* notification, int duration_ms) {
|
||||
DisplayLockGuard lock(this);
|
||||
if (notification_label_ == nullptr) {
|
||||
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));
|
||||
ESP_LOGW(TAG, "ShowNotification: %s", notification);
|
||||
}
|
||||
|
||||
void Display::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;
|
||||
}
|
||||
|
||||
// 如果静音状态改变,则更新图标
|
||||
if (codec->output_volume() == 0 && !muted_) {
|
||||
muted_ = true;
|
||||
lv_label_set_text(mute_label_, FONT_AWESOME_VOLUME_XMARK);
|
||||
} 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_);
|
||||
// 更新电池图标
|
||||
int battery_level;
|
||||
bool charging, discharging;
|
||||
const char* icon = nullptr;
|
||||
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
|
||||
if (charging) {
|
||||
icon = FONT_AWESOME_BATTERY_BOLT;
|
||||
} else {
|
||||
const char* levels[] = {
|
||||
FONT_AWESOME_BATTERY_EMPTY, // 0-19%
|
||||
FONT_AWESOME_BATTERY_QUARTER, // 20-39%
|
||||
FONT_AWESOME_BATTERY_HALF, // 40-59%
|
||||
FONT_AWESOME_BATTERY_THREE_QUARTERS, // 60-79%
|
||||
FONT_AWESOME_BATTERY_FULL, // 80-99%
|
||||
FONT_AWESOME_BATTERY_FULL, // 100%
|
||||
};
|
||||
icon = levels[battery_level / 20];
|
||||
}
|
||||
DisplayLockGuard lock(this);
|
||||
if (battery_label_ != nullptr && battery_icon_ != icon) {
|
||||
battery_icon_ = icon;
|
||||
lv_label_set_text(battery_label_, battery_icon_);
|
||||
}
|
||||
|
||||
if (low_battery_popup_ != nullptr) {
|
||||
if (strcmp(icon, FONT_AWESOME_BATTERY_EMPTY) == 0 && discharging) {
|
||||
if (lv_obj_has_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN)) { // 如果低电量提示框隐藏,则显示
|
||||
lv_obj_remove_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
||||
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)) { // 如果低电量提示框显示,则隐藏
|
||||
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 每 10 秒更新一次网络图标
|
||||
static int seconds_counter = 0;
|
||||
if (update_all || seconds_counter++ % 10 == 0) {
|
||||
// 升级固件时,不读取 4G 网络状态,避免占用 UART 资源
|
||||
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 Display::SetEmotion(const char* emotion) {
|
||||
const char* utf8 = font_awesome_get_utf8(emotion);
|
||||
DisplayLockGuard lock(this);
|
||||
if (emotion_label_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (utf8 != nullptr) {
|
||||
lv_label_set_text(emotion_label_, utf8);
|
||||
} else {
|
||||
lv_label_set_text(emotion_label_, FONT_AWESOME_NEUTRAL);
|
||||
}
|
||||
ESP_LOGW(TAG, "SetEmotion: %s", emotion);
|
||||
}
|
||||
|
||||
void Display::SetPreviewImage(const lv_img_dsc_t* image) {
|
||||
@ -227,37 +49,12 @@ void Display::SetPreviewImage(const lv_img_dsc_t* image) {
|
||||
}
|
||||
|
||||
void Display::SetChatMessage(const char* role, const char* content) {
|
||||
DisplayLockGuard lock(this);
|
||||
if (chat_message_label_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
lv_label_set_text(chat_message_label_, content);
|
||||
ESP_LOGW(TAG, "Role:%s", role);
|
||||
ESP_LOGW(TAG, " %s", content);
|
||||
}
|
||||
|
||||
void Display::SetTheme(const std::string& theme_name) {
|
||||
current_theme_name_ = theme_name;
|
||||
Settings settings("display", true);
|
||||
settings.SetString("theme", theme_name);
|
||||
void Display::SetTheme(Theme* theme) {
|
||||
}
|
||||
|
||||
void Display::SetPowerSaveMode(bool on) {
|
||||
if (on) {
|
||||
SetChatMessage("system", "");
|
||||
SetEmotion("sleepy");
|
||||
} else {
|
||||
SetChatMessage("system", "");
|
||||
SetEmotion("neutral");
|
||||
}
|
||||
}
|
||||
|
||||
void Display::UpdateStyle(const DisplayStyle& style) {
|
||||
DisplayLockGuard lock(this);
|
||||
if (style.text_font != nullptr) {
|
||||
lv_obj_set_style_text_font(lv_screen_active(), style.text_font, 0);
|
||||
style_.text_font = style.text_font;
|
||||
}
|
||||
if (style.emoji_collection != nullptr) {
|
||||
delete style_.emoji_collection;
|
||||
style_.emoji_collection = style.emoji_collection;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,11 +11,14 @@
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
|
||||
class Theme {
|
||||
public:
|
||||
Theme(const std::string& name) : name_(name) {}
|
||||
virtual ~Theme() = default;
|
||||
|
||||
struct DisplayStyle {
|
||||
const lv_font_t* text_font;
|
||||
const lv_font_t* icon_font;
|
||||
EmojiCollection* emoji_collection;
|
||||
inline std::string name() const { return name_; }
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
|
||||
class Display {
|
||||
@ -29,10 +32,9 @@ public:
|
||||
virtual void SetEmotion(const char* emotion);
|
||||
virtual void SetChatMessage(const char* role, const char* content);
|
||||
virtual void SetPreviewImage(const lv_img_dsc_t* image);
|
||||
virtual void SetTheme(const std::string& theme_name);
|
||||
virtual std::string GetTheme() { return current_theme_name_; }
|
||||
virtual void SetTheme(Theme* theme);
|
||||
virtual Theme* GetTheme() { return current_theme_; }
|
||||
virtual void UpdateStatusBar(bool update_all = false);
|
||||
virtual void UpdateStyle(const DisplayStyle& style);
|
||||
virtual void SetPowerSaveMode(bool on);
|
||||
|
||||
inline int width() const { return width_; }
|
||||
@ -41,28 +43,8 @@ public:
|
||||
protected:
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
|
||||
esp_pm_lock_handle_t pm_lock_ = nullptr;
|
||||
lv_display_t *display_ = nullptr;
|
||||
|
||||
lv_obj_t *emotion_label_ = nullptr;
|
||||
lv_obj_t *network_label_ = nullptr;
|
||||
lv_obj_t *status_label_ = nullptr;
|
||||
lv_obj_t *notification_label_ = nullptr;
|
||||
lv_obj_t *mute_label_ = nullptr;
|
||||
lv_obj_t *battery_label_ = nullptr;
|
||||
lv_obj_t* chat_message_label_ = nullptr;
|
||||
lv_obj_t* low_battery_popup_ = nullptr;
|
||||
lv_obj_t* low_battery_label_ = nullptr;
|
||||
|
||||
const char* battery_icon_ = nullptr;
|
||||
const char* network_icon_ = nullptr;
|
||||
bool muted_ = false;
|
||||
std::string current_theme_name_;
|
||||
DisplayStyle style_;
|
||||
|
||||
std::chrono::system_clock::time_point last_status_update_time_;
|
||||
esp_timer_handle_t notification_timer_ = nullptr;
|
||||
Theme* current_theme_ = nullptr;
|
||||
|
||||
friend class DisplayLockGuard;
|
||||
virtual bool Lock(int timeout_ms = 0) = 0;
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
#include "esplog_display.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#define TAG "EspLogDisplay"
|
||||
|
||||
|
||||
EspLogDisplay::EspLogDisplay()
|
||||
{}
|
||||
|
||||
EspLogDisplay::~EspLogDisplay()
|
||||
{}
|
||||
|
||||
void EspLogDisplay::SetStatus(const char* status)
|
||||
{
|
||||
ESP_LOGW(TAG, "SetStatus: %s", status);
|
||||
}
|
||||
|
||||
void EspLogDisplay::ShowNotification(const char* notification, int duration_ms)
|
||||
{
|
||||
ESP_LOGW(TAG, "ShowNotification: %s", notification);
|
||||
}
|
||||
void EspLogDisplay::ShowNotification(const std::string ¬ification, int duration_ms)
|
||||
{
|
||||
ShowNotification(notification.c_str(), duration_ms);
|
||||
}
|
||||
|
||||
|
||||
void EspLogDisplay::SetEmotion(const char* emotion)
|
||||
{
|
||||
ESP_LOGW(TAG, "SetEmotion: %s", emotion);
|
||||
}
|
||||
|
||||
void EspLogDisplay::SetChatMessage(const char* role, const char* content)
|
||||
{
|
||||
ESP_LOGW(TAG, "Role:%s", role);
|
||||
ESP_LOGW(TAG, " %s", content);
|
||||
}
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
#ifndef ESPLOG_DISPLAY_H_
|
||||
#define ESPLOG_DISPLAY_H_
|
||||
|
||||
#include "display.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
class EspLogDisplay : public Display {
|
||||
public:
|
||||
EspLogDisplay();
|
||||
~EspLogDisplay();
|
||||
|
||||
virtual void SetStatus(const char* status);
|
||||
virtual void ShowNotification(const char* notification, int duration_ms = 3000);
|
||||
virtual void ShowNotification(const std::string ¬ification, int duration_ms = 3000);
|
||||
virtual void SetEmotion(const char* emotion) override;
|
||||
virtual void SetChatMessage(const char* role, const char* content) override;
|
||||
virtual inline void SetPreviewImage(const lv_img_dsc_t* image) override {}
|
||||
virtual inline void SetTheme(const std::string& theme_name) override {}
|
||||
virtual inline void UpdateStatusBar(bool update_all = false) override {}
|
||||
|
||||
protected:
|
||||
virtual inline bool Lock(int timeout_ms = 0) override { return true; }
|
||||
virtual inline void Unlock() override {}
|
||||
};
|
||||
|
||||
#endif
|
||||
@ -1,6 +1,7 @@
|
||||
#include "lcd_display.h"
|
||||
#include "assets/lang_config.h"
|
||||
#include "settings.h"
|
||||
#include "lvgl_theme.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
@ -15,78 +16,62 @@
|
||||
|
||||
#define TAG "LcdDisplay"
|
||||
|
||||
// Color definitions for dark theme
|
||||
#define DARK_BACKGROUND_COLOR lv_color_hex(0x121212) // Dark background
|
||||
#define DARK_TEXT_COLOR lv_color_white() // White text
|
||||
#define DARK_CHAT_BACKGROUND_COLOR lv_color_hex(0x1E1E1E) // Slightly lighter than background
|
||||
#define DARK_USER_BUBBLE_COLOR lv_color_hex(0x1A6C37) // Dark green
|
||||
#define DARK_ASSISTANT_BUBBLE_COLOR lv_color_hex(0x333333) // Dark gray
|
||||
#define DARK_SYSTEM_BUBBLE_COLOR lv_color_hex(0x2A2A2A) // Medium gray
|
||||
#define DARK_SYSTEM_TEXT_COLOR lv_color_hex(0xAAAAAA) // Light gray text
|
||||
#define DARK_BORDER_COLOR lv_color_hex(0x333333) // Dark gray border
|
||||
#define DARK_LOW_BATTERY_COLOR lv_color_hex(0xFF0000) // Red for dark mode
|
||||
|
||||
// Color definitions for light theme
|
||||
#define LIGHT_BACKGROUND_COLOR lv_color_white() // White background
|
||||
#define LIGHT_TEXT_COLOR lv_color_black() // Black text
|
||||
#define LIGHT_CHAT_BACKGROUND_COLOR lv_color_hex(0xE0E0E0) // Light gray background
|
||||
#define LIGHT_USER_BUBBLE_COLOR lv_color_hex(0x95EC69) // WeChat green
|
||||
#define LIGHT_ASSISTANT_BUBBLE_COLOR lv_color_white() // White
|
||||
#define LIGHT_SYSTEM_BUBBLE_COLOR lv_color_hex(0xE0E0E0) // Light gray
|
||||
#define LIGHT_SYSTEM_TEXT_COLOR lv_color_hex(0x666666) // Dark gray text
|
||||
#define LIGHT_BORDER_COLOR lv_color_hex(0xE0E0E0) // Light gray border
|
||||
#define LIGHT_LOW_BATTERY_COLOR lv_color_black() // Black for light mode
|
||||
|
||||
|
||||
// Define dark theme colors
|
||||
const ThemeColors DARK_THEME = {
|
||||
.background = DARK_BACKGROUND_COLOR,
|
||||
.text = DARK_TEXT_COLOR,
|
||||
.chat_background = DARK_CHAT_BACKGROUND_COLOR,
|
||||
.user_bubble = DARK_USER_BUBBLE_COLOR,
|
||||
.assistant_bubble = DARK_ASSISTANT_BUBBLE_COLOR,
|
||||
.system_bubble = DARK_SYSTEM_BUBBLE_COLOR,
|
||||
.system_text = DARK_SYSTEM_TEXT_COLOR,
|
||||
.border = DARK_BORDER_COLOR,
|
||||
.low_battery = DARK_LOW_BATTERY_COLOR
|
||||
};
|
||||
|
||||
// Define light theme colors
|
||||
const ThemeColors LIGHT_THEME = {
|
||||
.background = LIGHT_BACKGROUND_COLOR,
|
||||
.text = LIGHT_TEXT_COLOR,
|
||||
.chat_background = LIGHT_CHAT_BACKGROUND_COLOR,
|
||||
.user_bubble = LIGHT_USER_BUBBLE_COLOR,
|
||||
.assistant_bubble = LIGHT_ASSISTANT_BUBBLE_COLOR,
|
||||
.system_bubble = LIGHT_SYSTEM_BUBBLE_COLOR,
|
||||
.system_text = LIGHT_SYSTEM_TEXT_COLOR,
|
||||
.border = LIGHT_BORDER_COLOR,
|
||||
.low_battery = LIGHT_LOW_BATTERY_COLOR
|
||||
};
|
||||
|
||||
LV_FONT_DECLARE(LVGL_TEXT_FONT);
|
||||
LV_FONT_DECLARE(LVGL_ICON_FONT);
|
||||
LV_FONT_DECLARE(font_awesome_30_4);
|
||||
|
||||
void LcdDisplay::InitializeLcdThemes() {
|
||||
auto text_font = std::make_shared<LvglBuiltInFont>(&LVGL_TEXT_FONT);
|
||||
auto icon_font = std::make_shared<LvglBuiltInFont>(&LVGL_ICON_FONT);
|
||||
auto large_icon_font = std::make_shared<LvglBuiltInFont>(&font_awesome_30_4);
|
||||
|
||||
// light theme
|
||||
auto light_theme = new LvglTheme("light");
|
||||
light_theme->set_background_color(lv_color_white());
|
||||
light_theme->set_text_color(lv_color_black());
|
||||
light_theme->set_chat_background_color(lv_color_hex(0xE0E0E0));
|
||||
light_theme->set_user_bubble_color(lv_color_hex(0x95EC69));
|
||||
light_theme->set_assistant_bubble_color(lv_color_white());
|
||||
light_theme->set_system_bubble_color(lv_color_hex(0xE0E0E0));
|
||||
light_theme->set_system_text_color(lv_color_hex(0x666666));
|
||||
light_theme->set_border_color(lv_color_hex(0xE0E0E0));
|
||||
light_theme->set_low_battery_color(lv_color_black());
|
||||
light_theme->set_text_font(text_font);
|
||||
light_theme->set_icon_font(icon_font);
|
||||
light_theme->set_large_icon_font(large_icon_font);
|
||||
|
||||
// dark theme
|
||||
auto dark_theme = new LvglTheme("dark");
|
||||
dark_theme->set_background_color(lv_color_hex(0x121212));
|
||||
dark_theme->set_text_color(lv_color_white());
|
||||
dark_theme->set_chat_background_color(lv_color_hex(0x1E1E1E));
|
||||
dark_theme->set_user_bubble_color(lv_color_hex(0x1A6C37));
|
||||
dark_theme->set_assistant_bubble_color(lv_color_hex(0x333333));
|
||||
dark_theme->set_system_bubble_color(lv_color_hex(0x2A2A2A));
|
||||
dark_theme->set_system_text_color(lv_color_hex(0xAAAAAA));
|
||||
dark_theme->set_border_color(lv_color_hex(0x333333));
|
||||
dark_theme->set_low_battery_color(lv_color_hex(0xFF0000));
|
||||
dark_theme->set_text_font(text_font);
|
||||
dark_theme->set_icon_font(icon_font);
|
||||
dark_theme->set_large_icon_font(large_icon_font);
|
||||
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
theme_manager.RegisterTheme("light", light_theme);
|
||||
theme_manager.RegisterTheme("dark", dark_theme);
|
||||
}
|
||||
|
||||
LcdDisplay::LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height)
|
||||
: panel_io_(panel_io), panel_(panel) {
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
style_ = {
|
||||
.text_font = &LVGL_TEXT_FONT,
|
||||
.icon_font = &LVGL_ICON_FONT,
|
||||
};
|
||||
|
||||
// Initialize LCD themes
|
||||
InitializeLcdThemes();
|
||||
|
||||
// Load theme from settings
|
||||
Settings settings("display", false);
|
||||
current_theme_name_ = settings.GetString("theme", "light");
|
||||
|
||||
// Update the theme
|
||||
if (current_theme_name_ == "dark") {
|
||||
current_theme_ = DARK_THEME;
|
||||
} else if (current_theme_name_ == "light") {
|
||||
current_theme_ = LIGHT_THEME;
|
||||
}
|
||||
std::string theme_name = settings.GetString("theme", "light");
|
||||
current_theme_ = LvglThemeManager::GetInstance().GetTheme(theme_name);
|
||||
|
||||
// Create a timer to hide the preview image
|
||||
esp_timer_create_args_t preview_timer_args = {
|
||||
@ -339,10 +324,15 @@ void LcdDisplay::Unlock() {
|
||||
void LcdDisplay::SetupUI() {
|
||||
DisplayLockGuard lock(this);
|
||||
|
||||
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
||||
auto text_font = lvgl_theme->text_font()->font();
|
||||
auto icon_font = lvgl_theme->icon_font()->font();
|
||||
auto large_icon_font = lvgl_theme->large_icon_font()->font();
|
||||
|
||||
auto screen = lv_screen_active();
|
||||
lv_obj_set_style_text_font(screen, style_.text_font, 0);
|
||||
lv_obj_set_style_text_color(screen, current_theme_.text, 0);
|
||||
lv_obj_set_style_bg_color(screen, current_theme_.background, 0);
|
||||
lv_obj_set_style_text_font(screen, text_font, 0);
|
||||
lv_obj_set_style_text_color(screen, lvgl_theme->text_color(), 0);
|
||||
lv_obj_set_style_bg_color(screen, lvgl_theme->background_color(), 0);
|
||||
|
||||
/* Container */
|
||||
container_ = lv_obj_create(screen);
|
||||
@ -351,24 +341,24 @@ void LcdDisplay::SetupUI() {
|
||||
lv_obj_set_style_pad_all(container_, 0, 0);
|
||||
lv_obj_set_style_border_width(container_, 0, 0);
|
||||
lv_obj_set_style_pad_row(container_, 0, 0);
|
||||
lv_obj_set_style_bg_color(container_, current_theme_.background, 0);
|
||||
lv_obj_set_style_border_color(container_, current_theme_.border, 0);
|
||||
lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0);
|
||||
lv_obj_set_style_border_color(container_, lvgl_theme->border_color(), 0);
|
||||
|
||||
/* Status bar */
|
||||
status_bar_ = lv_obj_create(container_);
|
||||
lv_obj_set_size(status_bar_, LV_HOR_RES, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_radius(status_bar_, 0, 0);
|
||||
lv_obj_set_style_bg_color(status_bar_, current_theme_.background, 0);
|
||||
lv_obj_set_style_text_color(status_bar_, current_theme_.text, 0);
|
||||
lv_obj_set_style_bg_color(status_bar_, lvgl_theme->background_color(), 0);
|
||||
lv_obj_set_style_text_color(status_bar_, lvgl_theme->text_color(), 0);
|
||||
|
||||
/* Content - Chat area */
|
||||
content_ = lv_obj_create(container_);
|
||||
lv_obj_set_style_radius(content_, 0, 0);
|
||||
lv_obj_set_width(content_, LV_HOR_RES);
|
||||
lv_obj_set_flex_grow(content_, 1);
|
||||
lv_obj_set_style_pad_all(content_, 10, 0);
|
||||
lv_obj_set_style_bg_color(content_, current_theme_.chat_background, 0); // Background for chat area
|
||||
lv_obj_set_style_border_color(content_, current_theme_.border, 0); // Border color for chat area
|
||||
lv_obj_set_style_pad_all(content_, lvgl_theme->spacing(4), 0);
|
||||
lv_obj_set_style_border_width(content_, 0, 0);
|
||||
lv_obj_set_style_bg_color(content_, lvgl_theme->chat_background_color(), 0); // Background for chat area
|
||||
|
||||
// Enable scrolling for chat content
|
||||
lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF);
|
||||
@ -377,7 +367,7 @@ void LcdDisplay::SetupUI() {
|
||||
// Create a flex container for chat messages
|
||||
lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||
lv_obj_set_style_pad_row(content_, 10, 0); // Space between messages
|
||||
lv_obj_set_style_pad_row(content_, lvgl_theme->spacing(4), 0); // Space between messages
|
||||
|
||||
// We'll create chat messages dynamically in SetChatMessage
|
||||
chat_message_label_ = nullptr;
|
||||
@ -387,23 +377,23 @@ void LcdDisplay::SetupUI() {
|
||||
lv_obj_set_style_pad_all(status_bar_, 0, 0);
|
||||
lv_obj_set_style_border_width(status_bar_, 0, 0);
|
||||
lv_obj_set_style_pad_column(status_bar_, 0, 0);
|
||||
lv_obj_set_style_pad_left(status_bar_, 10, 0);
|
||||
lv_obj_set_style_pad_right(status_bar_, 10, 0);
|
||||
lv_obj_set_style_pad_top(status_bar_, 2, 0);
|
||||
lv_obj_set_style_pad_bottom(status_bar_, 2, 0);
|
||||
lv_obj_set_style_pad_top(status_bar_, lvgl_theme->spacing(2), 0);
|
||||
lv_obj_set_style_pad_bottom(status_bar_, lvgl_theme->spacing(2), 0);
|
||||
lv_obj_set_style_pad_left(status_bar_, lvgl_theme->spacing(4), 0);
|
||||
lv_obj_set_style_pad_right(status_bar_, lvgl_theme->spacing(4), 0);
|
||||
lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF);
|
||||
// 设置状态栏的内容垂直居中
|
||||
lv_obj_set_flex_align(status_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
network_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(network_label_, "");
|
||||
lv_obj_set_style_text_font(network_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_color(network_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_font(network_label_, icon_font, 0);
|
||||
lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0);
|
||||
|
||||
notification_label_ = lv_label_create(status_bar_);
|
||||
lv_obj_set_flex_grow(notification_label_, 1);
|
||||
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_color(notification_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(notification_label_, lvgl_theme->text_color(), 0);
|
||||
lv_label_set_text(notification_label_, "");
|
||||
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
@ -411,26 +401,26 @@ void LcdDisplay::SetupUI() {
|
||||
lv_obj_set_flex_grow(status_label_, 1);
|
||||
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_color(status_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0);
|
||||
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
|
||||
|
||||
mute_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(mute_label_, "");
|
||||
lv_obj_set_style_text_font(mute_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_color(mute_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
|
||||
lv_obj_set_style_text_color(mute_label_, lvgl_theme->text_color(), 0);
|
||||
|
||||
battery_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(battery_label_, "");
|
||||
lv_obj_set_style_text_font(battery_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_color(battery_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_margin_left(battery_label_, 5, 0); // 添加左边距,与前面的元素分隔
|
||||
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
|
||||
lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0);
|
||||
lv_obj_set_style_margin_left(battery_label_, lvgl_theme->spacing(2), 0); // 添加左边距,与前面的元素分隔
|
||||
|
||||
low_battery_popup_ = lv_obj_create(screen);
|
||||
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, style_.text_font->line_height * 2);
|
||||
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, -10);
|
||||
lv_obj_set_style_bg_color(low_battery_popup_, current_theme_.low_battery, 0);
|
||||
lv_obj_set_style_radius(low_battery_popup_, 10, 0);
|
||||
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, text_font->line_height * 2);
|
||||
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, -lvgl_theme->spacing(4));
|
||||
lv_obj_set_style_bg_color(low_battery_popup_, lvgl_theme->low_battery_color(), 0);
|
||||
lv_obj_set_style_radius(low_battery_popup_, lvgl_theme->spacing(4), 0);
|
||||
low_battery_label_ = lv_label_create(low_battery_popup_);
|
||||
lv_label_set_text(low_battery_label_, Lang::Strings::BATTERY_NEED_CHARGE);
|
||||
lv_obj_set_style_text_color(low_battery_label_, lv_color_white(), 0);
|
||||
@ -438,13 +428,13 @@ void LcdDisplay::SetupUI() {
|
||||
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
emoji_image_ = lv_img_create(screen);
|
||||
lv_obj_align(emoji_image_, LV_ALIGN_TOP_MID, 0, style_.text_font->line_height + 10);
|
||||
lv_obj_align(emoji_image_, LV_ALIGN_TOP_MID, 0, text_font->line_height + lvgl_theme->spacing(2));
|
||||
|
||||
// Display AI logo while booting
|
||||
emotion_label_ = lv_label_create(screen);
|
||||
lv_obj_center(emotion_label_);
|
||||
lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_4, 0);
|
||||
lv_obj_set_style_text_color(emotion_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_font(emotion_label_, large_icon_font, 0);
|
||||
lv_obj_set_style_text_color(emotion_label_, lvgl_theme->text_color(), 0);
|
||||
lv_label_set_text(emotion_label_, FONT_AWESOME_MICROCHIP_AI);
|
||||
}
|
||||
#if CONFIG_IDF_TARGET_ESP32P4
|
||||
@ -501,20 +491,23 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
return;
|
||||
}
|
||||
|
||||
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_);
|
||||
lv_obj_set_style_radius(msg_bubble, 8, 0);
|
||||
lv_obj_set_scrollbar_mode(msg_bubble, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_style_border_width(msg_bubble, 1, 0);
|
||||
lv_obj_set_style_border_color(msg_bubble, current_theme_.border, 0);
|
||||
lv_obj_set_style_pad_all(msg_bubble, 8, 0);
|
||||
lv_obj_set_style_border_color(msg_bubble, lvgl_theme->border_color(), 0);
|
||||
lv_obj_set_style_pad_all(msg_bubble, lvgl_theme->spacing(4), 0);
|
||||
|
||||
// Create the message text
|
||||
lv_obj_t* msg_text = lv_label_create(msg_bubble);
|
||||
lv_label_set_text(msg_text, content);
|
||||
|
||||
// 计算文本实际宽度
|
||||
lv_coord_t text_width = lv_txt_get_width(content, strlen(content), style_.text_font, 0);
|
||||
lv_coord_t text_width = lv_txt_get_width(content, strlen(content), text_font, 0);
|
||||
|
||||
// 计算气泡宽度
|
||||
lv_coord_t max_width = LV_HOR_RES * 85 / 100 - 16; // 屏幕宽度的85%
|
||||
@ -544,9 +537,9 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
// Set alignment and style based on message role
|
||||
if (strcmp(role, "user") == 0) {
|
||||
// User messages are right-aligned with green background
|
||||
lv_obj_set_style_bg_color(msg_bubble, current_theme_.user_bubble, 0);
|
||||
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->user_bubble_color(), 0);
|
||||
// Set text color for contrast
|
||||
lv_obj_set_style_text_color(msg_text, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0);
|
||||
|
||||
// 设置自定义属性标记气泡类型
|
||||
lv_obj_set_user_data(msg_bubble, (void*)"user");
|
||||
@ -559,9 +552,9 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
lv_obj_set_style_flex_grow(msg_bubble, 0, 0);
|
||||
} else if (strcmp(role, "assistant") == 0) {
|
||||
// Assistant messages are left-aligned with white background
|
||||
lv_obj_set_style_bg_color(msg_bubble, current_theme_.assistant_bubble, 0);
|
||||
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->assistant_bubble_color(), 0);
|
||||
// Set text color for contrast
|
||||
lv_obj_set_style_text_color(msg_text, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0);
|
||||
|
||||
// 设置自定义属性标记气泡类型
|
||||
lv_obj_set_user_data(msg_bubble, (void*)"assistant");
|
||||
@ -574,9 +567,9 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
lv_obj_set_style_flex_grow(msg_bubble, 0, 0);
|
||||
} else if (strcmp(role, "system") == 0) {
|
||||
// System messages are center-aligned with light gray background
|
||||
lv_obj_set_style_bg_color(msg_bubble, current_theme_.system_bubble, 0);
|
||||
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->system_bubble_color(), 0);
|
||||
// Set text color for contrast
|
||||
lv_obj_set_style_text_color(msg_text, current_theme_.system_text, 0);
|
||||
lv_obj_set_style_text_color(msg_text, lvgl_theme->system_text_color(), 0);
|
||||
|
||||
// 设置自定义属性标记气泡类型
|
||||
lv_obj_set_user_data(msg_bubble, (void*)"system");
|
||||
@ -647,17 +640,18 @@ void LcdDisplay::SetPreviewImage(const lv_img_dsc_t* img_dsc) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
||||
if (img_dsc != nullptr) {
|
||||
// Create a message bubble for image preview
|
||||
lv_obj_t* img_bubble = lv_obj_create(content_);
|
||||
lv_obj_set_style_radius(img_bubble, 8, 0);
|
||||
lv_obj_set_scrollbar_mode(img_bubble, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_style_border_width(img_bubble, 1, 0);
|
||||
lv_obj_set_style_border_color(img_bubble, current_theme_.border, 0);
|
||||
lv_obj_set_style_pad_all(img_bubble, 8, 0);
|
||||
lv_obj_set_style_border_color(img_bubble, lvgl_theme->border_color(), 0);
|
||||
lv_obj_set_style_pad_all(img_bubble, lvgl_theme->spacing(4), 0);
|
||||
|
||||
// Set image bubble background color (similar to system message)
|
||||
lv_obj_set_style_bg_color(img_bubble, current_theme_.assistant_bubble, 0);
|
||||
lv_obj_set_style_bg_color(img_bubble, lvgl_theme->assistant_bubble_color(), 0);
|
||||
|
||||
// 设置自定义属性标记气泡类型
|
||||
lv_obj_set_user_data(img_bubble, (void*)"image");
|
||||
@ -722,11 +716,15 @@ void LcdDisplay::SetPreviewImage(const lv_img_dsc_t* img_dsc) {
|
||||
#else
|
||||
void LcdDisplay::SetupUI() {
|
||||
DisplayLockGuard lock(this);
|
||||
LvglTheme* lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
||||
auto text_font = lvgl_theme->text_font()->font();
|
||||
auto icon_font = lvgl_theme->icon_font()->font();
|
||||
auto large_icon_font = lvgl_theme->large_icon_font()->font();
|
||||
|
||||
auto screen = lv_screen_active();
|
||||
lv_obj_set_style_text_font(screen, style_.text_font, 0);
|
||||
lv_obj_set_style_text_color(screen, current_theme_.text, 0);
|
||||
lv_obj_set_style_bg_color(screen, current_theme_.background, 0);
|
||||
lv_obj_set_style_text_font(screen, text_font, 0);
|
||||
lv_obj_set_style_text_color(screen, lvgl_theme->text_color(), 0);
|
||||
lv_obj_set_style_bg_color(screen, lvgl_theme->background_color(), 0);
|
||||
|
||||
/* Container */
|
||||
container_ = lv_obj_create(screen);
|
||||
@ -735,15 +733,22 @@ void LcdDisplay::SetupUI() {
|
||||
lv_obj_set_style_pad_all(container_, 0, 0);
|
||||
lv_obj_set_style_border_width(container_, 0, 0);
|
||||
lv_obj_set_style_pad_row(container_, 0, 0);
|
||||
lv_obj_set_style_bg_color(container_, current_theme_.background, 0);
|
||||
lv_obj_set_style_border_color(container_, current_theme_.border, 0);
|
||||
lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0);
|
||||
lv_obj_set_style_border_color(container_, lvgl_theme->border_color(), 0);
|
||||
|
||||
/* Status bar */
|
||||
status_bar_ = lv_obj_create(container_);
|
||||
lv_obj_set_size(status_bar_, LV_HOR_RES, style_.text_font->line_height);
|
||||
lv_obj_set_size(status_bar_, LV_HOR_RES, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_radius(status_bar_, 0, 0);
|
||||
lv_obj_set_style_bg_color(status_bar_, current_theme_.background, 0);
|
||||
lv_obj_set_style_text_color(status_bar_, current_theme_.text, 0);
|
||||
lv_obj_set_style_bg_color(status_bar_, lvgl_theme->background_color(), 0);
|
||||
lv_obj_set_style_text_color(status_bar_, lvgl_theme->text_color(), 0);
|
||||
lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_top(status_bar_, lvgl_theme->spacing(2), 0);
|
||||
lv_obj_set_style_pad_bottom(status_bar_, lvgl_theme->spacing(2), 0);
|
||||
lv_obj_set_style_pad_left(status_bar_, lvgl_theme->spacing(4), 0);
|
||||
lv_obj_set_style_pad_right(status_bar_, lvgl_theme->spacing(4), 0);
|
||||
lv_obj_set_style_border_width(status_bar_, 0, 0);
|
||||
lv_obj_set_style_pad_column(status_bar_, 0, 0);
|
||||
|
||||
/* Content */
|
||||
content_ = lv_obj_create(container_);
|
||||
@ -751,9 +756,9 @@ void LcdDisplay::SetupUI() {
|
||||
lv_obj_set_style_radius(content_, 0, 0);
|
||||
lv_obj_set_width(content_, LV_HOR_RES);
|
||||
lv_obj_set_flex_grow(content_, 1);
|
||||
lv_obj_set_style_pad_all(content_, 5, 0);
|
||||
lv_obj_set_style_bg_color(content_, current_theme_.chat_background, 0);
|
||||
lv_obj_set_style_border_color(content_, current_theme_.border, 0); // Border color for content
|
||||
lv_obj_set_style_pad_all(content_, 0, 0);
|
||||
lv_obj_set_style_border_width(content_, 0, 0);
|
||||
lv_obj_set_style_bg_color(content_, lvgl_theme->chat_background_color(), 0);
|
||||
|
||||
lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); // 垂直布局(从上到下)
|
||||
lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_SPACE_EVENLY); // 子对象居中对齐,等距分布
|
||||
@ -765,8 +770,8 @@ void LcdDisplay::SetupUI() {
|
||||
lv_obj_set_style_border_width(emoji_box_, 0, 0);
|
||||
|
||||
emotion_label_ = lv_label_create(emoji_box_);
|
||||
lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_4, 0);
|
||||
lv_obj_set_style_text_color(emotion_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_font(emotion_label_, large_icon_font, 0);
|
||||
lv_obj_set_style_text_color(emotion_label_, lvgl_theme->text_color(), 0);
|
||||
lv_label_set_text(emotion_label_, FONT_AWESOME_MICROCHIP_AI);
|
||||
|
||||
emoji_image_ = lv_img_create(emoji_box_);
|
||||
@ -779,28 +784,21 @@ void LcdDisplay::SetupUI() {
|
||||
|
||||
chat_message_label_ = lv_label_create(content_);
|
||||
lv_label_set_text(chat_message_label_, "");
|
||||
lv_obj_set_width(chat_message_label_, LV_HOR_RES * 0.9); // 限制宽度为屏幕宽度的 90%
|
||||
lv_obj_set_width(chat_message_label_, width_ * 0.9); // 限制宽度为屏幕宽度的 90%
|
||||
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP); // 设置为自动换行模式
|
||||
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // 设置文本居中对齐
|
||||
lv_obj_set_style_text_color(chat_message_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(chat_message_label_, lvgl_theme->text_color(), 0);
|
||||
|
||||
/* Status bar */
|
||||
lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_all(status_bar_, 0, 0);
|
||||
lv_obj_set_style_border_width(status_bar_, 0, 0);
|
||||
lv_obj_set_style_pad_column(status_bar_, 0, 0);
|
||||
lv_obj_set_style_pad_left(status_bar_, 2, 0);
|
||||
lv_obj_set_style_pad_right(status_bar_, 2, 0);
|
||||
|
||||
network_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(network_label_, "");
|
||||
lv_obj_set_style_text_font(network_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_color(network_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_font(network_label_, icon_font, 0);
|
||||
lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0);
|
||||
|
||||
notification_label_ = lv_label_create(status_bar_);
|
||||
lv_obj_set_flex_grow(notification_label_, 1);
|
||||
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_color(notification_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(notification_label_, lvgl_theme->text_color(), 0);
|
||||
lv_label_set_text(notification_label_, "");
|
||||
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
@ -808,25 +806,26 @@ void LcdDisplay::SetupUI() {
|
||||
lv_obj_set_flex_grow(status_label_, 1);
|
||||
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_color(status_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0);
|
||||
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
|
||||
|
||||
mute_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(mute_label_, "");
|
||||
lv_obj_set_style_text_font(mute_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_color(mute_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
|
||||
lv_obj_set_style_text_color(mute_label_, lvgl_theme->text_color(), 0);
|
||||
|
||||
battery_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(battery_label_, "");
|
||||
lv_obj_set_style_text_font(battery_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_color(battery_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
|
||||
lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0);
|
||||
|
||||
low_battery_popup_ = lv_obj_create(screen);
|
||||
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, style_.text_font->line_height * 2);
|
||||
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, -10);
|
||||
lv_obj_set_style_bg_color(low_battery_popup_, current_theme_.low_battery, 0);
|
||||
lv_obj_set_style_radius(low_battery_popup_, 10, 0);
|
||||
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, text_font->line_height * 2);
|
||||
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, -lvgl_theme->spacing(4));
|
||||
lv_obj_set_style_bg_color(low_battery_popup_, lvgl_theme->low_battery_color(), 0);
|
||||
lv_obj_set_style_radius(low_battery_popup_, lvgl_theme->spacing(4), 0);
|
||||
|
||||
low_battery_label_ = lv_label_create(low_battery_popup_);
|
||||
lv_label_set_text(low_battery_label_, Lang::Strings::BATTERY_NEED_CHARGE);
|
||||
lv_obj_set_style_text_color(low_battery_label_, lv_color_white(), 0);
|
||||
@ -873,7 +872,8 @@ void LcdDisplay::SetEmotion(const char* emotion) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto img_dsc = style_.emoji_collection != nullptr ? style_.emoji_collection->GetEmojiImage(emotion) : nullptr;
|
||||
auto emoji_collection = static_cast<LvglTheme*>(current_theme_)->emoji_collection();
|
||||
auto img_dsc = emoji_collection != nullptr ? emoji_collection->GetEmojiImage(emotion) : nullptr;
|
||||
if (img_dsc == nullptr) {
|
||||
const char* utf8 = font_awesome_get_utf8(emotion);
|
||||
if (utf8 != nullptr && emotion_label_ != nullptr) {
|
||||
@ -901,62 +901,70 @@ void LcdDisplay::SetEmotion(const char* emotion) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void LcdDisplay::SetTheme(const std::string& theme_name) {
|
||||
void LcdDisplay::SetTheme(Theme* theme) {
|
||||
DisplayLockGuard lock(this);
|
||||
|
||||
if (theme_name == "dark" || theme_name == "DARK") {
|
||||
current_theme_ = DARK_THEME;
|
||||
} else if (theme_name == "light" || theme_name == "LIGHT") {
|
||||
current_theme_ = LIGHT_THEME;
|
||||
} else {
|
||||
// Invalid theme name, return false
|
||||
ESP_LOGE(TAG, "Invalid theme name: %s", theme_name.c_str());
|
||||
return;
|
||||
}
|
||||
auto lvgl_theme = static_cast<LvglTheme*>(theme);
|
||||
|
||||
// Get the active screen
|
||||
lv_obj_t* screen = lv_screen_active();
|
||||
|
||||
|
||||
// Set font
|
||||
auto text_font = lvgl_theme->text_font()->font();
|
||||
auto icon_font = lvgl_theme->icon_font()->font();
|
||||
auto large_icon_font = lvgl_theme->large_icon_font()->font();
|
||||
lv_obj_set_style_text_font(screen, text_font, 0);
|
||||
|
||||
if (text_font->line_height >= 30) {
|
||||
lv_obj_set_style_text_font(mute_label_, large_icon_font, 0);
|
||||
lv_obj_set_style_text_font(battery_label_, large_icon_font, 0);
|
||||
lv_obj_set_style_text_font(network_label_, large_icon_font, 0);
|
||||
} else {
|
||||
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
|
||||
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
|
||||
lv_obj_set_style_text_font(network_label_, icon_font, 0);
|
||||
}
|
||||
|
||||
// Update the screen colors
|
||||
lv_obj_set_style_bg_color(screen, current_theme_.background, 0);
|
||||
lv_obj_set_style_text_color(screen, current_theme_.text, 0);
|
||||
lv_obj_set_style_bg_color(screen, lvgl_theme->background_color(), 0);
|
||||
lv_obj_set_style_text_color(screen, lvgl_theme->text_color(), 0);
|
||||
|
||||
// Update container colors
|
||||
if (container_ != nullptr) {
|
||||
lv_obj_set_style_bg_color(container_, current_theme_.background, 0);
|
||||
lv_obj_set_style_border_color(container_, current_theme_.border, 0);
|
||||
lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0);
|
||||
lv_obj_set_style_border_color(container_, lvgl_theme->border_color(), 0);
|
||||
}
|
||||
|
||||
// Update status bar colors
|
||||
if (status_bar_ != nullptr) {
|
||||
lv_obj_set_style_bg_color(status_bar_, current_theme_.background, 0);
|
||||
lv_obj_set_style_text_color(status_bar_, current_theme_.text, 0);
|
||||
lv_obj_set_style_bg_color(status_bar_, lvgl_theme->background_color(), 0);
|
||||
lv_obj_set_style_text_color(status_bar_, lvgl_theme->text_color(), 0);
|
||||
|
||||
// Update status bar elements
|
||||
if (network_label_ != nullptr) {
|
||||
lv_obj_set_style_text_color(network_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0);
|
||||
}
|
||||
if (status_label_ != nullptr) {
|
||||
lv_obj_set_style_text_color(status_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0);
|
||||
}
|
||||
if (notification_label_ != nullptr) {
|
||||
lv_obj_set_style_text_color(notification_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(notification_label_, lvgl_theme->text_color(), 0);
|
||||
}
|
||||
if (mute_label_ != nullptr) {
|
||||
lv_obj_set_style_text_color(mute_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(mute_label_, lvgl_theme->text_color(), 0);
|
||||
}
|
||||
if (battery_label_ != nullptr) {
|
||||
lv_obj_set_style_text_color(battery_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0);
|
||||
}
|
||||
if (emotion_label_ != nullptr) {
|
||||
lv_obj_set_style_text_color(emotion_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(emotion_label_, lvgl_theme->text_color(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Update content area colors
|
||||
if (content_ != nullptr) {
|
||||
lv_obj_set_style_bg_color(content_, current_theme_.chat_background, 0);
|
||||
lv_obj_set_style_border_color(content_, current_theme_.border, 0);
|
||||
lv_obj_set_style_bg_color(content_, lvgl_theme->chat_background_color(), 0);
|
||||
lv_obj_set_style_border_color(content_, lvgl_theme->border_color(), 0);
|
||||
|
||||
// If we have the chat message style, update all message bubbles
|
||||
#if CONFIG_USE_WECHAT_MESSAGE_STYLE
|
||||
@ -996,17 +1004,17 @@ void LcdDisplay::SetTheme(const std::string& theme_name) {
|
||||
|
||||
// 根据气泡类型应用正确的颜色
|
||||
if (strcmp(bubble_type, "user") == 0) {
|
||||
lv_obj_set_style_bg_color(bubble, current_theme_.user_bubble, 0);
|
||||
lv_obj_set_style_bg_color(bubble, lvgl_theme->user_bubble_color(), 0);
|
||||
} else if (strcmp(bubble_type, "assistant") == 0) {
|
||||
lv_obj_set_style_bg_color(bubble, current_theme_.assistant_bubble, 0);
|
||||
lv_obj_set_style_bg_color(bubble, lvgl_theme->assistant_bubble_color(), 0);
|
||||
} else if (strcmp(bubble_type, "system") == 0) {
|
||||
lv_obj_set_style_bg_color(bubble, current_theme_.system_bubble, 0);
|
||||
lv_obj_set_style_bg_color(bubble, lvgl_theme->system_bubble_color(), 0);
|
||||
} else if (strcmp(bubble_type, "image") == 0) {
|
||||
lv_obj_set_style_bg_color(bubble, current_theme_.system_bubble, 0);
|
||||
lv_obj_set_style_bg_color(bubble, lvgl_theme->system_bubble_color(), 0);
|
||||
}
|
||||
|
||||
// Update border color
|
||||
lv_obj_set_style_border_color(bubble, current_theme_.border, 0);
|
||||
lv_obj_set_style_border_color(bubble, lvgl_theme->border_color(), 0);
|
||||
|
||||
// Update text color for the message
|
||||
if (lv_obj_get_child_cnt(bubble) > 0) {
|
||||
@ -1014,84 +1022,33 @@ void LcdDisplay::SetTheme(const std::string& theme_name) {
|
||||
if (text != nullptr) {
|
||||
// 根据气泡类型设置文本颜色
|
||||
if (strcmp(bubble_type, "system") == 0) {
|
||||
lv_obj_set_style_text_color(text, current_theme_.system_text, 0);
|
||||
lv_obj_set_style_text_color(text, lvgl_theme->system_text_color(), 0);
|
||||
} else {
|
||||
lv_obj_set_style_text_color(text, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(text, lvgl_theme->text_color(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果没有标记,回退到之前的逻辑(颜色比较)
|
||||
// ...保留原有的回退逻辑...
|
||||
lv_color_t bg_color = lv_obj_get_style_bg_color(bubble, 0);
|
||||
|
||||
// 改进bubble类型检测逻辑,不仅使用颜色比较
|
||||
bool is_user_bubble = false;
|
||||
bool is_assistant_bubble = false;
|
||||
bool is_system_bubble = false;
|
||||
|
||||
// 检查用户bubble
|
||||
if (lv_color_eq(bg_color, DARK_USER_BUBBLE_COLOR) ||
|
||||
lv_color_eq(bg_color, LIGHT_USER_BUBBLE_COLOR) ||
|
||||
lv_color_eq(bg_color, current_theme_.user_bubble)) {
|
||||
is_user_bubble = true;
|
||||
}
|
||||
// 检查系统bubble
|
||||
else if (lv_color_eq(bg_color, DARK_SYSTEM_BUBBLE_COLOR) ||
|
||||
lv_color_eq(bg_color, LIGHT_SYSTEM_BUBBLE_COLOR) ||
|
||||
lv_color_eq(bg_color, current_theme_.system_bubble)) {
|
||||
is_system_bubble = true;
|
||||
}
|
||||
// 剩余的都当作助手bubble处理
|
||||
else {
|
||||
is_assistant_bubble = true;
|
||||
}
|
||||
|
||||
// 根据bubble类型应用正确的颜色
|
||||
if (is_user_bubble) {
|
||||
lv_obj_set_style_bg_color(bubble, current_theme_.user_bubble, 0);
|
||||
} else if (is_assistant_bubble) {
|
||||
lv_obj_set_style_bg_color(bubble, current_theme_.assistant_bubble, 0);
|
||||
} else if (is_system_bubble) {
|
||||
lv_obj_set_style_bg_color(bubble, current_theme_.system_bubble, 0);
|
||||
}
|
||||
|
||||
// Update border color
|
||||
lv_obj_set_style_border_color(bubble, current_theme_.border, 0);
|
||||
|
||||
// Update text color for the message
|
||||
if (lv_obj_get_child_cnt(bubble) > 0) {
|
||||
lv_obj_t* text = lv_obj_get_child(bubble, 0);
|
||||
if (text != nullptr) {
|
||||
// 回退到颜色检测逻辑
|
||||
if (lv_color_eq(bg_color, current_theme_.system_bubble) ||
|
||||
lv_color_eq(bg_color, DARK_SYSTEM_BUBBLE_COLOR) ||
|
||||
lv_color_eq(bg_color, LIGHT_SYSTEM_BUBBLE_COLOR)) {
|
||||
lv_obj_set_style_text_color(text, current_theme_.system_text, 0);
|
||||
} else {
|
||||
lv_obj_set_style_text_color(text, current_theme_.text, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "child[%lu] Bubble type is not found", i);
|
||||
}
|
||||
}
|
||||
#else
|
||||
// Simple UI mode - just update the main chat message
|
||||
if (chat_message_label_ != nullptr) {
|
||||
lv_obj_set_style_text_color(chat_message_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(chat_message_label_, lvgl_theme->text_color(), 0);
|
||||
}
|
||||
|
||||
if (emotion_label_ != nullptr) {
|
||||
lv_obj_set_style_text_color(emotion_label_, current_theme_.text, 0);
|
||||
lv_obj_set_style_text_color(emotion_label_, lvgl_theme->text_color(), 0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Update low battery popup
|
||||
if (low_battery_popup_ != nullptr) {
|
||||
lv_obj_set_style_bg_color(low_battery_popup_, current_theme_.low_battery, 0);
|
||||
lv_obj_set_style_bg_color(low_battery_popup_, lvgl_theme->low_battery_color(), 0);
|
||||
}
|
||||
|
||||
// No errors occurred. Save theme to settings
|
||||
Display::SetTheme(theme_name);
|
||||
Display::SetTheme(lvgl_theme);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#ifndef LCD_DISPLAY_H
|
||||
#define LCD_DISPLAY_H
|
||||
|
||||
#include "display.h"
|
||||
#include "lvgl_display.h"
|
||||
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
@ -12,21 +12,8 @@
|
||||
|
||||
#define PREVIEW_IMAGE_DURATION_MS 5000
|
||||
|
||||
// Theme color structure
|
||||
struct ThemeColors {
|
||||
lv_color_t background;
|
||||
lv_color_t text;
|
||||
lv_color_t chat_background;
|
||||
lv_color_t user_bubble;
|
||||
lv_color_t assistant_bubble;
|
||||
lv_color_t system_bubble;
|
||||
lv_color_t system_text;
|
||||
lv_color_t border;
|
||||
lv_color_t low_battery;
|
||||
};
|
||||
|
||||
|
||||
class LcdDisplay : public Display {
|
||||
class LcdDisplay : public LvglDisplay {
|
||||
protected:
|
||||
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
|
||||
esp_lcd_panel_handle_t panel_ = nullptr;
|
||||
@ -41,8 +28,7 @@ protected:
|
||||
lv_obj_t* emoji_box_ = nullptr;
|
||||
esp_timer_handle_t preview_timer_ = nullptr;
|
||||
|
||||
ThemeColors current_theme_;
|
||||
|
||||
void InitializeLcdThemes();
|
||||
void SetupUI();
|
||||
virtual bool Lock(int timeout_ms = 0) override;
|
||||
virtual void Unlock() override;
|
||||
@ -60,7 +46,7 @@ public:
|
||||
#endif
|
||||
|
||||
// Add theme switching function
|
||||
virtual void SetTheme(const std::string& theme_name) override;
|
||||
virtual void SetTheme(Theme* theme) override;
|
||||
};
|
||||
|
||||
// SPI LCD显示器
|
||||
|
||||
@ -122,14 +122,14 @@ const lv_img_dsc_t* Twemoji64::GetEmojiImage(const char* name) const {
|
||||
}
|
||||
|
||||
|
||||
void CustomEmojiCollection::AddEmoji(const std::string& name, lv_img_dsc_t* image) {
|
||||
void CustomEmojiCollection::AddEmoji(const std::string& name, LvglImage* image) {
|
||||
emoji_collection_[name] = image;
|
||||
}
|
||||
|
||||
const lv_img_dsc_t* CustomEmojiCollection::GetEmojiImage(const char* name) const {
|
||||
auto it = emoji_collection_.find(name);
|
||||
if (it != emoji_collection_.end()) {
|
||||
return it->second;
|
||||
return it->second->image_dsc();
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Emoji not found: %s", name);
|
||||
@ -1,10 +1,13 @@
|
||||
#ifndef EMOJI_COLLECTION_H
|
||||
#define EMOJI_COLLECTION_H
|
||||
|
||||
#include "lvgl_image.h"
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
|
||||
// Define interface for emoji collection
|
||||
@ -26,9 +29,10 @@ public:
|
||||
|
||||
class CustomEmojiCollection : public EmojiCollection {
|
||||
private:
|
||||
std::map<std::string, lv_img_dsc_t*> emoji_collection_;
|
||||
std::map<std::string, LvglImage*> emoji_collection_;
|
||||
|
||||
public:
|
||||
void AddEmoji(const std::string& name, lv_img_dsc_t* image);
|
||||
void AddEmoji(const std::string& name, LvglImage* image);
|
||||
virtual const lv_img_dsc_t* GetEmojiImage(const char* name) const override;
|
||||
virtual ~CustomEmojiCollection();
|
||||
};
|
||||
251
main/display/lvgl_display/lvgl_display.cc
Normal file
251
main/display/lvgl_display/lvgl_display.cc
Normal file
@ -0,0 +1,251 @@
|
||||
#include <esp_log.h>
|
||||
#include <esp_err.h>
|
||||
#include <string>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <font_awesome.h>
|
||||
|
||||
#include "lvgl_display.h"
|
||||
#include "board.h"
|
||||
#include "application.h"
|
||||
#include "audio_codec.h"
|
||||
#include "settings.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#define TAG "Display"
|
||||
|
||||
LvglDisplay::LvglDisplay() {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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 (emotion_label_ != nullptr) {
|
||||
lv_obj_del(emotion_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) {
|
||||
DisplayLockGuard lock(this);
|
||||
if (status_label_ == nullptr) {
|
||||
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 ¬ification, int duration_ms) {
|
||||
ShowNotification(notification.c_str(), duration_ms);
|
||||
}
|
||||
|
||||
void LvglDisplay::ShowNotification(const char* notification, int duration_ms) {
|
||||
DisplayLockGuard lock(this);
|
||||
if (notification_label_ == nullptr) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 如果静音状态改变,则更新图标
|
||||
if (codec->output_volume() == 0 && !muted_) {
|
||||
muted_ = true;
|
||||
lv_label_set_text(mute_label_, FONT_AWESOME_VOLUME_XMARK);
|
||||
} 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_);
|
||||
// 更新电池图标
|
||||
int battery_level;
|
||||
bool charging, discharging;
|
||||
const char* icon = nullptr;
|
||||
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
|
||||
if (charging) {
|
||||
icon = FONT_AWESOME_BATTERY_BOLT;
|
||||
} else {
|
||||
const char* levels[] = {
|
||||
FONT_AWESOME_BATTERY_EMPTY, // 0-19%
|
||||
FONT_AWESOME_BATTERY_QUARTER, // 20-39%
|
||||
FONT_AWESOME_BATTERY_HALF, // 40-59%
|
||||
FONT_AWESOME_BATTERY_THREE_QUARTERS, // 60-79%
|
||||
FONT_AWESOME_BATTERY_FULL, // 80-99%
|
||||
FONT_AWESOME_BATTERY_FULL, // 100%
|
||||
};
|
||||
icon = levels[battery_level / 20];
|
||||
}
|
||||
DisplayLockGuard lock(this);
|
||||
if (battery_label_ != nullptr && battery_icon_ != icon) {
|
||||
battery_icon_ = icon;
|
||||
lv_label_set_text(battery_label_, battery_icon_);
|
||||
}
|
||||
|
||||
if (low_battery_popup_ != nullptr) {
|
||||
if (strcmp(icon, FONT_AWESOME_BATTERY_EMPTY) == 0 && discharging) {
|
||||
if (lv_obj_has_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN)) { // 如果低电量提示框隐藏,则显示
|
||||
lv_obj_remove_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
||||
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)) { // 如果低电量提示框显示,则隐藏
|
||||
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 每 10 秒更新一次网络图标
|
||||
static int seconds_counter = 0;
|
||||
if (update_all || seconds_counter++ % 10 == 0) {
|
||||
// 升级固件时,不读取 4G 网络状态,避免占用 UART 资源
|
||||
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::SetEmotion(const char* emotion) {
|
||||
const char* utf8 = font_awesome_get_utf8(emotion);
|
||||
DisplayLockGuard lock(this);
|
||||
if (emotion_label_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (utf8 != nullptr) {
|
||||
lv_label_set_text(emotion_label_, utf8);
|
||||
} else {
|
||||
lv_label_set_text(emotion_label_, FONT_AWESOME_NEUTRAL);
|
||||
}
|
||||
}
|
||||
|
||||
void LvglDisplay::SetPreviewImage(const lv_img_dsc_t* image) {
|
||||
// Do nothing but free the image
|
||||
if (image != nullptr) {
|
||||
heap_caps_free((void*)image->data);
|
||||
heap_caps_free((void*)image);
|
||||
}
|
||||
}
|
||||
|
||||
void LvglDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
DisplayLockGuard lock(this);
|
||||
if (chat_message_label_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
lv_label_set_text(chat_message_label_, content);
|
||||
}
|
||||
|
||||
void LvglDisplay::SetTheme(Theme* theme) {
|
||||
current_theme_ = theme;
|
||||
Settings settings("display", true);
|
||||
settings.SetString("theme", theme->name());
|
||||
}
|
||||
|
||||
void LvglDisplay::SetPowerSaveMode(bool on) {
|
||||
if (on) {
|
||||
SetChatMessage("system", "");
|
||||
SetEmotion("sleepy");
|
||||
} else {
|
||||
SetChatMessage("system", "");
|
||||
SetEmotion("neutral");
|
||||
}
|
||||
}
|
||||
57
main/display/lvgl_display/lvgl_display.h
Normal file
57
main/display/lvgl_display/lvgl_display.h
Normal file
@ -0,0 +1,57 @@
|
||||
#ifndef LVGL_DISPLAY_H
|
||||
#define LVGL_DISPLAY_H
|
||||
|
||||
#include "display.h"
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <esp_timer.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_pm.h>
|
||||
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
|
||||
class LvglDisplay : public Display {
|
||||
public:
|
||||
LvglDisplay();
|
||||
virtual ~LvglDisplay();
|
||||
|
||||
virtual void SetStatus(const char* status);
|
||||
virtual void ShowNotification(const char* notification, int duration_ms = 3000);
|
||||
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 SetPreviewImage(const lv_img_dsc_t* image);
|
||||
virtual void SetTheme(Theme* theme);
|
||||
virtual Theme* GetTheme() { return current_theme_; }
|
||||
virtual void UpdateStatusBar(bool update_all = false);
|
||||
virtual void SetPowerSaveMode(bool on);
|
||||
|
||||
protected:
|
||||
esp_pm_lock_handle_t pm_lock_ = nullptr;
|
||||
lv_display_t *display_ = nullptr;
|
||||
|
||||
lv_obj_t *emotion_label_ = nullptr;
|
||||
lv_obj_t *network_label_ = nullptr;
|
||||
lv_obj_t *status_label_ = nullptr;
|
||||
lv_obj_t *notification_label_ = nullptr;
|
||||
lv_obj_t *mute_label_ = nullptr;
|
||||
lv_obj_t *battery_label_ = nullptr;
|
||||
lv_obj_t* chat_message_label_ = nullptr;
|
||||
lv_obj_t* low_battery_popup_ = nullptr;
|
||||
lv_obj_t* low_battery_label_ = nullptr;
|
||||
|
||||
const char* battery_icon_ = nullptr;
|
||||
const char* network_icon_ = nullptr;
|
||||
bool muted_ = false;
|
||||
|
||||
std::chrono::system_clock::time_point last_status_update_time_;
|
||||
esp_timer_handle_t notification_timer_ = nullptr;
|
||||
|
||||
friend class DisplayLockGuard;
|
||||
virtual bool Lock(int timeout_ms = 0) = 0;
|
||||
virtual void Unlock() = 0;
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
13
main/display/lvgl_display/lvgl_font.cc
Normal file
13
main/display/lvgl_display/lvgl_font.cc
Normal file
@ -0,0 +1,13 @@
|
||||
#include "lvgl_font.h"
|
||||
#include <cbin_font.h>
|
||||
|
||||
|
||||
LvglCBinFont::LvglCBinFont(void* data) {
|
||||
font_ = cbin_font_create(static_cast<uint8_t*>(data));
|
||||
}
|
||||
|
||||
LvglCBinFont::~LvglCBinFont() {
|
||||
if (font_ != nullptr) {
|
||||
cbin_font_delete(font_);
|
||||
}
|
||||
}
|
||||
31
main/display/lvgl_display/lvgl_font.h
Normal file
31
main/display/lvgl_display/lvgl_font.h
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
|
||||
class LvglFont {
|
||||
public:
|
||||
virtual const lv_font_t* font() const = 0;
|
||||
virtual ~LvglFont() = default;
|
||||
};
|
||||
|
||||
// Built-in font
|
||||
class LvglBuiltInFont : public LvglFont {
|
||||
public:
|
||||
LvglBuiltInFont(const lv_font_t* font) : font_(font) {}
|
||||
virtual const lv_font_t* font() const override { return font_; }
|
||||
|
||||
private:
|
||||
const lv_font_t* font_;
|
||||
};
|
||||
|
||||
|
||||
class LvglCBinFont : public LvglFont {
|
||||
public:
|
||||
LvglCBinFont(void* data);
|
||||
virtual ~LvglCBinFont();
|
||||
virtual const lv_font_t* font() const override { return font_; }
|
||||
|
||||
private:
|
||||
lv_font_t* font_;
|
||||
};
|
||||
28
main/display/lvgl_display/lvgl_image.cc
Normal file
28
main/display/lvgl_display/lvgl_image.cc
Normal file
@ -0,0 +1,28 @@
|
||||
#include "lvgl_image.h"
|
||||
#include <cbin_font.h>
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <cstring>
|
||||
|
||||
#define TAG "LvglImage"
|
||||
|
||||
|
||||
LvglRawImage::LvglRawImage(void* data, size_t size) {
|
||||
bzero(&image_dsc_, sizeof(image_dsc_));
|
||||
image_dsc_.header.magic = LV_IMAGE_HEADER_MAGIC;
|
||||
image_dsc_.header.cf = LV_COLOR_FORMAT_RAW_ALPHA;
|
||||
image_dsc_.header.w = 0;
|
||||
image_dsc_.header.h = 0;
|
||||
image_dsc_.data_size = size;
|
||||
image_dsc_.data = static_cast<uint8_t*>(data);
|
||||
}
|
||||
|
||||
LvglCBinImage::LvglCBinImage(void* data) {
|
||||
image_dsc_ = cbin_img_dsc_create(static_cast<uint8_t*>(data));
|
||||
}
|
||||
|
||||
LvglCBinImage::~LvglCBinImage() {
|
||||
if (image_dsc_ != nullptr) {
|
||||
cbin_img_dsc_delete(image_dsc_);
|
||||
}
|
||||
}
|
||||
32
main/display/lvgl_display/lvgl_image.h
Normal file
32
main/display/lvgl_display/lvgl_image.h
Normal file
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
|
||||
// Wrap around lv_img_dsc_t
|
||||
class LvglImage {
|
||||
public:
|
||||
virtual const lv_img_dsc_t* image_dsc() const = 0;
|
||||
virtual ~LvglImage() = default;
|
||||
};
|
||||
|
||||
|
||||
class LvglRawImage : public LvglImage {
|
||||
public:
|
||||
LvglRawImage(void* data, size_t size);
|
||||
virtual const lv_img_dsc_t* image_dsc() const override { return &image_dsc_; }
|
||||
|
||||
private:
|
||||
lv_img_dsc_t image_dsc_;
|
||||
};
|
||||
|
||||
|
||||
class LvglCBinImage : public LvglImage {
|
||||
public:
|
||||
LvglCBinImage(void* data);
|
||||
virtual ~LvglCBinImage();
|
||||
virtual const lv_img_dsc_t* image_dsc() const override { return image_dsc_; }
|
||||
|
||||
private:
|
||||
lv_img_dsc_t* image_dsc_ = nullptr;
|
||||
};
|
||||
19
main/display/lvgl_display/lvgl_theme.cc
Normal file
19
main/display/lvgl_display/lvgl_theme.cc
Normal file
@ -0,0 +1,19 @@
|
||||
#include "lvgl_theme.h"
|
||||
|
||||
LvglTheme::LvglTheme(const std::string& name) : Theme(name) {
|
||||
}
|
||||
|
||||
LvglThemeManager::LvglThemeManager() {
|
||||
}
|
||||
|
||||
LvglTheme* LvglThemeManager::GetTheme(const std::string& theme_name) {
|
||||
auto it = themes_.find(theme_name);
|
||||
if (it != themes_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void LvglThemeManager::RegisterTheme(const std::string& theme_name, LvglTheme* theme) {
|
||||
themes_[theme_name] = theme;
|
||||
}
|
||||
92
main/display/lvgl_display/lvgl_theme.h
Normal file
92
main/display/lvgl_display/lvgl_theme.h
Normal file
@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "display.h"
|
||||
#include "lvgl_image.h"
|
||||
#include "lvgl_font.h"
|
||||
#include "emoji_collection.h"
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
|
||||
class LvglTheme : public Theme {
|
||||
public:
|
||||
LvglTheme(const std::string& name);
|
||||
|
||||
// Properties
|
||||
inline lv_color_t background_color() const { return background_color_; }
|
||||
inline lv_color_t text_color() const { return text_color_; }
|
||||
inline lv_color_t chat_background_color() const { return chat_background_color_; }
|
||||
inline lv_color_t user_bubble_color() const { return user_bubble_color_; }
|
||||
inline lv_color_t assistant_bubble_color() const { return assistant_bubble_color_; }
|
||||
inline lv_color_t system_bubble_color() const { return system_bubble_color_; }
|
||||
inline lv_color_t system_text_color() const { return system_text_color_; }
|
||||
inline lv_color_t border_color() const { return border_color_; }
|
||||
inline lv_color_t low_battery_color() const { return low_battery_color_; }
|
||||
inline std::shared_ptr<LvglImage> background_image() const { return background_image_; }
|
||||
inline std::shared_ptr<EmojiCollection> emoji_collection() const { return emoji_collection_; }
|
||||
inline std::shared_ptr<LvglFont> text_font() const { return text_font_; }
|
||||
inline std::shared_ptr<LvglFont> icon_font() const { return icon_font_; }
|
||||
inline std::shared_ptr<LvglFont> large_icon_font() const { return large_icon_font_; }
|
||||
inline int spacing(int scale) const { return spacing_ * scale; }
|
||||
|
||||
inline void set_background_color(lv_color_t background) { background_color_ = background; }
|
||||
inline void set_text_color(lv_color_t text) { text_color_ = text; }
|
||||
inline void set_chat_background_color(lv_color_t chat_background) { chat_background_color_ = chat_background; }
|
||||
inline void set_user_bubble_color(lv_color_t user_bubble) { user_bubble_color_ = user_bubble; }
|
||||
inline void set_assistant_bubble_color(lv_color_t assistant_bubble) { assistant_bubble_color_ = assistant_bubble; }
|
||||
inline void set_system_bubble_color(lv_color_t system_bubble) { system_bubble_color_ = system_bubble; }
|
||||
inline void set_system_text_color(lv_color_t system_text) { system_text_color_ = system_text; }
|
||||
inline void set_border_color(lv_color_t border) { border_color_ = border; }
|
||||
inline void set_low_battery_color(lv_color_t low_battery) { low_battery_color_ = low_battery; }
|
||||
inline void set_background_image(std::shared_ptr<LvglImage> background_image) { background_image_ = background_image; }
|
||||
inline void set_emoji_collection(std::shared_ptr<EmojiCollection> emoji_collection) { emoji_collection_ = emoji_collection; }
|
||||
inline void set_text_font(std::shared_ptr<LvglFont> text_font) { text_font_ = text_font; }
|
||||
inline void set_icon_font(std::shared_ptr<LvglFont> icon_font) { icon_font_ = icon_font; }
|
||||
inline void set_large_icon_font(std::shared_ptr<LvglFont> large_icon_font) { large_icon_font_ = large_icon_font; }
|
||||
|
||||
private:
|
||||
int spacing_ = 2;
|
||||
|
||||
// Colors
|
||||
lv_color_t background_color_;
|
||||
lv_color_t text_color_;
|
||||
lv_color_t chat_background_color_;
|
||||
lv_color_t user_bubble_color_;
|
||||
lv_color_t assistant_bubble_color_;
|
||||
lv_color_t system_bubble_color_;
|
||||
lv_color_t system_text_color_;
|
||||
lv_color_t border_color_;
|
||||
lv_color_t low_battery_color_;
|
||||
|
||||
// Background image
|
||||
std::shared_ptr<LvglImage> background_image_ = nullptr;
|
||||
|
||||
// fonts
|
||||
std::shared_ptr<LvglFont> text_font_ = nullptr;
|
||||
std::shared_ptr<LvglFont> icon_font_ = nullptr;
|
||||
std::shared_ptr<LvglFont> large_icon_font_ = nullptr;
|
||||
|
||||
// Emoji collection
|
||||
std::shared_ptr<EmojiCollection> emoji_collection_ = nullptr;
|
||||
};
|
||||
|
||||
|
||||
class LvglThemeManager {
|
||||
public:
|
||||
static LvglThemeManager& GetInstance() {
|
||||
static LvglThemeManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void RegisterTheme(const std::string& theme_name, LvglTheme* theme);
|
||||
LvglTheme* GetTheme(const std::string& theme_name);
|
||||
|
||||
private:
|
||||
LvglThemeManager();
|
||||
void InitializeDefaultThemes();
|
||||
|
||||
std::map<std::string, LvglTheme*> themes_;
|
||||
};
|
||||
@ -20,10 +20,8 @@ OledDisplay::OledDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handl
|
||||
: panel_io_(panel_io), panel_(panel) {
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
style_ = {
|
||||
.text_font = &LVGL_TEXT_FONT,
|
||||
.icon_font = &LVGL_ICON_FONT,
|
||||
};
|
||||
text_font_ = &LVGL_TEXT_FONT;
|
||||
icon_font_ = &LVGL_ICON_FONT;
|
||||
|
||||
ESP_LOGI(TAG, "Initialize LVGL");
|
||||
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
|
||||
@ -126,7 +124,7 @@ void OledDisplay::SetupUI_128x64() {
|
||||
DisplayLockGuard lock(this);
|
||||
|
||||
auto screen = lv_screen_active();
|
||||
lv_obj_set_style_text_font(screen, style_.text_font, 0);
|
||||
lv_obj_set_style_text_font(screen, text_font_, 0);
|
||||
lv_obj_set_style_text_color(screen, lv_color_black(), 0);
|
||||
|
||||
/* Container */
|
||||
@ -197,7 +195,7 @@ void OledDisplay::SetupUI_128x64() {
|
||||
|
||||
network_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(network_label_, "");
|
||||
lv_obj_set_style_text_font(network_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_font(network_label_, icon_font_, 0);
|
||||
|
||||
notification_label_ = lv_label_create(status_bar_);
|
||||
lv_obj_set_flex_grow(notification_label_, 1);
|
||||
@ -212,15 +210,15 @@ void OledDisplay::SetupUI_128x64() {
|
||||
|
||||
mute_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(mute_label_, "");
|
||||
lv_obj_set_style_text_font(mute_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_font(mute_label_, icon_font_, 0);
|
||||
|
||||
battery_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(battery_label_, "");
|
||||
lv_obj_set_style_text_font(battery_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_font(battery_label_, icon_font_, 0);
|
||||
|
||||
low_battery_popup_ = lv_obj_create(screen);
|
||||
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, style_.text_font->line_height * 2);
|
||||
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, text_font_->line_height * 2);
|
||||
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_obj_set_style_bg_color(low_battery_popup_, lv_color_black(), 0);
|
||||
lv_obj_set_style_radius(low_battery_popup_, 10, 0);
|
||||
@ -235,7 +233,7 @@ void OledDisplay::SetupUI_128x32() {
|
||||
DisplayLockGuard lock(this);
|
||||
|
||||
auto screen = lv_screen_active();
|
||||
lv_obj_set_style_text_font(screen, style_.text_font, 0);
|
||||
lv_obj_set_style_text_font(screen, text_font_, 0);
|
||||
|
||||
/* Container */
|
||||
container_ = lv_obj_create(screen);
|
||||
@ -288,15 +286,15 @@ void OledDisplay::SetupUI_128x32() {
|
||||
|
||||
mute_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(mute_label_, "");
|
||||
lv_obj_set_style_text_font(mute_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_font(mute_label_, icon_font_, 0);
|
||||
|
||||
network_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(network_label_, "");
|
||||
lv_obj_set_style_text_font(network_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_font(network_label_, icon_font_, 0);
|
||||
|
||||
battery_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(battery_label_, "");
|
||||
lv_obj_set_style_text_font(battery_label_, style_.icon_font, 0);
|
||||
lv_obj_set_style_text_font(battery_label_, icon_font_, 0);
|
||||
|
||||
chat_message_label_ = lv_label_create(side_bar_);
|
||||
lv_obj_set_size(chat_message_label_, width_ - 32, LV_SIZE_CONTENT);
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
#ifndef OLED_DISPLAY_H
|
||||
#define OLED_DISPLAY_H
|
||||
|
||||
#include "display.h"
|
||||
#include "lvgl_display.h"
|
||||
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
|
||||
|
||||
class OledDisplay : public Display {
|
||||
class OledDisplay : public LvglDisplay {
|
||||
private:
|
||||
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
|
||||
esp_lcd_panel_handle_t panel_ = nullptr;
|
||||
@ -18,7 +18,8 @@ private:
|
||||
lv_obj_t* content_right_ = nullptr;
|
||||
lv_obj_t* container_ = nullptr;
|
||||
lv_obj_t* side_bar_ = nullptr;
|
||||
DisplayStyle style_;
|
||||
const lv_font_t* text_font_ = nullptr;
|
||||
const lv_font_t* icon_font_ = nullptr;
|
||||
|
||||
virtual bool Lock(int timeout_ms = 0) override;
|
||||
virtual void Unlock() override;
|
||||
|
||||
@ -6,7 +6,7 @@ dependencies:
|
||||
espressif/esp_lcd_st77916: ^1.0.1
|
||||
espressif/esp_lcd_axs15231b: ^1.0.0
|
||||
espressif/esp_lcd_st7796:
|
||||
version: 1.3.2
|
||||
version: 1.3.4
|
||||
rules:
|
||||
- if: target not in [esp32c3]
|
||||
espressif/esp_lcd_spd2010: ==1.0.2
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
#include "display.h"
|
||||
#include "board.h"
|
||||
#include "settings.h"
|
||||
#include "lvgl_theme.h"
|
||||
|
||||
#define TAG "MCP"
|
||||
|
||||
@ -77,15 +78,21 @@ void McpServer::AddCommonTools() {
|
||||
}
|
||||
|
||||
auto display = board.GetDisplay();
|
||||
if (display && !display->GetTheme().empty()) {
|
||||
if (display && display->GetTheme() != nullptr) {
|
||||
AddTool("self.screen.set_theme",
|
||||
"Set the theme of the screen. The theme can be `light` or `dark`.",
|
||||
PropertyList({
|
||||
Property("theme", kPropertyTypeString)
|
||||
}),
|
||||
[display](const PropertyList& properties) -> ReturnValue {
|
||||
display->SetTheme(properties["theme"].value<std::string>().c_str());
|
||||
return true;
|
||||
auto theme_name = properties["theme"].value<std::string>();
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto theme = theme_manager.GetTheme(theme_name);
|
||||
if (theme != nullptr) {
|
||||
display->SetTheme(theme);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user