mirror of
https://github.com/78/xiaozhi-esp32.git
synced 2026-06-30 01:34:42 +08:00
Compare commits
2 Commits
331d69b83b
...
277cbd10ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
277cbd10ac | ||
|
|
e182471f8c |
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
3
main/boards/waveshare/esp32-s3-rgb-matrix/README.md
Normal file
3
main/boards/waveshare/esp32-s3-rgb-matrix/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
新增 微雪 开发板: ESP32-S3-RGB-Matrix
|
||||
产品链接:
|
||||
https://www.waveshare.net/shop/ESP32-S3-RGB-Matrix.htm
|
||||
54
main/boards/waveshare/esp32-s3-rgb-matrix/config.h
Normal file
54
main/boards/waveshare/esp32-s3-rgb-matrix/config.h
Normal 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_
|
||||
13
main/boards/waveshare/esp32-s3-rgb-matrix/config.json
Normal file
13
main/boards/waveshare/esp32-s3-rgb-matrix/config.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
493
main/boards/waveshare/esp32-s3-rgb-matrix/rgb_matrix_display.cc
Normal file
493
main/boards/waveshare/esp32-s3-rgb-matrix/rgb_matrix_display.cc
Normal 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);
|
||||
}
|
||||
@@ -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__
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user