forked from xiaozhi/xiaozhi-esp32
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe7ae99a4d | ||
|
|
147d71b9f1 | ||
|
|
384da9fd0f | ||
|
|
ae40f72a39 | ||
|
|
d0ba3a923c |
@@ -4,7 +4,7 @@
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(PROJECT_VER "2.0.0")
|
||||
set(PROJECT_VER "2.0.1")
|
||||
|
||||
# Add this line to disable the specific warning
|
||||
add_compile_options(-Wno-missing-field-initializers)
|
||||
|
||||
@@ -516,6 +516,11 @@ elseif(CONFIG_BOARD_TYPE_SURFER_C3_1_14TFT)
|
||||
set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
|
||||
set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
|
||||
set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_32})
|
||||
elseif(CONFIG_BOARD_TYPE_YUNLIAO_S3)
|
||||
set(BOARD_TYPE "yunliao-s3")
|
||||
set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
|
||||
set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
|
||||
set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
|
||||
endif()
|
||||
|
||||
file(GLOB BOARD_SOURCES
|
||||
|
||||
@@ -370,6 +370,9 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_SURFER_C3_1_14TFT
|
||||
bool "Surfer-C3-1-14TFT"
|
||||
depends on IDF_TARGET_ESP32C3
|
||||
config BOARD_TYPE_YUNLIAO_S3
|
||||
bool "小智云聊-S3"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
endchoice
|
||||
|
||||
choice ESP_S3_LCD_EV_Board_Version_TYPE
|
||||
@@ -529,7 +532,7 @@ config USE_AUDIO_PROCESSOR
|
||||
config USE_DEVICE_AEC
|
||||
bool "Enable Device-Side AEC"
|
||||
default n
|
||||
depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE || BOARD_TYPE_LICHUANG_DEV || BOARD_TYPE_ESP32S3_KORVO2_V3 || BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75 || BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06 || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC || BOARD_TYPE_ESP_S3_LCD_EV_Board_2)
|
||||
depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE || BOARD_TYPE_LICHUANG_DEV || BOARD_TYPE_ESP32S3_KORVO2_V3 || BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75 || BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06 || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC || BOARD_TYPE_ESP_S3_LCD_EV_Board_2 || BOARD_TYPE_YUNLIAO_S3)
|
||||
help
|
||||
因为性能不够,不建议和微信聊天界面风格同时开启
|
||||
|
||||
|
||||
@@ -540,6 +540,12 @@ void Application::Start() {
|
||||
// Play the success sound to indicate the device is ready
|
||||
audio_service_.PlaySound(Lang::Sounds::OGG_SUCCESS);
|
||||
}
|
||||
|
||||
// Start the main event loop task with priority 3
|
||||
xTaskCreate([](void* arg) {
|
||||
((Application*)arg)->MainEventLoop();
|
||||
vTaskDelete(NULL);
|
||||
}, "main_event_loop", 2048 * 4, this, 3, &main_event_loop_task_handle_);
|
||||
}
|
||||
|
||||
// Add a async task to MainLoop
|
||||
@@ -555,9 +561,6 @@ void Application::Schedule(std::function<void()> callback) {
|
||||
// If other tasks need to access the websocket or chat state,
|
||||
// they should use Schedule to call this function
|
||||
void Application::MainEventLoop() {
|
||||
// Raise the priority of the main event loop to avoid being interrupted by background tasks (which has priority 2)
|
||||
vTaskPrioritySet(NULL, 3);
|
||||
|
||||
while (true) {
|
||||
auto bits = xEventGroupWaitBits(event_group_, MAIN_EVENT_SCHEDULE |
|
||||
MAIN_EVENT_SEND_AUDIO |
|
||||
@@ -832,11 +835,20 @@ bool Application::CanEnterSleepMode() {
|
||||
}
|
||||
|
||||
void Application::SendMcpMessage(const std::string& payload) {
|
||||
Schedule([this, payload]() {
|
||||
if (protocol_) {
|
||||
if (protocol_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure you are using main thread to send MCP message
|
||||
if (xTaskGetCurrentTaskHandle() == main_event_loop_task_handle_) {
|
||||
ESP_LOGI(TAG, "Send MCP message in main thread");
|
||||
protocol_->SendMcpMessage(payload);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Send MCP message in sub thread");
|
||||
Schedule([this, payload = std::move(payload)]() {
|
||||
protocol_->SendMcpMessage(payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Application::SetAecMode(AecMode mode) {
|
||||
|
||||
@@ -83,6 +83,7 @@ private:
|
||||
bool aborted_ = false;
|
||||
int clock_ticks_ = 0;
|
||||
TaskHandle_t check_new_version_task_handle_ = nullptr;
|
||||
TaskHandle_t main_event_loop_task_handle_ = nullptr;
|
||||
|
||||
void OnWakeWordDetected();
|
||||
void CheckNewVersion(Ota& ota);
|
||||
@@ -91,4 +92,19 @@ private:
|
||||
void SetListeningMode(ListeningMode mode);
|
||||
};
|
||||
|
||||
|
||||
class TaskPriorityReset {
|
||||
public:
|
||||
TaskPriorityReset(BaseType_t priority) {
|
||||
original_priority_ = uxTaskPriorityGet(NULL);
|
||||
vTaskPrioritySet(NULL, priority);
|
||||
}
|
||||
~TaskPriorityReset() {
|
||||
vTaskPrioritySet(NULL, original_priority_);
|
||||
}
|
||||
|
||||
private:
|
||||
BaseType_t original_priority_;
|
||||
};
|
||||
|
||||
#endif // _APPLICATION_H_
|
||||
|
||||
@@ -100,11 +100,11 @@ void AudioService::Start() {
|
||||
|
||||
#if CONFIG_USE_AUDIO_PROCESSOR
|
||||
/* Start the audio input task */
|
||||
xTaskCreatePinnedToCore([](void* arg) {
|
||||
xTaskCreate([](void* arg) {
|
||||
AudioService* audio_service = (AudioService*)arg;
|
||||
audio_service->AudioInputTask();
|
||||
vTaskDelete(NULL);
|
||||
}, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_, 1);
|
||||
}, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_);
|
||||
|
||||
/* Start the audio output task */
|
||||
xTaskCreate([](void* arg) {
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
|
||||
Es8388AudioCodec::Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
|
||||
gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
|
||||
gpio_num_t pa_pin, uint8_t es8388_addr) {
|
||||
gpio_num_t pa_pin, uint8_t es8388_addr, bool input_reference) {
|
||||
duplex_ = true; // 是否双工
|
||||
input_reference_ = false; // 是否使用参考输入,实现回声消除
|
||||
input_channels_ = 1; // 输入通道数
|
||||
input_reference_ = input_reference; // 是否使用参考输入,实现回声消除
|
||||
input_channels_ = input_reference_ ? 2 : 1; // 输入通道数
|
||||
input_sample_rate_ = input_sample_rate;
|
||||
output_sample_rate_ = output_sample_rate;
|
||||
pa_pin_ = pa_pin; CreateDuplexChannels(mclk, bclk, ws, dout, din);
|
||||
pa_pin_ = pa_pin;
|
||||
CreateDuplexChannels(mclk, bclk, ws, dout, din);
|
||||
|
||||
// Do initialize of related interface: data_if, ctrl_if and gpio_if
|
||||
audio_codec_i2s_cfg_t i2s_cfg = {
|
||||
@@ -144,13 +145,21 @@ void Es8388AudioCodec::EnableInput(bool enable) {
|
||||
if (enable) {
|
||||
esp_codec_dev_sample_info_t fs = {
|
||||
.bits_per_sample = 16,
|
||||
.channel = 1,
|
||||
.channel_mask = 0,
|
||||
.channel = (uint8_t) input_channels_,
|
||||
.channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
|
||||
.sample_rate = (uint32_t)input_sample_rate_,
|
||||
.mclk_multiple = 0,
|
||||
};
|
||||
if (input_reference_) {
|
||||
fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1);
|
||||
}
|
||||
ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
|
||||
ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 24.0));
|
||||
if (input_reference_) {
|
||||
uint8_t gain = (11 << 4) + 0;
|
||||
ctrl_if_->write_reg(ctrl_if_, 0x09, 1, &gain, 1);
|
||||
}else{
|
||||
ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 24.0));
|
||||
}
|
||||
} else {
|
||||
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
|
||||
}
|
||||
@@ -175,6 +184,9 @@ void Es8388AudioCodec::EnableOutput(bool enable) {
|
||||
|
||||
// Set analog output volume to 0dB, default is -45dB
|
||||
uint8_t reg_val = 30; // 0dB
|
||||
if(input_reference_){
|
||||
reg_val = 27;
|
||||
}
|
||||
uint8_t regs[] = { 46, 47, 48, 49 }; // HP_LVOL, HP_RVOL, SPK_LVOL, SPK_RVOL
|
||||
for (uint8_t reg : regs) {
|
||||
ctrl_if_->write_reg(ctrl_if_, reg, 1, ®_val, 1);
|
||||
@@ -200,7 +212,7 @@ int Es8388AudioCodec::Read(int16_t* dest, int samples) {
|
||||
}
|
||||
|
||||
int Es8388AudioCodec::Write(const int16_t* data, int samples) {
|
||||
if (output_enabled_) {
|
||||
if (output_enabled_ && output_dev_ && data != nullptr) {
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
|
||||
}
|
||||
return samples;
|
||||
|
||||
@@ -29,7 +29,7 @@ private:
|
||||
public:
|
||||
Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
|
||||
gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
|
||||
gpio_num_t pa_pin, uint8_t es8388_addr);
|
||||
gpio_num_t pa_pin, uint8_t es8388_addr, bool input_reference = false);
|
||||
virtual ~Es8388AudioCodec();
|
||||
|
||||
virtual void SetOutputVolume(int volume) override;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "system_info.h"
|
||||
#include "settings.h"
|
||||
#include "display/display.h"
|
||||
#include "display/oled_display.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
@@ -154,6 +155,21 @@ std::string Board::GetSystemInfoJson() {
|
||||
json += R"("label":")" + std::string(ota_partition->label) + R"(")";
|
||||
json += R"(},)";
|
||||
|
||||
// Append display info
|
||||
auto display = GetDisplay();
|
||||
if (display) {
|
||||
json += R"("display":{)";
|
||||
if (dynamic_cast<OledDisplay*>(display)) {
|
||||
json += R"("monochrome":)" + std::string("true") + R"(,)";
|
||||
} else {
|
||||
json += R"("monochrome":)" + std::string("false") + R"(,)";
|
||||
}
|
||||
json += R"("width":)" + std::to_string(display->width()) + R"(,)";
|
||||
json += R"("height":)" + std::to_string(display->height()) + R"(,)";
|
||||
json.pop_back(); // Remove the last comma
|
||||
}
|
||||
json += R"(},)";
|
||||
|
||||
json += R"("board":)" + GetBoardJson();
|
||||
|
||||
// Close the JSON object
|
||||
|
||||
@@ -63,33 +63,26 @@ bool Esp32Camera::Capture() {
|
||||
// 显示预览图片
|
||||
auto display = dynamic_cast<LvglDisplay*>(Board::GetInstance().GetDisplay());
|
||||
if (display != nullptr) {
|
||||
// Create a new preview image
|
||||
auto img_dsc = (lv_img_dsc_t*)heap_caps_calloc(1, sizeof(lv_img_dsc_t), MALLOC_CAP_8BIT);
|
||||
img_dsc->header.magic = LV_IMAGE_HEADER_MAGIC;
|
||||
img_dsc->header.cf = LV_COLOR_FORMAT_RGB565;
|
||||
img_dsc->header.flags = 0;
|
||||
img_dsc->header.w = fb_->width;
|
||||
img_dsc->header.h = fb_->height;
|
||||
img_dsc->header.stride = fb_->width * 2;
|
||||
img_dsc->data_size = fb_->width * fb_->height * 2;
|
||||
img_dsc->data = (uint8_t*)heap_caps_malloc(img_dsc->data_size, MALLOC_CAP_SPIRAM);
|
||||
if (img_dsc->data == nullptr) {
|
||||
auto data = (uint8_t*)heap_caps_malloc(fb_->len, MALLOC_CAP_SPIRAM);
|
||||
if (data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for preview image");
|
||||
heap_caps_free(img_dsc);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto src = (uint16_t*)fb_->buf;
|
||||
auto dst = (uint16_t*)img_dsc->data;
|
||||
auto dst = (uint16_t*)data;
|
||||
size_t pixel_count = fb_->len / 2;
|
||||
for (size_t i = 0; i < pixel_count; i++) {
|
||||
// 交换每个16位字内的字节
|
||||
dst[i] = __builtin_bswap16(src[i]);
|
||||
}
|
||||
display->SetPreviewImage(img_dsc);
|
||||
|
||||
auto image = std::make_unique<LvglAllocatedImage>(data, fb_->len, fb_->width, fb_->height, fb_->width * 2, LV_COLOR_FORMAT_RGB565);
|
||||
display->SetPreviewImage(std::move(image));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Esp32Camera::SetHMirror(bool enabled) {
|
||||
sensor_t *s = esp_camera_sensor_get();
|
||||
if (s == nullptr) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "sscma_camera.h"
|
||||
#include "mcp_server.h"
|
||||
#include "lvgl_display.h"
|
||||
#include "lvgl_image.h"
|
||||
#include "board.h"
|
||||
#include "system_info.h"
|
||||
#include "config.h"
|
||||
@@ -245,7 +246,8 @@ bool SscmaCamera::Capture() {
|
||||
// 显示预览图片
|
||||
auto display = dynamic_cast<LvglDisplay*>(Board::GetInstance().GetDisplay());
|
||||
if (display != nullptr) {
|
||||
display->SetPreviewImage(&preview_image_);
|
||||
auto image = std::make_unique<LvglSourceImage>(&preview_image_);
|
||||
display->SetPreviewImage(std::move(image));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
88
main/boards/yunliao-s3/README.md
Normal file
88
main/boards/yunliao-s3/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 小智云聊S3
|
||||
|
||||
## 简介
|
||||
小智云聊S3是小智AI的魔改项目,是首个2.8寸护眼大屏+大字体+2000mah大电池的量产成品,做了大量创新和优化。
|
||||
|
||||
## 合并版
|
||||
合并版代码在小智AI主项目中维护,跟随主项目的一起版本更新,便于用户自行扩展和第三方固件扩展。支持语音唤醒、语音打断、OTA、4G自由切换等功能。
|
||||
|
||||
>### 按键操作
|
||||
>- **开机**: 关机状态,长按1秒后释放按键,自动开机
|
||||
>- **关机**: 开机状态,长按1秒后释放按键,标题栏会显示'请稍候',再等2秒自动关机
|
||||
>- **唤醒/打断**: 正常通话环境下,单击按键
|
||||
>- **切换4G/Wifi**: 启动过程或者配网界面,1秒钟内双击按键(需安装4G模块)
|
||||
>- **重新配网**: 开机状态,1秒钟内三击按键,会自动重启并进入配网界面
|
||||
|
||||
## 魔改版
|
||||
魔改版由于底层改动太大,代码单独维护,定期合并主项目代码。
|
||||
|
||||
>### 为什么是魔改
|
||||
>- 首个实现微信二维码配网。
|
||||
>- 首个支持单手机配网。
|
||||
>- 首个支持扫二维码访问控制台。
|
||||
>- 首发支持繁体、日文、英文版界面
|
||||
>- 首个全语音操控模式
|
||||
>- 独家提供一键刷机脚本等多种刷机方式
|
||||
|
||||
## 版本区别
|
||||
>| 特性 | 合并版 | 魔改版 |
|
||||
>| --- | --- | --- |
|
||||
>| 语音打断 | ✓ | ✓ |
|
||||
>| 4G功能 | ✓ | ✓ |
|
||||
>| 自动更新固件 | ✓ | X |
|
||||
>| 第三方固件支持 | ✓ | X |
|
||||
>| 天气待机界面 | X | ✓ |
|
||||
>| 闹钟提醒 | X | ✓ |
|
||||
>| 网络音乐播放 | X | ✓ |
|
||||
>| 微信扫码配网 | X | ✓ |
|
||||
>| 单手机配网 | X | ✓ |
|
||||
>| 扫码访问控制台 | X | ✓ |
|
||||
>| 繁日英文界面 | X | ✓ |
|
||||
>| 多语言支持 | X | ✓ |
|
||||
>| 外接蓝牙音箱 | X | ✓ |
|
||||
|
||||
|
||||
# 编译配置命令
|
||||
|
||||
**克隆工程**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/78/xiaozhi-esp32.git
|
||||
```
|
||||
|
||||
**进入工程**
|
||||
|
||||
```bash
|
||||
cd xiaozhi-esp32
|
||||
```
|
||||
|
||||
**配置编译目标为 ESP32S3**
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32s3
|
||||
```
|
||||
|
||||
**打开 menuconfig**
|
||||
|
||||
```bash
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||
**选择板子**
|
||||
|
||||
```bash
|
||||
- `Xiaozhi Assistant` → `Board Type` → 选择 `小智云聊-S3` → 选择 `Enable Device-Side AEC`
|
||||
```
|
||||
|
||||
**编译**
|
||||
|
||||
```ba
|
||||
idf.py build
|
||||
```
|
||||
|
||||
**下载并打开串口终端**
|
||||
|
||||
```bash
|
||||
idf.py build flash monitor
|
||||
```
|
||||
|
||||
59
main/boards/yunliao-s3/config.h
Normal file
59
main/boards/yunliao-s3/config.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifndef _BOARD_CONFIG_H_
|
||||
#define _BOARD_CONFIG_H_
|
||||
|
||||
#include <driver/gpio.h>
|
||||
|
||||
#define AUDIO_INPUT_REFERENCE true
|
||||
|
||||
#define AUDIO_INPUT_SAMPLE_RATE 24000
|
||||
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
|
||||
#define AUDIO_DEFAULT_OUTPUT_VOLUME 70
|
||||
|
||||
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_14
|
||||
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_13
|
||||
#define AUDIO_I2S_GPIO_WS GPIO_NUM_11
|
||||
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_12
|
||||
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_10
|
||||
|
||||
#define AUDIO_CODEC_PA_PIN GPIO_NUM_17
|
||||
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_21
|
||||
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_18
|
||||
#define AUDIO_CODEC_ES8388_ADDR ES8388_CODEC_DEFAULT_ADDR
|
||||
|
||||
#define BOOT_BUTTON_PIN GPIO_NUM_2
|
||||
#define BOOT_5V_PIN GPIO_NUM_3 //5V升压输出
|
||||
#define BOOT_4G_PIN GPIO_NUM_5 //4G模块使能
|
||||
#define MON_BATT_PIN GPIO_NUM_43 //检测PMU电池指示
|
||||
#define MON_BATT_CNT 70 //检测PMU电池秒数
|
||||
#define MON_USB_PIN GPIO_NUM_47 //检测USB插入
|
||||
|
||||
|
||||
#define ML307_RX_PIN GPIO_NUM_16
|
||||
#define ML307_TX_PIN GPIO_NUM_15
|
||||
|
||||
#define DISPLAY_SPI_LCD_HOST SPI2_HOST
|
||||
#define DISPLAY_SPI_CLOCK_HZ (40 * 1000 * 1000)
|
||||
#define DISPLAY_SPI_PIN_SCLK 42
|
||||
#define DISPLAY_SPI_PIN_MOSI 40
|
||||
#define DISPLAY_SPI_PIN_MISO -1
|
||||
#define DISPLAY_SPI_PIN_LCD_DC 41
|
||||
#define DISPLAY_SPI_PIN_LCD_RST 45
|
||||
#define DISPLAY_SPI_PIN_LCD_CS -1
|
||||
#define DISPLAY_PIN_TOUCH_CS -1
|
||||
|
||||
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_46
|
||||
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
|
||||
|
||||
#define DISPLAY_WIDTH 320
|
||||
#define DISPLAY_HEIGHT 240
|
||||
#define DISPLAY_SWAP_XY true
|
||||
#define DISPLAY_MIRROR_X false
|
||||
#define DISPLAY_MIRROR_Y true
|
||||
#define DISPLAY_INVERT_COLOR false
|
||||
#define DISPLAY_RGB_ORDER_COLOR LCD_RGB_ELEMENT_ORDER_RGB
|
||||
|
||||
#define DISPLAY_OFFSET_X 0
|
||||
#define DISPLAY_OFFSET_Y 0
|
||||
#define KEY_EXPIRE_MS 800
|
||||
|
||||
#endif // _BOARD_CONFIG_H_
|
||||
11
main/boards/yunliao-s3/config.json
Normal file
11
main/boards/yunliao-s3/config.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"target": "esp32s3",
|
||||
"builds": [
|
||||
{
|
||||
"name": "yunliao-s3",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_USE_DEVICE_AEC=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
203
main/boards/yunliao-s3/power_manager.cc
Normal file
203
main/boards/yunliao-s3/power_manager.cc
Normal file
@@ -0,0 +1,203 @@
|
||||
#include "power_manager.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "driver/rtc_io.h"
|
||||
#include "esp_log.h"
|
||||
#include "config.h"
|
||||
#include <esp_sleep.h>
|
||||
#include "esp_log.h"
|
||||
#include "settings.h"
|
||||
|
||||
#define TAG "PowerManager"
|
||||
|
||||
static QueueHandle_t gpio_evt_queue = NULL;
|
||||
uint16_t battCnt;//闪灯次数
|
||||
int battLife = -1; //电量
|
||||
|
||||
// 中断服务程序
|
||||
static void IRAM_ATTR batt_mon_isr_handler(void* arg) {
|
||||
uint32_t gpio_num = (uint32_t) arg;
|
||||
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
|
||||
}
|
||||
|
||||
// 添加任务处理函数
|
||||
static void batt_mon_task(void* arg) {
|
||||
uint32_t io_num;
|
||||
while(1) {
|
||||
if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
|
||||
battCnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void calBattLife() {
|
||||
// 计算电量
|
||||
battLife = battCnt;
|
||||
|
||||
if (battLife > 100){
|
||||
battLife = 100;
|
||||
}
|
||||
// ESP_LOGI(TAG, "Battery life:%d", (int)battLife);
|
||||
// 重置计数器
|
||||
battCnt = 0;
|
||||
}
|
||||
|
||||
PowerManager::PowerManager(){
|
||||
}
|
||||
|
||||
void PowerManager::Initialize(){
|
||||
// 初始化5V控制引脚
|
||||
gpio_config_t io_conf_5v = {
|
||||
.pin_bit_mask = 1<<BOOT_5V_PIN,
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
ESP_ERROR_CHECK(gpio_config(&io_conf_5v));
|
||||
|
||||
// 初始化4G控制引脚
|
||||
gpio_config_t io_conf_4g = {
|
||||
.pin_bit_mask = 1<<BOOT_4G_PIN,
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_ENABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
ESP_ERROR_CHECK(gpio_config(&io_conf_4g));
|
||||
|
||||
// 电池电量监测引脚配置
|
||||
gpio_config_t io_conf_batt_mon = {
|
||||
.pin_bit_mask = 1ull<<MON_BATT_PIN,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_POSEDGE,
|
||||
};
|
||||
ESP_ERROR_CHECK(gpio_config(&io_conf_batt_mon));
|
||||
// 创建电量GPIO事件队列
|
||||
gpio_evt_queue = xQueueCreate(2, sizeof(uint32_t));
|
||||
// 安装电量GPIO ISR服务
|
||||
ESP_ERROR_CHECK(gpio_install_isr_service(0));
|
||||
// 添加中断处理
|
||||
ESP_ERROR_CHECK(gpio_isr_handler_add(MON_BATT_PIN, batt_mon_isr_handler, (void*)MON_BATT_PIN));
|
||||
// 创建监控任务
|
||||
xTaskCreate(&batt_mon_task, "batt_mon_task", 1024, NULL, 10, NULL);
|
||||
|
||||
// 初始化监测引脚
|
||||
gpio_config_t mon_conf = {};
|
||||
mon_conf.pin_bit_mask = 1ULL << MON_USB_PIN;
|
||||
mon_conf.mode = GPIO_MODE_INPUT;
|
||||
mon_conf.pull_up_en = GPIO_PULLUP_DISABLE;
|
||||
mon_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||
gpio_config(&mon_conf);
|
||||
|
||||
// 创建电池电量检查定时器
|
||||
esp_timer_create_args_t timer_args = {
|
||||
.callback = [](void* arg) {
|
||||
PowerManager* self = static_cast<PowerManager*>(arg);
|
||||
self->CheckBatteryStatus();
|
||||
},
|
||||
.arg = this,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "battery_check_timer",
|
||||
.skip_unhandled_events = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_));
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000));
|
||||
}
|
||||
|
||||
void PowerManager::CheckBatteryStatus(){
|
||||
call_count_++;
|
||||
if(call_count_ >= MON_BATT_CNT) {
|
||||
calBattLife();
|
||||
call_count_ = 0;
|
||||
}
|
||||
|
||||
bool new_charging_status = IsCharging();
|
||||
if (new_charging_status != is_charging_) {
|
||||
is_charging_ = new_charging_status;
|
||||
if (charging_callback_) {
|
||||
charging_callback_(is_charging_);
|
||||
}
|
||||
}
|
||||
|
||||
bool new_discharging_status = IsDischarging();
|
||||
if (new_discharging_status != is_discharging_) {
|
||||
is_discharging_ = new_discharging_status;
|
||||
if (discharging_callback_) {
|
||||
discharging_callback_(is_discharging_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PowerManager::IsCharging() {
|
||||
return gpio_get_level(MON_USB_PIN) == 1 && !IsChargingDone();
|
||||
}
|
||||
|
||||
bool PowerManager::IsDischarging() {
|
||||
return gpio_get_level(MON_USB_PIN) == 0;
|
||||
}
|
||||
|
||||
bool PowerManager::IsChargingDone() {
|
||||
return battLife >= 95;
|
||||
}
|
||||
|
||||
int PowerManager::GetBatteryLevel() {
|
||||
return battLife;
|
||||
}
|
||||
|
||||
void PowerManager::OnChargingStatusChanged(std::function<void(bool)> callback) {
|
||||
charging_callback_ = callback;
|
||||
}
|
||||
|
||||
void PowerManager::OnChargingStatusDisChanged(std::function<void(bool)> callback) {
|
||||
discharging_callback_ = callback;
|
||||
}
|
||||
|
||||
void PowerManager::CheckStartup() {
|
||||
Settings settings1("board", true);
|
||||
if(settings1.GetInt("sleep_flag", 0) > 0){
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
if( gpio_get_level(BOOT_BUTTON_PIN) == 1) {
|
||||
Sleep(); //进入休眠模式
|
||||
}else{
|
||||
settings1.SetInt("sleep_flag", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PowerManager::Start5V() {
|
||||
gpio_set_level(BOOT_5V_PIN, 1);
|
||||
}
|
||||
|
||||
void PowerManager::Shutdown5V() {
|
||||
gpio_set_level(BOOT_5V_PIN, 0);
|
||||
}
|
||||
|
||||
void PowerManager::Start4G() {
|
||||
gpio_set_level(BOOT_4G_PIN, 1);
|
||||
}
|
||||
|
||||
void PowerManager::Shutdown4G() {
|
||||
gpio_set_level(BOOT_4G_PIN, 0);
|
||||
gpio_set_level(ML307_RX_PIN,1);
|
||||
gpio_set_level(ML307_TX_PIN,1);
|
||||
}
|
||||
|
||||
void PowerManager::Sleep() {
|
||||
ESP_LOGI(TAG, "Entering deep sleep");
|
||||
Settings settings("board", true);
|
||||
settings.SetInt("sleep_flag", 1);
|
||||
Shutdown4G();
|
||||
Shutdown5V();
|
||||
|
||||
if(gpio_evt_queue) {
|
||||
vQueueDelete(gpio_evt_queue);
|
||||
gpio_evt_queue = NULL;
|
||||
}
|
||||
ESP_ERROR_CHECK(gpio_isr_handler_remove(BOOT_BUTTON_PIN));
|
||||
ESP_ERROR_CHECK(esp_sleep_enable_ext0_wakeup(BOOT_BUTTON_PIN, 0));
|
||||
ESP_ERROR_CHECK(rtc_gpio_pulldown_dis(BOOT_BUTTON_PIN));
|
||||
ESP_ERROR_CHECK(rtc_gpio_pullup_en(BOOT_BUTTON_PIN));
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
37
main/boards/yunliao-s3/power_manager.h
Normal file
37
main/boards/yunliao-s3/power_manager.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef __POWERMANAGER_H__
|
||||
#define __POWERMANAGER_H__
|
||||
|
||||
#include <functional>
|
||||
#include "driver/gpio.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/timers.h"
|
||||
|
||||
class PowerManager{
|
||||
public:
|
||||
PowerManager();
|
||||
void Initialize();
|
||||
bool IsCharging();
|
||||
bool IsDischarging();
|
||||
bool IsChargingDone();
|
||||
int GetBatteryLevel();
|
||||
void CheckStartup();
|
||||
void Start5V();
|
||||
void Shutdown5V();
|
||||
void Start4G();
|
||||
void Shutdown4G();
|
||||
void Sleep();
|
||||
void CheckBatteryStatus();
|
||||
void OnChargingStatusChanged(std::function<void(bool)> callback);
|
||||
void OnChargingStatusDisChanged(std::function<void(bool)> callback);
|
||||
private:
|
||||
esp_timer_handle_t timer_handle_;
|
||||
std::function<void(bool)> charging_callback_;
|
||||
std::function<void(bool)> discharging_callback_;
|
||||
int is_charging_ = -1;
|
||||
int is_discharging_ = -1;
|
||||
int call_count_ = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
207
main/boards/yunliao-s3/yunliao_s3.cc
Normal file
207
main/boards/yunliao-s3/yunliao_s3.cc
Normal file
@@ -0,0 +1,207 @@
|
||||
#include "lvgl_theme.h"
|
||||
#include "dual_network_board.h"
|
||||
#include "codecs/es8388_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "power_save_timer.h"
|
||||
#include "power_manager.h"
|
||||
#include "assets/lang_config.h"
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
|
||||
#define TAG "YunliaoS3"
|
||||
|
||||
class YunliaoS3 : public DualNetworkBoard {
|
||||
private:
|
||||
i2c_master_bus_handle_t codec_i2c_bus_;
|
||||
Button boot_button_;
|
||||
SpiLcdDisplay* display_;
|
||||
PowerSaveTimer* power_save_timer_;
|
||||
PowerManager* power_manager_;
|
||||
|
||||
void InitializePowerSaveTimer() {
|
||||
power_save_timer_ = new PowerSaveTimer(-1, 60, 600);
|
||||
power_save_timer_->OnEnterSleepMode([this]() {
|
||||
GetDisplay()->SetPowerSaveMode(true);
|
||||
GetBacklight()->SetBrightness(10);
|
||||
});
|
||||
power_save_timer_->OnExitSleepMode([this]() {
|
||||
GetDisplay()->SetPowerSaveMode(false);
|
||||
GetBacklight()->RestoreBrightness();
|
||||
});
|
||||
power_save_timer_->OnShutdownRequest([this]() {
|
||||
ESP_LOGI(TAG, "Shutting down");
|
||||
power_manager_->Sleep();
|
||||
});
|
||||
power_save_timer_->SetEnabled(true);
|
||||
}
|
||||
|
||||
void InitializeI2c() {
|
||||
// Initialize I2C peripheral
|
||||
i2c_master_bus_config_t i2c_bus_cfg = {
|
||||
.i2c_port = 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, &codec_i2c_bus_));
|
||||
}
|
||||
|
||||
void InitializeSpi() {
|
||||
spi_bus_config_t buscfg = {};
|
||||
buscfg.mosi_io_num = DISPLAY_SPI_PIN_MOSI;
|
||||
buscfg.miso_io_num = DISPLAY_SPI_PIN_MISO;
|
||||
buscfg.sclk_io_num = DISPLAY_SPI_PIN_SCLK;
|
||||
buscfg.quadwp_io_num = GPIO_NUM_NC;
|
||||
buscfg.quadhd_io_num = GPIO_NUM_NC;
|
||||
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
|
||||
ESP_ERROR_CHECK(spi_bus_initialize(DISPLAY_SPI_LCD_HOST, &buscfg, SPI_DMA_CH_AUTO));
|
||||
}
|
||||
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
power_save_timer_->WakeUp();
|
||||
auto& app = Application::GetInstance();
|
||||
app.ToggleChatState();
|
||||
});
|
||||
boot_button_.OnDoubleClick([this]() {
|
||||
ESP_LOGI(TAG, "Button OnDoubleClick");
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
|
||||
SwitchNetworkType();
|
||||
}
|
||||
});
|
||||
boot_button_.OnMultipleClick([this]() {
|
||||
ESP_LOGI(TAG, "Button OnThreeClick");
|
||||
if (GetNetworkType() == NetworkType::WIFI) {
|
||||
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
|
||||
wifi_board.ResetWifiConfiguration();
|
||||
}
|
||||
},3);
|
||||
boot_button_.OnLongPress([this]() {
|
||||
ESP_LOGI(TAG, "Button LongPress to Sleep");
|
||||
display_->SetStatus(Lang::Strings::PLEASE_WAIT);
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
power_manager_->Sleep();
|
||||
});
|
||||
}
|
||||
void InitializeSt7789Display() {
|
||||
esp_lcd_panel_io_handle_t panel_io = nullptr;
|
||||
esp_lcd_panel_handle_t panel = nullptr;
|
||||
// 液晶屏控制IO初始化
|
||||
ESP_LOGD(TAG, "Install panel IO");
|
||||
esp_lcd_panel_io_spi_config_t io_config = {};
|
||||
io_config.cs_gpio_num = DISPLAY_SPI_PIN_LCD_CS;
|
||||
io_config.dc_gpio_num = DISPLAY_SPI_PIN_LCD_DC;
|
||||
io_config.spi_mode = 3;
|
||||
io_config.pclk_hz = DISPLAY_SPI_CLOCK_HZ;
|
||||
io_config.trans_queue_depth = 10;
|
||||
io_config.lcd_cmd_bits = 8;
|
||||
io_config.lcd_param_bits = 8;
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(DISPLAY_SPI_LCD_HOST, &io_config, &panel_io));
|
||||
|
||||
// 初始化液晶屏驱动芯片ST7789
|
||||
ESP_LOGD(TAG, "Install LCD driver");
|
||||
esp_lcd_panel_dev_config_t panel_config = {};
|
||||
panel_config.reset_gpio_num = DISPLAY_SPI_PIN_LCD_RST;
|
||||
panel_config.rgb_ele_order = DISPLAY_RGB_ORDER_COLOR;
|
||||
panel_config.bits_per_pixel = 16;
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
|
||||
|
||||
esp_lcd_panel_reset(panel);
|
||||
esp_lcd_panel_init(panel);
|
||||
esp_lcd_panel_invert_color(panel, DISPLAY_INVERT_COLOR);
|
||||
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
|
||||
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
|
||||
display_ = new SpiLcdDisplay(panel_io, panel, DISPLAY_WIDTH,
|
||||
DISPLAY_HEIGHT, DISPLAY_OFFSET_X,
|
||||
DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X,
|
||||
DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto theme = theme_manager.GetTheme("dark");
|
||||
if (theme != nullptr) {
|
||||
display_->SetTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
YunliaoS3() :
|
||||
DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, GPIO_NUM_NC, 0),
|
||||
boot_button_(BOOT_BUTTON_PIN),
|
||||
power_manager_(new PowerManager()){
|
||||
power_manager_->Start5V();
|
||||
power_manager_->Initialize();
|
||||
InitializeI2c();
|
||||
power_manager_->CheckStartup();
|
||||
InitializePowerSaveTimer();
|
||||
InitializeSpi();
|
||||
InitializeButtons();
|
||||
InitializeSt7789Display();
|
||||
power_manager_->OnChargingStatusDisChanged([this](bool is_discharging) {
|
||||
if(power_save_timer_){
|
||||
if (is_discharging) {
|
||||
power_save_timer_->SetEnabled(true);
|
||||
} else {
|
||||
power_save_timer_->SetEnabled(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
if(GetNetworkType() == NetworkType::WIFI){
|
||||
power_manager_->Shutdown4G();
|
||||
}
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
static Es8388AudioCodec audio_codec(
|
||||
codec_i2c_bus_,
|
||||
I2C_NUM_0,
|
||||
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_ES8388_ADDR,
|
||||
AUDIO_INPUT_REFERENCE
|
||||
);
|
||||
return &audio_codec;
|
||||
}
|
||||
|
||||
virtual Display* GetDisplay() override {
|
||||
return display_;
|
||||
}
|
||||
|
||||
virtual Backlight* GetBacklight() override {
|
||||
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
|
||||
return &backlight;
|
||||
}
|
||||
|
||||
virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override {
|
||||
level = power_manager_->GetBatteryLevel();
|
||||
charging = power_manager_->IsCharging();
|
||||
discharging = power_manager_->IsDischarging();
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void SetPowerSaveMode(bool enabled) override {
|
||||
if (!enabled) {
|
||||
power_save_timer_->WakeUp();
|
||||
}
|
||||
DualNetworkBoard::SetPowerSaveMode(enabled);
|
||||
}
|
||||
};
|
||||
|
||||
DECLARE_BOARD(YunliaoS3);
|
||||
@@ -28,30 +28,30 @@ void LcdDisplay::InitializeLcdThemes() {
|
||||
|
||||
// light theme
|
||||
auto light_theme = new LvglTheme("light");
|
||||
light_theme->set_background_color(lv_color_white());
|
||||
light_theme->set_text_color(lv_color_black());
|
||||
light_theme->set_chat_background_color(lv_color_hex(0xE0E0E0));
|
||||
light_theme->set_user_bubble_color(lv_color_hex(0x95EC69));
|
||||
light_theme->set_assistant_bubble_color(lv_color_white());
|
||||
light_theme->set_system_bubble_color(lv_color_hex(0xE0E0E0));
|
||||
light_theme->set_system_text_color(lv_color_hex(0x666666));
|
||||
light_theme->set_border_color(lv_color_hex(0xE0E0E0));
|
||||
light_theme->set_low_battery_color(lv_color_black());
|
||||
light_theme->set_background_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255)
|
||||
light_theme->set_text_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
|
||||
light_theme->set_chat_background_color(lv_color_hex(0xE0E0E0)); //rgb(224, 224, 224)
|
||||
light_theme->set_user_bubble_color(lv_color_hex(0x00FF00)); //rgb(0, 128, 0)
|
||||
light_theme->set_assistant_bubble_color(lv_color_hex(0xDDDDDD)); //rgb(221, 221, 221)
|
||||
light_theme->set_system_bubble_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255)
|
||||
light_theme->set_system_text_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
|
||||
light_theme->set_border_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
|
||||
light_theme->set_low_battery_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
|
||||
light_theme->set_text_font(text_font);
|
||||
light_theme->set_icon_font(icon_font);
|
||||
light_theme->set_large_icon_font(large_icon_font);
|
||||
|
||||
// dark theme
|
||||
auto dark_theme = new LvglTheme("dark");
|
||||
dark_theme->set_background_color(lv_color_hex(0x121212));
|
||||
dark_theme->set_text_color(lv_color_white());
|
||||
dark_theme->set_chat_background_color(lv_color_hex(0x1E1E1E));
|
||||
dark_theme->set_user_bubble_color(lv_color_hex(0x1A6C37));
|
||||
dark_theme->set_assistant_bubble_color(lv_color_hex(0x333333));
|
||||
dark_theme->set_system_bubble_color(lv_color_hex(0x2A2A2A));
|
||||
dark_theme->set_system_text_color(lv_color_hex(0xAAAAAA));
|
||||
dark_theme->set_border_color(lv_color_hex(0x333333));
|
||||
dark_theme->set_low_battery_color(lv_color_hex(0xFF0000));
|
||||
dark_theme->set_background_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
|
||||
dark_theme->set_text_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255)
|
||||
dark_theme->set_chat_background_color(lv_color_hex(0x1F1F1F)); //rgb(31, 31, 31)
|
||||
dark_theme->set_user_bubble_color(lv_color_hex(0x00FF00)); //rgb(0, 128, 0)
|
||||
dark_theme->set_assistant_bubble_color(lv_color_hex(0x222222)); //rgb(34, 34, 34)
|
||||
dark_theme->set_system_bubble_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
|
||||
dark_theme->set_system_text_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255)
|
||||
dark_theme->set_border_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255)
|
||||
dark_theme->set_low_battery_color(lv_color_hex(0xFF0000)); //rgb(255, 0, 0)
|
||||
dark_theme->set_text_font(text_font);
|
||||
dark_theme->set_icon_font(icon_font);
|
||||
dark_theme->set_large_icon_font(large_icon_font);
|
||||
@@ -120,6 +120,9 @@ SpiLcdDisplay::SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_h
|
||||
ESP_LOGI(TAG, "Initialize LVGL port");
|
||||
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
|
||||
port_cfg.task_priority = 1;
|
||||
#if CONFIG_SOC_CPU_CORES_NUM > 1
|
||||
port_cfg.task_affinity = 1;
|
||||
#endif
|
||||
lvgl_port_init(&port_cfg);
|
||||
|
||||
ESP_LOGI(TAG, "Adding LCD display");
|
||||
@@ -451,7 +454,7 @@ void LcdDisplay::SetupUI() {
|
||||
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
emoji_image_ = lv_img_create(screen);
|
||||
lv_obj_align(emoji_image_, LV_ALIGN_TOP_MID, 0, text_font->line_height + lvgl_theme->spacing(2));
|
||||
lv_obj_align(emoji_image_, LV_ALIGN_TOP_MID, 0, text_font->line_height + lvgl_theme->spacing(8));
|
||||
|
||||
// Display AI logo while booting
|
||||
emoji_label_ = lv_label_create(screen);
|
||||
@@ -521,8 +524,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
lv_obj_t* msg_bubble = lv_obj_create(content_);
|
||||
lv_obj_set_style_radius(msg_bubble, 8, 0);
|
||||
lv_obj_set_scrollbar_mode(msg_bubble, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_style_border_width(msg_bubble, 1, 0);
|
||||
lv_obj_set_style_border_color(msg_bubble, lvgl_theme->border_color(), 0);
|
||||
lv_obj_set_style_border_width(msg_bubble, 0, 0);
|
||||
lv_obj_set_style_pad_all(msg_bubble, lvgl_theme->spacing(4), 0);
|
||||
|
||||
// Create the message text
|
||||
@@ -561,6 +563,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
if (strcmp(role, "user") == 0) {
|
||||
// User messages are right-aligned with green background
|
||||
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->user_bubble_color(), 0);
|
||||
lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0);
|
||||
// Set text color for contrast
|
||||
lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0);
|
||||
|
||||
@@ -576,6 +579,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
} else if (strcmp(role, "assistant") == 0) {
|
||||
// Assistant messages are left-aligned with white background
|
||||
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->assistant_bubble_color(), 0);
|
||||
lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0);
|
||||
// Set text color for contrast
|
||||
lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0);
|
||||
|
||||
@@ -591,6 +595,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
} else if (strcmp(role, "system") == 0) {
|
||||
// System messages are center-aligned with light gray background
|
||||
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->system_bubble_color(), 0);
|
||||
lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0);
|
||||
// Set text color for contrast
|
||||
lv_obj_set_style_text_color(msg_text, lvgl_theme->system_text_color(), 0);
|
||||
|
||||
@@ -657,84 +662,88 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
chat_message_label_ = msg_text;
|
||||
}
|
||||
|
||||
void LcdDisplay::SetPreviewImage(const lv_img_dsc_t* img_dsc) {
|
||||
void LcdDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
|
||||
DisplayLockGuard lock(this);
|
||||
if (content_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (image == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
||||
if (img_dsc != nullptr) {
|
||||
// Create a message bubble for image preview
|
||||
lv_obj_t* img_bubble = lv_obj_create(content_);
|
||||
lv_obj_set_style_radius(img_bubble, 8, 0);
|
||||
lv_obj_set_scrollbar_mode(img_bubble, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_style_border_width(img_bubble, 1, 0);
|
||||
lv_obj_set_style_border_color(img_bubble, lvgl_theme->border_color(), 0);
|
||||
lv_obj_set_style_pad_all(img_bubble, lvgl_theme->spacing(4), 0);
|
||||
|
||||
// Set image bubble background color (similar to system message)
|
||||
lv_obj_set_style_bg_color(img_bubble, lvgl_theme->assistant_bubble_color(), 0);
|
||||
|
||||
// 设置自定义属性标记气泡类型
|
||||
lv_obj_set_user_data(img_bubble, (void*)"image");
|
||||
// Create a message bubble for image preview
|
||||
lv_obj_t* img_bubble = lv_obj_create(content_);
|
||||
lv_obj_set_style_radius(img_bubble, 8, 0);
|
||||
lv_obj_set_scrollbar_mode(img_bubble, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_style_border_width(img_bubble, 0, 0);
|
||||
lv_obj_set_style_pad_all(img_bubble, lvgl_theme->spacing(4), 0);
|
||||
|
||||
// Set image bubble background color (similar to system message)
|
||||
lv_obj_set_style_bg_color(img_bubble, lvgl_theme->assistant_bubble_color(), 0);
|
||||
lv_obj_set_style_bg_opa(img_bubble, LV_OPA_70, 0);
|
||||
|
||||
// 设置自定义属性标记气泡类型
|
||||
lv_obj_set_user_data(img_bubble, (void*)"image");
|
||||
|
||||
// Create the image object inside the bubble
|
||||
lv_obj_t* preview_image = lv_image_create(img_bubble);
|
||||
|
||||
// Calculate appropriate size for the image
|
||||
lv_coord_t max_width = LV_HOR_RES * 70 / 100; // 70% of screen width
|
||||
lv_coord_t max_height = LV_VER_RES * 50 / 100; // 50% of screen height
|
||||
|
||||
// Calculate zoom factor to fit within maximum dimensions
|
||||
lv_coord_t img_width = img_dsc->header.w;
|
||||
lv_coord_t img_height = img_dsc->header.h;
|
||||
if (img_width == 0 || img_height == 0) {
|
||||
img_width = max_width;
|
||||
img_height = max_height;
|
||||
ESP_LOGW(TAG, "Invalid image dimensions: %ld x %ld, using default dimensions: %ld x %ld", img_width, img_height, max_width, max_height);
|
||||
}
|
||||
|
||||
lv_coord_t zoom_w = (max_width * 256) / img_width;
|
||||
lv_coord_t zoom_h = (max_height * 256) / img_height;
|
||||
lv_coord_t zoom = (zoom_w < zoom_h) ? zoom_w : zoom_h;
|
||||
|
||||
// Ensure zoom doesn't exceed 256 (100%)
|
||||
if (zoom > 256) zoom = 256;
|
||||
|
||||
// Set image properties
|
||||
lv_image_set_src(preview_image, img_dsc);
|
||||
lv_image_set_scale(preview_image, zoom);
|
||||
|
||||
// Add event handler to clean up copied data when image is deleted
|
||||
lv_obj_add_event_cb(preview_image, [](lv_event_t* e) {
|
||||
lv_img_dsc_t* img_dsc = (lv_img_dsc_t*)lv_event_get_user_data(e);
|
||||
if (img_dsc != nullptr) {
|
||||
heap_caps_free((void*)img_dsc->data);
|
||||
heap_caps_free(img_dsc);
|
||||
}
|
||||
}, LV_EVENT_DELETE, (void*)img_dsc);
|
||||
|
||||
// Calculate actual scaled image dimensions
|
||||
lv_coord_t scaled_width = (img_width * zoom) / 256;
|
||||
lv_coord_t scaled_height = (img_height * zoom) / 256;
|
||||
|
||||
// Set bubble size to be 16 pixels larger than the image (8 pixels on each side)
|
||||
lv_obj_set_width(img_bubble, scaled_width + 16);
|
||||
lv_obj_set_height(img_bubble, scaled_height + 16);
|
||||
|
||||
// Don't grow in flex layout
|
||||
lv_obj_set_style_flex_grow(img_bubble, 0, 0);
|
||||
|
||||
// Center the image within the bubble
|
||||
lv_obj_center(preview_image);
|
||||
|
||||
// Left align the image bubble like assistant messages
|
||||
lv_obj_align(img_bubble, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
// Auto-scroll to the image bubble
|
||||
lv_obj_scroll_to_view_recursive(img_bubble, LV_ANIM_ON);
|
||||
// Create the image object inside the bubble
|
||||
lv_obj_t* preview_image = lv_image_create(img_bubble);
|
||||
|
||||
// Calculate appropriate size for the image
|
||||
lv_coord_t max_width = LV_HOR_RES * 70 / 100; // 70% of screen width
|
||||
lv_coord_t max_height = LV_VER_RES * 50 / 100; // 50% of screen height
|
||||
|
||||
// Calculate zoom factor to fit within maximum dimensions
|
||||
auto img_dsc = image->image_dsc();
|
||||
lv_coord_t img_width = img_dsc->header.w;
|
||||
lv_coord_t img_height = img_dsc->header.h;
|
||||
if (img_width == 0 || img_height == 0) {
|
||||
img_width = max_width;
|
||||
img_height = max_height;
|
||||
ESP_LOGW(TAG, "Invalid image dimensions: %ld x %ld, using default dimensions: %ld x %ld", img_width, img_height, max_width, max_height);
|
||||
}
|
||||
|
||||
lv_coord_t zoom_w = (max_width * 256) / img_width;
|
||||
lv_coord_t zoom_h = (max_height * 256) / img_height;
|
||||
lv_coord_t zoom = (zoom_w < zoom_h) ? zoom_w : zoom_h;
|
||||
|
||||
// Ensure zoom doesn't exceed 256 (100%)
|
||||
if (zoom > 256) zoom = 256;
|
||||
|
||||
// Set image properties
|
||||
lv_image_set_src(preview_image, img_dsc);
|
||||
lv_image_set_scale(preview_image, zoom);
|
||||
|
||||
// Add event handler to clean up LvglImage when image is deleted
|
||||
// We need to transfer ownership of the unique_ptr to the event callback
|
||||
LvglImage* raw_image = image.release(); // 释放智能指针的所有权
|
||||
lv_obj_add_event_cb(preview_image, [](lv_event_t* e) {
|
||||
LvglImage* img = (LvglImage*)lv_event_get_user_data(e);
|
||||
if (img != nullptr) {
|
||||
delete img; // 通过删除 LvglImage 对象来正确释放内存
|
||||
}
|
||||
}, LV_EVENT_DELETE, (void*)raw_image);
|
||||
|
||||
// Calculate actual scaled image dimensions
|
||||
lv_coord_t scaled_width = (img_width * zoom) / 256;
|
||||
lv_coord_t scaled_height = (img_height * zoom) / 256;
|
||||
|
||||
// Set bubble size to be 16 pixels larger than the image (8 pixels on each side)
|
||||
lv_obj_set_width(img_bubble, scaled_width + 16);
|
||||
lv_obj_set_height(img_bubble, scaled_height + 16);
|
||||
|
||||
// Don't grow in flex layout
|
||||
lv_obj_set_style_flex_grow(img_bubble, 0, 0);
|
||||
|
||||
// Center the image within the bubble
|
||||
lv_obj_center(preview_image);
|
||||
|
||||
// Left align the image bubble like assistant messages
|
||||
lv_obj_align(img_bubble, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
// Auto-scroll to the image bubble
|
||||
lv_obj_scroll_to_view_recursive(img_bubble, LV_ANIM_ON);
|
||||
}
|
||||
#else
|
||||
void LcdDisplay::SetupUI() {
|
||||
@@ -858,37 +867,35 @@ void LcdDisplay::SetupUI() {
|
||||
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
void LcdDisplay::SetPreviewImage(const lv_img_dsc_t* img_dsc) {
|
||||
void LcdDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
|
||||
DisplayLockGuard lock(this);
|
||||
if (preview_image_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Preview image is not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
auto old_src = (const lv_img_dsc_t*)lv_image_get_src(preview_image_);
|
||||
if (old_src != nullptr) {
|
||||
lv_image_set_src(preview_image_, nullptr);
|
||||
heap_caps_free((void*)old_src->data);
|
||||
heap_caps_free((void*)old_src);
|
||||
}
|
||||
|
||||
if (img_dsc != nullptr) {
|
||||
// 设置图片源并显示预览图片
|
||||
lv_image_set_src(preview_image_, img_dsc);
|
||||
if (img_dsc->header.w > 0 && img_dsc->header.h > 0) {
|
||||
// zoom factor 0.5
|
||||
lv_image_set_scale(preview_image_, 128 * width_ / img_dsc->header.w);
|
||||
}
|
||||
// Hide emoji_box_
|
||||
lv_obj_add_flag(emoji_box_, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
|
||||
esp_timer_stop(preview_timer_);
|
||||
ESP_ERROR_CHECK(esp_timer_start_once(preview_timer_, PREVIEW_IMAGE_DURATION_MS * 1000));
|
||||
} else {
|
||||
if (image == nullptr) {
|
||||
esp_timer_stop(preview_timer_);
|
||||
lv_obj_remove_flag(emoji_box_, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
|
||||
preview_image_cached_.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
preview_image_cached_ = std::move(image);
|
||||
auto img_dsc = preview_image_cached_->image_dsc();
|
||||
// 设置图片源并显示预览图片
|
||||
lv_image_set_src(preview_image_, img_dsc);
|
||||
if (img_dsc->header.w > 0 && img_dsc->header.h > 0) {
|
||||
// zoom factor 0.5
|
||||
lv_image_set_scale(preview_image_, 128 * width_ / img_dsc->header.w);
|
||||
}
|
||||
|
||||
// Hide emoji_box_
|
||||
lv_obj_add_flag(emoji_box_, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
|
||||
esp_timer_stop(preview_timer_);
|
||||
ESP_ERROR_CHECK(esp_timer_start_once(preview_timer_, PREVIEW_IMAGE_DURATION_MS * 1000));
|
||||
}
|
||||
|
||||
void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
@@ -955,7 +962,8 @@ void LcdDisplay::SetEmotion(const char* emotion) {
|
||||
|
||||
#if CONFIG_USE_WECHAT_MESSAGE_STYLE
|
||||
// Wechat message style中,如果emotion是neutral,则不显示
|
||||
if (strcmp(emotion, "neutral") == 0) {
|
||||
uint32_t child_count = lv_obj_get_child_cnt(content_);
|
||||
if (strcmp(emotion, "neutral") == 0 && child_count > 0) {
|
||||
// Stop GIF animation if running
|
||||
if (gif_controller_) {
|
||||
gif_controller_->Stop();
|
||||
|
||||
@@ -31,6 +31,7 @@ protected:
|
||||
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;
|
||||
|
||||
void InitializeLcdThemes();
|
||||
void SetupUI();
|
||||
@@ -44,8 +45,8 @@ protected:
|
||||
public:
|
||||
~LcdDisplay();
|
||||
virtual void SetEmotion(const char* emotion) override;
|
||||
virtual void SetPreviewImage(const lv_img_dsc_t* img_dsc) override;
|
||||
virtual void SetChatMessage(const char* role, const char* content) override;
|
||||
virtual void SetPreviewImage(std::unique_ptr<LvglImage> image) override;
|
||||
|
||||
// Add theme switching function
|
||||
virtual void SetTheme(Theme* theme) override;
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#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))
|
||||
@@ -80,13 +83,13 @@ static gd_GIF * gif_open(gd_GIF * gif_base)
|
||||
/* Header */
|
||||
f_gif_read(gif_base, sigver, 3);
|
||||
if(memcmp(sigver, "GIF", 3) != 0) {
|
||||
LV_LOG_WARN("invalid signature");
|
||||
ESP_LOGW(TAG, "invalid signature");
|
||||
goto fail;
|
||||
}
|
||||
/* Version */
|
||||
f_gif_read(gif_base, sigver, 3);
|
||||
if(memcmp(sigver, "89a", 3) != 0) {
|
||||
LV_LOG_WARN("invalid version");
|
||||
if(memcmp(sigver, "89a", 3) != 0 && memcmp(sigver, "87a", 3) != 0) {
|
||||
ESP_LOGW(TAG, "invalid version");
|
||||
goto fail;
|
||||
}
|
||||
/* Width x Height */
|
||||
@@ -96,7 +99,7 @@ static gd_GIF * gif_open(gd_GIF * gif_base)
|
||||
f_gif_read(gif_base, &fdsz, 1);
|
||||
/* Presence of GCT */
|
||||
if(!(fdsz & 0x80)) {
|
||||
LV_LOG_WARN("no global color table");
|
||||
ESP_LOGW(TAG, "no global color table");
|
||||
goto fail;
|
||||
}
|
||||
/* Color Space's Depth */
|
||||
@@ -110,18 +113,18 @@ static gd_GIF * gif_open(gd_GIF * gif_base)
|
||||
f_gif_read(gif_base, &aspect, 1);
|
||||
/* Create gd_GIF Structure. */
|
||||
if(0 == width || 0 == height){
|
||||
LV_LOG_WARN("Zero size image");
|
||||
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){
|
||||
LV_LOG_WARN("Image dimensions are too large");
|
||||
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){
|
||||
LV_LOG_WARN("Image dimensions are too large");
|
||||
ESP_LOGW(TAG, "Image dimensions are too large");
|
||||
goto fail;
|
||||
}
|
||||
gif = lv_malloc(sizeof(gd_GIF) + 5 * width * height);
|
||||
@@ -292,7 +295,7 @@ read_ext(gd_GIF * gif)
|
||||
read_application_ext(gif);
|
||||
break;
|
||||
default:
|
||||
LV_LOG_WARN("unknown extension: %02X\n", label);
|
||||
ESP_LOGW(TAG, "unknown extension: %02X\n", label);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,7 +389,7 @@ read_image_data(gd_GIF *gif, int interlace)
|
||||
/* copy data to frame buffer */
|
||||
while (sp > p_stack) {
|
||||
if(frm_off >= frm_size){
|
||||
LV_LOG_WARN("LZW table token overflows the frame buffer");
|
||||
ESP_LOGW(TAG, "LZW table token overflows the frame buffer");
|
||||
return -1;
|
||||
}
|
||||
*ptr++ = *(--sp);
|
||||
@@ -593,7 +596,7 @@ read_image_data(gd_GIF * gif, int interlace)
|
||||
entry = table->entries[key];
|
||||
str_len = entry.length;
|
||||
if(frm_off + str_len > frm_size){
|
||||
LV_LOG_WARN("LZW table token overflows the frame buffer");
|
||||
ESP_LOGW(TAG, "LZW table token overflows the frame buffer");
|
||||
lv_free(table);
|
||||
return -1;
|
||||
}
|
||||
@@ -635,7 +638,7 @@ read_image(gd_GIF * 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){
|
||||
LV_LOG_WARN("Frame coordinates out of image bounds");
|
||||
ESP_LOGW(TAG, "Frame coordinates out of image bounds");
|
||||
return -1;
|
||||
}
|
||||
f_gif_read(gif, &fisrz, 1);
|
||||
|
||||
@@ -14,6 +14,7 @@ LvglGif::LvglGif(const lv_img_dsc_t* img_dsc)
|
||||
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
|
||||
@@ -33,7 +34,7 @@ LvglGif::LvglGif(const lv_img_dsc_t* img_dsc)
|
||||
}
|
||||
|
||||
loaded_ = true;
|
||||
ESP_LOGI(TAG, "GIF loaded from image descriptor: %dx%d", gif_->width, gif_->height);
|
||||
ESP_LOGD(TAG, "GIF loaded from image descriptor: %dx%d", gif_->width, gif_->height);
|
||||
}
|
||||
|
||||
// Destructor
|
||||
@@ -72,7 +73,7 @@ void LvglGif::Start() {
|
||||
// Render first frame
|
||||
NextFrame();
|
||||
|
||||
ESP_LOGI(TAG, "GIF animation started");
|
||||
ESP_LOGD(TAG, "GIF animation started");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +81,7 @@ void LvglGif::Pause() {
|
||||
if (timer_) {
|
||||
playing_ = false;
|
||||
lv_timer_pause(timer_);
|
||||
ESP_LOGI(TAG, "GIF animation paused");
|
||||
ESP_LOGD(TAG, "GIF animation paused");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +94,7 @@ void LvglGif::Resume() {
|
||||
if (timer_) {
|
||||
playing_ = true;
|
||||
lv_timer_resume(timer_);
|
||||
ESP_LOGI(TAG, "GIF animation resumed");
|
||||
ESP_LOGD(TAG, "GIF animation resumed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +107,7 @@ void LvglGif::Stop() {
|
||||
if (gif_) {
|
||||
gd_rewind(gif_);
|
||||
NextFrame();
|
||||
ESP_LOGI(TAG, "GIF animation stopped and rewound");
|
||||
ESP_LOGD(TAG, "GIF animation stopped and rewound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +173,7 @@ void LvglGif::NextFrame() {
|
||||
if (timer_) {
|
||||
lv_timer_pause(timer_);
|
||||
}
|
||||
ESP_LOGI(TAG, "GIF animation completed");
|
||||
ESP_LOGD(TAG, "GIF animation completed");
|
||||
}
|
||||
|
||||
// Render current frame
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <font_awesome.h>
|
||||
#include <img_converters.h>
|
||||
|
||||
#include "lvgl_display.h"
|
||||
#include "board.h"
|
||||
@@ -201,12 +202,7 @@ void LvglDisplay::UpdateStatusBar(bool update_all) {
|
||||
esp_pm_lock_release(pm_lock_);
|
||||
}
|
||||
|
||||
void LvglDisplay::SetPreviewImage(const lv_img_dsc_t* image) {
|
||||
// Do nothing but free the image
|
||||
if (image != nullptr) {
|
||||
heap_caps_free((void*)image->data);
|
||||
heap_caps_free((void*)image);
|
||||
}
|
||||
void LvglDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
|
||||
}
|
||||
|
||||
void LvglDisplay::SetPowerSaveMode(bool on) {
|
||||
@@ -218,3 +214,29 @@ void LvglDisplay::SetPowerSaveMode(bool on) {
|
||||
SetEmotion("neutral");
|
||||
}
|
||||
}
|
||||
|
||||
bool LvglDisplay::SnapshotToJpeg(uint8_t*& jpeg_output_data, size_t& jpeg_output_data_size, int quality) {
|
||||
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) {
|
||||
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]);
|
||||
}
|
||||
|
||||
if (!fmt2jpg(draw_buffer->data, draw_buffer->data_size, draw_buffer->header.w, draw_buffer->header.h,
|
||||
PIXFORMAT_RGB565, quality, &jpeg_output_data, &jpeg_output_data_size)) {
|
||||
lv_draw_buf_destroy(draw_buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
lv_draw_buf_destroy(draw_buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define LVGL_DISPLAY_H
|
||||
|
||||
#include "display.h"
|
||||
#include "lvgl_image.h"
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <esp_timer.h>
|
||||
@@ -19,9 +20,10 @@ public:
|
||||
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(const lv_img_dsc_t* image);
|
||||
virtual void SetPreviewImage(std::unique_ptr<LvglImage> image);
|
||||
virtual void UpdateStatusBar(bool update_all = false);
|
||||
virtual void SetPowerSaveMode(bool on);
|
||||
virtual bool SnapshotToJpeg(uint8_t*& jpeg_output_data, size_t& jpeg_output_size, int quality = 80);
|
||||
|
||||
protected:
|
||||
esp_pm_lock_handle_t pm_lock_ = nullptr;
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
#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;
|
||||
image_dsc_.data_size = size;
|
||||
image_dsc_.data = static_cast<uint8_t*>(data);
|
||||
}
|
||||
|
||||
bool LvglRawImage::IsGif() const {
|
||||
@@ -31,3 +33,32 @@ LvglCBinImage::~LvglCBinImage() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -39,4 +39,15 @@ public:
|
||||
|
||||
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_;
|
||||
};
|
||||
@@ -40,6 +40,9 @@ OledDisplay::OledDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handl
|
||||
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");
|
||||
|
||||
@@ -15,7 +15,7 @@ dependencies:
|
||||
78/esp_lcd_nv3023: ~1.0.0
|
||||
78/esp-wifi-connect: ~2.5.2
|
||||
78/esp-opus-encoder: ~2.4.1
|
||||
78/esp-ml307: ~3.3.3
|
||||
78/esp-ml307: ~3.3.5
|
||||
78/xiaozhi-fonts: ~1.5.2
|
||||
espressif/led_strip: ~3.0.1
|
||||
espressif/esp_codec_dev: ~1.4.0
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <nvs_flash.h>
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_event.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include "application.h"
|
||||
#include "system_info.h"
|
||||
@@ -27,5 +29,4 @@ extern "C" void app_main(void)
|
||||
// Launch the application
|
||||
auto& app = Application::GetInstance();
|
||||
app.Start();
|
||||
app.MainEventLoop();
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
|
||||
#define TAG "MCP"
|
||||
|
||||
#define DEFAULT_TOOLCALL_STACK_SIZE 6144
|
||||
|
||||
McpServer::McpServer() {
|
||||
}
|
||||
|
||||
@@ -111,6 +109,9 @@ void McpServer::AddCommonTools() {
|
||||
Property("question", kPropertyTypeString)
|
||||
}),
|
||||
[camera](const PropertyList& properties) -> ReturnValue {
|
||||
// Lower the priority to do the camera capture
|
||||
TaskPriorityReset priority_reset(1);
|
||||
|
||||
if (!camera->Capture()) {
|
||||
throw std::runtime_error("Failed to capture photo");
|
||||
}
|
||||
@@ -138,10 +139,10 @@ void McpServer::AddUserOnlyTools() {
|
||||
PropertyList(),
|
||||
[this](const PropertyList& properties) -> ReturnValue {
|
||||
auto& app = Application::GetInstance();
|
||||
app.Schedule([]() {
|
||||
app.Schedule([&app]() {
|
||||
ESP_LOGW(TAG, "User requested reboot");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
auto& app = Application::GetInstance();
|
||||
|
||||
app.Reboot();
|
||||
});
|
||||
return true;
|
||||
@@ -157,8 +158,7 @@ void McpServer::AddUserOnlyTools() {
|
||||
ESP_LOGI(TAG, "User requested firmware upgrade from URL: %s", url.c_str());
|
||||
|
||||
auto& app = Application::GetInstance();
|
||||
app.Schedule([url]() {
|
||||
auto& app = Application::GetInstance();
|
||||
app.Schedule([url, &app]() {
|
||||
auto ota = std::make_unique<Ota>();
|
||||
|
||||
bool success = app.UpgradeFirmware(*ota, url);
|
||||
@@ -187,6 +187,63 @@ void McpServer::AddUserOnlyTools() {
|
||||
}
|
||||
return json;
|
||||
});
|
||||
|
||||
AddUserOnlyTool("self.screen.snapshot", "Snapshot the screen and upload it to a specific URL",
|
||||
PropertyList({
|
||||
Property("url", kPropertyTypeString),
|
||||
Property("quality", kPropertyTypeInteger, 80, 1, 100)
|
||||
}),
|
||||
[display](const PropertyList& properties) -> ReturnValue {
|
||||
auto url = properties["url"].value<std::string>();
|
||||
auto quality = properties["quality"].value<int>();
|
||||
|
||||
uint8_t* jpeg_output_data = nullptr;
|
||||
size_t jpeg_output_size = 0;
|
||||
if (!display->SnapshotToJpeg(jpeg_output_data, jpeg_output_size, quality)) {
|
||||
throw std::runtime_error("Failed to snapshot screen");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Upload snapshot %u bytes to %s", jpeg_output_size, url.c_str());
|
||||
|
||||
// 构造multipart/form-data请求体
|
||||
std::string boundary = "----ESP32_SCREEN_SNAPSHOT_BOUNDARY";
|
||||
|
||||
auto http = Board::GetInstance().GetNetwork()->CreateHttp(3);
|
||||
http->SetHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
|
||||
if (!http->Open("POST", url)) {
|
||||
free(jpeg_output_data);
|
||||
throw std::runtime_error("Failed to open URL: " + url);
|
||||
}
|
||||
{
|
||||
// 文件字段头部
|
||||
std::string file_header;
|
||||
file_header += "--" + boundary + "\r\n";
|
||||
file_header += "Content-Disposition: form-data; name=\"file\"; filename=\"screenshot.jpg\"\r\n";
|
||||
file_header += "Content-Type: image/jpeg\r\n";
|
||||
file_header += "\r\n";
|
||||
http->Write(file_header.c_str(), file_header.size());
|
||||
}
|
||||
|
||||
// JPEG数据
|
||||
http->Write((const char*)jpeg_output_data, jpeg_output_size);
|
||||
free(jpeg_output_data);
|
||||
|
||||
{
|
||||
// multipart尾部
|
||||
std::string multipart_footer;
|
||||
multipart_footer += "\r\n--" + boundary + "--\r\n";
|
||||
http->Write(multipart_footer.c_str(), multipart_footer.size());
|
||||
}
|
||||
http->Write("", 0);
|
||||
|
||||
if (http->GetStatusCode() != 200) {
|
||||
throw std::runtime_error("Unexpected status code: " + std::to_string(http->GetStatusCode()));
|
||||
}
|
||||
std::string result = http->ReadAll();
|
||||
http->Close();
|
||||
ESP_LOGI(TAG, "Snapshot screen result: %s", result.c_str());
|
||||
return true;
|
||||
});
|
||||
|
||||
AddUserOnlyTool("self.screen.preview_image", "Preview an image on the screen",
|
||||
PropertyList({
|
||||
@@ -199,12 +256,16 @@ void McpServer::AddUserOnlyTools() {
|
||||
if (!http->Open("GET", url)) {
|
||||
throw std::runtime_error("Failed to open URL: " + url);
|
||||
}
|
||||
if (http->GetStatusCode() != 200) {
|
||||
throw std::runtime_error("Unexpected status code: " + std::to_string(http->GetStatusCode()));
|
||||
int status_code = http->GetStatusCode();
|
||||
if (status_code != 200) {
|
||||
throw std::runtime_error("Unexpected status code: " + std::to_string(status_code));
|
||||
}
|
||||
|
||||
size_t content_length = http->GetBodyLength();
|
||||
char* data = (char*)heap_caps_malloc(content_length, MALLOC_CAP_8BIT);
|
||||
if (data == nullptr) {
|
||||
throw std::runtime_error("Failed to allocate memory for image: " + url);
|
||||
}
|
||||
size_t total_read = 0;
|
||||
while (total_read < content_length) {
|
||||
int ret = http->Read(data + total_read, content_length - total_read);
|
||||
@@ -212,24 +273,15 @@ void McpServer::AddUserOnlyTools() {
|
||||
heap_caps_free(data);
|
||||
throw std::runtime_error("Failed to download image: " + url);
|
||||
}
|
||||
if (ret == 0) {
|
||||
break;
|
||||
}
|
||||
total_read += ret;
|
||||
}
|
||||
http->Close();
|
||||
|
||||
auto img_dsc = (lv_img_dsc_t*)heap_caps_calloc(1, sizeof(lv_img_dsc_t), MALLOC_CAP_8BIT);
|
||||
img_dsc->data_size = content_length;
|
||||
img_dsc->data = (uint8_t*)data;
|
||||
if (lv_image_decoder_get_info(img_dsc, &img_dsc->header) != LV_RESULT_OK) {
|
||||
heap_caps_free(data);
|
||||
heap_caps_free(img_dsc);
|
||||
throw std::runtime_error("Failed to get image info");
|
||||
}
|
||||
ESP_LOGI(TAG, "Preview image: %s size: %d resolution: %d x %d", url.c_str(), content_length, img_dsc->header.w, img_dsc->header.h);
|
||||
|
||||
auto& app = Application::GetInstance();
|
||||
app.Schedule([display, img_dsc]() {
|
||||
display->SetPreviewImage(img_dsc);
|
||||
});
|
||||
auto image = std::make_unique<LvglAllocatedImage>(data, content_length);
|
||||
display->SetPreviewImage(std::move(image));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -381,13 +433,7 @@ void McpServer::ParseMessage(const cJSON* json) {
|
||||
ReplyError(id_int, "Invalid arguments");
|
||||
return;
|
||||
}
|
||||
auto stack_size = cJSON_GetObjectItem(params, "stackSize");
|
||||
if (stack_size != nullptr && !cJSON_IsNumber(stack_size)) {
|
||||
ESP_LOGE(TAG, "tools/call: Invalid stackSize");
|
||||
ReplyError(id_int, "Invalid stackSize");
|
||||
return;
|
||||
}
|
||||
DoToolCall(id_int, std::string(tool_name->valuestring), tool_arguments, stack_size ? stack_size->valueint : DEFAULT_TOOLCALL_STACK_SIZE);
|
||||
DoToolCall(id_int, std::string(tool_name->valuestring), tool_arguments);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Method not implemented: %s", method_str.c_str());
|
||||
ReplyError(id_int, "Method not implemented: " + method_str);
|
||||
@@ -467,7 +513,7 @@ void McpServer::GetToolsList(int id, const std::string& cursor, bool list_user_o
|
||||
ReplyResult(id, json);
|
||||
}
|
||||
|
||||
void McpServer::DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments, int stack_size) {
|
||||
void McpServer::DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments) {
|
||||
auto tool_iter = std::find_if(tools_.begin(), tools_.end(),
|
||||
[&tool_name](const McpTool* tool) {
|
||||
return tool->name() == tool_name;
|
||||
@@ -509,15 +555,9 @@ void McpServer::DoToolCall(int id, const std::string& tool_name, const cJSON* to
|
||||
return;
|
||||
}
|
||||
|
||||
// Start a task to receive data with stack size
|
||||
esp_pthread_cfg_t cfg = esp_pthread_get_default_config();
|
||||
cfg.thread_name = "tool_call";
|
||||
cfg.stack_size = stack_size;
|
||||
cfg.prio = 1;
|
||||
esp_pthread_set_cfg(&cfg);
|
||||
|
||||
// Use a thread to call the tool to avoid blocking the main thread
|
||||
tool_call_thread_ = std::thread([this, id, tool_iter, arguments = std::move(arguments)]() {
|
||||
// Use main thread to call the tool
|
||||
auto& app = Application::GetInstance();
|
||||
app.Schedule([this, id, tool_iter, arguments = std::move(arguments)]() {
|
||||
try {
|
||||
ReplyResult(id, (*tool_iter)->Call(arguments));
|
||||
} catch (const std::exception& e) {
|
||||
@@ -525,5 +565,4 @@ void McpServer::DoToolCall(int id, const std::string& tool_name, const cJSON* to
|
||||
ReplyError(id, e.what());
|
||||
}
|
||||
});
|
||||
tool_call_thread_.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,10 +336,9 @@ private:
|
||||
void ReplyError(int id, const std::string& message);
|
||||
|
||||
void GetToolsList(int id, const std::string& cursor, bool list_user_only_tools);
|
||||
void DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments, int stack_size);
|
||||
void DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments);
|
||||
|
||||
std::vector<McpTool*> tools_;
|
||||
std::thread tool_call_thread_;
|
||||
};
|
||||
|
||||
#endif // MCP_SERVER_H
|
||||
|
||||
@@ -55,6 +55,7 @@ CONFIG_LV_USE_IMGFONT=y
|
||||
CONFIG_LV_USE_ASSERT_STYLE=y
|
||||
CONFIG_LV_USE_GIF=y
|
||||
CONFIG_LV_USE_LODEPNG=y
|
||||
CONFIG_LV_USE_SNAPSHOT=y
|
||||
|
||||
# Use compressed font
|
||||
CONFIG_LV_FONT_FMT_TXT_LARGE=y
|
||||
|
||||
Reference in New Issue
Block a user