diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 2e82a24a..a60b6170 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -2,10 +2,12 @@ set(SOURCES "audio_codecs/audio_codec.cc" "audio_codecs/no_audio_codec.cc" "audio_codecs/box_audio_codec.cc" "audio_codecs/es8311_audio_codec.cc" + "audio_codecs/es8388_audio_codec.cc" "audio_codecs/cores3_audio_codec.cc" "audio_codecs/tcircles3_audio_codec.cc" "led/single_led.cc" "led/circular_strip.cc" + "display/atk_st7789_80i.cc" "display/display.cc" "display/no_display.cc" "display/lcd_display.cc" @@ -75,7 +77,11 @@ elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD) elseif(CONFIG_BOARD_TYPE_LILYGO_T_CIRCLE_S3) set(BOARD_TYPE "lilygo-t-circle-s3") elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI_ESP32S3) - set(BOARD_TYPE "movecall-moji-esp32s3") + set(BOARD_TYPE "movecall-moji-esp32s3") +elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3) + set(BOARD_TYPE "atk-dnesp32s3") +elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX) + set(BOARD_TYPE "atk-dnesp32s3-box") endif() file(GLOB BOARD_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.cc) list(APPEND SOURCES ${BOARD_SOURCES}) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 5fb7a721..6512dde0 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -76,6 +76,10 @@ choice BOARD_TYPE bool "LILYGO T-Circle-S3" config BOARD_TYPE_MOVECALL_MOJI_ESP32S3 bool "Movecall Moji 小智AI衍生版" + config BOARD_TYPE_ATK_DNESP32S3 + bool "正点原子DNESP32S3开发板" + config BOARD_TYPE_ATK_DNESP32S3_BOX + bool "正点原子DNESP32S3-BOX" endchoice choice DISPLAY_LCD_TYPE diff --git a/main/audio_codecs/es8388_audio_codec.cc b/main/audio_codecs/es8388_audio_codec.cc new file mode 100644 index 00000000..4db8153d --- /dev/null +++ b/main/audio_codecs/es8388_audio_codec.cc @@ -0,0 +1,191 @@ +#include "es8388_audio_codec.h" + +#include + +static const char TAG[] = "Es8388AudioCodec"; + +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) { + duplex_ = true; // 是否双工 + input_reference_ = false; // 是否使用参考输入,实现回声消除 + input_channels_ = 1; // 输入通道数 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + 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 = { + .port = I2S_NUM_0, + .rx_handle = rx_handle_, + .tx_handle = tx_handle_, + }; + data_if_ = audio_codec_new_i2s_data(&i2s_cfg); + assert(data_if_ != NULL); + + // Output + audio_codec_i2c_cfg_t i2c_cfg = { + .port = i2c_port, + .addr = es8388_addr, + .bus_handle = i2c_master_handle, + }; + ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(ctrl_if_ != NULL); + + gpio_if_ = audio_codec_new_gpio(); + assert(gpio_if_ != NULL); + + es8388_codec_cfg_t es8388_cfg = {}; + es8388_cfg.ctrl_if = ctrl_if_; + es8388_cfg.gpio_if = gpio_if_; + es8388_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; + es8388_cfg.master_mode = true; + es8388_cfg.pa_pin = pa_pin; + es8388_cfg.pa_reverted = false; + es8388_cfg.hw_gain.pa_voltage = 5.0; + es8388_cfg.hw_gain.codec_dac_voltage = 3.3; + codec_if_ = es8388_codec_new(&es8388_cfg); + assert(codec_if_ != NULL); + + esp_codec_dev_cfg_t outdev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = codec_if_, + .data_if = data_if_, + }; + output_dev_ = esp_codec_dev_new(&outdev_cfg); + assert(output_dev_ != NULL); + + esp_codec_dev_cfg_t indev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_IN, + .codec_if = codec_if_, + .data_if = data_if_, + }; + input_dev_ = esp_codec_dev_new(&indev_cfg); + assert(input_dev_ != NULL); + + ESP_LOGI(TAG, "Es8388AudioCodec initialized"); +} + +Es8388AudioCodec::~Es8388AudioCodec() { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + esp_codec_dev_delete(output_dev_); + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + esp_codec_dev_delete(input_dev_); + + audio_codec_delete_codec_if(codec_if_); + audio_codec_delete_ctrl_if(ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void Es8388AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din){ + assert(input_sample_rate_ == output_sample_rate_); + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240 * 3, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256 + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = I2S_STD_SLOT_BOTH, + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + +void Es8388AudioCodec::SetOutputVolume(int volume) { + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); + AudioCodec::SetOutputVolume(volume); +} + +void Es8388AudioCodec::EnableInput(bool enable) { + if (enable == input_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 1, + .channel_mask = 0, + .sample_rate = (uint32_t)input_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 40.0)); + } else { + // ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); //输入输出共用的i2s,只关闭了输出后会把输入也关没了,所以得注释关闭 + } + AudioCodec::EnableInput(enable); +} + +void Es8388AudioCodec::EnableOutput(bool enable) { + if (enable == output_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 1, + .channel_mask = 0, + .sample_rate = (uint32_t)output_sample_rate_, + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); + } else { + // ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); //输入输出共用的i2s,只关闭了输出后会把输入也关没了,所以得注释关闭 + } + AudioCodec::EnableOutput(enable); +} + +int Es8388AudioCodec::Read(int16_t* dest, int samples) { + if (input_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); + } + return samples; +} + +int Es8388AudioCodec::Write(const int16_t* data, int samples) { + if (output_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); + } + return samples; +} \ No newline at end of file diff --git a/main/audio_codecs/es8388_audio_codec.h b/main/audio_codecs/es8388_audio_codec.h new file mode 100644 index 00000000..949ffb5c --- /dev/null +++ b/main/audio_codecs/es8388_audio_codec.h @@ -0,0 +1,36 @@ +#ifndef _ES8388_AUDIO_CODEC_H +#define _ES8388_AUDIO_CODEC_H + +#include "audio_codec.h" + +#include +#include +#include + +class Es8388AudioCodec : public AudioCodec { +private: + const audio_codec_data_if_t* data_if_ = nullptr; + const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; + const audio_codec_if_t* codec_if_ = nullptr; + const audio_codec_gpio_if_t* gpio_if_ = nullptr; + + esp_codec_dev_handle_t output_dev_ = nullptr; + esp_codec_dev_handle_t input_dev_ = nullptr; + + void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); + + virtual int Read(int16_t* dest, int samples) override; + virtual int Write(const int16_t* data, int samples) override; + +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); + virtual ~Es8388AudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _ES8388_AUDIO_CODEC_H diff --git a/main/audio_codecs/no_audio_codec.cc b/main/audio_codecs/no_audio_codec.cc index cc62f136..56c486d5 100644 --- a/main/audio_codecs/no_audio_codec.cc +++ b/main/audio_codecs/no_audio_codec.cc @@ -74,6 +74,60 @@ NoAudioCodecDuplex::NoAudioCodecDuplex(int input_sample_rate, int output_sample_ ESP_LOGI(TAG, "Duplex channels created"); } +ATK_NoAudioCodecDuplex::ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { + duplex_ = true; + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 6, + .dma_frame_num = 240, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = (uint32_t)output_sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256 + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = I2S_STD_SLOT_BOTH, + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + }, + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + + NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din) { duplex_ = false; input_sample_rate_ = input_sample_rate; diff --git a/main/audio_codecs/no_audio_codec.h b/main/audio_codecs/no_audio_codec.h index 9a1b0029..087b7af8 100644 --- a/main/audio_codecs/no_audio_codec.h +++ b/main/audio_codecs/no_audio_codec.h @@ -20,6 +20,11 @@ public: NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); }; +class ATK_NoAudioCodecDuplex : public NoAudioCodec { +public: + ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); +}; + class NoAudioCodecSimplex : public NoAudioCodec { public: NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din); diff --git a/main/boards/atk-dnesp32s3-box/atk_dnesp32s3_box.cc b/main/boards/atk-dnesp32s3-box/atk_dnesp32s3_box.cc new file mode 100644 index 00000000..a0b2bc05 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box/atk_dnesp32s3_box.cc @@ -0,0 +1,129 @@ +#include "wifi_board.h" +#include "audio_codec.h" +#include "audio_codecs/no_audio_codec.h" +#include "display/atk_st7789_80i.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "led/single_led.h" +#include "iot/thing_manager.h" +#include +#include +#include +#include "i2c_device.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/timers.h" + +#define TAG "atk_dnesp32s3_box" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class XL9555 : public I2cDevice { +public: + XL9555(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(0x06, 0x1B); + WriteReg(0x07, 0xFE); + } + + void SetOutputState(uint8_t bit, uint8_t level) { + uint16_t data; + if (bit < 8) { + data = ReadReg(0x02); + } else { + data = ReadReg(0x03); + bit -= 8; + } + + data = (data & ~(1 << bit)) | (level << bit); + + if (bit < 8) { + WriteReg(0x02, data); + } else { + WriteReg(0x03, data); + } + } +}; + +class atk_dnesp32s3_box : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + i2c_master_dev_handle_t xl9555_handle_; + Button boot_button_; + ATK_ST7789_80_Display* display_; + XL9555* xl9555_; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = GPIO_NUM_48, + .scl_io_num = GPIO_NUM_45, + .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_)); + + // Initialize XL9555 + xl9555_ = new XL9555(i2c_bus_, 0x20); + } + + void InitializeATK_ST7789_80_Display() { + display_ = new ATK_ST7789_80_Display(DISPLAY_BACKLIGHT_PIN,DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, + DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + xl9555_->SetOutputState(5, 1); + xl9555_->SetOutputState(7, 1); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) + { + ResetWifiConfiguration(); + } + }); + boot_button_.OnPressDown([this]() { + Application::GetInstance().StartListening(); + }); + boot_button_.OnPressUp([this]() { + Application::GetInstance().StopListening(); + }); + + auto codec = GetAudioCodec(); + GetAudioCodec()->SetOutputVolume(50); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Lamp")); + } + +public: + atk_dnesp32s3_box() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeATK_ST7789_80_Display(); + InitializeButtons(); + InitializeIot(); + } + + virtual AudioCodec* GetAudioCodec() override { + static ATK_NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } +}; + +DECLARE_BOARD(atk_dnesp32s3_box); diff --git a/main/boards/atk-dnesp32s3-box/config.h b/main/boards/atk-dnesp32s3-box/config.h new file mode 100644 index 00000000..2479fc57 --- /dev/null +++ b/main/boards/atk-dnesp32s3-box/config.h @@ -0,0 +1,28 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_WS GPIO_NUM_13 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_21 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_47 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_14 + +#define BUILTIN_LED_GPIO GPIO_NUM_4 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_SWAP_XY true +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/atk-dnesp32s3/atk_dnesp32s3.cc b/main/boards/atk-dnesp32s3/atk_dnesp32s3.cc new file mode 100644 index 00000000..e21e1db4 --- /dev/null +++ b/main/boards/atk-dnesp32s3/atk_dnesp32s3.cc @@ -0,0 +1,188 @@ +#include "wifi_board.h" +#include "es8388_audio_codec.h" +#include "display/lcd_display.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "i2c_device.h" +#include "iot/thing_manager.h" +#include "led/single_led.h" + +#include +#include +#include +#include +#include + +#define TAG "atk_dnesp32s3" + +LV_FONT_DECLARE(font_puhui_20_4); +LV_FONT_DECLARE(font_awesome_20_4); + +class XL9555 : public I2cDevice { +public: + XL9555(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(0x06, 0x03); + WriteReg(0x07, 0xF0); + } + + void SetOutputState(uint8_t bit, uint8_t level) { + uint16_t data; + if (bit < 8) { + data = ReadReg(0x02); + } else { + data = ReadReg(0x03); + bit -= 8; + } + + data = (data & ~(1 << bit)) | (level << bit); + + if (bit < 8) { + WriteReg(0x02, data); + } else { + WriteReg(0x03, data); + } + } +}; + + +class atk_dnesp32s3 : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + Button boot_button_; + LcdDisplay* display_; + XL9555* xl9555_; + + void InitializeI2c() + { + // 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_)); + + // Initialize XL9555 + xl9555_ = new XL9555(i2c_bus_, 0x20); + } + + // Initialize spi peripheral + void InitializeSpi() + { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = LCD_MOSI_PIN; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = LCD_SCLK_PIN; + 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(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + void InitializeButtons() + { + boot_button_.OnClick([this]() + { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()){ + ResetWifiConfiguration(); + } + }); + boot_button_.OnPressDown([this]() + { + Application::GetInstance().StartListening(); + }); + boot_button_.OnPressUp([this]() + { + Application::GetInstance().StopListening(); + }); + } + + void InitializeSt7789Display() + { + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + ESP_LOGD(TAG, "Install panel IO"); + // 液晶屏控制IO初始化 + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = LCD_CS_PIN; + io_config.dc_gpio_num = LCD_DC_PIN; + io_config.spi_mode = 0; + io_config.pclk_hz = 20 * 1000 * 1000; + io_config.trans_queue_depth = 7; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + esp_lcd_new_panel_io_spi(SPI2_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 = GPIO_NUM_NC; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + panel_config.data_endian = LCD_RGB_DATA_ENDIAN_BIG, + esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel); + + esp_lcd_panel_reset(panel); + xl9555_->SetOutputState(8, 1); + xl9555_->SetOutputState(2, 0); + + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, true); + esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + display_ = new LcdDisplay(panel_io, panel, DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY, + { + .text_font = &font_puhui_20_4, + .icon_font = &font_awesome_20_4, + .emoji_font = emoji_font_64_lite_init(), + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot(){ + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + } + +public: + atk_dnesp32s3() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + InitializeSpi(); + InitializeSt7789Display(); + InitializeButtons(); + InitializeIot(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { + static Es8388AudioCodec* audio_codec = nullptr; + if (audio_codec == nullptr) { + audio_codec = new Es8388AudioCodec(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, + GPIO_NUM_NC, AUDIO_CODEC_ES8388_ADDR); + + audio_codec->SetOutputVolume(AUDIO_DEFAULT_OUTPUT_VOLUME); //设置默认音量 + } + return audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } +}; + +DECLARE_BOARD(atk_dnesp32s3); \ No newline at end of file diff --git a/main/boards/atk-dnesp32s3/config.h b/main/boards/atk-dnesp32s3/config.h new file mode 100644 index 00000000..8cde6207 --- /dev/null +++ b/main/boards/atk-dnesp32s3/config.h @@ -0,0 +1,44 @@ + +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include + +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 +#define AUDIO_DEFAULT_OUTPUT_VOLUME 90 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_3 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_9 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_46 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_14 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_10 + +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_41 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_42 +#define AUDIO_CODEC_ES8388_ADDR ES8388_CODEC_DEFAULT_ADDR + +#define BOOT_BUTTON_GPIO GPIO_NUM_0 + +#define BUILTIN_LED_GPIO GPIO_NUM_1 + +#define LCD_SCLK_PIN GPIO_NUM_12 +#define LCD_MOSI_PIN GPIO_NUM_11 +#define LCD_MISO_PIN GPIO_NUM_13 +#define LCD_DC_PIN GPIO_NUM_40 +#define LCD_CS_PIN GPIO_NUM_21 + +#define DISPLAY_WIDTH 320 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ + diff --git a/main/display/atk_st7789_80i.cc b/main/display/atk_st7789_80i.cc new file mode 100644 index 00000000..3d32ff9a --- /dev/null +++ b/main/display/atk_st7789_80i.cc @@ -0,0 +1,337 @@ +#include "atk_st7789_80i.h" +#include "font_awesome_symbols.h" + +#include +#include +#include +#include "driver/gpio.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_ops.h" + + +#define TAG "atk_st7789" + +#define ATK_ST7789_80_LVGL_TICK_PERIOD_MS 2 +#define ATK_ST7789_80_LVGL_TASK_MAX_DELAY_MS 20 +#define ATK_ST7789_80_LVGL_TASK_MIN_DELAY_MS 1 +#define ATK_ST7789_80_LVGL_TASK_STACK_SIZE (10 * 1024) +#define ATK_ST7789_80_LVGL_TASK_PRIORITY 10 + +// Pin Definitions +#define LCD_NUM_CS GPIO_NUM_1 +#define LCD_NUM_DC GPIO_NUM_2 +#define LCD_NUM_RD GPIO_NUM_41 +#define LCD_NUM_WR GPIO_NUM_42 +#define LCD_NUM_RST GPIO_NUM_NC + +#define GPIO_LCD_D0 GPIO_NUM_40 +#define GPIO_LCD_D1 GPIO_NUM_39 +#define GPIO_LCD_D2 GPIO_NUM_38 +#define GPIO_LCD_D3 GPIO_NUM_12 +#define GPIO_LCD_D4 GPIO_NUM_11 +#define GPIO_LCD_D5 GPIO_NUM_10 +#define GPIO_LCD_D6 GPIO_NUM_9 +#define GPIO_LCD_D7 GPIO_NUM_46 + +LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_awesome_30_1); +LV_FONT_DECLARE(font_awesome_14_1); + +static lv_disp_drv_t disp_drv; +static bool example_notify_lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx) { + lv_disp_drv_t *disp_driver = (lv_disp_drv_t *)user_ctx; + lv_disp_flush_ready(disp_driver); + return false; +} + +static void lvgl_disp_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) { + esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t)drv->user_data; + // copy a buffer's content to a specific area of the display + esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, color_map); +} + +void ATK_ST7789_80_Display::LvglTask() { + ESP_LOGI(TAG, "Starting LVGL task"); + uint32_t task_delay_ms = ATK_ST7789_80_LVGL_TASK_MAX_DELAY_MS; + while (1) { + // Lock the mutex due to the LVGL APIs are not thread-safe + if (Lock()) { + task_delay_ms = lv_timer_handler(); + Unlock(); + } + if (task_delay_ms > ATK_ST7789_80_LVGL_TASK_MAX_DELAY_MS) { + task_delay_ms = ATK_ST7789_80_LVGL_TASK_MAX_DELAY_MS; + } + else if (task_delay_ms < ATK_ST7789_80_LVGL_TASK_MIN_DELAY_MS) { + task_delay_ms = ATK_ST7789_80_LVGL_TASK_MIN_DELAY_MS; + } + + vTaskDelay(pdMS_TO_TICKS(task_delay_ms)); + } +} + +extern "C" void emoji_font_init(); + + +ATK_ST7789_80_Display::ATK_ST7789_80_Display(gpio_num_t backlight_pin, + int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy) + : backlight_pin_(backlight_pin),mirror_x_(mirror_x), mirror_y_(mirror_y), swap_xy_(swap_xy) { + width_ = 320; + height_ = 240; + + width_ = width; + height_ = height; + offset_x_ = offset_x; + offset_y_ = offset_y; + esp_lcd_panel_io_handle_t io_handle = NULL; + esp_lcd_panel_handle_t panel_handle = NULL; + static lv_disp_draw_buf_t disp_buf; // contains internal graphic buffer(s) called draw buffer(s) + gpio_config_t gpio_init_struct; + InitializeBacklight(backlight_pin); // light set + emoji_font_init(); + + /* 配置RD引脚 */ + gpio_init_struct.intr_type = GPIO_INTR_DISABLE; + gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT; + gpio_init_struct.pin_bit_mask = 1ull << LCD_NUM_RD; + gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; + gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&gpio_init_struct); + gpio_set_level(LCD_NUM_RD, 1); + + esp_lcd_i80_bus_handle_t i80_bus = NULL; + esp_lcd_i80_bus_config_t bus_config = { + .dc_gpio_num = LCD_NUM_DC, + .wr_gpio_num = LCD_NUM_WR, + .clk_src = LCD_CLK_SRC_DEFAULT, + .data_gpio_nums = { + GPIO_LCD_D0, + GPIO_LCD_D1, + GPIO_LCD_D2, + GPIO_LCD_D3, + GPIO_LCD_D4, + GPIO_LCD_D5, + GPIO_LCD_D6, + GPIO_LCD_D7, + }, + .bus_width = 8, + .max_transfer_bytes = width_ * height_ * sizeof(uint16_t), + .psram_trans_align = 64, + .sram_trans_align = 4, + }; + ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus)); + + esp_lcd_panel_io_i80_config_t io_config = { + .cs_gpio_num = LCD_NUM_CS, + .pclk_hz = (10 * 1000 * 1000), + .trans_queue_depth = 10, + .on_color_trans_done = example_notify_lvgl_flush_ready, + .user_ctx = &disp_drv, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .dc_levels = { + .dc_idle_level = 0, + .dc_cmd_level = 0, + .dc_dummy_level = 0, + .dc_data_level = 1, + }, + .flags = { + .swap_color_bytes = 0, + }, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handle)); + + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = LCD_NUM_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = 16, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle)); + + esp_lcd_panel_reset(panel_handle); + esp_lcd_panel_init(panel_handle); + esp_lcd_panel_invert_color(panel_handle, true); + esp_lcd_panel_set_gap(panel_handle, 0, 0); + uint8_t data0[] = {0x00}; + uint8_t data1[] = {0x65}; + esp_lcd_panel_io_tx_param(io_handle, 0x36, data0, 1); + esp_lcd_panel_io_tx_param(io_handle, 0x3A, data1, 1); + esp_lcd_panel_swap_xy(panel_handle, swap_xy); + esp_lcd_panel_mirror(panel_handle, mirror_x, mirror_y); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); + + ESP_LOGI(TAG, "Initialize LVGL library"); + lv_init(); + void *buf1 = NULL; + void *buf2 = NULL; + + buf1 = heap_caps_malloc(width_ * 60 * sizeof(lv_color_t), MALLOC_CAP_DMA); + buf2 = heap_caps_malloc(width_ * 60 * sizeof(lv_color_t), MALLOC_CAP_DMA); + + lv_disp_draw_buf_init(&disp_buf, buf1, buf2, width_ * 60); + + ESP_LOGI(TAG, "Register display driver to LVGL"); + lv_disp_drv_init(&disp_drv); + disp_drv.hor_res = width_; + disp_drv.ver_res = height_; + disp_drv.offset_x = offset_x_; + disp_drv.offset_y = offset_y_; + disp_drv.flush_cb = lvgl_disp_flush_cb; + disp_drv.draw_buf = &disp_buf; + disp_drv.user_data = panel_handle; + lv_disp_drv_register(&disp_drv); + + ESP_LOGI(TAG, "Install LVGL tick timer"); + // Tick interface for LVGL (using esp_timer to generate 2ms periodic event) + const esp_timer_create_args_t lvgl_tick_timer_args = { + .callback = [](void* arg) { + lv_tick_inc(ATK_ST7789_80_LVGL_TICK_PERIOD_MS); + }, + .arg = NULL, + .dispatch_method = ESP_TIMER_TASK, + .name = "LVGL Tick Timer", + .skip_unhandled_events = false + }; + ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer_)); + ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer_, ATK_ST7789_80_LVGL_TICK_PERIOD_MS * 1000)); + + lvgl_mutex_ = xSemaphoreCreateRecursiveMutex(); + assert(lvgl_mutex_ != nullptr); + ESP_LOGI(TAG, "Create LVGL task"); + xTaskCreate([](void *arg) { + static_cast(arg)->LvglTask(); + vTaskDelete(NULL); + }, "LVGL", ATK_ST7789_80_LVGL_TASK_STACK_SIZE, this, ATK_ST7789_80_LVGL_TASK_PRIORITY, NULL); + + SetupUI(); +} + +ATK_ST7789_80_Display::~ATK_ST7789_80_Display() { + ESP_ERROR_CHECK(esp_timer_stop(lvgl_tick_timer_)); + ESP_ERROR_CHECK(esp_timer_delete(lvgl_tick_timer_)); + + if (content_ != nullptr) { + lv_obj_del(content_); + } + + if (status_bar_ != nullptr) { + lv_obj_del(status_bar_); + } + + if (side_bar_ != nullptr) { + lv_obj_del(side_bar_); + } + + if (container_ != nullptr) { + lv_obj_del(container_); + } + + if (panel_ != nullptr) { + esp_lcd_panel_del(panel_); + } + + if (panel_io_ != nullptr) { + esp_lcd_panel_io_del(panel_io_); + } + + vSemaphoreDelete(lvgl_mutex_); +} + +void ATK_ST7789_80_Display::InitializeBacklight(gpio_num_t backlight_pin) { + if (backlight_pin == GPIO_NUM_NC) { + return; + } + /* Setup LEDC peripheral for PWM backlight control */ +} + +bool ATK_ST7789_80_Display::Lock(int timeout_ms) { + // Convert timeout in milliseconds to FreeRTOS ticks + // If `timeout_ms` is set to 0, the program will block until the condition is met + const TickType_t timeout_ticks = (timeout_ms == 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms); + return xSemaphoreTakeRecursive(lvgl_mutex_, timeout_ticks) == pdTRUE; +} + +void ATK_ST7789_80_Display::Unlock() { + xSemaphoreGiveRecursive(lvgl_mutex_); +} + +void ATK_ST7789_80_Display::SetupUI() { + DisplayLockGuard lock(this); + + auto screen = lv_disp_get_scr_act(lv_disp_get_default()); + lv_obj_set_style_text_font(screen, &font_puhui_14_1, 0); + lv_obj_set_style_text_color(screen, lv_color_black(), 0); + + /* Container */ + container_ = lv_obj_create(screen); + lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES); + lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(container_, 0, 0); + lv_obj_set_style_border_width(container_, 0, 0); + lv_obj_set_style_pad_row(container_, 0, 0); + + /* Status bar */ + status_bar_ = lv_obj_create(container_); + lv_obj_set_size(status_bar_, LV_HOR_RES, 18); + lv_obj_set_style_radius(status_bar_, 0, 0); + + /* Content */ + content_ = lv_obj_create(container_); + lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_radius(content_, 0, 0); + lv_obj_set_width(content_, LV_HOR_RES); + lv_obj_set_flex_grow(content_, 1); + + lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); // 垂直布局(从上到下) + lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_SPACE_EVENLY); // 子对象居中对齐,等距分布 + + emotion_label_ = lv_label_create(content_); + lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_1, 0); + lv_label_set_text(emotion_label_, FONT_AWESOME_AI_CHIP); + // lv_obj_center(emotion_label_); + + chat_message_label_ = lv_label_create(content_); + lv_label_set_text(chat_message_label_, ""); + lv_obj_set_width(chat_message_label_, LV_HOR_RES * 0.8); // 限制宽度为屏幕宽度的 80% + lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP); // 设置为自动换行模式 + lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // 设置文本居中对齐 + + /* Status bar */ + lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_all(status_bar_, 0, 0); + lv_obj_set_style_border_width(status_bar_, 0, 0); + lv_obj_set_style_pad_column(status_bar_, 0, 0); + + network_label_ = lv_label_create(status_bar_); + lv_label_set_text(network_label_, ""); + lv_obj_set_style_text_font(network_label_, &font_awesome_14_1, 0); + + notification_label_ = lv_label_create(status_bar_); + lv_obj_set_flex_grow(notification_label_, 1); + lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_text(notification_label_, "通知"); + lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN); + + status_label_ = lv_label_create(status_bar_); + lv_obj_set_flex_grow(status_label_, 1); + lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_label_set_text(status_label_, "正在初始化"); + lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0); + + mute_label_ = lv_label_create(status_bar_); + lv_label_set_text(mute_label_, ""); + lv_obj_set_style_text_font(mute_label_, &font_awesome_14_1, 0); + + battery_label_ = lv_label_create(status_bar_); + lv_label_set_text(battery_label_, ""); + lv_obj_set_style_text_font(battery_label_, &font_awesome_14_1, 0); +} + +void ATK_ST7789_80_Display::SetChatMessage(const std::string &role, const std::string &content) { + if (chat_message_label_ == nullptr) { + return; + } + + lv_label_set_text(chat_message_label_, content.c_str()); +} diff --git a/main/display/atk_st7789_80i.h b/main/display/atk_st7789_80i.h new file mode 100644 index 00000000..32297793 --- /dev/null +++ b/main/display/atk_st7789_80i.h @@ -0,0 +1,46 @@ +#ifndef ATK_ST7789_80I_H +#define ATK_ST7789_80I_H + +#include "display.h" + +#include +#include +#include +#include +#include +#include + +class ATK_ST7789_80_Display : public Display { +protected: + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + gpio_num_t backlight_pin_ = GPIO_NUM_4; + bool backlight_output_invert_ = false; + bool mirror_x_ = false; + bool mirror_y_ = false; + bool swap_xy_ = false; + int offset_x_ = 0; + int offset_y_ = 0; + SemaphoreHandle_t lvgl_mutex_ = nullptr; + esp_timer_handle_t lvgl_tick_timer_ = nullptr; + + lv_obj_t* status_bar_ = nullptr; + lv_obj_t* content_ = nullptr; + lv_obj_t* container_ = nullptr; + lv_obj_t* side_bar_ = nullptr; + lv_obj_t* chat_message_label_ = nullptr; + + void InitializeBacklight(gpio_num_t backlight_pin); + void LvglTask(); + + virtual void SetupUI(); + virtual bool Lock(int timeout_ms = 0) override; + virtual void Unlock() override; + +public: + ATK_ST7789_80_Display(gpio_num_t backlight_pin,int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy); + ~ATK_ST7789_80_Display(); + void SetChatMessage(const std::string &role, const std::string &content) override; +}; + +#endif // LCD_DISPLAY_H