Initial commit
This commit is contained in:
60
main/display/display.cc
Normal file
60
main/display/display.cc
Normal file
@@ -0,0 +1,60 @@
|
||||
#include <esp_log.h>
|
||||
#include <esp_err.h>
|
||||
#include <string>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <font_awesome.h>
|
||||
|
||||
#include "display.h"
|
||||
#include "board.h"
|
||||
#include "application.h"
|
||||
#include "audio_codec.h"
|
||||
#include "settings.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#define TAG "Display"
|
||||
|
||||
Display::Display() {
|
||||
}
|
||||
|
||||
Display::~Display() {
|
||||
}
|
||||
|
||||
void Display::SetStatus(const char* status) {
|
||||
ESP_LOGW(TAG, "SetStatus: %s", status);
|
||||
}
|
||||
|
||||
void Display::ShowNotification(const std::string ¬ification, int duration_ms) {
|
||||
ShowNotification(notification.c_str(), duration_ms);
|
||||
}
|
||||
|
||||
void Display::ShowNotification(const char* notification, int duration_ms) {
|
||||
ESP_LOGW(TAG, "ShowNotification: %s", notification);
|
||||
}
|
||||
|
||||
void Display::UpdateStatusBar(bool update_all) {
|
||||
}
|
||||
|
||||
|
||||
void Display::SetEmotion(const char* emotion) {
|
||||
ESP_LOGW(TAG, "SetEmotion: %s", emotion);
|
||||
}
|
||||
|
||||
void Display::SetChatMessage(const char* role, const char* content) {
|
||||
ESP_LOGW(TAG, "Role:%s", role);
|
||||
ESP_LOGW(TAG, " %s", content);
|
||||
}
|
||||
|
||||
void Display::ClearChatMessages() {
|
||||
// Default empty implementation, override in subclasses if needed
|
||||
}
|
||||
|
||||
void Display::SetTheme(Theme* theme) {
|
||||
current_theme_ = theme;
|
||||
Settings settings("display", true);
|
||||
settings.SetString("theme", theme->name());
|
||||
}
|
||||
|
||||
void Display::SetPowerSaveMode(bool on) {
|
||||
ESP_LOGW(TAG, "SetPowerSaveMode: %d", on);
|
||||
}
|
||||
87
main/display/display.h
Normal file
87
main/display/display.h
Normal file
@@ -0,0 +1,87 @@
|
||||
#ifndef DISPLAY_H
|
||||
#define DISPLAY_H
|
||||
|
||||
#include "emoji_collection.h"
|
||||
|
||||
#ifndef CONFIG_USE_EMOTE_MESSAGE_STYLE
|
||||
#define HAVE_LVGL 1
|
||||
#include <lvgl.h>
|
||||
#endif
|
||||
|
||||
#include <esp_timer.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_pm.h>
|
||||
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
|
||||
class Theme {
|
||||
public:
|
||||
Theme(const std::string& name) : name_(name) {}
|
||||
virtual ~Theme() = default;
|
||||
|
||||
inline std::string name() const { return name_; }
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
|
||||
class Display {
|
||||
public:
|
||||
Display();
|
||||
virtual ~Display();
|
||||
|
||||
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 ClearChatMessages();
|
||||
virtual void SetTheme(Theme* theme);
|
||||
virtual Theme* GetTheme() { return current_theme_; }
|
||||
virtual void UpdateStatusBar(bool update_all = false);
|
||||
virtual void SetPowerSaveMode(bool on);
|
||||
virtual void SetupUI() {
|
||||
setup_ui_called_ = true;
|
||||
}
|
||||
|
||||
inline int width() const { return width_; }
|
||||
inline int height() const { return height_; }
|
||||
inline bool IsSetupUICalled() const { return setup_ui_called_; }
|
||||
|
||||
protected:
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
bool setup_ui_called_ = false; // Track if SetupUI() has been called
|
||||
|
||||
Theme* current_theme_ = nullptr;
|
||||
|
||||
friend class DisplayLockGuard;
|
||||
virtual bool Lock(int timeout_ms = 0) = 0;
|
||||
virtual void Unlock() = 0;
|
||||
};
|
||||
|
||||
|
||||
class DisplayLockGuard {
|
||||
public:
|
||||
DisplayLockGuard(Display *display) : display_(display) {
|
||||
if (!display_->Lock(30000)) {
|
||||
ESP_LOGE("Display", "Failed to lock display");
|
||||
}
|
||||
}
|
||||
~DisplayLockGuard() {
|
||||
display_->Unlock();
|
||||
}
|
||||
|
||||
private:
|
||||
Display *display_;
|
||||
};
|
||||
|
||||
class NoDisplay : public Display {
|
||||
private:
|
||||
virtual bool Lock(int timeout_ms = 0) override {
|
||||
return true;
|
||||
}
|
||||
virtual void Unlock() override {}
|
||||
};
|
||||
|
||||
#endif
|
||||
250
main/display/emote_display.cc
Normal file
250
main/display/emote_display.cc
Normal file
@@ -0,0 +1,250 @@
|
||||
#include "emote_display.h"
|
||||
|
||||
// Standard C++ headers
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <tuple>
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
// Standard C headers
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
// ESP-IDF headers
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_timer.h>
|
||||
#include <lvgl.h>
|
||||
|
||||
// FreeRTOS headers
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
// Project headers
|
||||
#include "assets/lang_config.h"
|
||||
#include "assets.h"
|
||||
#include "board.h"
|
||||
#include "gfx.h"
|
||||
#include "expression_emote.h"
|
||||
|
||||
|
||||
namespace emote {
|
||||
|
||||
// ============================================================================
|
||||
// Constants and Type Definitions
|
||||
// ============================================================================
|
||||
|
||||
static const char* TAG = "EmoteDisplay";
|
||||
|
||||
// ============================================================================
|
||||
// Forward Declarations
|
||||
// ============================================================================
|
||||
|
||||
class EmoteDisplay;
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
static bool OnFlushIoReady(const esp_lcd_panel_io_handle_t panel_io,
|
||||
esp_lcd_panel_io_event_data_t* const edata, void* user_ctx)
|
||||
{
|
||||
emote_handle_t handle = static_cast<emote_handle_t>(user_ctx);
|
||||
if (handle) {
|
||||
emote_notify_flush_finished(handle);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flush callback for emote
|
||||
static void OnFlushCallback(int x_start, int y_start, int x_end, int y_end, const void* data, emote_handle_t handle)
|
||||
{
|
||||
esp_lcd_panel_handle_t panel = (esp_lcd_panel_handle_t)emote_get_user_data(handle);
|
||||
if (panel != nullptr) {
|
||||
esp_lcd_panel_draw_bitmap(panel, x_start, y_start, x_end, y_end, data);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Graphics Initialization Functions
|
||||
// ============================================================================
|
||||
|
||||
static emote_handle_t InitializeEmote(const esp_lcd_panel_handle_t panel, const int width, const int height)
|
||||
{
|
||||
if (!panel) {
|
||||
ESP_LOGE(TAG, "Invalid panel");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
emote_config_t emote_cfg = {
|
||||
.flags = {
|
||||
.swap = true,
|
||||
.double_buffer = true,
|
||||
.buff_dma = false,
|
||||
},
|
||||
.gfx_emote = {
|
||||
.h_res = width,
|
||||
.v_res = height,
|
||||
.fps = 30,
|
||||
},
|
||||
.buffers = {
|
||||
.buf_pixels = static_cast<size_t>(width * 16),
|
||||
},
|
||||
.task = {
|
||||
.task_priority = 5,
|
||||
.task_stack = 6 * 1024,
|
||||
.task_affinity = 0,
|
||||
.task_stack_in_ext = false,
|
||||
},
|
||||
.flush_cb = OnFlushCallback,
|
||||
.user_data = (void*)panel,
|
||||
};
|
||||
|
||||
emote_handle_t emote_handle = emote_init(&emote_cfg);
|
||||
if (!emote_handle) {
|
||||
ESP_LOGE(TAG, "Failed to initialize emote");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return emote_handle;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EmoteDisplay Class Implementation
|
||||
// ============================================================================
|
||||
|
||||
EmoteDisplay::EmoteDisplay(const esp_lcd_panel_handle_t panel, const esp_lcd_panel_io_handle_t panel_io,
|
||||
const int width, const int height)
|
||||
{
|
||||
emote_handle_ = InitializeEmote(panel, width, height);
|
||||
|
||||
const esp_lcd_panel_io_callbacks_t cbs = {
|
||||
.on_color_trans_done = OnFlushIoReady,
|
||||
};
|
||||
esp_lcd_panel_io_register_event_callbacks(panel_io, &cbs, emote_handle_);
|
||||
}
|
||||
|
||||
EmoteDisplay::~EmoteDisplay()
|
||||
{
|
||||
if (emote_handle_) {
|
||||
emote_deinit(emote_handle_);
|
||||
emote_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetEmotion(const char* const emotion)
|
||||
{
|
||||
ESP_LOGI(TAG, "SetEmotion: %s", emotion);
|
||||
if (emote_handle_ && emotion && strlen(emotion) > 0) {
|
||||
emote_set_anim_emoji(emote_handle_, emotion);
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetChatMessage(const char* const role, const char* const content)
|
||||
{
|
||||
ESP_LOGI(TAG, "SetChatMessage: %s, %s", role, content);
|
||||
if (emote_handle_ && content && strlen(content) > 0) {
|
||||
if ((std::strcmp(role, "system") == 0) && std::strstr(content, "xiaozhi.me")) {
|
||||
size_t len = strlen(content);
|
||||
char* new_content = new char[len + 1];
|
||||
strcpy(new_content, content);
|
||||
std::replace(new_content, new_content + len, static_cast<char>(0x0A), static_cast<char>(0x20));
|
||||
emote_set_event_msg(emote_handle_, EMOTE_MGR_EVT_SYS, new_content);
|
||||
delete[] new_content;
|
||||
} else {
|
||||
emote_set_event_msg(emote_handle_, EMOTE_MGR_EVT_SPEAK, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetStatus(const char* const status)
|
||||
{
|
||||
ESP_LOGI(TAG, "SetStatus: %s", status);
|
||||
if (emote_handle_ && status && strlen(status) > 0) {
|
||||
if (std::strcmp(status, Lang::Strings::LISTENING) == 0) {
|
||||
emote_set_event_msg(emote_handle_, EMOTE_MGR_EVT_LISTEN, NULL);
|
||||
} else if (std::strcmp(status, Lang::Strings::STANDBY) == 0) {
|
||||
emote_set_event_msg(emote_handle_, EMOTE_MGR_EVT_IDLE, NULL);
|
||||
} else if (std::strcmp(status, Lang::Strings::SPEAKING) == 0) {
|
||||
emote_set_event_msg(emote_handle_, EMOTE_MGR_EVT_SPEAK, NULL);
|
||||
} else if (std::strcmp(status, Lang::Strings::ERROR) == 0) {
|
||||
emote_set_event_msg(emote_handle_, EMOTE_MGR_EVT_SET, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::ShowNotification(const char* notification, int duration_ms)
|
||||
{
|
||||
ESP_LOGI(TAG, "ShowNotification: %s", notification);
|
||||
if (emote_handle_ && notification && strlen(notification) > 0) {
|
||||
emote_set_event_msg(emote_handle_, EMOTE_MGR_EVT_SYS, notification);
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::UpdateStatusBar(bool update_all)
|
||||
{
|
||||
ESP_LOGD(TAG, "UpdateStatusBar: %s", update_all ? "true" : "false");
|
||||
if (!emote_handle_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetPowerSaveMode(bool on)
|
||||
{
|
||||
ESP_LOGI(TAG, "SetPowerSaveMode: %s", on ? "ON" : "OFF");
|
||||
if (!emote_handle_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetPreviewImage(const void* image)
|
||||
{
|
||||
if (image) {
|
||||
ESP_LOGI(TAG, "SetPreviewImage: Preview image not supported, using default icon");
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetTheme(Theme* const theme)
|
||||
{
|
||||
ESP_LOGI(TAG, "SetTheme: %p", theme);
|
||||
}
|
||||
|
||||
bool EmoteDisplay::Lock(const int timeout_ms)
|
||||
{
|
||||
(void)timeout_ms;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EmoteDisplay::Unlock()
|
||||
{
|
||||
}
|
||||
|
||||
bool EmoteDisplay::StopAnimDialog()
|
||||
{
|
||||
ESP_LOGI(TAG, "StopAnimDialog");
|
||||
if (emote_handle_) {
|
||||
return emote_stop_anim_dialog(emote_handle_);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EmoteDisplay::InsertAnimDialog(const char* emoji_name, uint32_t duration_ms)
|
||||
{
|
||||
ESP_LOGI(TAG, "InsertAnimDialog: %s, %" PRIu32, emoji_name, duration_ms);
|
||||
if (emote_handle_ && emoji_name) {
|
||||
return emote_insert_anim_dialog(emote_handle_, emoji_name, duration_ms);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void EmoteDisplay::RefreshAll()
|
||||
{
|
||||
if (emote_handle_) {
|
||||
emote_notify_all_refresh(emote_handle_);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace emote
|
||||
42
main/display/emote_display.h
Normal file
42
main/display/emote_display.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "display.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include "expression_emote.h"
|
||||
|
||||
namespace emote {
|
||||
|
||||
class EmoteDisplay : public Display {
|
||||
public:
|
||||
EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io, int width, int height);
|
||||
virtual ~EmoteDisplay();
|
||||
|
||||
virtual void SetEmotion(const char* emotion) override;
|
||||
virtual void SetStatus(const char* status) override;
|
||||
virtual void SetChatMessage(const char* role, const char* content) override;
|
||||
virtual void SetTheme(Theme* theme) override;
|
||||
virtual void ShowNotification(const char* notification, int duration_ms = 3000) override;
|
||||
virtual void UpdateStatusBar(bool update_all = false) override;
|
||||
virtual void SetPowerSaveMode(bool on) override;
|
||||
virtual void SetPreviewImage(const void* image);
|
||||
|
||||
bool StopAnimDialog();
|
||||
bool InsertAnimDialog(const char* emoji_name, uint32_t duration_ms);
|
||||
|
||||
void RefreshAll();
|
||||
|
||||
// Get emote handle for internal use
|
||||
emote_handle_t GetEmoteHandle() const { return emote_handle_; }
|
||||
|
||||
private:
|
||||
virtual bool Lock(int timeout_ms = 0) override;
|
||||
virtual void Unlock() override;
|
||||
|
||||
emote_handle_t emote_handle_ = nullptr;
|
||||
|
||||
};
|
||||
|
||||
} // namespace emote
|
||||
1310
main/display/lcd_display.cc
Normal file
1310
main/display/lcd_display.cc
Normal file
File diff suppressed because it is too large
Load Diff
85
main/display/lcd_display.h
Normal file
85
main/display/lcd_display.h
Normal file
@@ -0,0 +1,85 @@
|
||||
#ifndef LCD_DISPLAY_H
|
||||
#define LCD_DISPLAY_H
|
||||
|
||||
#include "lvgl_display.h"
|
||||
#include "gif/lvgl_gif.h"
|
||||
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <font_emoji.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#define PREVIEW_IMAGE_DURATION_MS 5000
|
||||
|
||||
|
||||
class LcdDisplay : public LvglDisplay {
|
||||
protected:
|
||||
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
|
||||
esp_lcd_panel_handle_t panel_ = nullptr;
|
||||
|
||||
lv_draw_buf_t draw_buf_;
|
||||
lv_obj_t* top_bar_ = nullptr;
|
||||
lv_obj_t* status_bar_ = nullptr;
|
||||
lv_obj_t* content_ = nullptr;
|
||||
lv_obj_t* container_ = nullptr;
|
||||
lv_obj_t* side_bar_ = nullptr;
|
||||
lv_obj_t* bottom_bar_ = nullptr;
|
||||
lv_obj_t* preview_image_ = nullptr;
|
||||
lv_obj_t* emoji_label_ = nullptr;
|
||||
lv_obj_t* emoji_image_ = nullptr;
|
||||
std::unique_ptr<LvglGif> gif_controller_ = nullptr;
|
||||
lv_obj_t* emoji_box_ = nullptr;
|
||||
lv_obj_t* chat_message_label_ = nullptr;
|
||||
esp_timer_handle_t preview_timer_ = nullptr;
|
||||
std::unique_ptr<LvglImage> preview_image_cached_ = nullptr;
|
||||
bool hide_subtitle_ = false; // Control whether to hide chat messages/subtitles
|
||||
|
||||
void InitializeLcdThemes();
|
||||
virtual bool Lock(int timeout_ms = 0) override;
|
||||
virtual void Unlock() override;
|
||||
|
||||
protected:
|
||||
// Add protected constructor
|
||||
LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height);
|
||||
|
||||
public:
|
||||
~LcdDisplay();
|
||||
virtual void SetEmotion(const char* emotion) override;
|
||||
virtual void SetChatMessage(const char* role, const char* content) override;
|
||||
virtual void ClearChatMessages() override;
|
||||
virtual void SetPreviewImage(std::unique_ptr<LvglImage> image) override;
|
||||
virtual void SetupUI() override;
|
||||
// Add theme switching function
|
||||
virtual void SetTheme(Theme* theme) override;
|
||||
|
||||
// Set whether to hide chat messages/subtitles
|
||||
void SetHideSubtitle(bool hide);
|
||||
};
|
||||
|
||||
// SPI LCD display
|
||||
class SpiLcdDisplay : public LcdDisplay {
|
||||
public:
|
||||
SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
|
||||
int width, int height, int offset_x, int offset_y,
|
||||
bool mirror_x, bool mirror_y, bool swap_xy);
|
||||
};
|
||||
|
||||
// RGB LCD display
|
||||
class RgbLcdDisplay : public LcdDisplay {
|
||||
public:
|
||||
RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
|
||||
int width, int height, int offset_x, int offset_y,
|
||||
bool mirror_x, bool mirror_y, bool swap_xy);
|
||||
};
|
||||
|
||||
// MIPI LCD display
|
||||
class MipiLcdDisplay : public LcdDisplay {
|
||||
public:
|
||||
MipiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
|
||||
int width, int height, int offset_x, int offset_y,
|
||||
bool mirror_x, bool mirror_y, bool swap_xy);
|
||||
};
|
||||
|
||||
#endif // LCD_DISPLAY_H
|
||||
123
main/display/lvgl_display/emoji_collection.cc
Normal file
123
main/display/lvgl_display/emoji_collection.cc
Normal file
@@ -0,0 +1,123 @@
|
||||
#include "emoji_collection.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
#define TAG "EmojiCollection"
|
||||
|
||||
void EmojiCollection::AddEmoji(const std::string& name, LvglImage* image) {
|
||||
emoji_collection_[name] = image;
|
||||
}
|
||||
|
||||
const LvglImage* EmojiCollection::GetEmojiImage(const char* name) {
|
||||
auto it = emoji_collection_.find(name);
|
||||
if (it != emoji_collection_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Emoji not found: %s", name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EmojiCollection::~EmojiCollection() {
|
||||
for (auto it = emoji_collection_.begin(); it != emoji_collection_.end(); ++it) {
|
||||
delete it->second;
|
||||
}
|
||||
emoji_collection_.clear();
|
||||
}
|
||||
|
||||
// These are declared in xiaozhi-fonts/src/font_emoji_32.c
|
||||
extern const lv_image_dsc_t emoji_1f636_32; // neutral
|
||||
extern const lv_image_dsc_t emoji_1f642_32; // happy
|
||||
extern const lv_image_dsc_t emoji_1f606_32; // laughing
|
||||
extern const lv_image_dsc_t emoji_1f602_32; // funny
|
||||
extern const lv_image_dsc_t emoji_1f614_32; // sad
|
||||
extern const lv_image_dsc_t emoji_1f620_32; // angry
|
||||
extern const lv_image_dsc_t emoji_1f62d_32; // crying
|
||||
extern const lv_image_dsc_t emoji_1f60d_32; // loving
|
||||
extern const lv_image_dsc_t emoji_1f633_32; // embarrassed
|
||||
extern const lv_image_dsc_t emoji_1f62f_32; // surprised
|
||||
extern const lv_image_dsc_t emoji_1f631_32; // shocked
|
||||
extern const lv_image_dsc_t emoji_1f914_32; // thinking
|
||||
extern const lv_image_dsc_t emoji_1f609_32; // winking
|
||||
extern const lv_image_dsc_t emoji_1f60e_32; // cool
|
||||
extern const lv_image_dsc_t emoji_1f60c_32; // relaxed
|
||||
extern const lv_image_dsc_t emoji_1f924_32; // delicious
|
||||
extern const lv_image_dsc_t emoji_1f618_32; // kissy
|
||||
extern const lv_image_dsc_t emoji_1f60f_32; // confident
|
||||
extern const lv_image_dsc_t emoji_1f634_32; // sleepy
|
||||
extern const lv_image_dsc_t emoji_1f61c_32; // silly
|
||||
extern const lv_image_dsc_t emoji_1f644_32; // confused
|
||||
|
||||
Twemoji32::Twemoji32() {
|
||||
AddEmoji("neutral", new LvglSourceImage(&emoji_1f636_32));
|
||||
AddEmoji("happy", new LvglSourceImage(&emoji_1f642_32));
|
||||
AddEmoji("laughing", new LvglSourceImage(&emoji_1f606_32));
|
||||
AddEmoji("funny", new LvglSourceImage(&emoji_1f602_32));
|
||||
AddEmoji("sad", new LvglSourceImage(&emoji_1f614_32));
|
||||
AddEmoji("angry", new LvglSourceImage(&emoji_1f620_32));
|
||||
AddEmoji("crying", new LvglSourceImage(&emoji_1f62d_32));
|
||||
AddEmoji("loving", new LvglSourceImage(&emoji_1f60d_32));
|
||||
AddEmoji("embarrassed", new LvglSourceImage(&emoji_1f633_32));
|
||||
AddEmoji("surprised", new LvglSourceImage(&emoji_1f62f_32));
|
||||
AddEmoji("shocked", new LvglSourceImage(&emoji_1f631_32));
|
||||
AddEmoji("thinking", new LvglSourceImage(&emoji_1f914_32));
|
||||
AddEmoji("winking", new LvglSourceImage(&emoji_1f609_32));
|
||||
AddEmoji("cool", new LvglSourceImage(&emoji_1f60e_32));
|
||||
AddEmoji("relaxed", new LvglSourceImage(&emoji_1f60c_32));
|
||||
AddEmoji("delicious", new LvglSourceImage(&emoji_1f924_32));
|
||||
AddEmoji("kissy", new LvglSourceImage(&emoji_1f618_32));
|
||||
AddEmoji("confident", new LvglSourceImage(&emoji_1f60f_32));
|
||||
AddEmoji("sleepy", new LvglSourceImage(&emoji_1f634_32));
|
||||
AddEmoji("silly", new LvglSourceImage(&emoji_1f61c_32));
|
||||
AddEmoji("confused", new LvglSourceImage(&emoji_1f644_32));
|
||||
}
|
||||
|
||||
|
||||
// These are declared in xiaozhi-fonts/src/font_emoji_64.c
|
||||
extern const lv_image_dsc_t emoji_1f636_64; // neutral
|
||||
extern const lv_image_dsc_t emoji_1f642_64; // happy
|
||||
extern const lv_image_dsc_t emoji_1f606_64; // laughing
|
||||
extern const lv_image_dsc_t emoji_1f602_64; // funny
|
||||
extern const lv_image_dsc_t emoji_1f614_64; // sad
|
||||
extern const lv_image_dsc_t emoji_1f620_64; // angry
|
||||
extern const lv_image_dsc_t emoji_1f62d_64; // crying
|
||||
extern const lv_image_dsc_t emoji_1f60d_64; // loving
|
||||
extern const lv_image_dsc_t emoji_1f633_64; // embarrassed
|
||||
extern const lv_image_dsc_t emoji_1f62f_64; // surprised
|
||||
extern const lv_image_dsc_t emoji_1f631_64; // shocked
|
||||
extern const lv_image_dsc_t emoji_1f914_64; // thinking
|
||||
extern const lv_image_dsc_t emoji_1f609_64; // winking
|
||||
extern const lv_image_dsc_t emoji_1f60e_64; // cool
|
||||
extern const lv_image_dsc_t emoji_1f60c_64; // relaxed
|
||||
extern const lv_image_dsc_t emoji_1f924_64; // delicious
|
||||
extern const lv_image_dsc_t emoji_1f618_64; // kissy
|
||||
extern const lv_image_dsc_t emoji_1f60f_64; // confident
|
||||
extern const lv_image_dsc_t emoji_1f634_64; // sleepy
|
||||
extern const lv_image_dsc_t emoji_1f61c_64; // silly
|
||||
extern const lv_image_dsc_t emoji_1f644_64; // confused
|
||||
|
||||
Twemoji64::Twemoji64() {
|
||||
AddEmoji("neutral", new LvglSourceImage(&emoji_1f636_64));
|
||||
AddEmoji("happy", new LvglSourceImage(&emoji_1f642_64));
|
||||
AddEmoji("laughing", new LvglSourceImage(&emoji_1f606_64));
|
||||
AddEmoji("funny", new LvglSourceImage(&emoji_1f602_64));
|
||||
AddEmoji("sad", new LvglSourceImage(&emoji_1f614_64));
|
||||
AddEmoji("angry", new LvglSourceImage(&emoji_1f620_64));
|
||||
AddEmoji("crying", new LvglSourceImage(&emoji_1f62d_64));
|
||||
AddEmoji("loving", new LvglSourceImage(&emoji_1f60d_64));
|
||||
AddEmoji("embarrassed", new LvglSourceImage(&emoji_1f633_64));
|
||||
AddEmoji("surprised", new LvglSourceImage(&emoji_1f62f_64));
|
||||
AddEmoji("shocked", new LvglSourceImage(&emoji_1f631_64));
|
||||
AddEmoji("thinking", new LvglSourceImage(&emoji_1f914_64));
|
||||
AddEmoji("winking", new LvglSourceImage(&emoji_1f609_64));
|
||||
AddEmoji("cool", new LvglSourceImage(&emoji_1f60e_64));
|
||||
AddEmoji("relaxed", new LvglSourceImage(&emoji_1f60c_64));
|
||||
AddEmoji("delicious", new LvglSourceImage(&emoji_1f924_64));
|
||||
AddEmoji("kissy", new LvglSourceImage(&emoji_1f618_64));
|
||||
AddEmoji("confident", new LvglSourceImage(&emoji_1f60f_64));
|
||||
AddEmoji("sleepy", new LvglSourceImage(&emoji_1f634_64));
|
||||
AddEmoji("silly", new LvglSourceImage(&emoji_1f61c_64));
|
||||
AddEmoji("confused", new LvglSourceImage(&emoji_1f644_64));
|
||||
}
|
||||
34
main/display/lvgl_display/emoji_collection.h
Normal file
34
main/display/lvgl_display/emoji_collection.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#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
|
||||
class EmojiCollection {
|
||||
public:
|
||||
virtual void AddEmoji(const std::string& name, LvglImage* image);
|
||||
virtual const LvglImage* GetEmojiImage(const char* name);
|
||||
virtual ~EmojiCollection();
|
||||
|
||||
private:
|
||||
std::map<std::string, LvglImage*> emoji_collection_;
|
||||
};
|
||||
|
||||
class Twemoji32 : public EmojiCollection {
|
||||
public:
|
||||
Twemoji32();
|
||||
};
|
||||
|
||||
class Twemoji64 : public EmojiCollection {
|
||||
public:
|
||||
Twemoji64();
|
||||
};
|
||||
|
||||
#endif
|
||||
2
main/display/lvgl_display/gif/LICENSE.txt
Normal file
2
main/display/lvgl_display/gif/LICENSE.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
All of the source code and documentation for gifdec is released into the
|
||||
public domain and provided without warranty of any kind.
|
||||
17
main/display/lvgl_display/gif/README.md
Normal file
17
main/display/lvgl_display/gif/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 说明 / Description
|
||||
|
||||
## 中文
|
||||
|
||||
本目录代码移植自 LVGL 的 GIF 程序。
|
||||
|
||||
主要修复和改进:
|
||||
- 修复了透明背景问题
|
||||
- 兼容了 87a 版本的 GIF 格式
|
||||
|
||||
## English
|
||||
|
||||
The code in this directory is ported from LVGL's GIF program.
|
||||
|
||||
Main fixes and improvements:
|
||||
- Fixed transparent background issues
|
||||
- Added compatibility for GIF 87a version format
|
||||
821
main/display/lvgl_display/gif/gifdec.c
Normal file
821
main/display/lvgl_display/gif/gifdec.c
Normal file
@@ -0,0 +1,821 @@
|
||||
#include "gifdec.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
#define TAG "GIF"
|
||||
|
||||
#define MIN(A, B) ((A) < (B) ? (A) : (B))
|
||||
#define MAX(A, B) ((A) > (B) ? (A) : (B))
|
||||
|
||||
typedef struct Entry {
|
||||
uint16_t length;
|
||||
uint16_t prefix;
|
||||
uint8_t suffix;
|
||||
} Entry;
|
||||
|
||||
typedef struct Table {
|
||||
int bulk;
|
||||
int nentries;
|
||||
Entry * entries;
|
||||
} Table;
|
||||
|
||||
#if LV_GIF_CACHE_DECODE_DATA
|
||||
#define LZW_MAXBITS 12
|
||||
#define LZW_TABLE_SIZE (1 << LZW_MAXBITS)
|
||||
#define LZW_CACHE_SIZE (LZW_TABLE_SIZE * 4)
|
||||
#endif
|
||||
|
||||
static gd_GIF * gif_open(gd_GIF * gif);
|
||||
static bool f_gif_open(gd_GIF * gif, const void * path, bool is_file);
|
||||
static inline void f_gif_read(gd_GIF * gif, void * buf, size_t len);
|
||||
static inline int f_gif_seek(gd_GIF * gif, size_t pos, int k);
|
||||
static void f_gif_close(gd_GIF * gif);
|
||||
|
||||
#if LV_USE_DRAW_SW_ASM == LV_DRAW_SW_ASM_HELIUM
|
||||
#include "gifdec_mve.h"
|
||||
#endif
|
||||
|
||||
static uint16_t
|
||||
read_num(gd_GIF * gif)
|
||||
{
|
||||
uint8_t bytes[2];
|
||||
|
||||
f_gif_read(gif, bytes, 2);
|
||||
return bytes[0] + (((uint16_t) bytes[1]) << 8);
|
||||
}
|
||||
|
||||
gd_GIF *
|
||||
gd_open_gif_file(const char * fname)
|
||||
{
|
||||
gd_GIF gif_base;
|
||||
memset(&gif_base, 0, sizeof(gif_base));
|
||||
|
||||
bool res = f_gif_open(&gif_base, fname, true);
|
||||
if(!res) return NULL;
|
||||
|
||||
return gif_open(&gif_base);
|
||||
}
|
||||
|
||||
gd_GIF *
|
||||
gd_open_gif_data(const void * data)
|
||||
{
|
||||
gd_GIF gif_base;
|
||||
memset(&gif_base, 0, sizeof(gif_base));
|
||||
|
||||
bool res = f_gif_open(&gif_base, data, false);
|
||||
if(!res) return NULL;
|
||||
|
||||
return gif_open(&gif_base);
|
||||
}
|
||||
|
||||
static gd_GIF * gif_open(gd_GIF * gif_base)
|
||||
{
|
||||
uint8_t sigver[3];
|
||||
uint16_t width, height, depth;
|
||||
uint8_t fdsz, bgidx, aspect;
|
||||
uint8_t * bgcolor;
|
||||
int gct_sz;
|
||||
gd_GIF * gif = NULL;
|
||||
|
||||
/* Header */
|
||||
f_gif_read(gif_base, sigver, 3);
|
||||
if(memcmp(sigver, "GIF", 3) != 0) {
|
||||
ESP_LOGW(TAG, "invalid signature");
|
||||
goto fail;
|
||||
}
|
||||
/* Version */
|
||||
f_gif_read(gif_base, sigver, 3);
|
||||
if(memcmp(sigver, "89a", 3) != 0 && memcmp(sigver, "87a", 3) != 0) {
|
||||
ESP_LOGW(TAG, "invalid version");
|
||||
goto fail;
|
||||
}
|
||||
/* Width x Height */
|
||||
width = read_num(gif_base);
|
||||
height = read_num(gif_base);
|
||||
/* FDSZ */
|
||||
f_gif_read(gif_base, &fdsz, 1);
|
||||
/* Presence of GCT */
|
||||
if(!(fdsz & 0x80)) {
|
||||
ESP_LOGW(TAG, "no global color table");
|
||||
goto fail;
|
||||
}
|
||||
/* Color Space's Depth */
|
||||
depth = ((fdsz >> 4) & 7) + 1;
|
||||
/* Ignore Sort Flag. */
|
||||
/* GCT Size */
|
||||
gct_sz = 1 << ((fdsz & 0x07) + 1);
|
||||
/* Background Color Index */
|
||||
f_gif_read(gif_base, &bgidx, 1);
|
||||
/* Aspect Ratio */
|
||||
f_gif_read(gif_base, &aspect, 1);
|
||||
/* Create gd_GIF Structure. */
|
||||
if(0 == width || 0 == height){
|
||||
ESP_LOGW(TAG, "Zero size image");
|
||||
goto fail;
|
||||
}
|
||||
#if LV_GIF_CACHE_DECODE_DATA
|
||||
if(0 == (INT_MAX - sizeof(gd_GIF) - LZW_CACHE_SIZE) / width / height / 5){
|
||||
ESP_LOGW(TAG, "Image dimensions are too large");
|
||||
goto fail;
|
||||
}
|
||||
gif = lv_malloc(sizeof(gd_GIF) + 5 * width * height + LZW_CACHE_SIZE);
|
||||
#else
|
||||
if(0 == (INT_MAX - sizeof(gd_GIF)) / width / height / 5){
|
||||
ESP_LOGW(TAG, "Image dimensions are too large");
|
||||
goto fail;
|
||||
}
|
||||
gif = lv_malloc(sizeof(gd_GIF) + 5 * width * height);
|
||||
#endif
|
||||
if(!gif) goto fail;
|
||||
memcpy(gif, gif_base, sizeof(gd_GIF));
|
||||
gif->width = width;
|
||||
gif->height = height;
|
||||
gif->depth = depth;
|
||||
/* Read GCT */
|
||||
gif->gct.size = gct_sz;
|
||||
f_gif_read(gif, gif->gct.colors, 3 * gif->gct.size);
|
||||
gif->palette = &gif->gct;
|
||||
gif->bgindex = bgidx;
|
||||
gif->canvas = (uint8_t *) &gif[1];
|
||||
gif->frame = &gif->canvas[4 * width * height];
|
||||
if(gif->bgindex) {
|
||||
memset(gif->frame, gif->bgindex, gif->width * gif->height);
|
||||
}
|
||||
bgcolor = &gif->palette->colors[gif->bgindex * 3];
|
||||
#if LV_GIF_CACHE_DECODE_DATA
|
||||
gif->lzw_cache = gif->frame + width * height;
|
||||
#endif
|
||||
|
||||
#ifdef GIFDEC_FILL_BG
|
||||
GIFDEC_FILL_BG(gif->canvas, gif->width * gif->height, 1, gif->width * gif->height, bgcolor, 0x00);
|
||||
#else
|
||||
for(int i = 0; i < gif->width * gif->height; i++) {
|
||||
gif->canvas[i * 4 + 0] = *(bgcolor + 2);
|
||||
gif->canvas[i * 4 + 1] = *(bgcolor + 1);
|
||||
gif->canvas[i * 4 + 2] = *(bgcolor + 0);
|
||||
gif->canvas[i * 4 + 3] = 0x00; // 初始化为透明,让第一帧根据自己的透明度设置来渲染
|
||||
}
|
||||
#endif
|
||||
gif->anim_start = f_gif_seek(gif, 0, LV_FS_SEEK_CUR);
|
||||
gif->loop_count = -1;
|
||||
goto ok;
|
||||
fail:
|
||||
f_gif_close(gif_base);
|
||||
ok:
|
||||
return gif;
|
||||
}
|
||||
|
||||
static void
|
||||
discard_sub_blocks(gd_GIF * gif)
|
||||
{
|
||||
uint8_t size;
|
||||
|
||||
do {
|
||||
f_gif_read(gif, &size, 1);
|
||||
f_gif_seek(gif, size, LV_FS_SEEK_CUR);
|
||||
} while(size);
|
||||
}
|
||||
|
||||
static void
|
||||
read_plain_text_ext(gd_GIF * gif)
|
||||
{
|
||||
if(gif->plain_text) {
|
||||
uint16_t tx, ty, tw, th;
|
||||
uint8_t cw, ch, fg, bg;
|
||||
size_t sub_block;
|
||||
f_gif_seek(gif, 1, LV_FS_SEEK_CUR); /* block size = 12 */
|
||||
tx = read_num(gif);
|
||||
ty = read_num(gif);
|
||||
tw = read_num(gif);
|
||||
th = read_num(gif);
|
||||
f_gif_read(gif, &cw, 1);
|
||||
f_gif_read(gif, &ch, 1);
|
||||
f_gif_read(gif, &fg, 1);
|
||||
f_gif_read(gif, &bg, 1);
|
||||
sub_block = f_gif_seek(gif, 0, LV_FS_SEEK_CUR);
|
||||
gif->plain_text(gif, tx, ty, tw, th, cw, ch, fg, bg);
|
||||
f_gif_seek(gif, sub_block, LV_FS_SEEK_SET);
|
||||
}
|
||||
else {
|
||||
/* Discard plain text metadata. */
|
||||
f_gif_seek(gif, 13, LV_FS_SEEK_CUR);
|
||||
}
|
||||
/* Discard plain text sub-blocks. */
|
||||
discard_sub_blocks(gif);
|
||||
}
|
||||
|
||||
static void
|
||||
read_graphic_control_ext(gd_GIF * gif)
|
||||
{
|
||||
uint8_t rdit;
|
||||
|
||||
/* Discard block size (always 0x04). */
|
||||
f_gif_seek(gif, 1, LV_FS_SEEK_CUR);
|
||||
f_gif_read(gif, &rdit, 1);
|
||||
gif->gce.disposal = (rdit >> 2) & 3;
|
||||
gif->gce.input = rdit & 2;
|
||||
gif->gce.transparency = rdit & 1;
|
||||
gif->gce.delay = read_num(gif);
|
||||
f_gif_read(gif, &gif->gce.tindex, 1);
|
||||
/* Skip block terminator. */
|
||||
f_gif_seek(gif, 1, LV_FS_SEEK_CUR);
|
||||
}
|
||||
|
||||
static void
|
||||
read_comment_ext(gd_GIF * gif)
|
||||
{
|
||||
if(gif->comment) {
|
||||
size_t sub_block = f_gif_seek(gif, 0, LV_FS_SEEK_CUR);
|
||||
gif->comment(gif);
|
||||
f_gif_seek(gif, sub_block, LV_FS_SEEK_SET);
|
||||
}
|
||||
/* Discard comment sub-blocks. */
|
||||
discard_sub_blocks(gif);
|
||||
}
|
||||
|
||||
static void
|
||||
read_application_ext(gd_GIF * gif)
|
||||
{
|
||||
char app_id[8];
|
||||
char app_auth_code[3];
|
||||
uint16_t loop_count;
|
||||
|
||||
/* Discard block size (always 0x0B). */
|
||||
f_gif_seek(gif, 1, LV_FS_SEEK_CUR);
|
||||
/* Application Identifier. */
|
||||
f_gif_read(gif, app_id, 8);
|
||||
/* Application Authentication Code. */
|
||||
f_gif_read(gif, app_auth_code, 3);
|
||||
if(!strncmp(app_id, "NETSCAPE", sizeof(app_id))) {
|
||||
/* Discard block size (0x03) and constant byte (0x01). */
|
||||
f_gif_seek(gif, 2, LV_FS_SEEK_CUR);
|
||||
loop_count = read_num(gif);
|
||||
if(gif->loop_count < 0) {
|
||||
if(loop_count == 0) {
|
||||
gif->loop_count = 0;
|
||||
}
|
||||
else {
|
||||
gif->loop_count = loop_count + 1;
|
||||
}
|
||||
}
|
||||
/* Skip block terminator. */
|
||||
f_gif_seek(gif, 1, LV_FS_SEEK_CUR);
|
||||
}
|
||||
else if(gif->application) {
|
||||
size_t sub_block = f_gif_seek(gif, 0, LV_FS_SEEK_CUR);
|
||||
gif->application(gif, app_id, app_auth_code);
|
||||
f_gif_seek(gif, sub_block, LV_FS_SEEK_SET);
|
||||
discard_sub_blocks(gif);
|
||||
}
|
||||
else {
|
||||
discard_sub_blocks(gif);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
read_ext(gd_GIF * gif)
|
||||
{
|
||||
uint8_t label;
|
||||
|
||||
f_gif_read(gif, &label, 1);
|
||||
switch(label) {
|
||||
case 0x01:
|
||||
read_plain_text_ext(gif);
|
||||
break;
|
||||
case 0xF9:
|
||||
read_graphic_control_ext(gif);
|
||||
break;
|
||||
case 0xFE:
|
||||
read_comment_ext(gif);
|
||||
break;
|
||||
case 0xFF:
|
||||
read_application_ext(gif);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "unknown extension: %02X\n", label);
|
||||
}
|
||||
}
|
||||
|
||||
static uint16_t
|
||||
get_key(gd_GIF *gif, int key_size, uint8_t *sub_len, uint8_t *shift, uint8_t *byte)
|
||||
{
|
||||
int bits_read;
|
||||
int rpad;
|
||||
int frag_size;
|
||||
uint16_t key;
|
||||
|
||||
key = 0;
|
||||
for (bits_read = 0; bits_read < key_size; bits_read += frag_size) {
|
||||
rpad = (*shift + bits_read) % 8;
|
||||
if (rpad == 0) {
|
||||
/* Update byte. */
|
||||
if (*sub_len == 0) {
|
||||
f_gif_read(gif, sub_len, 1); /* Must be nonzero! */
|
||||
if (*sub_len == 0) return 0x1000;
|
||||
}
|
||||
f_gif_read(gif, byte, 1);
|
||||
(*sub_len)--;
|
||||
}
|
||||
frag_size = MIN(key_size - bits_read, 8 - rpad);
|
||||
key |= ((uint16_t) ((*byte) >> rpad)) << bits_read;
|
||||
}
|
||||
/* Clear extra bits to the left. */
|
||||
key &= (1 << key_size) - 1;
|
||||
*shift = (*shift + key_size) % 8;
|
||||
return key;
|
||||
}
|
||||
|
||||
#if LV_GIF_CACHE_DECODE_DATA
|
||||
/* Decompress image pixels.
|
||||
* Return 0 on success or -1 on out-of-memory (w.r.t. LZW code table) or parse error. */
|
||||
static int
|
||||
read_image_data(gd_GIF *gif, int interlace)
|
||||
{
|
||||
uint8_t sub_len, shift, byte;
|
||||
int ret = 0;
|
||||
int key_size;
|
||||
int y, pass, linesize;
|
||||
uint8_t *ptr = NULL;
|
||||
uint8_t *ptr_row_start = NULL;
|
||||
uint8_t *ptr_base = NULL;
|
||||
size_t start, end;
|
||||
uint16_t key, clear_code, stop_code, curr_code;
|
||||
int frm_off, frm_size,curr_size,top_slot,new_codes,slot;
|
||||
/* The first value of the value sequence corresponding to key */
|
||||
int first_value;
|
||||
int last_key;
|
||||
uint8_t *sp = NULL;
|
||||
uint8_t *p_stack = NULL;
|
||||
uint8_t *p_suffix = NULL;
|
||||
uint16_t *p_prefix = NULL;
|
||||
|
||||
/* get initial key size and clear code, stop code */
|
||||
f_gif_read(gif, &byte, 1);
|
||||
key_size = (int) byte;
|
||||
clear_code = 1 << key_size;
|
||||
stop_code = clear_code + 1;
|
||||
key = 0;
|
||||
|
||||
start = f_gif_seek(gif, 0, LV_FS_SEEK_CUR);
|
||||
discard_sub_blocks(gif);
|
||||
end = f_gif_seek(gif, 0, LV_FS_SEEK_CUR);
|
||||
f_gif_seek(gif, start, LV_FS_SEEK_SET);
|
||||
|
||||
linesize = gif->width;
|
||||
ptr_base = &gif->frame[gif->fy * linesize + gif->fx];
|
||||
ptr_row_start = ptr_base;
|
||||
ptr = ptr_row_start;
|
||||
sub_len = shift = 0;
|
||||
/* decoder */
|
||||
pass = 0;
|
||||
y = 0;
|
||||
p_stack = gif->lzw_cache;
|
||||
p_suffix = gif->lzw_cache + LZW_TABLE_SIZE;
|
||||
p_prefix = (uint16_t*)(gif->lzw_cache + LZW_TABLE_SIZE * 2);
|
||||
frm_off = 0;
|
||||
frm_size = gif->fw * gif->fh;
|
||||
curr_size = key_size + 1;
|
||||
top_slot = 1 << curr_size;
|
||||
new_codes = clear_code + 2;
|
||||
slot = new_codes;
|
||||
first_value = -1;
|
||||
last_key = -1;
|
||||
sp = p_stack;
|
||||
|
||||
while (frm_off < frm_size) {
|
||||
/* copy data to frame buffer */
|
||||
while (sp > p_stack) {
|
||||
if(frm_off >= frm_size){
|
||||
ESP_LOGW(TAG, "LZW table token overflows the frame buffer");
|
||||
return -1;
|
||||
}
|
||||
*ptr++ = *(--sp);
|
||||
frm_off += 1;
|
||||
/* read one line */
|
||||
if ((ptr - ptr_row_start) == gif->fw) {
|
||||
if (interlace) {
|
||||
switch(pass) {
|
||||
case 0:
|
||||
case 1:
|
||||
y += 8;
|
||||
ptr_row_start += linesize * 8;
|
||||
break;
|
||||
case 2:
|
||||
y += 4;
|
||||
ptr_row_start += linesize * 4;
|
||||
break;
|
||||
case 3:
|
||||
y += 2;
|
||||
ptr_row_start += linesize * 2;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
while (y >= gif->fh) {
|
||||
y = 4 >> pass;
|
||||
ptr_row_start = ptr_base + linesize * y;
|
||||
pass++;
|
||||
}
|
||||
} else {
|
||||
ptr_row_start += linesize;
|
||||
}
|
||||
ptr = ptr_row_start;
|
||||
}
|
||||
}
|
||||
|
||||
key = get_key(gif, curr_size, &sub_len, &shift, &byte);
|
||||
|
||||
if (key == stop_code || key >= LZW_TABLE_SIZE)
|
||||
break;
|
||||
|
||||
if (key == clear_code) {
|
||||
curr_size = key_size + 1;
|
||||
slot = new_codes;
|
||||
top_slot = 1 << curr_size;
|
||||
first_value = last_key = -1;
|
||||
sp = p_stack;
|
||||
continue;
|
||||
}
|
||||
|
||||
curr_code = key;
|
||||
/*
|
||||
* If the current code is a code that will be added to the decoding
|
||||
* dictionary, it is composed of the data list corresponding to the
|
||||
* previous key and its first data.
|
||||
* */
|
||||
if (curr_code == slot && first_value >= 0) {
|
||||
*sp++ = first_value;
|
||||
curr_code = last_key;
|
||||
}else if(curr_code >= slot)
|
||||
break;
|
||||
|
||||
while (curr_code >= new_codes) {
|
||||
*sp++ = p_suffix[curr_code];
|
||||
curr_code = p_prefix[curr_code];
|
||||
}
|
||||
*sp++ = curr_code;
|
||||
|
||||
/* Add code to decoding dictionary */
|
||||
if (slot < top_slot && last_key >= 0) {
|
||||
p_suffix[slot] = curr_code;
|
||||
p_prefix[slot++] = last_key;
|
||||
}
|
||||
first_value = curr_code;
|
||||
last_key = key;
|
||||
if (slot >= top_slot) {
|
||||
if (curr_size < LZW_MAXBITS) {
|
||||
top_slot <<= 1;
|
||||
curr_size += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key == stop_code) f_gif_read(gif, &sub_len, 1); /* Must be zero! */
|
||||
f_gif_seek(gif, end, LV_FS_SEEK_SET);
|
||||
return ret;
|
||||
}
|
||||
#else
|
||||
static Table *
|
||||
new_table(int key_size)
|
||||
{
|
||||
int key;
|
||||
int init_bulk = MAX(1 << (key_size + 1), 0x100);
|
||||
Table * table = lv_malloc(sizeof(*table) + sizeof(Entry) * init_bulk);
|
||||
if(table) {
|
||||
table->bulk = init_bulk;
|
||||
table->nentries = (1 << key_size) + 2;
|
||||
table->entries = (Entry *) &table[1];
|
||||
for(key = 0; key < (1 << key_size); key++)
|
||||
table->entries[key] = (Entry) {
|
||||
1, 0xFFF, key
|
||||
};
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
/* Add table entry. Return value:
|
||||
* 0 on success
|
||||
* +1 if key size must be incremented after this addition
|
||||
* -1 if could not realloc table */
|
||||
static int
|
||||
add_entry(Table ** tablep, uint16_t length, uint16_t prefix, uint8_t suffix)
|
||||
{
|
||||
Table * table = *tablep;
|
||||
if(table->nentries == table->bulk) {
|
||||
table->bulk *= 2;
|
||||
table = lv_realloc(table, sizeof(*table) + sizeof(Entry) * table->bulk);
|
||||
if(!table) return -1;
|
||||
table->entries = (Entry *) &table[1];
|
||||
*tablep = table;
|
||||
}
|
||||
table->entries[table->nentries] = (Entry) {
|
||||
length, prefix, suffix
|
||||
};
|
||||
table->nentries++;
|
||||
if((table->nentries & (table->nentries - 1)) == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Compute output index of y-th input line, in frame of height h. */
|
||||
static int
|
||||
interlaced_line_index(int h, int y)
|
||||
{
|
||||
int p; /* number of lines in current pass */
|
||||
|
||||
p = (h - 1) / 8 + 1;
|
||||
if(y < p) /* pass 1 */
|
||||
return y * 8;
|
||||
y -= p;
|
||||
p = (h - 5) / 8 + 1;
|
||||
if(y < p) /* pass 2 */
|
||||
return y * 8 + 4;
|
||||
y -= p;
|
||||
p = (h - 3) / 4 + 1;
|
||||
if(y < p) /* pass 3 */
|
||||
return y * 4 + 2;
|
||||
y -= p;
|
||||
/* pass 4 */
|
||||
return y * 2 + 1;
|
||||
}
|
||||
|
||||
/* Decompress image pixels.
|
||||
* Return 0 on success or -1 on out-of-memory (w.r.t. LZW code table) or parse error. */
|
||||
static int
|
||||
read_image_data(gd_GIF * gif, int interlace)
|
||||
{
|
||||
uint8_t sub_len, shift, byte;
|
||||
int init_key_size, key_size, table_is_full = 0;
|
||||
int frm_off, frm_size, str_len = 0, i, p, x, y;
|
||||
uint16_t key, clear, stop;
|
||||
int ret;
|
||||
Table * table;
|
||||
Entry entry = {0};
|
||||
size_t start, end;
|
||||
|
||||
f_gif_read(gif, &byte, 1);
|
||||
key_size = (int) byte;
|
||||
start = f_gif_seek(gif, 0, LV_FS_SEEK_CUR);
|
||||
discard_sub_blocks(gif);
|
||||
end = f_gif_seek(gif, 0, LV_FS_SEEK_CUR);
|
||||
f_gif_seek(gif, start, LV_FS_SEEK_SET);
|
||||
clear = 1 << key_size;
|
||||
stop = clear + 1;
|
||||
table = new_table(key_size);
|
||||
key_size++;
|
||||
init_key_size = key_size;
|
||||
sub_len = shift = 0;
|
||||
key = get_key(gif, key_size, &sub_len, &shift, &byte); /* clear code */
|
||||
frm_off = 0;
|
||||
ret = 0;
|
||||
frm_size = gif->fw * gif->fh;
|
||||
while(frm_off < frm_size) {
|
||||
if(key == clear) {
|
||||
key_size = init_key_size;
|
||||
table->nentries = (1 << (key_size - 1)) + 2;
|
||||
table_is_full = 0;
|
||||
}
|
||||
else if(!table_is_full) {
|
||||
ret = add_entry(&table, str_len + 1, key, entry.suffix);
|
||||
if(ret == -1) {
|
||||
lv_free(table);
|
||||
return -1;
|
||||
}
|
||||
if(table->nentries == 0x1000) {
|
||||
ret = 0;
|
||||
table_is_full = 1;
|
||||
}
|
||||
}
|
||||
key = get_key(gif, key_size, &sub_len, &shift, &byte);
|
||||
if(key == clear) continue;
|
||||
if(key == stop || key == 0x1000) break;
|
||||
if(ret == 1) key_size++;
|
||||
entry = table->entries[key];
|
||||
str_len = entry.length;
|
||||
if(frm_off + str_len > frm_size){
|
||||
ESP_LOGW(TAG, "LZW table token overflows the frame buffer");
|
||||
lv_free(table);
|
||||
return -1;
|
||||
}
|
||||
for(i = 0; i < str_len; i++) {
|
||||
p = frm_off + entry.length - 1;
|
||||
x = p % gif->fw;
|
||||
y = p / gif->fw;
|
||||
if(interlace)
|
||||
y = interlaced_line_index((int) gif->fh, y);
|
||||
gif->frame[(gif->fy + y) * gif->width + gif->fx + x] = entry.suffix;
|
||||
if(entry.prefix == 0xFFF)
|
||||
break;
|
||||
else
|
||||
entry = table->entries[entry.prefix];
|
||||
}
|
||||
frm_off += str_len;
|
||||
if(key < table->nentries - 1 && !table_is_full)
|
||||
table->entries[table->nentries - 1].suffix = entry.suffix;
|
||||
}
|
||||
lv_free(table);
|
||||
if(key == stop) f_gif_read(gif, &sub_len, 1); /* Must be zero! */
|
||||
f_gif_seek(gif, end, LV_FS_SEEK_SET);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/* Read image.
|
||||
* Return 0 on success or -1 on out-of-memory (w.r.t. LZW code table) or parse error. */
|
||||
static int
|
||||
read_image(gd_GIF * gif)
|
||||
{
|
||||
uint8_t fisrz;
|
||||
int interlace;
|
||||
|
||||
/* Image Descriptor. */
|
||||
gif->fx = read_num(gif);
|
||||
gif->fy = read_num(gif);
|
||||
gif->fw = read_num(gif);
|
||||
gif->fh = read_num(gif);
|
||||
if(gif->fx + (uint32_t)gif->fw > gif->width || gif->fy + (uint32_t)gif->fh > gif->height){
|
||||
ESP_LOGW(TAG, "Frame coordinates out of image bounds");
|
||||
return -1;
|
||||
}
|
||||
f_gif_read(gif, &fisrz, 1);
|
||||
interlace = fisrz & 0x40;
|
||||
/* Ignore Sort Flag. */
|
||||
/* Local Color Table? */
|
||||
if(fisrz & 0x80) {
|
||||
/* Read LCT */
|
||||
gif->lct.size = 1 << ((fisrz & 0x07) + 1);
|
||||
f_gif_read(gif, gif->lct.colors, 3 * gif->lct.size);
|
||||
gif->palette = &gif->lct;
|
||||
}
|
||||
else
|
||||
gif->palette = &gif->gct;
|
||||
/* Image Data. */
|
||||
return read_image_data(gif, interlace);
|
||||
}
|
||||
|
||||
static void
|
||||
render_frame_rect(gd_GIF * gif, uint8_t * buffer)
|
||||
{
|
||||
int i = gif->fy * gif->width + gif->fx;
|
||||
#ifdef GIFDEC_RENDER_FRAME
|
||||
GIFDEC_RENDER_FRAME(&buffer[i * 4], gif->fw, gif->fh, gif->width,
|
||||
&gif->frame[i], gif->palette->colors,
|
||||
gif->gce.transparency ? gif->gce.tindex : 0x100);
|
||||
#else
|
||||
int j, k;
|
||||
uint8_t index, * color;
|
||||
|
||||
for(j = 0; j < gif->fh; j++) {
|
||||
for(k = 0; k < gif->fw; k++) {
|
||||
index = gif->frame[(gif->fy + j) * gif->width + gif->fx + k];
|
||||
color = &gif->palette->colors[index * 3];
|
||||
if(!gif->gce.transparency || index != gif->gce.tindex) {
|
||||
buffer[(i + k) * 4 + 0] = *(color + 2);
|
||||
buffer[(i + k) * 4 + 1] = *(color + 1);
|
||||
buffer[(i + k) * 4 + 2] = *(color + 0);
|
||||
buffer[(i + k) * 4 + 3] = 0xFF;
|
||||
}
|
||||
}
|
||||
i += gif->width;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void
|
||||
dispose(gd_GIF * gif)
|
||||
{
|
||||
int i;
|
||||
uint8_t * bgcolor;
|
||||
switch(gif->gce.disposal) {
|
||||
case 2: /* Restore to background color. */
|
||||
bgcolor = &gif->palette->colors[gif->bgindex * 3];
|
||||
|
||||
uint8_t opa = 0xff;
|
||||
if(gif->gce.transparency) opa = 0x00;
|
||||
|
||||
i = gif->fy * gif->width + gif->fx;
|
||||
#ifdef GIFDEC_FILL_BG
|
||||
GIFDEC_FILL_BG(&(gif->canvas[i * 4]), gif->fw, gif->fh, gif->width, bgcolor, opa);
|
||||
#else
|
||||
int j, k;
|
||||
for(j = 0; j < gif->fh; j++) {
|
||||
for(k = 0; k < gif->fw; k++) {
|
||||
gif->canvas[(i + k) * 4 + 0] = *(bgcolor + 2);
|
||||
gif->canvas[(i + k) * 4 + 1] = *(bgcolor + 1);
|
||||
gif->canvas[(i + k) * 4 + 2] = *(bgcolor + 0);
|
||||
gif->canvas[(i + k) * 4 + 3] = opa;
|
||||
}
|
||||
i += gif->width;
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case 3: /* Restore to previous, i.e., don't update canvas.*/
|
||||
break;
|
||||
default:
|
||||
/* Add frame non-transparent pixels to canvas. */
|
||||
render_frame_rect(gif, gif->canvas);
|
||||
}
|
||||
}
|
||||
|
||||
/* Return 1 if got a frame; 0 if got GIF trailer; -1 if error. */
|
||||
int
|
||||
gd_get_frame(gd_GIF * gif)
|
||||
{
|
||||
char sep;
|
||||
|
||||
dispose(gif);
|
||||
f_gif_read(gif, &sep, 1);
|
||||
while(sep != ',') {
|
||||
if(sep == ';') {
|
||||
f_gif_seek(gif, gif->anim_start, LV_FS_SEEK_SET);
|
||||
if(gif->loop_count == 1 || gif->loop_count < 0) {
|
||||
return 0;
|
||||
}
|
||||
else if(gif->loop_count > 1) {
|
||||
gif->loop_count--;
|
||||
}
|
||||
}
|
||||
else if(sep == '!')
|
||||
read_ext(gif);
|
||||
else return -1;
|
||||
f_gif_read(gif, &sep, 1);
|
||||
}
|
||||
if(read_image(gif) == -1)
|
||||
return -1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void
|
||||
gd_render_frame(gd_GIF * gif, uint8_t * buffer)
|
||||
{
|
||||
render_frame_rect(gif, buffer);
|
||||
}
|
||||
|
||||
void
|
||||
gd_rewind(gd_GIF * gif)
|
||||
{
|
||||
gif->loop_count = -1;
|
||||
f_gif_seek(gif, gif->anim_start, LV_FS_SEEK_SET);
|
||||
}
|
||||
|
||||
void
|
||||
gd_close_gif(gd_GIF * gif)
|
||||
{
|
||||
f_gif_close(gif);
|
||||
lv_free(gif);
|
||||
}
|
||||
|
||||
static bool f_gif_open(gd_GIF * gif, const void * path, bool is_file)
|
||||
{
|
||||
gif->f_rw_p = 0;
|
||||
gif->data = NULL;
|
||||
gif->is_file = is_file;
|
||||
|
||||
if(is_file) {
|
||||
lv_fs_res_t res = lv_fs_open(&gif->fd, path, LV_FS_MODE_RD);
|
||||
if(res != LV_FS_RES_OK) return false;
|
||||
else return true;
|
||||
}
|
||||
else {
|
||||
gif->data = path;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static void f_gif_read(gd_GIF * gif, void * buf, size_t len)
|
||||
{
|
||||
if(gif->is_file) {
|
||||
lv_fs_read(&gif->fd, buf, len, NULL);
|
||||
}
|
||||
else {
|
||||
memcpy(buf, &gif->data[gif->f_rw_p], len);
|
||||
gif->f_rw_p += len;
|
||||
}
|
||||
}
|
||||
|
||||
static int f_gif_seek(gd_GIF * gif, size_t pos, int k)
|
||||
{
|
||||
if(gif->is_file) {
|
||||
lv_fs_seek(&gif->fd, pos, k);
|
||||
uint32_t x;
|
||||
lv_fs_tell(&gif->fd, &x);
|
||||
return x;
|
||||
}
|
||||
else {
|
||||
if(k == LV_FS_SEEK_CUR) gif->f_rw_p += pos;
|
||||
else if(k == LV_FS_SEEK_SET) gif->f_rw_p = pos;
|
||||
return gif->f_rw_p;
|
||||
}
|
||||
}
|
||||
|
||||
static void f_gif_close(gd_GIF * gif)
|
||||
{
|
||||
if(gif->is_file) {
|
||||
lv_fs_close(&gif->fd);
|
||||
}
|
||||
}
|
||||
|
||||
68
main/display/lvgl_display/gif/gifdec.h
Normal file
68
main/display/lvgl_display/gif/gifdec.h
Normal file
@@ -0,0 +1,68 @@
|
||||
#ifndef GIFDEC_H
|
||||
#define GIFDEC_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct _gd_Palette {
|
||||
int size;
|
||||
uint8_t colors[0x100 * 3];
|
||||
} gd_Palette;
|
||||
|
||||
typedef struct _gd_GCE {
|
||||
uint16_t delay;
|
||||
uint8_t tindex;
|
||||
uint8_t disposal;
|
||||
int input;
|
||||
int transparency;
|
||||
} gd_GCE;
|
||||
|
||||
|
||||
|
||||
typedef struct _gd_GIF {
|
||||
lv_fs_file_t fd;
|
||||
const char * data;
|
||||
uint8_t is_file;
|
||||
uint32_t f_rw_p;
|
||||
int32_t anim_start;
|
||||
uint16_t width, height;
|
||||
uint16_t depth;
|
||||
int32_t loop_count;
|
||||
gd_GCE gce;
|
||||
gd_Palette * palette;
|
||||
gd_Palette lct, gct;
|
||||
void (*plain_text)(
|
||||
struct _gd_GIF * gif, uint16_t tx, uint16_t ty,
|
||||
uint16_t tw, uint16_t th, uint8_t cw, uint8_t ch,
|
||||
uint8_t fg, uint8_t bg
|
||||
);
|
||||
void (*comment)(struct _gd_GIF * gif);
|
||||
void (*application)(struct _gd_GIF * gif, char id[8], char auth[3]);
|
||||
uint16_t fx, fy, fw, fh;
|
||||
uint8_t bgindex;
|
||||
uint8_t * canvas, * frame;
|
||||
#if LV_GIF_CACHE_DECODE_DATA
|
||||
uint8_t *lzw_cache;
|
||||
#endif
|
||||
} gd_GIF;
|
||||
|
||||
gd_GIF * gd_open_gif_file(const char * fname);
|
||||
|
||||
gd_GIF * gd_open_gif_data(const void * data);
|
||||
|
||||
void gd_render_frame(gd_GIF * gif, uint8_t * buffer);
|
||||
|
||||
int gd_get_frame(gd_GIF * gif);
|
||||
void gd_rewind(gd_GIF * gif);
|
||||
void gd_close_gif(gd_GIF * gif);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif /* GIFDEC_H */
|
||||
140
main/display/lvgl_display/gif/gifdec_mve.h
Normal file
140
main/display/lvgl_display/gif/gifdec_mve.h
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @file gifdec_mve.h
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GIFDEC_MVE_H
|
||||
#define GIFDEC_MVE_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* INCLUDES
|
||||
*********************/
|
||||
#include <stdint.h>
|
||||
#include "../../misc/lv_color.h"
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
#define GIFDEC_FILL_BG(dst, w, h, stride, color, opa) \
|
||||
_gifdec_fill_bg_mve(dst, w, h, stride, color, opa)
|
||||
|
||||
#define GIFDEC_RENDER_FRAME(dst, w, h, stride, frame, pattern, tindex) \
|
||||
_gifdec_render_frame_mve(dst, w, h, stride, frame, pattern, tindex)
|
||||
|
||||
/**********************
|
||||
* MACROS
|
||||
**********************/
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/**********************
|
||||
* GLOBAL PROTOTYPES
|
||||
**********************/
|
||||
|
||||
static inline void _gifdec_fill_bg_mve(uint8_t * dst, uint16_t w, uint16_t h, uint16_t stride, uint8_t * color,
|
||||
uint8_t opa)
|
||||
{
|
||||
lv_color32_t c = lv_color32_make(*(color + 0), *(color + 1), *(color + 2), opa);
|
||||
uint32_t color_32 = *(uint32_t *)&c;
|
||||
|
||||
__asm volatile(
|
||||
".p2align 2 \n"
|
||||
"vdup.32 q0, %[src] \n"
|
||||
"3: \n"
|
||||
"mov r0, %[dst] \n"
|
||||
|
||||
"wlstp.32 lr, %[w], 1f \n"
|
||||
"2: \n"
|
||||
|
||||
"vstrw.32 q0, [r0], #16 \n"
|
||||
"letp lr, 2b \n"
|
||||
"1: \n"
|
||||
"add %[dst], %[iTargetStride] \n"
|
||||
"subs %[h], #1 \n"
|
||||
"bne 3b \n"
|
||||
: [dst] "+r"(dst),
|
||||
[h] "+r"(h)
|
||||
: [src] "r"(color_32),
|
||||
[w] "r"(w),
|
||||
[iTargetStride] "r"(stride * sizeof(uint32_t))
|
||||
: "r0", "q0", "memory", "r14", "cc");
|
||||
}
|
||||
|
||||
static inline void _gifdec_render_frame_mve(uint8_t * dst, uint16_t w, uint16_t h, uint16_t stride, uint8_t * frame,
|
||||
uint8_t * pattern, uint16_t tindex)
|
||||
{
|
||||
if(w == 0 || h == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
__asm volatile(
|
||||
"vmov.u16 q3, #255 \n"
|
||||
"vshl.u16 q3, q3, #8 \n" /* left shift 8 for a*/
|
||||
|
||||
"mov r0, #2 \n"
|
||||
"vidup.u16 q6, r0, #4 \n" /* [2, 6, 10, 14, 18, 22, 26, 30] */
|
||||
"mov r0, #0 \n"
|
||||
"vidup.u16 q7, r0, #4 \n" /* [0, 4, 8, 12, 16, 20, 24, 28] */
|
||||
|
||||
"3: \n"
|
||||
"mov r1, %[dst] \n"
|
||||
"mov r2, %[frame] \n"
|
||||
|
||||
"wlstp.16 lr, %[w], 1f \n"
|
||||
"2: \n"
|
||||
|
||||
"mov r0, #3 \n"
|
||||
"vldrb.u16 q4, [r2], #8 \n"
|
||||
"vmul.u16 q5, q4, r0 \n"
|
||||
|
||||
"mov r0, #1 \n"
|
||||
"vldrb.u16 q2, [%[pattern], q5] \n" /* load 8 pixel r*/
|
||||
|
||||
"vadd.u16 q5, q5, r0 \n"
|
||||
"vldrb.u16 q1, [%[pattern], q5] \n" /* load 8 pixel g*/
|
||||
|
||||
"vadd.u16 q5, q5, r0 \n"
|
||||
"vldrb.u16 q0, [%[pattern], q5] \n" /* load 8 pixel b*/
|
||||
|
||||
"vshl.u16 q1, q1, #8 \n" /* left shift 8 for g*/
|
||||
|
||||
"vorr.u16 q0, q0, q1 \n" /* make 8 pixel gb*/
|
||||
"vorr.u16 q1, q2, q3 \n" /* make 8 pixel ar*/
|
||||
|
||||
"vcmp.i16 ne, q4, %[tindex] \n"
|
||||
"vpstt \n"
|
||||
"vstrht.16 q0, [r1, q7] \n"
|
||||
"vstrht.16 q1, [r1, q6] \n"
|
||||
"add r1, r1, #32 \n"
|
||||
|
||||
"letp lr, 2b \n"
|
||||
|
||||
"1: \n"
|
||||
"mov r0, %[stride], LSL #2 \n"
|
||||
"add %[dst], r0 \n"
|
||||
"add %[frame], %[stride] \n"
|
||||
"subs %[h], #1 \n"
|
||||
"bne 3b \n"
|
||||
|
||||
: [dst] "+r"(dst),
|
||||
[frame] "+r"(frame),
|
||||
[h] "+r"(h)
|
||||
: [pattern] "r"(pattern),
|
||||
[w] "r"(w),
|
||||
[stride] "r"(stride),
|
||||
[tindex] "r"(tindex)
|
||||
: "r0", "r1", "r2", "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "memory", "r14", "cc");
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /*extern "C"*/
|
||||
#endif
|
||||
|
||||
#endif /*GIFDEC_MVE_H*/
|
||||
252
main/display/lvgl_display/gif/lvgl_gif.cc
Normal file
252
main/display/lvgl_display/gif/lvgl_gif.cc
Normal file
@@ -0,0 +1,252 @@
|
||||
#include "lvgl_gif.h"
|
||||
#include <esp_log.h>
|
||||
#include <cstring>
|
||||
|
||||
#define TAG "LvglGif"
|
||||
|
||||
LvglGif::LvglGif(const lv_img_dsc_t* img_dsc)
|
||||
: gif_(nullptr), timer_(nullptr), last_call_(0), playing_(false), loaded_(false),
|
||||
loop_delay_ms_(0), loop_waiting_(false), loop_wait_start_(0) {
|
||||
if (!img_dsc || !img_dsc->data) {
|
||||
ESP_LOGE(TAG, "Invalid image descriptor");
|
||||
return;
|
||||
}
|
||||
|
||||
gif_ = gd_open_gif_data(img_dsc->data);
|
||||
if (!gif_) {
|
||||
ESP_LOGE(TAG, "Failed to open GIF from image descriptor");
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup LVGL image descriptor
|
||||
memset(&img_dsc_, 0, sizeof(img_dsc_));
|
||||
img_dsc_.header.magic = LV_IMAGE_HEADER_MAGIC;
|
||||
img_dsc_.header.flags = LV_IMAGE_FLAGS_MODIFIABLE;
|
||||
img_dsc_.header.cf = LV_COLOR_FORMAT_ARGB8888;
|
||||
img_dsc_.header.w = gif_->width;
|
||||
img_dsc_.header.h = gif_->height;
|
||||
img_dsc_.header.stride = gif_->width * 4;
|
||||
img_dsc_.data = gif_->canvas;
|
||||
img_dsc_.data_size = gif_->width * gif_->height * 4;
|
||||
|
||||
// Render first frame
|
||||
if (gif_->canvas) {
|
||||
gd_render_frame(gif_, gif_->canvas);
|
||||
}
|
||||
|
||||
loaded_ = true;
|
||||
ESP_LOGD(TAG, "GIF loaded from image descriptor: %dx%d", gif_->width, gif_->height);
|
||||
}
|
||||
|
||||
// Destructor
|
||||
LvglGif::~LvglGif() {
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
// LvglImage interface implementation
|
||||
const lv_img_dsc_t* LvglGif::image_dsc() const {
|
||||
if (!loaded_) {
|
||||
return nullptr;
|
||||
}
|
||||
return &img_dsc_;
|
||||
}
|
||||
|
||||
// Animation control methods
|
||||
void LvglGif::Start() {
|
||||
if (!loaded_ || !gif_) {
|
||||
ESP_LOGW(TAG, "GIF not loaded, cannot start");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!timer_) {
|
||||
timer_ = lv_timer_create([](lv_timer_t* timer) {
|
||||
LvglGif* gif_obj = static_cast<LvglGif*>(lv_timer_get_user_data(timer));
|
||||
gif_obj->NextFrame();
|
||||
}, 10, this);
|
||||
}
|
||||
|
||||
if (timer_) {
|
||||
playing_ = true;
|
||||
loop_waiting_ = false; // Reset loop waiting state
|
||||
last_call_ = lv_tick_get();
|
||||
lv_timer_resume(timer_);
|
||||
lv_timer_reset(timer_);
|
||||
|
||||
// Render first frame
|
||||
NextFrame();
|
||||
|
||||
ESP_LOGD(TAG, "GIF animation started");
|
||||
}
|
||||
}
|
||||
|
||||
void LvglGif::Pause() {
|
||||
if (timer_) {
|
||||
playing_ = false;
|
||||
lv_timer_pause(timer_);
|
||||
ESP_LOGD(TAG, "GIF animation paused");
|
||||
}
|
||||
}
|
||||
|
||||
void LvglGif::Resume() {
|
||||
if (!loaded_ || !gif_) {
|
||||
ESP_LOGW(TAG, "GIF not loaded, cannot resume");
|
||||
return;
|
||||
}
|
||||
|
||||
if (timer_) {
|
||||
playing_ = true;
|
||||
lv_timer_resume(timer_);
|
||||
ESP_LOGD(TAG, "GIF animation resumed");
|
||||
}
|
||||
}
|
||||
|
||||
void LvglGif::Stop() {
|
||||
if (timer_) {
|
||||
playing_ = false;
|
||||
lv_timer_pause(timer_);
|
||||
}
|
||||
|
||||
// Reset loop waiting state
|
||||
loop_waiting_ = false;
|
||||
|
||||
if (gif_) {
|
||||
gd_rewind(gif_);
|
||||
// Render first frame without advancing
|
||||
if (gif_->canvas) {
|
||||
gd_render_frame(gif_, gif_->canvas);
|
||||
}
|
||||
ESP_LOGD(TAG, "GIF animation stopped and rewound");
|
||||
}
|
||||
}
|
||||
|
||||
bool LvglGif::IsPlaying() const {
|
||||
return playing_;
|
||||
}
|
||||
|
||||
bool LvglGif::IsLoaded() const {
|
||||
return loaded_;
|
||||
}
|
||||
|
||||
int32_t LvglGif::GetLoopCount() const {
|
||||
if (!loaded_ || !gif_) {
|
||||
return -1;
|
||||
}
|
||||
return gif_->loop_count;
|
||||
}
|
||||
|
||||
void LvglGif::SetLoopCount(int32_t count) {
|
||||
if (!loaded_ || !gif_) {
|
||||
ESP_LOGW(TAG, "GIF not loaded, cannot set loop count");
|
||||
return;
|
||||
}
|
||||
gif_->loop_count = count;
|
||||
}
|
||||
|
||||
uint32_t LvglGif::GetLoopDelay() const {
|
||||
return loop_delay_ms_;
|
||||
}
|
||||
|
||||
void LvglGif::SetLoopDelay(uint32_t delay_ms) {
|
||||
loop_delay_ms_ = delay_ms;
|
||||
ESP_LOGD(TAG, "Loop delay set to %lu ms", delay_ms);
|
||||
}
|
||||
|
||||
uint16_t LvglGif::width() const {
|
||||
if (!loaded_ || !gif_) {
|
||||
return 0;
|
||||
}
|
||||
return gif_->width;
|
||||
}
|
||||
|
||||
uint16_t LvglGif::height() const {
|
||||
if (!loaded_ || !gif_) {
|
||||
return 0;
|
||||
}
|
||||
return gif_->height;
|
||||
}
|
||||
|
||||
void LvglGif::SetFrameCallback(std::function<void()> callback) {
|
||||
frame_callback_ = callback;
|
||||
}
|
||||
|
||||
void LvglGif::NextFrame() {
|
||||
if (!loaded_ || !gif_ || !playing_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're in loop wait state (only for infinite loop GIFs with delay)
|
||||
if (loop_waiting_) {
|
||||
uint32_t wait_elapsed = lv_tick_elaps(loop_wait_start_);
|
||||
if (wait_elapsed < loop_delay_ms_) {
|
||||
// Still waiting for loop delay
|
||||
return;
|
||||
}
|
||||
// Loop delay completed, continue playing
|
||||
loop_waiting_ = false;
|
||||
ESP_LOGD(TAG, "Loop delay completed, continuing GIF");
|
||||
}
|
||||
|
||||
// Check if enough time has passed for the next frame
|
||||
uint32_t elapsed = lv_tick_elaps(last_call_);
|
||||
if (elapsed < gif_->gce.delay * 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
last_call_ = lv_tick_get();
|
||||
|
||||
// Save file position before getting next frame to detect loop
|
||||
uint32_t pos_before = gif_->f_rw_p;
|
||||
|
||||
// Get next frame
|
||||
int has_next = gd_get_frame(gif_);
|
||||
if (has_next == 0) {
|
||||
// Animation truly finished (non-infinite loop)
|
||||
playing_ = false;
|
||||
if (timer_) {
|
||||
lv_timer_pause(timer_);
|
||||
}
|
||||
ESP_LOGD(TAG, "GIF animation completed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect loop by checking if file position jumped back (rewound to start)
|
||||
// This works for looping GIFs regardless of when loop_count is set
|
||||
if (loop_delay_ms_ > 0 && gif_->f_rw_p < pos_before) {
|
||||
// File position decreased, meaning GIF looped back to beginning
|
||||
// Start waiting before rendering this frame
|
||||
loop_waiting_ = true;
|
||||
loop_wait_start_ = lv_tick_get();
|
||||
ESP_LOGD(TAG, "GIF completed one cycle, waiting %lu ms before next loop", loop_delay_ms_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render current frame
|
||||
if (gif_->canvas) {
|
||||
gd_render_frame(gif_, gif_->canvas);
|
||||
|
||||
// Call frame callback if set
|
||||
if (frame_callback_) {
|
||||
frame_callback_();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LvglGif::Cleanup() {
|
||||
// Stop and delete timer
|
||||
if (timer_) {
|
||||
lv_timer_delete(timer_);
|
||||
timer_ = nullptr;
|
||||
}
|
||||
|
||||
// Close GIF decoder
|
||||
if (gif_) {
|
||||
gd_close_gif(gif_);
|
||||
gif_ = nullptr;
|
||||
}
|
||||
|
||||
playing_ = false;
|
||||
loaded_ = false;
|
||||
|
||||
// Clear image descriptor
|
||||
memset(&img_dsc_, 0, sizeof(img_dsc_));
|
||||
}
|
||||
117
main/display/lvgl_display/gif/lvgl_gif.h
Normal file
117
main/display/lvgl_display/gif/lvgl_gif.h
Normal file
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include "../lvgl_image.h"
|
||||
#include "gifdec.h"
|
||||
#include <lvgl.h>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
|
||||
/**
|
||||
* C++ implementation of LVGL GIF widget
|
||||
* Provides GIF animation functionality using gifdec library
|
||||
*/
|
||||
class LvglGif {
|
||||
public:
|
||||
explicit LvglGif(const lv_img_dsc_t* img_dsc);
|
||||
virtual ~LvglGif();
|
||||
|
||||
// LvglImage interface implementation
|
||||
virtual const lv_img_dsc_t* image_dsc() const;
|
||||
|
||||
/**
|
||||
* Start/restart GIF animation
|
||||
*/
|
||||
void Start();
|
||||
|
||||
/**
|
||||
* Pause GIF animation
|
||||
*/
|
||||
void Pause();
|
||||
|
||||
/**
|
||||
* Resume GIF animation
|
||||
*/
|
||||
void Resume();
|
||||
|
||||
/**
|
||||
* Stop GIF animation and rewind to first frame
|
||||
*/
|
||||
void Stop();
|
||||
|
||||
/**
|
||||
* Check if GIF is currently playing
|
||||
*/
|
||||
bool IsPlaying() const;
|
||||
|
||||
/**
|
||||
* Check if GIF was loaded successfully
|
||||
*/
|
||||
bool IsLoaded() const;
|
||||
|
||||
/**
|
||||
* Get loop count
|
||||
*/
|
||||
int32_t GetLoopCount() const;
|
||||
|
||||
/**
|
||||
* Set loop count
|
||||
*/
|
||||
void SetLoopCount(int32_t count);
|
||||
|
||||
/**
|
||||
* Get loop delay in milliseconds (delay between loops)
|
||||
*/
|
||||
uint32_t GetLoopDelay() const;
|
||||
|
||||
/**
|
||||
* Set loop delay in milliseconds (delay between loops)
|
||||
* @param delay_ms Delay in milliseconds before starting next loop. 0 means no delay.
|
||||
*/
|
||||
void SetLoopDelay(uint32_t delay_ms);
|
||||
|
||||
/**
|
||||
* Get GIF dimensions
|
||||
*/
|
||||
uint16_t width() const;
|
||||
uint16_t height() const;
|
||||
|
||||
/**
|
||||
* Set frame update callback
|
||||
*/
|
||||
void SetFrameCallback(std::function<void()> callback);
|
||||
|
||||
private:
|
||||
// GIF decoder instance
|
||||
gd_GIF* gif_;
|
||||
|
||||
// LVGL image descriptor
|
||||
lv_img_dsc_t img_dsc_;
|
||||
|
||||
// Animation timer
|
||||
lv_timer_t* timer_;
|
||||
|
||||
// Last frame update time
|
||||
uint32_t last_call_;
|
||||
|
||||
// Animation state
|
||||
bool playing_;
|
||||
bool loaded_;
|
||||
|
||||
// Loop delay configuration
|
||||
uint32_t loop_delay_ms_; // Delay between loops in milliseconds
|
||||
bool loop_waiting_; // Whether we're waiting for the next loop
|
||||
uint32_t loop_wait_start_; // Timestamp when loop wait started
|
||||
|
||||
// Frame update callback
|
||||
std::function<void()> frame_callback_;
|
||||
|
||||
/**
|
||||
* Update to next frame
|
||||
*/
|
||||
void NextFrame();
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
void Cleanup();
|
||||
};
|
||||
467
main/display/lvgl_display/jpg/image_to_jpeg.cpp
Normal file
467
main/display/lvgl_display/jpg/image_to_jpeg.cpp
Normal file
@@ -0,0 +1,467 @@
|
||||
#include <esp_attr.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_log.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <utility>
|
||||
|
||||
#include "esp_jpeg_common.h"
|
||||
#include "esp_jpeg_enc.h"
|
||||
#include "esp_imgfx_color_convert.h"
|
||||
|
||||
#if CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
|
||||
#include "driver/jpeg_encode.h"
|
||||
#endif
|
||||
#include "image_to_jpeg.h"
|
||||
|
||||
#define TAG "image_to_jpeg"
|
||||
|
||||
static void* malloc_psram(size_t size) {
|
||||
void* p = malloc(size);
|
||||
if (p)
|
||||
return p;
|
||||
#if (CONFIG_SPIRAM_SUPPORT && (CONFIG_SPIRAM_USE_CAPS_ALLOC || CONFIG_SPIRAM_USE_MALLOC))
|
||||
return heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
static __always_inline uint8_t expand_5_to_8(uint8_t v) {
|
||||
return (uint8_t)((v << 3) | (v >> 2));
|
||||
}
|
||||
|
||||
static __always_inline uint8_t expand_6_to_8(uint8_t v) {
|
||||
return (uint8_t)((v << 2) | (v >> 4));
|
||||
}
|
||||
|
||||
static uint8_t* convert_input_to_encoder_buf(const uint8_t* src, uint16_t width, uint16_t height, v4l2_pix_fmt_t format,
|
||||
jpeg_pixel_format_t* out_fmt, int* out_size) {
|
||||
// GRAY 直接作为 JPEG_PIXEL_FORMAT_GRAY 输入
|
||||
if (format == V4L2_PIX_FMT_GREY) {
|
||||
int sz = (int)width * (int)height;
|
||||
uint8_t* buf = (uint8_t*)jpeg_calloc_align(sz, 16);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
memcpy(buf, src, sz);
|
||||
if (out_fmt)
|
||||
*out_fmt = JPEG_PIXEL_FORMAT_GRAY;
|
||||
if (out_size)
|
||||
*out_size = sz;
|
||||
return buf;
|
||||
}
|
||||
|
||||
// V4L2 YUYV (Y Cb Y Cr) 可直接作为 JPEG_PIXEL_FORMAT_YCbYCr 输入
|
||||
if (format == V4L2_PIX_FMT_YUYV) {
|
||||
int sz = (int)width * (int)height * 2;
|
||||
uint8_t* buf = (uint8_t*)jpeg_calloc_align(sz, 16);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
memcpy(buf, src, sz);
|
||||
if (out_fmt)
|
||||
*out_fmt = JPEG_PIXEL_FORMAT_YCbYCr;
|
||||
if (out_size)
|
||||
*out_size = sz;
|
||||
return buf;
|
||||
}
|
||||
|
||||
// V4L2 UYVY (Cb Y Cr Y) -> 重排为 YUYV 再作为 YCbYCr 输入
|
||||
// 当前版本暂时不会出现 UYVY 格式
|
||||
if (format == V4L2_PIX_FMT_UYVY) [[unlikely]] {
|
||||
int sz = (int)width * (int)height * 2;
|
||||
const uint8_t* s = src;
|
||||
uint8_t* buf = (uint8_t*)jpeg_calloc_align(sz, 16);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
uint8_t* d = buf;
|
||||
for (int i = 0; i < sz; i += 4) {
|
||||
// src: Cb, Y0, Cr, Y1 -> dst: Y0, Cb, Y1, Cr
|
||||
d[0] = s[1];
|
||||
d[1] = s[0];
|
||||
d[2] = s[3];
|
||||
d[3] = s[2];
|
||||
s += 4;
|
||||
d += 4;
|
||||
}
|
||||
if (out_fmt)
|
||||
*out_fmt = JPEG_PIXEL_FORMAT_YCbYCr;
|
||||
if (out_size)
|
||||
*out_size = sz;
|
||||
return buf;
|
||||
}
|
||||
|
||||
// V4L2 YUV422P (YUV422 Planar) -> 重排为 YUYV (YCbYCr)
|
||||
// 当前版本暂时不会出现 YUV422P 格式
|
||||
if (format == V4L2_PIX_FMT_YUV422P) [[unlikely]] {
|
||||
int sz = (int)width * (int)height * 2;
|
||||
const uint8_t* y_plane = src;
|
||||
const uint8_t* u_plane = y_plane + (int)width * (int)height;
|
||||
const uint8_t* v_plane = u_plane + ((int)width / 2) * (int)height;
|
||||
uint8_t* buf = (uint8_t*)jpeg_calloc_align(sz, 16);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
uint8_t* dst = buf;
|
||||
for (int y = 0; y < height; y++) {
|
||||
const uint8_t* y_row = y_plane + y * (int)width;
|
||||
const uint8_t* u_row = u_plane + y * ((int)width / 2);
|
||||
const uint8_t* v_row = v_plane + y * ((int)width / 2);
|
||||
for (int x = 0; x < width; x += 2) {
|
||||
uint8_t y0 = y_row[x + 0];
|
||||
uint8_t y1 = y_row[x + 1];
|
||||
uint8_t cb = u_row[x / 2];
|
||||
uint8_t cr = v_row[x / 2];
|
||||
dst[0] = y0;
|
||||
dst[1] = cb;
|
||||
dst[2] = y1;
|
||||
dst[3] = cr;
|
||||
dst += 4;
|
||||
}
|
||||
}
|
||||
if (out_fmt)
|
||||
*out_fmt = JPEG_PIXEL_FORMAT_YCbYCr;
|
||||
if (out_size)
|
||||
*out_size = sz;
|
||||
return buf;
|
||||
}
|
||||
|
||||
// RGB 转换为 YUV422 (YCbYCr) 再输入
|
||||
// 见 https://github.com/78/xiaozhi-esp32/issues/1380#issuecomment-3497156378
|
||||
else if (format == V4L2_PIX_FMT_RGB24 || format == V4L2_PIX_FMT_RGB565 || format == V4L2_PIX_FMT_RGB565X) {
|
||||
esp_imgfx_pixel_fmt_t in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_RGB888;
|
||||
uint32_t src_len = 0;
|
||||
switch (format) {
|
||||
case V4L2_PIX_FMT_RGB24:
|
||||
in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_RGB888;
|
||||
src_len = static_cast<uint32_t>(width * height * 3);
|
||||
break;
|
||||
case V4L2_PIX_FMT_RGB565:
|
||||
in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_RGB565_LE;
|
||||
src_len = static_cast<uint32_t>(width * height * 2);
|
||||
break;
|
||||
[[unlikely]] case V4L2_PIX_FMT_RGB565X: // 当前版本暂时不会出现 RGB565X
|
||||
in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_RGB565_BE;
|
||||
src_len = static_cast<uint32_t>(width * height * 2);
|
||||
break;
|
||||
[[unlikely]] default:
|
||||
ESP_LOGE(TAG, "[Unreachable Case] unsupported format: 0x%08lx", format);
|
||||
std::unreachable();
|
||||
}
|
||||
int sz = (int)width * (int)height * 2;
|
||||
uint8_t* buf = (uint8_t*)jpeg_calloc_align(sz, 16);
|
||||
if (!buf)
|
||||
return nullptr;
|
||||
esp_imgfx_color_convert_cfg_t convert_cfg = {
|
||||
.in_res = {.width = static_cast<int16_t>(width),
|
||||
.height = static_cast<int16_t>(height)},
|
||||
.in_pixel_fmt = in_pixel_fmt,
|
||||
.out_pixel_fmt = ESP_IMGFX_PIXEL_FMT_YUYV,
|
||||
.color_space_std = ESP_IMGFX_COLOR_SPACE_STD_BT601,
|
||||
};
|
||||
esp_imgfx_color_convert_handle_t convert_handle = nullptr;
|
||||
esp_imgfx_err_t err = esp_imgfx_color_convert_open(&convert_cfg, &convert_handle);
|
||||
if (err != ESP_IMGFX_ERR_OK || convert_handle == nullptr) {
|
||||
ESP_LOGE(TAG, "esp_imgfx_color_convert_open failed");
|
||||
jpeg_free_align(buf);
|
||||
return nullptr;
|
||||
}
|
||||
esp_imgfx_data_t convert_input_data = {
|
||||
.data = const_cast<uint8_t*>(src),
|
||||
.data_len = static_cast<uint32_t>(src_len),
|
||||
};
|
||||
esp_imgfx_data_t convert_output_data = {
|
||||
.data = buf,
|
||||
.data_len = static_cast<uint32_t>(sz),
|
||||
};
|
||||
err = esp_imgfx_color_convert_process(convert_handle, &convert_input_data, &convert_output_data);
|
||||
if (err != ESP_IMGFX_ERR_OK) {
|
||||
ESP_LOGE(TAG, "esp_imgfx_color_convert_process failed");
|
||||
jpeg_free_align(buf);
|
||||
return nullptr;
|
||||
}
|
||||
esp_imgfx_color_convert_close(convert_handle);
|
||||
convert_handle = nullptr;
|
||||
if (out_fmt)
|
||||
*out_fmt = JPEG_PIXEL_FORMAT_YCbYCr;
|
||||
if (out_size)
|
||||
*out_size = sz;
|
||||
return buf;
|
||||
}
|
||||
ESP_LOGE(TAG, "unsupported format: 0x%08lx", format);
|
||||
if (out_size)
|
||||
*out_size = 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#if CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
|
||||
static jpeg_encoder_handle_t s_hw_jpeg_handle = NULL;
|
||||
|
||||
static bool hw_jpeg_ensure_inited(void) {
|
||||
if (s_hw_jpeg_handle) {
|
||||
return true;
|
||||
}
|
||||
jpeg_encode_engine_cfg_t eng_cfg = {
|
||||
.intr_priority = 0,
|
||||
.timeout_ms = 100,
|
||||
};
|
||||
esp_err_t er = jpeg_new_encoder_engine(&eng_cfg, &s_hw_jpeg_handle);
|
||||
if (er != ESP_OK) {
|
||||
ESP_LOGE(TAG, "jpeg_new_encoder_engine failed: %d", (int)er);
|
||||
s_hw_jpeg_handle = NULL;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint8_t* convert_input_to_hw_encoder_buf(const uint8_t* src, uint16_t width, uint16_t height, v4l2_pix_fmt_t format,
|
||||
jpeg_enc_input_format_t* out_fmt, int* out_size) {
|
||||
if (format == V4L2_PIX_FMT_GREY) {
|
||||
int sz = (int)width * (int)height;
|
||||
uint8_t* buf = (uint8_t*)malloc_psram(sz);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
memcpy(buf, src, sz);
|
||||
if (out_fmt)
|
||||
*out_fmt = JPEG_ENCODE_IN_FORMAT_GRAY;
|
||||
if (out_size)
|
||||
*out_size = sz;
|
||||
return buf;
|
||||
}
|
||||
|
||||
if (format == V4L2_PIX_FMT_RGB24) {
|
||||
int sz = (int)width * (int)height * 3;
|
||||
uint8_t* buf = (uint8_t*)malloc_psram(sz);
|
||||
if (!buf) {
|
||||
ESP_LOGE(TAG, "malloc_psram failed");
|
||||
return NULL;
|
||||
}
|
||||
memcpy(buf, src, sz);
|
||||
if (out_fmt)
|
||||
*out_fmt = JPEG_ENCODE_IN_FORMAT_RGB888;
|
||||
if (out_size)
|
||||
*out_size = sz;
|
||||
return buf;
|
||||
}
|
||||
|
||||
if (format == V4L2_PIX_FMT_RGB565) {
|
||||
int sz = (int)width * (int)height * 2;
|
||||
uint8_t* buf = (uint8_t*)malloc_psram(sz);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
memcpy(buf, src, sz);
|
||||
if (out_fmt)
|
||||
*out_fmt = JPEG_ENCODE_IN_FORMAT_RGB565;
|
||||
if (out_size)
|
||||
*out_size = sz;
|
||||
return buf;
|
||||
}
|
||||
|
||||
if (format == V4L2_PIX_FMT_YUYV) {
|
||||
// 硬件需要 | Y1 V Y0 U | 的“大端”格式,因此需要 bswap16
|
||||
int sz = (int)width * (int)height * 2;
|
||||
uint16_t* buf = (uint16_t*)malloc_psram(sz);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
const uint16_t* bsrc = (const uint16_t*)src;
|
||||
for (int i = 0; i < sz / 2; i++) {
|
||||
buf[i] = __builtin_bswap16(bsrc[i]);
|
||||
}
|
||||
if (out_fmt)
|
||||
*out_fmt = JPEG_ENCODE_IN_FORMAT_YUV422;
|
||||
if (out_size)
|
||||
*out_size = sz;
|
||||
return (uint8_t*)buf;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool encode_with_hw_jpeg(const uint8_t* src, size_t src_len, uint16_t width, uint16_t height,
|
||||
v4l2_pix_fmt_t format, uint8_t quality, uint8_t** jpg_out, size_t* jpg_out_len,
|
||||
jpg_out_cb cb, void* cb_arg) {
|
||||
if (quality < 1)
|
||||
quality = 1;
|
||||
if (quality > 100)
|
||||
quality = 100;
|
||||
|
||||
jpeg_enc_input_format_t enc_src_type = JPEG_ENCODE_IN_FORMAT_RGB888;
|
||||
int enc_in_size = 0;
|
||||
uint8_t* enc_in = convert_input_to_hw_encoder_buf(src, width, height, format, &enc_src_type, &enc_in_size);
|
||||
if (!enc_in) {
|
||||
ESP_LOGW(TAG, "hw jpeg: unsupported format, fallback to sw");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hw_jpeg_ensure_inited()) {
|
||||
free(enc_in);
|
||||
return false;
|
||||
}
|
||||
|
||||
jpeg_encode_cfg_t enc_cfg = {0};
|
||||
enc_cfg.width = width;
|
||||
enc_cfg.height = height;
|
||||
enc_cfg.src_type = enc_src_type;
|
||||
enc_cfg.image_quality = quality;
|
||||
enc_cfg.sub_sample = (enc_src_type == JPEG_ENCODE_IN_FORMAT_GRAY) ? JPEG_DOWN_SAMPLING_GRAY : JPEG_DOWN_SAMPLING_YUV422;
|
||||
|
||||
size_t out_cap = (size_t)width * (size_t)height * 3 / 2 + 64 * 1024;
|
||||
if (out_cap < 128 * 1024)
|
||||
out_cap = 128 * 1024;
|
||||
jpeg_encode_memory_alloc_cfg_t jpeg_enc_output_mem_cfg = { .buffer_direction = JPEG_ENC_ALLOC_OUTPUT_BUFFER };
|
||||
size_t out_cap_aligned = 0;
|
||||
uint8_t* outbuf = (uint8_t*)jpeg_alloc_encoder_mem(out_cap, &jpeg_enc_output_mem_cfg, &out_cap_aligned);
|
||||
if (!outbuf) {
|
||||
free(enc_in);
|
||||
ESP_LOGE(TAG, "alloc out buffer failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t out_len = 0;
|
||||
esp_err_t er = jpeg_encoder_process(s_hw_jpeg_handle, &enc_cfg, enc_in, (uint32_t)enc_in_size, outbuf, (uint32_t)out_cap_aligned, &out_len);
|
||||
free(enc_in);
|
||||
|
||||
if (er != ESP_OK) {
|
||||
free(outbuf);
|
||||
ESP_LOGE(TAG, "jpeg_encoder_process failed: %d", (int)er);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cb) {
|
||||
cb(cb_arg, 0, outbuf, (size_t)out_len);
|
||||
cb(cb_arg, 1, NULL, 0);
|
||||
free(outbuf);
|
||||
if (jpg_out)
|
||||
*jpg_out = NULL;
|
||||
if (jpg_out_len)
|
||||
*jpg_out_len = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (jpg_out && jpg_out_len) {
|
||||
*jpg_out = outbuf;
|
||||
*jpg_out_len = (size_t)out_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
free(outbuf);
|
||||
return true;
|
||||
}
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
|
||||
|
||||
static bool encode_with_esp_new_jpeg(const uint8_t* src, size_t src_len, uint16_t width, uint16_t height,
|
||||
v4l2_pix_fmt_t format, uint8_t quality, uint8_t** jpg_out, size_t* jpg_out_len,
|
||||
jpg_out_cb cb, void* cb_arg) {
|
||||
if (quality < 1)
|
||||
quality = 1;
|
||||
if (quality > 100)
|
||||
quality = 100;
|
||||
|
||||
jpeg_pixel_format_t enc_src_type = JPEG_PIXEL_FORMAT_RGB888;
|
||||
int enc_in_size = 0;
|
||||
uint8_t* enc_in = convert_input_to_encoder_buf(src, width, height, format, &enc_src_type, &enc_in_size);
|
||||
if (!enc_in) {
|
||||
ESP_LOGE(TAG, "alloc/convert input failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
jpeg_enc_config_t cfg = DEFAULT_JPEG_ENC_CONFIG();
|
||||
cfg.width = width;
|
||||
cfg.height = height;
|
||||
cfg.src_type = enc_src_type;
|
||||
cfg.subsampling = (enc_src_type == JPEG_PIXEL_FORMAT_GRAY) ? JPEG_SUBSAMPLE_GRAY : JPEG_SUBSAMPLE_420;
|
||||
cfg.quality = quality;
|
||||
cfg.rotate = JPEG_ROTATE_0D;
|
||||
cfg.task_enable = false;
|
||||
|
||||
jpeg_enc_handle_t h = NULL;
|
||||
jpeg_error_t ret = jpeg_enc_open(&cfg, &h);
|
||||
if (ret != JPEG_ERR_OK) {
|
||||
jpeg_free_align(enc_in);
|
||||
ESP_LOGE(TAG, "jpeg_enc_open failed: %d", (int)ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 估算输出缓冲区:宽高的 1.5 倍 + 64KB
|
||||
size_t out_cap = (size_t)width * (size_t)height * 3 / 2 + 64 * 1024;
|
||||
if (out_cap < 128 * 1024)
|
||||
out_cap = 128 * 1024;
|
||||
uint8_t* outbuf = (uint8_t*)malloc_psram(out_cap);
|
||||
if (!outbuf) {
|
||||
jpeg_enc_close(h);
|
||||
jpeg_free_align(enc_in);
|
||||
ESP_LOGE(TAG, "alloc out buffer failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
int out_len = 0;
|
||||
ret = jpeg_enc_process(h, enc_in, enc_in_size, outbuf, (int)out_cap, &out_len);
|
||||
jpeg_enc_close(h);
|
||||
jpeg_free_align(enc_in);
|
||||
|
||||
if (ret != JPEG_ERR_OK) {
|
||||
free(outbuf);
|
||||
ESP_LOGE(TAG, "jpeg_enc_process failed: %d", (int)ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cb) {
|
||||
cb(cb_arg, 0, outbuf, (size_t)out_len);
|
||||
cb(cb_arg, 1, NULL, 0); // 结束信号
|
||||
free(outbuf);
|
||||
if (jpg_out)
|
||||
*jpg_out = NULL;
|
||||
if (jpg_out_len)
|
||||
*jpg_out_len = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (jpg_out && jpg_out_len) {
|
||||
*jpg_out = outbuf;
|
||||
*jpg_out_len = (size_t)out_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
free(outbuf);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool image_to_jpeg(uint8_t* src, size_t src_len, uint16_t width, uint16_t height, v4l2_pix_fmt_t format,
|
||||
uint8_t quality, uint8_t** out, size_t* out_len) {
|
||||
#ifdef CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
if (format == V4L2_PIX_FMT_JPEG) {
|
||||
uint8_t * out_data = (uint8_t*)heap_caps_malloc(src_len, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (!out_data) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for JPEG output");
|
||||
return false;
|
||||
}
|
||||
memcpy(out_data, src, src_len);
|
||||
*out = out_data;
|
||||
*out_len = src_len;
|
||||
return true;
|
||||
}
|
||||
#endif // CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
#if CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
|
||||
if (encode_with_hw_jpeg(src, src_len, width, height, format, quality, out, out_len, NULL, NULL)) {
|
||||
return true;
|
||||
}
|
||||
// Fallback to esp_new_jpeg
|
||||
#endif
|
||||
return encode_with_esp_new_jpeg(src, src_len, width, height, format, quality, out, out_len, NULL, NULL);
|
||||
}
|
||||
|
||||
bool image_to_jpeg_cb(uint8_t* src, size_t src_len, uint16_t width, uint16_t height, v4l2_pix_fmt_t format,
|
||||
uint8_t quality, jpg_out_cb cb, void* arg) {
|
||||
#ifdef CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
if (format == V4L2_PIX_FMT_JPEG) {
|
||||
cb(arg, 0, src, src_len);
|
||||
cb(arg, 1, nullptr, 0); // end signal
|
||||
return true;
|
||||
}
|
||||
#endif // CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
#if CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
|
||||
if (encode_with_hw_jpeg(src, src_len, width, height, format, quality, NULL, NULL, cb, arg)) {
|
||||
return true;
|
||||
}
|
||||
// Fallback to esp_new_jpeg
|
||||
#endif
|
||||
return encode_with_esp_new_jpeg(src, src_len, width, height, format, quality, NULL, NULL, cb, arg);
|
||||
}
|
||||
86
main/display/lvgl_display/jpg/image_to_jpeg.h
Normal file
86
main/display/lvgl_display/jpg/image_to_jpeg.h
Normal file
@@ -0,0 +1,86 @@
|
||||
// image_to_jpeg.h - 图像到JPEG转换的高效编码接口
|
||||
// 节省约8KB SRAM的JPEG编码实现
|
||||
#pragma once
|
||||
#include "sdkconfig.h"
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
// ESP32-P4 使用 esp_video 组件提供的 V4L2 头文件
|
||||
#include <linux/videodev2.h>
|
||||
#else
|
||||
// ESP32-S3 等其他芯片:定义常用的 V4L2 像素格式
|
||||
#define V4L2_PIX_FMT_RGB565 0x50424752 // 'RGBP'
|
||||
#define V4L2_PIX_FMT_RGB565X 0x52474250 // 'PRGB'
|
||||
#define V4L2_PIX_FMT_RGB24 0x33424752 // 'RGB3'
|
||||
#define V4L2_PIX_FMT_YUYV 0x56595559 // 'YUYV'
|
||||
#define V4L2_PIX_FMT_YUV422P 0x36315559 // 'YU16'
|
||||
#define V4L2_PIX_FMT_YUV420 0x32315559 // 'YU12'
|
||||
#define V4L2_PIX_FMT_GREY 0x59455247 // 'GREY'
|
||||
#define V4L2_PIX_FMT_UYVY 0x59565955 // 'UYVY'
|
||||
#define V4L2_PIX_FMT_JPEG 0x4745504A // 'JPEG'
|
||||
#endif
|
||||
|
||||
typedef uint32_t v4l2_pix_fmt_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
// JPEG输出回调函数类型
|
||||
// arg: 用户自定义参数, index: 当前数据索引, data: JPEG数据块, len: 数据块长度
|
||||
// 返回: 实际处理的字节数
|
||||
typedef size_t (*jpg_out_cb)(void *arg, size_t index, const void *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 将图像格式高效转换为JPEG
|
||||
*
|
||||
* 这个函数使用优化的JPEG编码器进行编码,主要特点:
|
||||
* - 节省约8KB的SRAM使用(静态变量改为堆分配)
|
||||
* - 支持多种图像格式输入
|
||||
* - 高质量JPEG输出
|
||||
*
|
||||
* @param src 源图像数据
|
||||
* @param src_len 源图像数据长度
|
||||
* @param width 图像宽度
|
||||
* @param height 图像高度
|
||||
* @param format 图像格式 (PIXFORMAT_RGB565, PIXFORMAT_RGB888, 等)
|
||||
* @param quality JPEG质量 (1-100)
|
||||
* @param out 输出JPEG数据指针 (需要调用者释放)
|
||||
* @param out_len 输出JPEG数据长度
|
||||
*
|
||||
* @return true 成功, false 失败
|
||||
*/
|
||||
bool image_to_jpeg(uint8_t *src, size_t src_len, uint16_t width, uint16_t height,
|
||||
v4l2_pix_fmt_t format, uint8_t quality, uint8_t **out, size_t *out_len);
|
||||
|
||||
/**
|
||||
* @brief 将图像格式转换为JPEG(回调版本)
|
||||
*
|
||||
* 使用回调函数处理JPEG输出数据,适合流式传输或分块处理:
|
||||
* - 节省约8KB的SRAM使用(静态变量改为堆分配)
|
||||
* - 支持流式输出,无需预分配大缓冲区
|
||||
* - 通过回调函数逐块处理JPEG数据
|
||||
*
|
||||
* @param src 源图像数据
|
||||
* @param src_len 源图像数据长度
|
||||
* @param width 图像宽度
|
||||
* @param height 图像高度
|
||||
* @param format 图像格式
|
||||
* @param quality JPEG质量 (1-100)
|
||||
* @param cb 输出回调函数
|
||||
* @param arg 传递给回调函数的用户参数
|
||||
*
|
||||
* @return true 成功, false 失败
|
||||
*/
|
||||
bool image_to_jpeg_cb(uint8_t *src, size_t src_len, uint16_t width, uint16_t height,
|
||||
v4l2_pix_fmt_t format, uint8_t quality, jpg_out_cb cb, void *arg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // ndef CONFIG_IDF_TARGET_ESP32
|
||||
264
main/display/lvgl_display/jpg/jpeg_to_image.c
Normal file
264
main/display/lvgl_display/jpg/jpeg_to_image.c
Normal file
@@ -0,0 +1,264 @@
|
||||
#include <esp_check.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <sys/param.h>
|
||||
|
||||
#include "esp_jpeg_common.h"
|
||||
#include "esp_jpeg_dec.h"
|
||||
|
||||
#include "jpeg_to_image.h"
|
||||
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_CAMERA_DEBUG_MODE
|
||||
#undef LOG_LOCAL_LEVEL
|
||||
#define LOG_LOCAL_LEVEL MAX(CONFIG_LOG_DEFAULT_LEVEL, ESP_LOG_DEBUG)
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_CAMERA_DEBUG_MODE
|
||||
#include <esp_log.h>
|
||||
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_DECODER
|
||||
#include "driver/jpeg_decode.h"
|
||||
#endif
|
||||
|
||||
#define TAG "jpeg_to_image"
|
||||
|
||||
static esp_err_t decode_with_new_jpeg(const uint8_t* src, size_t src_len, uint8_t** out, size_t* out_len, size_t* width,
|
||||
size_t* height, size_t* stride) {
|
||||
ESP_LOGD(TAG, "Decoding JPEG with software decoder");
|
||||
esp_err_t ret = ESP_OK;
|
||||
jpeg_error_t jpeg_ret = JPEG_ERR_OK;
|
||||
uint8_t* out_buf = NULL;
|
||||
jpeg_dec_io_t jpeg_io = {0};
|
||||
jpeg_dec_header_info_t out_info = {0};
|
||||
|
||||
jpeg_dec_config_t config = DEFAULT_JPEG_DEC_CONFIG();
|
||||
config.output_type = JPEG_PIXEL_FORMAT_RGB565_LE;
|
||||
config.rotate = JPEG_ROTATE_0D;
|
||||
|
||||
jpeg_dec_handle_t jpeg_dec = NULL;
|
||||
jpeg_ret = jpeg_dec_open(&config, &jpeg_dec);
|
||||
if (jpeg_ret != JPEG_ERR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to open JPEG decoder");
|
||||
ret = ESP_FAIL;
|
||||
goto jpeg_dec_failed;
|
||||
}
|
||||
|
||||
jpeg_io.inbuf = (uint8_t*)src;
|
||||
jpeg_io.inbuf_len = (int)src_len;
|
||||
|
||||
jpeg_ret = jpeg_dec_parse_header(jpeg_dec, &jpeg_io, &out_info);
|
||||
if (jpeg_ret != JPEG_ERR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to parse JPEG header");
|
||||
ret = ESP_ERR_INVALID_ARG;
|
||||
goto jpeg_dec_failed;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "JPEG header info: width=%d, height=%d", out_info.width, out_info.height);
|
||||
|
||||
out_buf = jpeg_calloc_align(out_info.width * out_info.height * 2, 16);
|
||||
if (out_buf == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for JPEG output buffer");
|
||||
ret = ESP_ERR_NO_MEM;
|
||||
goto jpeg_dec_failed;
|
||||
}
|
||||
|
||||
jpeg_io.outbuf = out_buf;
|
||||
jpeg_ret = jpeg_dec_process(jpeg_dec, &jpeg_io);
|
||||
if (jpeg_ret != JPEG_ERR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to decode JPEG");
|
||||
ret = ESP_FAIL;
|
||||
goto jpeg_dec_failed;
|
||||
}
|
||||
|
||||
ESP_LOG_BUFFER_HEXDUMP(TAG, out_buf, MIN(out_info.width * out_info.height * 2, 256), ESP_LOG_DEBUG);
|
||||
|
||||
*out = out_buf;
|
||||
out_buf = NULL;
|
||||
*out_len = (size_t)(out_info.width * out_info.height * 2);
|
||||
*width = (size_t)out_info.width;
|
||||
*height = (size_t)out_info.height;
|
||||
*stride = (size_t)out_info.width * 2;
|
||||
jpeg_dec_close(jpeg_dec);
|
||||
jpeg_dec = NULL;
|
||||
|
||||
return ret;
|
||||
|
||||
jpeg_dec_failed:
|
||||
if (jpeg_dec) {
|
||||
jpeg_dec_close(jpeg_dec);
|
||||
jpeg_dec = NULL;
|
||||
}
|
||||
if (out_buf) {
|
||||
jpeg_free_align(out_buf);
|
||||
out_buf = NULL;
|
||||
}
|
||||
|
||||
*out = NULL;
|
||||
*out_len = 0;
|
||||
*width = 0;
|
||||
*height = 0;
|
||||
*stride = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_DECODER
|
||||
static esp_err_t decode_with_hardware_jpeg(const uint8_t* src, size_t src_len, uint8_t** out, size_t* out_len,
|
||||
size_t* width, size_t* height, size_t* stride) {
|
||||
ESP_LOGD(TAG, "Decoding JPEG with hardware decoder");
|
||||
esp_err_t ret = ESP_OK;
|
||||
|
||||
jpeg_decoder_handle_t jpeg_dec = NULL;
|
||||
uint8_t* bit_stream = NULL;
|
||||
uint8_t* out_buf = NULL;
|
||||
size_t out_buf_len = 0;
|
||||
size_t tx_buffer_size = 0;
|
||||
size_t rx_buffer_size = 0;
|
||||
|
||||
jpeg_decode_engine_cfg_t eng_cfg = {
|
||||
.intr_priority = 1,
|
||||
.timeout_ms = 1000,
|
||||
};
|
||||
|
||||
jpeg_decode_cfg_t decode_cfg_rgb = {
|
||||
.output_format = JPEG_DECODE_OUT_FORMAT_RGB565,
|
||||
.rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_BGR,
|
||||
};
|
||||
|
||||
ret = jpeg_new_decoder_engine(&eng_cfg, &jpeg_dec);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create JPEG decoder engine");
|
||||
goto jpeg_hw_dec_failed;
|
||||
}
|
||||
|
||||
jpeg_decode_memory_alloc_cfg_t tx_mem_cfg = {
|
||||
.buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER,
|
||||
};
|
||||
|
||||
jpeg_decode_memory_alloc_cfg_t rx_mem_cfg = {
|
||||
.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
|
||||
};
|
||||
|
||||
bit_stream = (uint8_t*)jpeg_alloc_decoder_mem(src_len, &tx_mem_cfg, &tx_buffer_size);
|
||||
if (bit_stream == NULL || tx_buffer_size < src_len) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for JPEG bit stream");
|
||||
ret = ESP_ERR_NO_MEM;
|
||||
goto jpeg_hw_dec_failed;
|
||||
}
|
||||
|
||||
memcpy(bit_stream, src, src_len);
|
||||
|
||||
jpeg_decode_picture_info_t header_info;
|
||||
ESP_GOTO_ON_ERROR(jpeg_decoder_get_info(bit_stream, src_len, &header_info), jpeg_hw_dec_failed, TAG,
|
||||
"Failed to get JPEG header info");
|
||||
|
||||
ESP_LOGD(TAG, "JPEG header info: width=%d, height=%d, sample_method=%d", header_info.width, header_info.height,
|
||||
(int)header_info.sample_method);
|
||||
|
||||
switch (header_info.sample_method) {
|
||||
case JPEG_DOWN_SAMPLING_GRAY:
|
||||
case JPEG_DOWN_SAMPLING_YUV444:
|
||||
out_buf_len = header_info.width * header_info.height * 2;
|
||||
*stride = header_info.width * 2;
|
||||
break;
|
||||
case JPEG_DOWN_SAMPLING_YUV422:
|
||||
case JPEG_DOWN_SAMPLING_YUV420:
|
||||
out_buf_len = ((header_info.width + 15) & ~15) * ((header_info.height + 15) & ~15) * 2;
|
||||
*stride = ((header_info.width + 15) & ~15) * 2;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Unsupported JPEG sample method");
|
||||
ret = ESP_ERR_NOT_SUPPORTED;
|
||||
goto jpeg_hw_dec_failed;
|
||||
}
|
||||
|
||||
out_buf = (uint8_t*)jpeg_alloc_decoder_mem(out_buf_len, &rx_mem_cfg, &rx_buffer_size);
|
||||
if (out_buf == NULL || rx_buffer_size < out_buf_len) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for JPEG output buffer");
|
||||
ret = ESP_ERR_NO_MEM;
|
||||
goto jpeg_hw_dec_failed;
|
||||
}
|
||||
|
||||
uint32_t out_size = 0;
|
||||
|
||||
ESP_GOTO_ON_ERROR(
|
||||
jpeg_decoder_process(jpeg_dec, &decode_cfg_rgb, bit_stream, src_len, out_buf, out_buf_len, &out_size),
|
||||
jpeg_hw_dec_failed, TAG, "Failed to decode JPEG");
|
||||
|
||||
ESP_LOGD(TAG, "Expected %d bytes, got %" PRIu32 " bytes", out_buf_len, out_size);
|
||||
|
||||
if (out_size != out_buf_len) {
|
||||
ESP_LOGE(TAG, "Decoded image size mismatch: Expected %zu bytes, got %" PRIu32 " bytes", out_buf_len, out_size);
|
||||
ret = ESP_ERR_INVALID_SIZE;
|
||||
goto jpeg_hw_dec_failed;
|
||||
}
|
||||
|
||||
if (header_info.sample_method == JPEG_DOWN_SAMPLING_GRAY) {
|
||||
// convert GRAY8 to RGB565
|
||||
uint32_t i = header_info.width * header_info.height;
|
||||
do {
|
||||
--i;
|
||||
uint8_t r = (out_buf[i] >> 3) & 0x1F;
|
||||
uint8_t g = (out_buf[i] >> 2) & 0x3F;
|
||||
// b is same as r
|
||||
uint16_t rgb565 = (r << 11) | (g << 5) | r;
|
||||
out_buf[2 * i + 1] = (rgb565 >> 8) & 0xFF;
|
||||
out_buf[2 * i] = rgb565 & 0xFF;
|
||||
} while (i != 0);
|
||||
out_size = header_info.width * header_info.height * 2;
|
||||
ESP_LOGD(TAG, "Converted GRAY8 to RGB565, new size: %zu", out_size);
|
||||
}
|
||||
|
||||
ESP_LOG_BUFFER_HEXDUMP(TAG, out_buf, MIN(out_size, 256), ESP_LOG_DEBUG);
|
||||
|
||||
*out = out_buf;
|
||||
out_buf = NULL;
|
||||
*out_len = (size_t)out_size;
|
||||
jpeg_del_decoder_engine(jpeg_dec);
|
||||
jpeg_dec = NULL;
|
||||
heap_caps_free(bit_stream);
|
||||
bit_stream = NULL;
|
||||
*width = header_info.width;
|
||||
*height = header_info.height;
|
||||
|
||||
return ret;
|
||||
|
||||
jpeg_hw_dec_failed:
|
||||
if (out_buf) {
|
||||
heap_caps_free(out_buf);
|
||||
out_buf = NULL;
|
||||
}
|
||||
if (bit_stream) {
|
||||
heap_caps_free(bit_stream);
|
||||
bit_stream = NULL;
|
||||
}
|
||||
if (jpeg_dec) {
|
||||
jpeg_del_decoder_engine(jpeg_dec);
|
||||
jpeg_dec = NULL;
|
||||
}
|
||||
*out = NULL;
|
||||
*out_len = 0;
|
||||
*width = 0;
|
||||
*height = 0;
|
||||
*stride = 0;
|
||||
return ret;
|
||||
}
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_DECODER
|
||||
|
||||
esp_err_t jpeg_to_image(const uint8_t* src, size_t src_len, uint8_t** out, size_t* out_len, size_t* width,
|
||||
size_t* height, size_t* stride) {
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_CAMERA_DEBUG_MODE
|
||||
esp_log_level_set(TAG, ESP_LOG_DEBUG);
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_CAMERA_DEBUG_MODE
|
||||
if (src == NULL || src_len == 0 || out == NULL || out_len == NULL || width == NULL || height == NULL ||
|
||||
stride == NULL) {
|
||||
ESP_LOGE(TAG, "Invalid parameters");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_DECODER
|
||||
esp_err_t ret = decode_with_hardware_jpeg(src, src_len, out, out_len, width, height, stride);
|
||||
if (ret == ESP_OK) {
|
||||
return ret;
|
||||
}
|
||||
ESP_LOGW(TAG, "Failed to decode with hardware JPEG, fallback to software decoder");
|
||||
// Fallback to esp_new_jpeg
|
||||
#endif
|
||||
return decode_with_new_jpeg(src, src_len, out, out_len, width, height, stride);
|
||||
}
|
||||
62
main/display/lvgl_display/jpg/jpeg_to_image.h
Normal file
62
main/display/lvgl_display/jpg/jpeg_to_image.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "sdkconfig.h"
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32
|
||||
|
||||
#include <esp_err.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Decodes a JPEG image from memory to raw RGB565 pixel data
|
||||
*
|
||||
* This function attempts to decode a JPEG image using hardware acceleration first (if enabled),
|
||||
* falling back to a software decoder if hardware decoding fails or is unavailable.
|
||||
*
|
||||
* @param[in] src Pointer to the JPEG bitstream in memory
|
||||
* @param[in] src_len Length of the JPEG bitstream in bytes
|
||||
* @param[out] out Pointer to a buffer pointer that will be set to the decoded image data.
|
||||
* This buffer is allocated internally and MUST be freed by the caller using heap_caps_free().
|
||||
* @param[out] out_len Pointer to a variable that will receive the size of the decoded image data in bytes
|
||||
* @param[out] width Pointer to a variable that will receive the image width in pixels
|
||||
* @param[out] height Pointer to a variable that will receive the image height in pixels
|
||||
* @param[out] stride Pointer to a variable that will receive the image stride in bytes
|
||||
*
|
||||
* @return ESP_OK on successful decoding
|
||||
* @return ESP_ERR_INVALID_ARG on invalid parameters
|
||||
* @return ESP_ERR_NO_MEM on memory allocation failure
|
||||
* @return ESP_FAIL on failure
|
||||
*
|
||||
* @attention Memory Management for `*out`:
|
||||
* - The function allocates memory for the decoded image internally
|
||||
* - On success, the caller takes ownership of this memory and SHOULD free it using heap_caps_free()
|
||||
* - On failure, `*out` is guaranteed to be NULL and no freeing is required
|
||||
* - Example usage:
|
||||
* @code{.c}
|
||||
* uint8_t *image = NULL;
|
||||
* size_t len, width, height;
|
||||
* if (jpeg_to_image(jpeg_data, jpeg_len, &image, &len, &width, &height)) {
|
||||
* // Use image data...
|
||||
* heap_caps_free(image); // Critical: use heap_caps_free
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @note Configuration dependency:
|
||||
* - When CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_DECODER is enabled, hardware acceleration is attempted first
|
||||
* - Both hardware and software paths allocate memory that requires heap_caps_free() for deallocation
|
||||
* - The decoded image format is always RGB565 (2 bytes per pixel)
|
||||
*
|
||||
* @note When using hardware decoder, the decoded image dimensions might be aligned up to 16-byte boundaries.
|
||||
* For YUV420 or YUV422 compressed images, both width and height will be rounded up to the nearest multiple of 16.
|
||||
* See details at
|
||||
* <https://docs.espressif.com/projects/esp-idf/en/stable/esp32p4/api-reference/peripherals/jpeg.html#jpeg-decoder-engine>
|
||||
*
|
||||
*/
|
||||
esp_err_t jpeg_to_image(const uint8_t* src, size_t src_len, uint8_t** out, size_t* out_len, size_t* width,
|
||||
size_t* height, size_t* stride);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // CONFIG_IDF_TARGET_ESP32
|
||||
274
main/display/lvgl_display/lvgl_display.cc
Normal file
274
main/display/lvgl_display/lvgl_display.cc
Normal file
@@ -0,0 +1,274 @@
|
||||
#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"
|
||||
#include "jpg/image_to_jpeg.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( 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 ¬ification, 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_, 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_);
|
||||
// Update battery icon
|
||||
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_);
|
||||
}
|
||||
|
||||
// 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, FONT_AWESOME_BATTERY_EMPTY) == 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
|
||||
}
|
||||
53
main/display/lvgl_display/lvgl_display.h
Normal file
53
main/display/lvgl_display/lvgl_display.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#ifndef LVGL_DISPLAY_H
|
||||
#define LVGL_DISPLAY_H
|
||||
|
||||
#include "display.h"
|
||||
#include "lvgl_image.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 SetPreviewImage(std::unique_ptr<LvglImage> image);
|
||||
virtual void UpdateStatusBar(bool update_all = false);
|
||||
virtual void SetPowerSaveMode(bool on);
|
||||
virtual bool SnapshotToJpeg(std::string& jpeg_data, int quality = 80);
|
||||
|
||||
protected:
|
||||
esp_pm_lock_handle_t pm_lock_ = nullptr;
|
||||
lv_display_t *display_ = 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* 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_;
|
||||
};
|
||||
64
main/display/lvgl_display/lvgl_image.cc
Normal file
64
main/display/lvgl_display/lvgl_image.cc
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "lvgl_image.h"
|
||||
#include <cbin_font.h>
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <stdexcept>
|
||||
#include <cstring>
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
#define TAG "LvglImage"
|
||||
|
||||
|
||||
LvglRawImage::LvglRawImage(void* data, size_t size) {
|
||||
bzero(&image_dsc_, sizeof(image_dsc_));
|
||||
image_dsc_.data_size = size;
|
||||
image_dsc_.data = static_cast<uint8_t*>(data);
|
||||
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;
|
||||
}
|
||||
|
||||
bool LvglRawImage::IsGif() const {
|
||||
auto ptr = (const uint8_t*)image_dsc_.data;
|
||||
return ptr[0] == 'G' && ptr[1] == 'I' && ptr[2] == 'F';
|
||||
}
|
||||
|
||||
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_);
|
||||
}
|
||||
}
|
||||
|
||||
LvglAllocatedImage::LvglAllocatedImage(void* data, size_t size) {
|
||||
bzero(&image_dsc_, sizeof(image_dsc_));
|
||||
image_dsc_.data_size = size;
|
||||
image_dsc_.data = static_cast<uint8_t*>(data);
|
||||
|
||||
if (lv_image_decoder_get_info(&image_dsc_, &image_dsc_.header) != LV_RESULT_OK) {
|
||||
ESP_LOGE(TAG, "Failed to get image info, data: %p size: %u", data, size);
|
||||
throw std::runtime_error("Failed to get image info");
|
||||
}
|
||||
}
|
||||
|
||||
LvglAllocatedImage::LvglAllocatedImage(void* data, size_t size, int width, int height, int stride, int color_format) {
|
||||
bzero(&image_dsc_, sizeof(image_dsc_));
|
||||
image_dsc_.data_size = size;
|
||||
image_dsc_.data = static_cast<uint8_t*>(data);
|
||||
image_dsc_.header.magic = LV_IMAGE_HEADER_MAGIC;
|
||||
image_dsc_.header.cf = color_format;
|
||||
image_dsc_.header.w = width;
|
||||
image_dsc_.header.h = height;
|
||||
image_dsc_.header.stride = stride;
|
||||
}
|
||||
|
||||
LvglAllocatedImage::~LvglAllocatedImage() {
|
||||
if (image_dsc_.data) {
|
||||
heap_caps_free((void*)image_dsc_.data);
|
||||
image_dsc_.data = nullptr;
|
||||
}
|
||||
}
|
||||
53
main/display/lvgl_display/lvgl_image.h
Normal file
53
main/display/lvgl_display/lvgl_image.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#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 bool IsGif() const { return false; }
|
||||
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_; }
|
||||
virtual bool IsGif() const;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
class LvglSourceImage : public LvglImage {
|
||||
public:
|
||||
LvglSourceImage(const lv_img_dsc_t* image_dsc) : image_dsc_(image_dsc) {}
|
||||
virtual const lv_img_dsc_t* image_dsc() const override { return image_dsc_; }
|
||||
|
||||
private:
|
||||
const lv_img_dsc_t* image_dsc_;
|
||||
};
|
||||
|
||||
class LvglAllocatedImage : public LvglImage {
|
||||
public:
|
||||
LvglAllocatedImage(void* data, size_t size);
|
||||
LvglAllocatedImage(void* data, size_t size, int width, int height, int stride, int color_format);
|
||||
virtual ~LvglAllocatedImage();
|
||||
virtual const lv_img_dsc_t* image_dsc() const override { return &image_dsc_; }
|
||||
|
||||
private:
|
||||
lv_img_dsc_t image_dsc_;
|
||||
};
|
||||
30
main/display/lvgl_display/lvgl_theme.cc
Normal file
30
main/display/lvgl_display/lvgl_theme.cc
Normal file
@@ -0,0 +1,30 @@
|
||||
#include "lvgl_theme.h"
|
||||
|
||||
LvglTheme::LvglTheme(const std::string& name) : Theme(name) {
|
||||
}
|
||||
|
||||
lv_color_t LvglTheme::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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
94
main/display/lvgl_display/lvgl_theme.h
Normal file
94
main/display/lvgl_display/lvgl_theme.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#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:
|
||||
static lv_color_t ParseColor(const std::string& color);
|
||||
|
||||
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_;
|
||||
};
|
||||
408
main/display/oled_display.cc
Normal file
408
main/display/oled_display.cc
Normal file
@@ -0,0 +1,408 @@
|
||||
#include "oled_display.h"
|
||||
#include "assets/lang_config.h"
|
||||
#include "lvgl_theme.h"
|
||||
#include "lvgl_font.h"
|
||||
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_lvgl_port.h>
|
||||
#include <font_awesome.h>
|
||||
|
||||
#define TAG "OledDisplay"
|
||||
|
||||
LV_FONT_DECLARE(BUILTIN_TEXT_FONT);
|
||||
LV_FONT_DECLARE(BUILTIN_ICON_FONT);
|
||||
LV_FONT_DECLARE(font_awesome_30_1);
|
||||
|
||||
OledDisplay::OledDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
|
||||
int width, int height, bool mirror_x, bool mirror_y)
|
||||
: panel_io_(panel_io), panel_(panel) {
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
|
||||
auto text_font = std::make_shared<LvglBuiltInFont>(&BUILTIN_TEXT_FONT);
|
||||
auto icon_font = std::make_shared<LvglBuiltInFont>(&BUILTIN_ICON_FONT);
|
||||
auto large_icon_font = std::make_shared<LvglBuiltInFont>(&font_awesome_30_1);
|
||||
|
||||
auto dark_theme = new LvglTheme("dark");
|
||||
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("dark", dark_theme);
|
||||
current_theme_ = dark_theme;
|
||||
|
||||
ESP_LOGI(TAG, "Initialize LVGL");
|
||||
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
|
||||
port_cfg.task_priority = 1;
|
||||
port_cfg.task_stack = 6144;
|
||||
#if CONFIG_SOC_CPU_CORES_NUM > 1
|
||||
port_cfg.task_affinity = 1;
|
||||
#endif
|
||||
lvgl_port_init(&port_cfg);
|
||||
|
||||
ESP_LOGI(TAG, "Adding OLED display");
|
||||
const lvgl_port_display_cfg_t display_cfg = {
|
||||
.io_handle = panel_io_,
|
||||
.panel_handle = panel_,
|
||||
.control_handle = nullptr,
|
||||
.buffer_size = static_cast<uint32_t>(width_ * height_),
|
||||
.double_buffer = false,
|
||||
.trans_size = 0,
|
||||
.hres = static_cast<uint32_t>(width_),
|
||||
.vres = static_cast<uint32_t>(height_),
|
||||
.monochrome = true,
|
||||
.rotation = {
|
||||
.swap_xy = false,
|
||||
.mirror_x = mirror_x,
|
||||
.mirror_y = mirror_y,
|
||||
},
|
||||
.flags = {
|
||||
.buff_dma = 1,
|
||||
.buff_spiram = 0,
|
||||
.sw_rotate = 0,
|
||||
.full_refresh = 0,
|
||||
.direct_mode = 0,
|
||||
},
|
||||
};
|
||||
|
||||
display_ = lvgl_port_add_disp(&display_cfg);
|
||||
if (display_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to add display");
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: SetupUI() should be called by Application::Initialize(), not in constructor
|
||||
// to ensure lvgl objects are created after the display is fully initialized.
|
||||
}
|
||||
|
||||
void OledDisplay::SetupUI() {
|
||||
// Prevent duplicate calls - if already called, return early
|
||||
if (setup_ui_called_) {
|
||||
ESP_LOGW(TAG, "SetupUI() called multiple times, skipping duplicate call");
|
||||
return;
|
||||
}
|
||||
|
||||
Display::SetupUI(); // Mark SetupUI as called
|
||||
if (height_ == 64) {
|
||||
SetupUI_128x64();
|
||||
} else {
|
||||
SetupUI_128x32();
|
||||
}
|
||||
}
|
||||
|
||||
OledDisplay::~OledDisplay() {
|
||||
if (content_ != nullptr) {
|
||||
lv_obj_del(content_);
|
||||
}
|
||||
|
||||
bool is_128x64_layout = (top_bar_ != nullptr);
|
||||
if (status_bar_ != nullptr && is_128x64_layout) {
|
||||
status_label_ = nullptr;
|
||||
notification_label_ = nullptr;
|
||||
lv_obj_del(status_bar_);
|
||||
}
|
||||
if (top_bar_ != nullptr) {
|
||||
network_label_ = nullptr;
|
||||
mute_label_ = nullptr;
|
||||
battery_label_ = nullptr;
|
||||
lv_obj_del(top_bar_);
|
||||
}
|
||||
if (side_bar_ != nullptr) {
|
||||
if (!is_128x64_layout) {
|
||||
status_label_ = nullptr;
|
||||
notification_label_ = nullptr;
|
||||
network_label_ = nullptr;
|
||||
mute_label_ = nullptr;
|
||||
battery_label_ = nullptr;
|
||||
}
|
||||
lv_obj_del(side_bar_);
|
||||
}
|
||||
if (container_ != nullptr) {
|
||||
lv_obj_del(container_);
|
||||
}
|
||||
|
||||
if (panel_ != nullptr) {
|
||||
esp_lcd_panel_del(panel_);
|
||||
}
|
||||
if (panel_io_ != nullptr) {
|
||||
esp_lcd_panel_io_del(panel_io_);
|
||||
}
|
||||
lvgl_port_deinit();
|
||||
}
|
||||
|
||||
bool OledDisplay::Lock(int timeout_ms) {
|
||||
return lvgl_port_lock(timeout_ms);
|
||||
}
|
||||
|
||||
void OledDisplay::Unlock() {
|
||||
lvgl_port_unlock();
|
||||
}
|
||||
|
||||
void OledDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
DisplayLockGuard lock(this);
|
||||
if (chat_message_label_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace all newlines with spaces
|
||||
std::string content_str = content;
|
||||
std::replace(content_str.begin(), content_str.end(), '\n', ' ');
|
||||
|
||||
if (content_right_ == nullptr) {
|
||||
lv_label_set_text(chat_message_label_, content_str.c_str());
|
||||
} else {
|
||||
if (content == nullptr || content[0] == '\0') {
|
||||
lv_obj_add_flag(content_right_, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_label_set_text(chat_message_label_, content_str.c_str());
|
||||
lv_obj_remove_flag(content_right_, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OledDisplay::SetupUI_128x64() {
|
||||
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, text_font, 0);
|
||||
lv_obj_set_style_text_color(screen, lv_color_black(), 0);
|
||||
|
||||
/* Container */
|
||||
container_ = lv_obj_create(screen);
|
||||
lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES);
|
||||
lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_COLUMN);
|
||||
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);
|
||||
|
||||
/* Layer 1: Top bar - for status icons */
|
||||
top_bar_ = lv_obj_create(container_);
|
||||
lv_obj_set_size(top_bar_, LV_HOR_RES, 16);
|
||||
lv_obj_set_style_radius(top_bar_, 0, 0);
|
||||
lv_obj_set_style_bg_opa(top_bar_, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_border_width(top_bar_, 0, 0);
|
||||
lv_obj_set_style_pad_all(top_bar_, 0, 0);
|
||||
lv_obj_set_flex_flow(top_bar_, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollbar_mode(top_bar_, LV_SCROLLBAR_MODE_OFF);
|
||||
|
||||
network_label_ = lv_label_create(top_bar_);
|
||||
lv_label_set_text(network_label_, "");
|
||||
lv_obj_set_style_text_font(network_label_, icon_font, 0);
|
||||
|
||||
lv_obj_t* right_icons = lv_obj_create(top_bar_);
|
||||
lv_obj_set_size(right_icons, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_bg_opa(right_icons, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_border_width(right_icons, 0, 0);
|
||||
lv_obj_set_style_pad_all(right_icons, 0, 0);
|
||||
lv_obj_set_flex_flow(right_icons, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
mute_label_ = lv_label_create(right_icons);
|
||||
lv_label_set_text(mute_label_, "");
|
||||
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
|
||||
|
||||
battery_label_ = lv_label_create(right_icons);
|
||||
lv_label_set_text(battery_label_, "");
|
||||
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
|
||||
|
||||
/* Layer 2: Status bar - for center text labels */
|
||||
status_bar_ = lv_obj_create(screen);
|
||||
lv_obj_set_size(status_bar_, LV_HOR_RES, 16);
|
||||
lv_obj_set_style_radius(status_bar_, 0, 0);
|
||||
lv_obj_set_style_bg_opa(status_bar_, LV_OPA_TRANSP, 0); // Transparent background
|
||||
lv_obj_set_style_border_width(status_bar_, 0, 0);
|
||||
lv_obj_set_style_pad_all(status_bar_, 0, 0);
|
||||
lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0); // Use absolute positioning
|
||||
lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_
|
||||
|
||||
notification_label_ = lv_label_create(status_bar_);
|
||||
lv_obj_set_width(notification_label_, LV_HOR_RES);
|
||||
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_label_set_text(notification_label_, "");
|
||||
lv_obj_align(notification_label_, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
status_label_ = lv_label_create(status_bar_);
|
||||
lv_obj_set_width(status_label_, LV_HOR_RES);
|
||||
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_label_set_text(status_label_, Lang::Strings::INITIALIZING);
|
||||
lv_obj_align(status_label_, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
/* Content */
|
||||
content_ = lv_obj_create(container_);
|
||||
lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_style_radius(content_, 0, 0);
|
||||
lv_obj_set_style_pad_all(content_, 0, 0);
|
||||
lv_obj_set_width(content_, LV_HOR_RES);
|
||||
lv_obj_set_flex_grow(content_, 1);
|
||||
lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_flex_main_place(content_, LV_FLEX_ALIGN_CENTER, 0);
|
||||
|
||||
content_left_ = lv_obj_create(content_);
|
||||
lv_obj_set_size(content_left_, 32, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(content_left_, 0, 0);
|
||||
lv_obj_set_style_border_width(content_left_, 0, 0);
|
||||
|
||||
emotion_label_ = lv_label_create(content_left_);
|
||||
lv_obj_set_style_text_font(emotion_label_, large_icon_font, 0);
|
||||
lv_label_set_text(emotion_label_, FONT_AWESOME_MICROCHIP_AI);
|
||||
lv_obj_center(emotion_label_);
|
||||
lv_obj_set_style_pad_top(emotion_label_, 8, 0);
|
||||
|
||||
content_right_ = lv_obj_create(content_);
|
||||
lv_obj_set_size(content_right_, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(content_right_, 0, 0);
|
||||
lv_obj_set_style_border_width(content_right_, 0, 0);
|
||||
lv_obj_set_flex_grow(content_right_, 1);
|
||||
lv_obj_add_flag(content_right_, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
chat_message_label_ = lv_label_create(content_right_);
|
||||
lv_label_set_text(chat_message_label_, "");
|
||||
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_LEFT, 0);
|
||||
lv_obj_set_width(chat_message_label_, width_ - 32);
|
||||
lv_obj_set_style_pad_top(chat_message_label_, 14, 0);
|
||||
|
||||
// Start scrolling subtitle after a delay
|
||||
static lv_anim_t a;
|
||||
lv_anim_init(&a);
|
||||
lv_anim_set_delay(&a, 1000);
|
||||
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);
|
||||
lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN);
|
||||
lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN);
|
||||
|
||||
low_battery_popup_ = lv_obj_create(screen);
|
||||
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
|
||||
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);
|
||||
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);
|
||||
lv_obj_center(low_battery_label_);
|
||||
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
void OledDisplay::SetupUI_128x32() {
|
||||
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, text_font, 0);
|
||||
|
||||
/* Container */
|
||||
container_ = lv_obj_create(screen);
|
||||
lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES);
|
||||
lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_all(container_, 0, 0);
|
||||
lv_obj_set_style_border_width(container_, 0, 0);
|
||||
lv_obj_set_style_pad_column(container_, 0, 0);
|
||||
|
||||
/* Emotion label on the left side */
|
||||
content_ = lv_obj_create(container_);
|
||||
lv_obj_set_size(content_, 32, 32);
|
||||
lv_obj_set_style_pad_all(content_, 0, 0);
|
||||
lv_obj_set_style_border_width(content_, 0, 0);
|
||||
lv_obj_set_style_radius(content_, 0, 0);
|
||||
|
||||
emotion_label_ = lv_label_create(content_);
|
||||
lv_obj_set_style_text_font(emotion_label_, large_icon_font, 0);
|
||||
lv_label_set_text(emotion_label_, FONT_AWESOME_MICROCHIP_AI);
|
||||
lv_obj_center(emotion_label_);
|
||||
|
||||
/* Right side */
|
||||
side_bar_ = lv_obj_create(container_);
|
||||
lv_obj_set_size(side_bar_, width_ - 32, 32);
|
||||
lv_obj_set_flex_flow(side_bar_, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_all(side_bar_, 0, 0);
|
||||
lv_obj_set_style_border_width(side_bar_, 0, 0);
|
||||
lv_obj_set_style_radius(side_bar_, 0, 0);
|
||||
lv_obj_set_style_pad_row(side_bar_, 0, 0);
|
||||
|
||||
/* Status bar */
|
||||
status_bar_ = lv_obj_create(side_bar_);
|
||||
lv_obj_set_size(status_bar_, width_ - 32, 16);
|
||||
lv_obj_set_style_radius(status_bar_, 0, 0);
|
||||
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);
|
||||
|
||||
status_label_ = lv_label_create(status_bar_);
|
||||
lv_obj_set_flex_grow(status_label_, 1);
|
||||
lv_obj_set_style_pad_left(status_label_, 2, 0);
|
||||
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
|
||||
|
||||
notification_label_ = lv_label_create(status_bar_);
|
||||
lv_obj_set_flex_grow(notification_label_, 1);
|
||||
lv_obj_set_style_pad_left(notification_label_, 2, 0);
|
||||
lv_label_set_text(notification_label_, "");
|
||||
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
mute_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(mute_label_, "");
|
||||
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_, icon_font, 0);
|
||||
|
||||
battery_label_ = lv_label_create(status_bar_);
|
||||
lv_label_set_text(battery_label_, "");
|
||||
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);
|
||||
lv_obj_set_style_pad_left(chat_message_label_, 2, 0);
|
||||
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
lv_label_set_text(chat_message_label_, "");
|
||||
|
||||
// Start scrolling subtitle after a delay
|
||||
static lv_anim_t a;
|
||||
lv_anim_init(&a);
|
||||
lv_anim_set_delay(&a, 1000);
|
||||
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);
|
||||
lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN);
|
||||
lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN);
|
||||
}
|
||||
|
||||
void OledDisplay::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 OledDisplay::SetTheme(Theme* theme) {
|
||||
DisplayLockGuard lock(this);
|
||||
|
||||
auto lvgl_theme = static_cast<LvglTheme*>(theme);
|
||||
auto text_font = lvgl_theme->text_font()->font();
|
||||
|
||||
auto screen = lv_screen_active();
|
||||
lv_obj_set_style_text_font(screen, text_font, 0);
|
||||
}
|
||||
41
main/display/oled_display.h
Normal file
41
main/display/oled_display.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifndef OLED_DISPLAY_H
|
||||
#define OLED_DISPLAY_H
|
||||
|
||||
#include "lvgl_display.h"
|
||||
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
|
||||
|
||||
class OledDisplay : public LvglDisplay {
|
||||
private:
|
||||
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
|
||||
esp_lcd_panel_handle_t panel_ = nullptr;
|
||||
|
||||
lv_obj_t* top_bar_ = nullptr;
|
||||
lv_obj_t* status_bar_ = nullptr;
|
||||
lv_obj_t* content_ = nullptr;
|
||||
lv_obj_t* content_left_ = nullptr;
|
||||
lv_obj_t* content_right_ = nullptr;
|
||||
lv_obj_t* container_ = nullptr;
|
||||
lv_obj_t* side_bar_ = nullptr;
|
||||
lv_obj_t *emotion_label_ = nullptr;
|
||||
lv_obj_t* chat_message_label_ = nullptr;
|
||||
|
||||
virtual bool Lock(int timeout_ms = 0) override;
|
||||
virtual void Unlock() override;
|
||||
|
||||
void SetupUI_128x64();
|
||||
void SetupUI_128x32();
|
||||
|
||||
public:
|
||||
OledDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height, bool mirror_x, bool mirror_y);
|
||||
~OledDisplay();
|
||||
|
||||
virtual void SetupUI() override;
|
||||
virtual void SetChatMessage(const char* role, const char* content) override;
|
||||
virtual void SetEmotion(const char* emotion) override;
|
||||
virtual void SetTheme(Theme* theme) override;
|
||||
};
|
||||
|
||||
#endif // OLED_DISPLAY_H
|
||||
Reference in New Issue
Block a user