Compare commits

...

2 Commits

Author SHA1 Message Date
CQ793
277cbd10ac add esp32-s3-rgb-matrix (#2026)
* Content: add esp32-s3-rgb-matrix

* Content: fixed config.json error

* fixed-esp-hub75 version

* Fixed introduction error of version yml component
2026-05-29 06:07:08 +08:00
3em0
e182471f8c Validate MQTT goodbye session id (#2023)
Co-authored-by: Codex <codex@local>
2026-05-29 05:55:37 +08:00
10 changed files with 720 additions and 2 deletions

View File

@@ -374,6 +374,12 @@ elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_83)
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
set(BUILTIN_ICON_FONT font_awesome_16_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_RGB_MATRIX)
set(MANUFACTURER "waveshare")
set(BOARD_TYPE "esp32-s3-rgb-matrix")
set(BUILTIN_TEXT_FONT font_puhui_basic_14_1)
set(BUILTIN_ICON_FONT font_awesome_14_1)
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_85C)
set(MANUFACTURER "waveshare")
set(BOARD_TYPE "esp32-s3-touch-lcd-1.85c")

View File

@@ -311,6 +311,9 @@ choice BOARD_TYPE
config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_83
bool "Waveshare ESP32-S3-Touch-LCD-1.83"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_WAVESHARE_ESP32_S3_RGB_MATRIX
bool "Waveshare ESP32-S3-RGB-Matrix"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_4B
bool "Waveshare ESP32-S3-Touch-LCD-4B"
depends on IDF_TARGET_ESP32S3

View File

@@ -0,0 +1,3 @@
新增 微雪 开发板: ESP32-S3-RGB-Matrix
产品链接:
https://www.waveshare.net/shop/ESP32-S3-RGB-Matrix.htm

View File

@@ -0,0 +1,54 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
// 音频采样率
#define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 16000
#define AUDIO_INPUT_REFERENCE true
// I2S 引脚
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_12
#define AUDIO_I2S_GPIO_WS GPIO_NUM_38
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_43
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_39
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_21
// Audio Codec 相关
#define AUDIO_CODEC_PA_PIN GPIO_NUM_11
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_47
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_48
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR
#define HUB75_MIN_REFRESH_RATE 240
#define HUB75_PANEL_WIDTH 64
#define HUB75_PANEL_HEIGHT 64
#define HUB75_CHAIN_COLUMNS 1
#define HUB75_CHAIN_ROWS 1
#define HUB75_SCAN_WIRING Hub75ScanWiring::STANDARD_TWO_SCAN
#define HUB75_SHIFT_DRIVER Hub75ShiftDriver::FM6126A
#define HUB75_R1 GPIO_NUM_4
#define HUB75_G1 GPIO_NUM_5
#define HUB75_B1 GPIO_NUM_6
#define HUB75_R2 GPIO_NUM_7
#define HUB75_G2 GPIO_NUM_15
#define HUB75_B2 GPIO_NUM_16
#define HUB75_A GPIO_NUM_18
#define HUB75_B GPIO_NUM_8
#define HUB75_C GPIO_NUM_3
#define HUB75_D GPIO_NUM_42
#define HUB75_E GPIO_NUM_9
#define HUB75_CLK GPIO_NUM_41
#define HUB75_LAT GPIO_NUM_40
#define HUB75_OE GPIO_NUM_2
#define DISPLAY_WIDTH (HUB75_PANEL_WIDTH * HUB75_CHAIN_COLUMNS)
#define DISPLAY_HEIGHT (HUB75_PANEL_HEIGHT * HUB75_CHAIN_ROWS)
#define LVGL_DMA_BUFF_LEN (DISPLAY_WIDTH * HUB75_PANEL_HEIGHT * 1)
#define LVGL_SPIRAM_BUFF_LEN (DISPLAY_WIDTH * DISPLAY_HEIGHT * 1)
#endif // _BOARD_CONFIG_H_

View File

@@ -0,0 +1,13 @@
{
"manufacturer": "waveshare",
"target": "esp32s3",
"builds": [
{
"name": "esp32-s3-rgb-matrix",
"sdkconfig_append": [
"CONFIG_USE_WECHAT_MESSAGE_STYLE=n",
"CONFIG_USE_DEVICE_AEC=y"
]
}
]
}

View File

@@ -0,0 +1,96 @@
#include <driver/i2c_master.h>
#include "codecs/box_audio_codec.h"
#include "config.h"
#include "rgb_matrix_display.h"
#include "settings.h"
#include "wifi_board.h"
class CustomBoard : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
CustomMatrixDisplay* display_;
void init_I2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)I2C_NUM_0,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags =
{
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
}
void init_HUB75() {
display_ = new CustomMatrixDisplay(DISPLAY_WIDTH, DISPLAY_HEIGHT);
}
void init_Audio() {
if (AUDIO_CODEC_PA_PIN != GPIO_NUM_NC) {
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = (1ULL << AUDIO_CODEC_PA_PIN);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
ESP_ERROR_CHECK(gpio_config(&io_conf));
gpio_set_level(AUDIO_CODEC_PA_PIN, 1);
}
Settings settings("audio", false);
const int stored_volume = settings.GetInt("output_volume", 70);
if (stored_volume > 0) {
return;
}
Settings persist("audio", true);
persist.SetInt("output_volume", 70);
}
public:
CustomBoard() {
init_I2c();
init_Audio();
init_HUB75();
}
virtual AudioCodec* GetAudioCodec() override {
static BoxAudioCodec audio_codec(
i2c_bus_, AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN,
AUDIO_CODEC_PA_PIN, AUDIO_CODEC_ES8311_ADDR, AUDIO_CODEC_ES7210_ADDR,
AUDIO_INPUT_REFERENCE);
return &audio_codec;
}
virtual Display* GetDisplay() override { return display_; }
virtual Backlight* GetBacklight() override {
class MatrixBacklight : public Backlight {
public:
explicit MatrixBacklight(CustomMatrixDisplay* display) : display_(display) {}
protected:
CustomMatrixDisplay* display_;
void SetBrightnessImpl(uint8_t brightness) override {
if (display_ == nullptr) {
return;
}
display_->SetBrightness(brightness);
}
};
static MatrixBacklight backlight(display_);
return &backlight;
}
};
DECLARE_BOARD(CustomBoard);

View File

@@ -0,0 +1,493 @@
#include "rgb_matrix_display.h"
#include <esp_heap_caps.h>
#include <esp_log.h>
#include <esp_lvgl_port.h>
#include <font_awesome.h>
#include <stdio.h>
#include <time.h>
#include <cstring>
#include <string>
#include "application.h"
#include "assets/lang_config.h"
#include "board.h"
#include "config.h"
#include "emoji_collection.h"
#include "hub75.h"
// 声明中文字体
LV_FONT_DECLARE(font_puhui_14_1);
LV_FONT_DECLARE(BUILTIN_ICON_FONT);
LV_FONT_DECLARE(font_awesome_30_4);
// 声明32x32 Emoji集合
class Twemoji32;
struct Hub75Context {
Hub75Driver driver;
};
namespace {
const char* LogTag = "Hub75Display";
constexpr size_t Len = 16;
bool StartsWith(const char* text, const char* prefix) {
return strncmp(text, prefix, strlen(prefix)) == 0;
}
bool IsInlineSpace(char ch) { return (ch == ' ') || (ch == '\r') || (ch == '\n') || (ch == '\t'); }
bool UseBuiltInEmotionIcon(const char* emotion) {
if (emotion == nullptr) {
return false;
}
if (strcmp(emotion, "microchip_ai") == 0) {
return true;
}
if (strcmp(emotion, "link") == 0) {
return true;
}
return false;
}
const char* MakeVersionText(const char* text) {
const char* p = text + strlen(Lang::Strings::VERSION);
while (*p == ' ') {
p++;
}
if (*p == '\0') {
return "v";
}
thread_local char buf[3 + Len]{0};
buf[0] = 'v';
buf[1] = ':';
size_t n = 0;
while (n < Len) {
const char c = *p++;
if (c == '\0') {
break;
}
buf[2 + n] = c;
n++;
}
buf[2 + n] = '\0';
return buf;
}
std::string MakeSingleLineText(const char* text) {
if (text == nullptr) {
return "";
}
std::string out;
out.reserve(strlen(text));
bool prev_space = true;
while (*text != '\0') {
const char ch = *text++;
if (IsInlineSpace(ch)) {
if (!prev_space) {
out.push_back(' ');
}
prev_space = true;
continue;
}
out.push_back(ch);
prev_space = false;
}
if (!out.empty() && out.back() == ' ') {
out.pop_back();
}
return out;
}
std::string TransformMessageText(const char* role, const char* content) {
if (content == nullptr) {
return "";
}
if ((role != nullptr) && (strcmp(role, "system") == 0)) {
return MakeSingleLineText(content);
}
return content;
}
// 重新映射状态文本,将原始文本更换为更简短的文本
const char* RemapStatusText(const char* text) {
if (text == nullptr) {
return nullptr;
}
struct MapItem {
const char* from;
const char* to;
};
static const MapItem Map[] = {
{Lang::Strings::SCANNING_WIFI, "扫描中"},
{Lang::Strings::CONNECTING, "连接中"},
{Lang::Strings::WIFI_CONFIG_MODE, "配网中"},
{Lang::Strings::CHECKING_NEW_VERSION, "检查中"},
{Lang::Strings::LOADING_PROTOCOL, "登录中"},
{Lang::Strings::REGISTERING_NETWORK, "配网中"},
{Lang::Strings::DETECTING_MODULE, "检测中"},
{Lang::Strings::ACTIVATION, "激活中"},
{Lang::Strings::PLEASE_WAIT, "等待中"},
{Lang::Strings::LISTENING, "聆听"},
{Lang::Strings::SPEAKING, "说话"},
{Lang::Strings::STANDBY, "待命"},
};
size_t i = 0;
while (true) {
if (i >= (sizeof(Map) / sizeof(Map[0]))) {
break;
}
if (strcmp(text, Map[i].from) == 0) {
return Map[i].to;
}
i++;
}
if (StartsWith(text, Lang::Strings::CONNECT_TO)) {
return "连接中";
}
if (StartsWith(text, Lang::Strings::CONNECTED_TO)) {
return "已连接";
}
if (StartsWith(text, Lang::Strings::VERSION)) {
return MakeVersionText(text);
}
return text;
}
const char* TransformStatusText(const char* text) { return RemapStatusText(text); }
void SetObjectVisible(lv_obj_t* obj, bool visible) {
if (obj == nullptr) {
return;
}
if (visible) {
lv_obj_remove_flag(obj, LV_OBJ_FLAG_HIDDEN);
return;
}
lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN);
}
} // namespace
CustomMatrixDisplay::CustomMatrixDisplay(int width, int height) : LvglDisplay() {
width_ = width;
height_ = height;
Hub75Config hub75_config{};
hub75_config.min_refresh_rate = HUB75_MIN_REFRESH_RATE;
hub75_config.panel_width = HUB75_PANEL_WIDTH;
hub75_config.panel_height = HUB75_PANEL_HEIGHT;
hub75_config.scan_wiring = HUB75_SCAN_WIRING;
hub75_config.shift_driver = HUB75_SHIFT_DRIVER;
hub75_config.layout_cols = HUB75_CHAIN_COLUMNS;
hub75_config.layout_rows = HUB75_CHAIN_ROWS;
hub75_config.pins.r1 = static_cast<int>(HUB75_R1);
hub75_config.pins.g1 = static_cast<int>(HUB75_G1);
hub75_config.pins.b1 = static_cast<int>(HUB75_B1);
hub75_config.pins.r2 = static_cast<int>(HUB75_R2);
hub75_config.pins.g2 = static_cast<int>(HUB75_G2);
hub75_config.pins.b2 = static_cast<int>(HUB75_B2);
hub75_config.pins.a = static_cast<int>(HUB75_A);
hub75_config.pins.b = static_cast<int>(HUB75_B);
hub75_config.pins.c = static_cast<int>(HUB75_C);
hub75_config.pins.d = static_cast<int>(HUB75_D);
hub75_config.pins.e = static_cast<int>(HUB75_E);
hub75_config.pins.lat = static_cast<int>(HUB75_LAT);
hub75_config.pins.oe = static_cast<int>(HUB75_OE);
hub75_config.pins.clk = static_cast<int>(HUB75_CLK);
hub75_context_ = new Hub75Context{Hub75Driver(hub75_config)};
const bool hub75_begin_ok = hub75_context_->driver.begin();
if (!hub75_begin_ok) {
ESP_LOGE(LogTag, "Hub75 begin failed");
delete hub75_context_;
hub75_context_ = nullptr;
return;
}
hub75_context_->driver.clear();
lv_init();
lvgl_port_cfg_t lvgl_port_config = ESP_LVGL_PORT_INIT_CONFIG();
lvgl_port_config.task_priority = 2;
lvgl_port_config.timer_period_ms = 16;
lvgl_port_init(&lvgl_port_config);
// Lock before creating display
lvgl_port_lock(0);
const int ui_width_px = width_;
const int ui_height_px = height_;
const size_t render_buffer_pixels = static_cast<size_t>(ui_width_px) * 40;
const size_t render_buffer_bytes =
render_buffer_pixels * LV_COLOR_FORMAT_GET_SIZE(LV_COLOR_FORMAT_RGB565);
void* render_buffer_1 =
heap_caps_malloc(render_buffer_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (render_buffer_1 == nullptr) {
ESP_LOGE(LogTag, "LVGL buffer alloc failed");
lvgl_port_unlock();
return;
}
display_ = lv_display_create(ui_width_px, ui_height_px);
if (display_ == nullptr) {
ESP_LOGE(LogTag, "LVGL display create failed");
free(render_buffer_1);
lvgl_port_unlock();
return;
}
lv_display_set_flush_cb(display_, LvglFlushCallback);
lv_display_set_user_data(display_, this);
lv_display_set_buffers(display_, render_buffer_1, nullptr, render_buffer_bytes,
LV_DISPLAY_RENDER_MODE_PARTIAL);
lvgl_port_unlock();
}
bool CustomMatrixDisplay::Lock(int timeout_ms) { return lvgl_port_lock(timeout_ms); }
void CustomMatrixDisplay::Unlock() { lvgl_port_unlock(); }
void CustomMatrixDisplay::SetupUI() {
if (setup_ui_called_) {
return;
}
Display::SetupUI();
// 初始化 Emoji 资源(用于 SetEmotion
emoji_collection_ = std::make_shared<Twemoji32>();
const int ui_width_px = width_;
const int ui_height_px = height_;
// 屏幕根对象:全黑背景
auto* screen = lv_screen_active();
lv_obj_set_style_bg_color(screen, lv_color_black(), 0);
// 主容器:承载所有控件
main_container_ = lv_obj_create(screen);
lv_obj_set_size(main_container_, ui_width_px, ui_height_px);
lv_obj_set_style_bg_color(main_container_, lv_color_black(), 0);
lv_obj_set_style_border_width(main_container_, 0, 0);
lv_obj_set_style_pad_all(main_container_, 0, 0);
lv_obj_center(main_container_);
// 左上角WiFi 图标UpdateStatusBar 更新内容)
network_label_ = lv_label_create(main_container_);
lv_label_set_text(network_label_, "");
lv_obj_set_style_text_font(network_label_, &BUILTIN_ICON_FONT, 0);
lv_obj_set_style_text_color(network_label_, lv_color_white(), 0);
lv_obj_align(network_label_, LV_ALIGN_TOP_LEFT, 0, 0);
// 右上角:状态文本 + 时间
status_label_ = lv_label_create(main_container_);
lv_obj_set_size(status_label_, ui_width_px - 16, 16);
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_CLIP);
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_set_style_text_color(status_label_, lv_color_white(), 0);
lv_obj_set_style_text_font(status_label_, &font_puhui_14_1, 0);
lv_obj_align(status_label_, LV_ALIGN_TOP_RIGHT, 0, -1);
status_text_ = "初始化";
RefreshStatusLabelLocked();
// 底部滚动文本SetChatMessage 使用
message_label_ = lv_label_create(main_container_);
lv_obj_set_width(message_label_, ui_width_px);
lv_label_set_long_mode(message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_style_text_align(message_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(message_label_, lv_color_white(), 0);
lv_obj_set_style_text_font(message_label_, &font_puhui_14_1, 0);
lv_obj_align(message_label_, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_label_set_text(message_label_, "hi 小智");
// Emoji 图片SetEmotion 使用
emoji_image_ = lv_image_create(main_container_);
lv_obj_align(emoji_image_, LV_ALIGN_CENTER, 0, -1);
lv_obj_add_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN);
emotion_icon_label_ = lv_label_create(main_container_);
lv_label_set_text(emotion_icon_label_, FONT_AWESOME_MICROCHIP_AI);
lv_obj_set_style_text_font(emotion_icon_label_, &font_awesome_30_4, 0);
lv_obj_set_style_text_color(emotion_icon_label_, lv_color_white(), 0);
lv_obj_align(emotion_icon_label_, LV_ALIGN_CENTER, 0, -1);
lv_obj_remove_flag(emotion_icon_label_, LV_OBJ_FLAG_HIDDEN);
}
void CustomMatrixDisplay::SetEmotion(const char* emotion) {
DisplayLockGuard lock(this);
if (emotion == nullptr) {
SetObjectVisible(emoji_image_, false);
SetObjectVisible(emotion_icon_label_, false);
return;
}
// 尝试获取表情图片
const LvglImage* emoji_lvgl_image = nullptr;
if (emoji_collection_ && !UseBuiltInEmotionIcon(emotion)) {
emoji_lvgl_image = emoji_collection_->GetEmojiImage(emotion);
}
if (emoji_lvgl_image != nullptr) {
if (emoji_image_ == nullptr) {
return;
}
lv_image_set_src(emoji_image_, emoji_lvgl_image->image_dsc());
SetObjectVisible(emoji_image_, true);
SetObjectVisible(emotion_icon_label_, false);
return;
}
SetObjectVisible(emoji_image_, false);
if (emotion_icon_label_ == nullptr) {
return;
}
lv_label_set_text(emotion_icon_label_, FONT_AWESOME_MICROCHIP_AI);
SetObjectVisible(emotion_icon_label_, true);
}
void CustomMatrixDisplay::SetChatMessage(const char* role, const char* content) {
DisplayLockGuard lock(this);
if (message_label_ == nullptr) {
return;
}
const std::string text = TransformMessageText(role, content);
lv_label_set_text(message_label_, text.c_str());
SetObjectVisible(message_label_, !text.empty());
}
void CustomMatrixDisplay::SetStatus(const char* status) {
DisplayLockGuard lock(this);
status_text_ = TransformStatusText(status);
RefreshStatusLabelLocked();
last_status_update_time_ = std::chrono::system_clock::now();
}
void CustomMatrixDisplay::ShowNotification(const char* notification, int duration_ms) {
static_cast<void>(duration_ms);
SetStatus(notification);
}
void CustomMatrixDisplay::UpdateStatusBar(bool update_all) {
static_cast<void>(update_all);
auto& app = Application::GetInstance();
auto& board = Board::GetInstance();
const char* network_icon = board.GetNetworkStateIcon();
{
DisplayLockGuard lock(this);
if (network_label_ != nullptr && network_icon != nullptr) {
lv_label_set_text(network_label_, network_icon);
}
}
time_t now = time(nullptr);
tm timeinfo{};
localtime_r(&now, &timeinfo);
if (timeinfo.tm_year < 2025 - 1900) {
return;
}
char buf[6]{0};
snprintf(buf, sizeof(buf), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
const bool idle = app.GetDeviceState() == kDeviceStateIdle;
if (idle) {
const auto now_tp = std::chrono::system_clock::now();
if (last_status_update_time_ + std::chrono::seconds(10) < now_tp) {
status_text_.clear();
time_text_ = buf;
last_status_update_time_ = now_tp;
}
}
if (!idle) {
time_text_.clear();
}
DisplayLockGuard lock(this);
RefreshStatusLabelLocked();
}
CustomMatrixDisplay::~CustomMatrixDisplay() {
if (hub75_context_ == nullptr) {
return;
}
hub75_context_->driver.end();
delete hub75_context_;
hub75_context_ = nullptr;
}
void CustomMatrixDisplay::SetBrightness(uint8_t brightness_0_100) {
if (hub75_context_ == nullptr) {
return;
}
if (brightness_0_100 > 100) {
brightness_0_100 = 100;
}
uint32_t value = static_cast<uint32_t>(brightness_0_100) * 255u;
uint8_t basis = static_cast<uint8_t>(value / 100u);
hub75_context_->driver.set_brightness(basis);
}
void CustomMatrixDisplay::RefreshStatusLabelLocked() {
if (status_label_ == nullptr) {
return;
}
std::string text = status_text_;
if (!time_text_.empty()) {
if (!text.empty()) {
text += " ";
}
text += time_text_;
}
lv_label_set_text(status_label_, text.c_str());
SetObjectVisible(status_label_, true);
}
void CustomMatrixDisplay::LvglFlushCallback(lv_display_t* disp, const lv_area_t* area,
uint8_t* color_map) {
if (disp == nullptr) {
return;
}
auto* display = static_cast<CustomMatrixDisplay*>(lv_display_get_user_data(disp));
if (display == nullptr) {
lv_disp_flush_ready(disp);
return;
}
if (display->hub75_context_ == nullptr) {
lv_disp_flush_ready(disp);
return;
}
const auto* pixel_buffer = reinterpret_cast<const uint16_t*>(color_map);
const uint16_t start_x = static_cast<uint16_t>(area->x1);
const uint16_t start_y = static_cast<uint16_t>(area->y1);
const uint16_t width_px = static_cast<uint16_t>(area->x2 - area->x1 + 1);
const uint16_t height_px = static_cast<uint16_t>(area->y2 - area->y1 + 1);
display->hub75_context_->driver.draw_pixels(
start_x, start_y, width_px, height_px, reinterpret_cast<const uint8_t*>(pixel_buffer),
Hub75PixelFormat::RGB565, Hub75ColorOrder::RGB, false);
lv_disp_flush_ready(disp);
}

View File

@@ -0,0 +1,46 @@
#ifndef __CUSTOM_MATRIX_DISPLAY_H__
#define __CUSTOM_MATRIX_DISPLAY_H__
#include <memory>
#include <string>
#include "lvgl_display.h"
struct Hub75Context;
class EmojiCollection;
class CustomMatrixDisplay : public LvglDisplay {
public:
CustomMatrixDisplay(int width, int height);
~CustomMatrixDisplay();
void SetBrightness(uint8_t brightness_0_100);
virtual void SetEmotion(const char* emotion) override;
virtual void SetChatMessage(const char* role, const char* content) override;
virtual void SetStatus(const char* status) override;
virtual void ShowNotification(const char* notification, int duration_ms = 3000) override;
virtual void UpdateStatusBar(bool update_all = false) override;
protected:
virtual bool Lock(int timeout_ms = 0) override;
virtual void Unlock() override;
private:
void SetupUI();
void RefreshStatusLabelLocked();
static void LvglFlushCallback(lv_display_t* disp, const lv_area_t* area, uint8_t* color_map);
Hub75Context* hub75_context_ = nullptr;
// UI 控件
lv_obj_t* main_container_ = nullptr;
lv_obj_t* emoji_image_ = nullptr;
lv_obj_t* emotion_icon_label_ = nullptr;
lv_obj_t* message_label_ = nullptr;
std::shared_ptr<EmojiCollection> emoji_collection_ = nullptr;
std::string status_text_;
std::string time_text_;
};
#endif // __CUSTOM_MATRIX_DISPLAY_H__

View File

@@ -34,6 +34,10 @@ dependencies:
espressif/esp-sr: ~2.3.0
espressif/button: ~4.1.5
espressif/knob: ^1.0.0
esphome/esp-hub75:
version: ^0.3.5
rules:
- if: target in [esp32p4, esp32s3]
espressif/esp32-camera:
version: ^2.1.6
rules:

View File

@@ -114,8 +114,8 @@ bool MqttProtocol::StartMqttClient(bool report_error) {
ParseServerHello(root);
} else if (strcmp(type->valuestring, "goodbye") == 0) {
auto session_id = cJSON_GetObjectItem(root, "session_id");
ESP_LOGI(TAG, "Received goodbye message, session_id: %s", session_id ? session_id->valuestring : "null");
if (session_id == nullptr || session_id_ == session_id->valuestring) {
ESP_LOGI(TAG, "Received goodbye message, session_id: %s", cJSON_IsString(session_id) ? session_id->valuestring : "null");
if (cJSON_IsString(session_id) && session_id_ == session_id->valuestring) {
auto alive = alive_; // Capture alive flag
Application::GetInstance().Schedule([this, alive]() {
if (*alive) {