diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index d98f6b18..e5b1eb81 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -212,6 +212,11 @@ elseif(CONFIG_BOARD_TYPE_ECHOEAR)
set(BUILTIN_TEXT_FONT font_puhui_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
+elseif(CONFIG_BOARD_TYPE_ESP_SENSAIRSHUTTLE)
+ set(BOARD_TYPE "esp-sensairshuttle")
+ set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
+ set(BUILTIN_ICON_FONT font_awesome_16_4)
+ set(DEFAULT_EMOJI_COLLECTION twemoji_32)
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_AUDIO_BOARD)
set(BOARD_TYPE "waveshare-s3-audio-board")
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild
index 13d825cc..5e7a7278 100644
--- a/main/Kconfig.projbuild
+++ b/main/Kconfig.projbuild
@@ -150,6 +150,9 @@ choice BOARD_TYPE
config BOARD_TYPE_ESP_SPARKBOT
bool "Espressif SparkBot"
depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP_SENSAIRSHUTTLE
+ bool "Espressif ESP-SensairShuttle"
+ depends on IDF_TARGET_ESP32C5
config BOARD_TYPE_ESP_SPOT_S3
bool "Espressif Spot-S3"
depends on IDF_TARGET_ESP32S3
@@ -582,7 +585,7 @@ choice DISPLAY_STYLE
config USE_EMOTE_MESSAGE_STYLE
bool "Emote animation style"
- depends on BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ECHOEAR || BOARD_TYPE_LICHUANG_DEV_S3
+ depends on BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ECHOEAR || BOARD_TYPE_LICHUANG_DEV_S3 || BOARD_TYPE_ESP_SENSAIRSHUTTLE
endchoice
choice WAKE_WORD_TYPE
diff --git a/main/boards/esp-sensairshuttle/README.md b/main/boards/esp-sensairshuttle/README.md
new file mode 100644
index 00000000..b9641c2d
--- /dev/null
+++ b/main/boards/esp-sensairshuttle/README.md
@@ -0,0 +1,39 @@
+# ESP-SensairShuttle
+
+## 简介
+
+
+
+ESP-SensairShuttle 是乐鑫携手 Bosch Sensortec 面向**动作感知**与**大模型人机交互**场景联合推出的开发板。
+
+ESP-SensairShuttle 主控采用乐鑫 ESP32-C5-WROOM-1-N16R8 模组,具有 2.4 & 5 GHz 双频 Wi-Fi 6 (802.11ax)、Bluetooth® 5 (LE)、Zigbee 及 Thread (802.15.4) 无线通信能力。
+
+## 传感器 & _ShuttleBoard_ 子板支持
+
+即将推出,敬请期待。
+
+## 配置、编译命令
+
+由于 ESP-SensairShuttle 需要配置较多的 sdkconfig 选项,推荐使用编译脚本编译。
+
+**编译**
+
+```bash
+python ./scripts/release.py esp-sensairshuttle
+```
+
+如需手动编译,请参考 `main/boards/esp-sensairshuttle/config.json` 修改 menuconfig 对应选项。
+
+**烧录**
+
+```bash
+idf.py flash
+```
diff --git a/main/boards/esp-sensairshuttle/adc_pdm_audio_codec.cc b/main/boards/esp-sensairshuttle/adc_pdm_audio_codec.cc
new file mode 100644
index 00000000..e48792b1
--- /dev/null
+++ b/main/boards/esp-sensairshuttle/adc_pdm_audio_codec.cc
@@ -0,0 +1,249 @@
+#include "adc_pdm_audio_codec.h"
+
+#include
+#include
+#include
+#include
+#include
+#include "adc_mic.h"
+#include "driver/i2s_pdm.h"
+#include "soc/gpio_sig_map.h"
+#include "soc/io_mux_reg.h"
+#include "hal/rtc_io_hal.h"
+#include "hal/gpio_ll.h"
+#include "settings.h"
+#include "config.h"
+
+static const char TAG[] = "AdcPdmAudioCodec";
+
+#define BSP_I2S_GPIO_CFG(_dout) \
+ { \
+ .clk = GPIO_NUM_NC, \
+ .dout = _dout, \
+ .invert_flags = { \
+ .clk_inv = false, \
+ }, \
+ }
+
+/**
+ * @brief Mono Duplex I2S configuration structure
+ *
+ * This configuration is used by default in bsp_audio_init()
+ */
+#define BSP_I2S_DUPLEX_MONO_CFG(_sample_rate, _dout) \
+ { \
+ .clk_cfg = I2S_PDM_TX_CLK_DEFAULT_CONFIG(_sample_rate), \
+ .slot_cfg = I2S_PDM_TX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), \
+ .gpio_cfg = BSP_I2S_GPIO_CFG(_dout), \
+ }
+
+AdcPdmAudioCodec::AdcPdmAudioCodec(int input_sample_rate, int output_sample_rate,
+ uint32_t adc_mic_channel, gpio_num_t pdm_speak_p,gpio_num_t pdm_speak_n, gpio_num_t pa_ctl) {
+
+ input_reference_ = false;
+ input_sample_rate_ = input_sample_rate;
+ output_sample_rate_ = output_sample_rate;
+
+ uint8_t adc_channel[1] = {0};
+ adc_channel[0] = adc_mic_channel;
+
+ audio_codec_adc_cfg_t cfg = {
+ .handle = NULL,
+ .max_store_buf_size = 1024 * 2,
+ .conv_frame_size = 1024,
+ .unit_id = ADC_UNIT_1,
+ .adc_channel_list = adc_channel,
+ .adc_channel_num = sizeof(adc_channel) / sizeof(adc_channel[0]),
+ .sample_rate_hz = (uint32_t)input_sample_rate,
+ };
+ const audio_codec_data_if_t *adc_if = audio_codec_new_adc_data(&cfg);
+
+ esp_codec_dev_cfg_t codec_dev_cfg = {
+ .dev_type = ESP_CODEC_DEV_TYPE_IN,
+ .data_if = adc_if,
+ };
+ input_dev_ = esp_codec_dev_new(&codec_dev_cfg);
+ if (!input_dev_) {
+ ESP_LOGE(TAG, "Failed to create codec device");
+ return;
+ }
+
+ i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
+ chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer
+ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, NULL));
+
+ i2s_pdm_tx_config_t pdm_cfg_default = BSP_I2S_DUPLEX_MONO_CFG((uint32_t)output_sample_rate, pdm_speak_p);
+ pdm_cfg_default.clk_cfg.up_sample_fs = AUDIO_PDM_UPSAMPLE_FS;
+ pdm_cfg_default.slot_cfg.sd_scale = I2S_PDM_SIG_SCALING_MUL_4;
+ pdm_cfg_default.slot_cfg.hp_scale = I2S_PDM_SIG_SCALING_MUL_4;
+ pdm_cfg_default.slot_cfg.lp_scale = I2S_PDM_SIG_SCALING_MUL_4;
+ pdm_cfg_default.slot_cfg.sinc_scale = I2S_PDM_SIG_SCALING_MUL_4;
+ const i2s_pdm_tx_config_t *p_i2s_cfg = &pdm_cfg_default;
+
+ ESP_ERROR_CHECK(i2s_channel_init_pdm_tx_mode(tx_handle_, p_i2s_cfg));
+
+ audio_codec_i2s_cfg_t i2s_cfg = {
+ .port = I2S_NUM_0,
+ .rx_handle = NULL,
+ .tx_handle = tx_handle_,
+ };
+
+ const audio_codec_data_if_t *i2s_data_if = audio_codec_new_i2s_data(&i2s_cfg);
+
+ codec_dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_OUT;
+ codec_dev_cfg.codec_if = NULL;
+ codec_dev_cfg.data_if = i2s_data_if;
+ output_dev_ = esp_codec_dev_new(&codec_dev_cfg);
+
+ output_volume_ = 100;
+ if(pa_ctl != GPIO_NUM_NC) {
+ pa_ctrl_pin_ = pa_ctl;
+ gpio_config_t io_conf = {};
+ io_conf.intr_type = GPIO_INTR_DISABLE;
+ io_conf.mode = GPIO_MODE_OUTPUT;
+ io_conf.pin_bit_mask = (1ULL << pa_ctrl_pin_);
+ io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
+ io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
+ gpio_config(&io_conf);
+ }
+ gpio_set_drive_capability(pdm_speak_p, GPIO_DRIVE_CAP_0);
+
+ if(pdm_speak_n != GPIO_NUM_NC){
+ PIN_FUNC_SELECT(IO_MUX_GPIO10_REG, PIN_FUNC_GPIO);
+ gpio_set_direction(pdm_speak_n, GPIO_MODE_OUTPUT);
+ esp_rom_gpio_connect_out_signal(pdm_speak_n, I2SO_SD_OUT_IDX, 1, 0); //反转输出 SD OUT 信号
+ gpio_set_drive_capability(pdm_speak_n, GPIO_DRIVE_CAP_0);
+ }
+
+ // 初始化输出定时器
+ esp_timer_create_args_t output_timer_args = {
+ .callback = &AdcPdmAudioCodec::OutputTimerCallback,
+ .arg = this,
+ .dispatch_method = ESP_TIMER_TASK,
+ .name = "output_timer"
+ };
+ ESP_ERROR_CHECK(esp_timer_create(&output_timer_args, &output_timer_));
+
+ ESP_LOGI(TAG, "AdcPdmAudioCodec initialized");
+}
+
+AdcPdmAudioCodec::~AdcPdmAudioCodec() {
+ // 删除定时器
+ if (output_timer_) {
+ esp_timer_stop(output_timer_);
+ esp_timer_delete(output_timer_);
+ output_timer_ = nullptr;
+ }
+
+ 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_);
+}
+
+void AdcPdmAudioCodec::SetOutputVolume(int volume) {
+ ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
+ AudioCodec::SetOutputVolume(volume);
+}
+
+void AdcPdmAudioCodec::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 = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
+ .sample_rate = (uint32_t)input_sample_rate_,
+ .mclk_multiple = 0,
+ };
+ ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
+ } else {
+ ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
+ }
+ AudioCodec::EnableInput(enable);
+}
+
+void AdcPdmAudioCodec::EnableOutput(bool enable) {
+ if (enable == output_enabled_) {
+ return;
+ }
+ if (enable) {
+ // Play 16bit 1 channel
+ 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_));
+
+ // 强制按板卡配置重配PDM TX时钟,覆盖第三方库在set_fmt中的默认up_sample_fs
+ // 若通道已启用,先禁用再重配,最后再启用
+ ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_channel_disable(tx_handle_));
+ i2s_pdm_tx_clk_config_t clk_cfg = I2S_PDM_TX_CLK_DEFAULT_CONFIG((uint32_t)output_sample_rate_);
+ clk_cfg.up_sample_fs = AUDIO_PDM_UPSAMPLE_FS;
+ ESP_ERROR_CHECK(i2s_channel_reconfig_pdm_tx_clock(tx_handle_, &clk_cfg));
+ ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
+ if(pa_ctrl_pin_ != GPIO_NUM_NC){
+ gpio_set_level(pa_ctrl_pin_, 1);
+ }
+ // 启用输出时启动定时器
+ if (output_timer_) {
+ esp_timer_start_once(output_timer_, TIMER_TIMEOUT_US);
+ }
+
+ } else {
+ // 禁用输出时停止定时器
+ if (output_timer_) {
+ esp_timer_stop(output_timer_);
+ }
+ if(pa_ctrl_pin_ != GPIO_NUM_NC){
+ gpio_set_level(pa_ctrl_pin_, 0);
+ }
+ ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
+ }
+ AudioCodec::EnableOutput(enable);
+}
+
+int AdcPdmAudioCodec::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 AdcPdmAudioCodec::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)));
+ // 重置输出定时器
+ if (output_timer_) {
+ esp_timer_stop(output_timer_);
+ esp_timer_start_once(output_timer_, TIMER_TIMEOUT_US);
+ }
+ }
+ return samples;
+}
+
+void AdcPdmAudioCodec::Start() {
+ Settings settings("audio", false);
+ output_volume_ = settings.GetInt("output_volume", output_volume_);
+ if (output_volume_ <= 0) {
+ ESP_LOGW(TAG, "Output volume value (%d) is too small, setting to default (10)", output_volume_);
+ output_volume_ = 10;
+ }
+
+ EnableInput(true);
+ EnableOutput(true);
+ ESP_LOGI(TAG, "Audio codec started");
+}
+
+// 定时器回调函数实现
+void AdcPdmAudioCodec::OutputTimerCallback(void* arg) {
+ AdcPdmAudioCodec* codec = static_cast(arg);
+ if (codec && codec->output_enabled_) {
+ codec->EnableOutput(false);
+ }
+}
diff --git a/main/boards/esp-sensairshuttle/adc_pdm_audio_codec.h b/main/boards/esp-sensairshuttle/adc_pdm_audio_codec.h
new file mode 100644
index 00000000..8c9e8363
--- /dev/null
+++ b/main/boards/esp-sensairshuttle/adc_pdm_audio_codec.h
@@ -0,0 +1,37 @@
+#ifndef _BOX_AUDIO_CODEC_H
+#define _BOX_AUDIO_CODEC_H
+
+#include "audio_codec.h"
+
+#include
+#include
+#include
+
+class AdcPdmAudioCodec : public AudioCodec {
+private:
+ esp_codec_dev_handle_t output_dev_ = nullptr;
+ esp_codec_dev_handle_t input_dev_ = nullptr;
+ gpio_num_t pa_ctrl_pin_ = GPIO_NUM_NC;
+
+ // 定时器相关成员变量
+ esp_timer_handle_t output_timer_ = nullptr;
+ static constexpr uint64_t TIMER_TIMEOUT_US = 120000; // 120ms = 120000us
+
+ // 定时器回调函数
+ static void OutputTimerCallback(void* arg);
+
+ virtual int Read(int16_t* dest, int samples) override;
+ virtual int Write(const int16_t* data, int samples) override;
+
+public:
+ AdcPdmAudioCodec(int input_sample_rate, int output_sample_rate,
+ uint32_t adc_mic_channel, gpio_num_t pdm_speak_p, gpio_num_t pdm_speak_n, gpio_num_t pa_ctl);
+ virtual ~AdcPdmAudioCodec();
+
+ virtual void SetOutputVolume(int volume) override;
+ virtual void EnableInput(bool enable) override;
+ virtual void EnableOutput(bool enable) override;
+ void Start();
+};
+
+#endif // _BOX_AUDIO_CODEC_H
diff --git a/main/boards/esp-sensairshuttle/config.h b/main/boards/esp-sensairshuttle/config.h
new file mode 100644
index 00000000..84d066fa
--- /dev/null
+++ b/main/boards/esp-sensairshuttle/config.h
@@ -0,0 +1,40 @@
+#ifndef _BOARD_CONFIG_H_
+#define _BOARD_CONFIG_H_
+
+#include
+
+#define AUDIO_INPUT_SAMPLE_RATE 16000
+#define AUDIO_OUTPUT_SAMPLE_RATE 24000
+
+#define AUDIO_PDM_UPSAMPLE_FS 480
+
+#define AUDIO_ADC_MIC_CHANNEL 5
+#define AUDIO_PDM_SPEAK_P_GPIO GPIO_NUM_7
+#define AUDIO_PDM_SPEAK_N_GPIO GPIO_NUM_8
+#define AUDIO_PA_CTL_GPIO GPIO_NUM_1
+
+#define BOOT_BUTTON_GPIO GPIO_NUM_28
+#define DISPLAY_MOSI_PIN GPIO_NUM_23
+#define DISPLAY_CLK_PIN GPIO_NUM_24
+#define DISPLAY_DC_PIN GPIO_NUM_26
+#define DISPLAY_RST_PIN GPIO_NUM_NC
+#define DISPLAY_CS_PIN GPIO_NUM_25
+
+#define LCD_TP_SCL GPIO_NUM_3
+#define LCD_TP_SDA GPIO_NUM_2
+
+#define LCD_TYPE_ST7789_SERIAL
+#define DISPLAY_WIDTH 284
+#define DISPLAY_HEIGHT 240
+#define DISPLAY_MIRROR_X false
+#define DISPLAY_MIRROR_Y true
+#define DISPLAY_SWAP_XY true
+
+#define DISPLAY_INVERT_COLOR true
+#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB
+#define DISPLAY_OFFSET_X 36
+#define DISPLAY_OFFSET_Y 0
+#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
+#define DISPLAY_SPI_MODE 0
+
+#endif // _BOARD_CONFIG_H_
diff --git a/main/boards/esp-sensairshuttle/config.json b/main/boards/esp-sensairshuttle/config.json
new file mode 100644
index 00000000..d0fe643b
--- /dev/null
+++ b/main/boards/esp-sensairshuttle/config.json
@@ -0,0 +1,28 @@
+{
+ "target": "esp32c5",
+ "builds": [
+ {
+ "name": "esp-sensairshuttle",
+ "sdkconfig_append": [
+ "CONFIG_IDF_TARGET=\"esp32c5\"",
+ "CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=6",
+ "CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=n",
+ "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=n",
+ "CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM=0",
+ "CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=n",
+ "CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=768",
+ "CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=2048",
+ "CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA=y",
+ "CONFIG_SPIRAM=y",
+ "CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=3072",
+ "CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y",
+ "CONFIG_LWIP_IPV6=n",
+ "CONFIG_USE_ESP_WAKE_WORD=y",
+ "CONFIG_SR_WN_WN9S_HIESP=y",
+ "CONFIG_USE_EMOTE_MESSAGE_STYLE=y",
+ "CONFIG_FLASH_CUSTOM_ASSETS=y",
+ "CONFIG_CUSTOM_ASSETS_FILE=\"https://dl.espressif.com/AE/wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-echoear.bin\""
+ ]
+ }
+ ]
+}
diff --git a/main/boards/esp-sensairshuttle/esp-sensairshuttle.cc b/main/boards/esp-sensairshuttle/esp-sensairshuttle.cc
new file mode 100644
index 00000000..4d3d88fa
--- /dev/null
+++ b/main/boards/esp-sensairshuttle/esp-sensairshuttle.cc
@@ -0,0 +1,315 @@
+#include "wifi_board.h"
+#include "adc_pdm_audio_codec.h"
+#include "application.h"
+#include "button.h"
+#include "config.h"
+#include "mcp_server.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "display/lcd_display.h"
+#include
+#include
+#include
+#include "esp_lcd_ili9341.h"
+
+#include "display/emote_display.h"
+
+#include "assets/lang_config.h"
+#include "anim_player.h"
+#include "led_strip.h"
+#include "driver/rmt_tx.h"
+#include "i2c_device.h"
+
+#include
+#include
+
+#include "sdkconfig.h"
+
+constexpr char TAG[] = "ESP_SensairShuttle";
+
+static const ili9341_lcd_init_cmd_t vendor_specific_init[] = {
+ // {cmd, { data }, data_size, delay_ms}
+ {0x11, NULL, 0, 120}, // Sleep Out
+ {0x36, (uint8_t []){0x00}, 1, 0}, // Memory Data Access Control
+ {0x3A, (uint8_t []){0x05}, 1, 0}, // Interface Pixel Format (16-bit)
+ {0xB2, (uint8_t []){0x0C, 0x0C, 0x00, 0x33, 0x33}, 5, 0}, // Porch Setting
+ {0xB7, (uint8_t []){0x05}, 1, 0}, // Gate Control
+ {0xBB, (uint8_t []){0x21}, 1, 0}, // VCOM Setting
+ {0xC0, (uint8_t []){0x2C}, 1, 0}, // LCM Control
+ {0xC2, (uint8_t []){0x01}, 1, 0}, // VDV and VRH Command Enable
+ {0xC3, (uint8_t []){0x15}, 1, 0}, // VRH Set
+ {0xC6, (uint8_t []){0x0F}, 1, 0}, // Frame Rate Control
+ {0xD0, (uint8_t []){0xA7}, 1, 0}, // Power Control 1
+ {0xD0, (uint8_t []){0xA4, 0xA1}, 2, 0}, // Power Control 1
+ {0xD6, (uint8_t []){0xA1}, 1, 0}, // Gate output GND in sleep mode
+ {
+ 0xE0, (uint8_t [])
+ {
+ 0xF0, 0x05, 0x0E, 0x08, 0x0A, 0x17, 0x39, 0x54,
+ 0x4E, 0x37, 0x12, 0x12, 0x31, 0x37
+ }, 14, 0
+ }, // Positive Gamma Control
+ {
+ 0xE1, (uint8_t [])
+ {
+ 0xF0, 0x10, 0x14, 0x0D, 0x0B, 0x05, 0x39, 0x44,
+ 0x4D, 0x38, 0x14, 0x14, 0x2E, 0x35
+ }, 14, 0
+ }, // Negative Gamma Control
+ {0xE4, (uint8_t []){0x23, 0x00, 0x00}, 3, 0}, // Gate position control
+ {0x21, NULL, 0, 0}, // Display Inversion On
+ {0x29, NULL, 0, 0}, // Display On
+ {0x2C, NULL, 0, 0}, // Memory Write
+};
+
+class Cst816d : public I2cDevice {
+public:
+ struct TouchPoint_t {
+ int num = 0;
+ int x = -1;
+ int y = -1;
+ };
+
+ enum TouchEvent {
+ TOUCH_NONE,
+ TOUCH_PRESS,
+ TOUCH_RELEASE,
+ TOUCH_HOLD
+ };
+
+ Cst816d(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr)
+ {
+ read_buffer_ = new uint8_t[6];
+ was_touched_ = false;
+ press_count_ = 0;
+ }
+
+ ~Cst816d()
+ {
+ delete[] read_buffer_;
+ }
+
+ void UpdateTouchPoint()
+ {
+ ReadRegs(0x02, read_buffer_, 6);
+ tp_.num = read_buffer_[0] & 0x0F;
+ tp_.x = ((read_buffer_[1] & 0x0F) << 8) | read_buffer_[2];
+ tp_.y = ((read_buffer_[3] & 0x0F) << 8) | read_buffer_[4];
+ }
+
+ const TouchPoint_t &GetTouchPoint()
+ {
+ return tp_;
+ }
+
+ TouchEvent CheckTouchEvent()
+ {
+ bool is_touched = (tp_.num > 0);
+ TouchEvent event = TOUCH_NONE;
+
+ if (is_touched && !was_touched_) {
+ // Press event (transition from not touched to touched)
+ press_count_++;
+ event = TOUCH_PRESS;
+ ESP_LOGI(TAG, "TOUCH PRESS - count: %d, x: %d, y: %d", press_count_, tp_.x, tp_.y);
+ } else if (!is_touched && was_touched_) {
+ // Release event (transition from touched to not touched)
+ event = TOUCH_RELEASE;
+ ESP_LOGI(TAG, "TOUCH RELEASE - total presses: %d", press_count_);
+ } else if (is_touched && was_touched_) {
+ // Continuous touch (hold)
+ event = TOUCH_HOLD;
+ ESP_LOGD(TAG, "TOUCH HOLD - x: %d, y: %d", tp_.x, tp_.y);
+ }
+
+ // Update previous state
+ was_touched_ = is_touched;
+ return event;
+ }
+
+ int GetPressCount() const
+ {
+ return press_count_;
+ }
+
+ void ResetPressCount()
+ {
+ press_count_ = 0;
+ }
+
+private:
+ uint8_t* read_buffer_ = nullptr;
+ TouchPoint_t tp_;
+
+ // Touch state tracking
+ bool was_touched_;
+ int press_count_;
+};
+
+class EspSensairShuttle : public WifiBoard {
+private:
+ i2c_master_bus_handle_t i2c_bus_;
+ Cst816d* cst816d_;
+ Display* display_ = nullptr;
+ Button boot_button_;
+
+ void InitializeI2c()
+ {
+ i2c_master_bus_config_t i2c_bus_cfg = {
+ .i2c_port = I2C_NUM_0,
+ .sda_io_num = LCD_TP_SDA,
+ .scl_io_num = LCD_TP_SCL,
+ .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_));
+ }
+
+ static void touch_event_task(void* arg)
+ {
+ Cst816d* touchpad = static_cast(arg);
+ if (touchpad == nullptr) {
+ ESP_LOGE(TAG, "Invalid touchpad pointer in touch_event_task");
+ vTaskDelete(NULL);
+ return;
+ }
+
+ while (true) {
+ touchpad->UpdateTouchPoint();
+ auto touch_event = touchpad->CheckTouchEvent();
+
+ if (touch_event == Cst816d::TOUCH_RELEASE) {
+ auto &app = Application::GetInstance();
+ auto &board = (EspSensairShuttle &)Board::GetInstance();
+
+ if (app.GetDeviceState() == kDeviceStateStarting) {
+ board.EnterWifiConfigMode();
+ } else {
+ app.ToggleChatState();
+ }
+ }
+
+ vTaskDelay(pdMS_TO_TICKS(50)); // Poll every 50ms
+ }
+ }
+
+ void InitializeCst816dTouchPad()
+ {
+ cst816d_ = new Cst816d(i2c_bus_, 0x15);
+ xTaskCreate(touch_event_task, "touch_task", 2 * 1024, cst816d_, 5, NULL);
+ }
+
+ void InitializeButtons()
+ {
+ boot_button_.OnClick([this]() {
+ auto& app = Application::GetInstance();
+ if (app.GetDeviceState() == kDeviceStateStarting) {
+ ESP_LOGI(TAG, "Boot button pressed, enter WiFi configuration mode");
+ EnterWifiConfigMode();
+ return;
+ }
+ app.ToggleChatState();
+ });
+ }
+
+ void InitializeSpi()
+ {
+ spi_bus_config_t buscfg = {};
+ buscfg.mosi_io_num = DISPLAY_MOSI_PIN;
+ buscfg.miso_io_num = GPIO_NUM_NC;
+ buscfg.sclk_io_num = DISPLAY_CLK_PIN;
+ buscfg.quadwp_io_num = GPIO_NUM_NC;
+ buscfg.quadhd_io_num = GPIO_NUM_NC;
+ buscfg.max_transfer_sz = DISPLAY_WIDTH * 10 * sizeof(uint16_t);
+ ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
+ }
+
+ void InitializeLcdDisplay()
+ {
+ esp_lcd_panel_io_handle_t panel_io = nullptr;
+ esp_lcd_panel_handle_t panel = nullptr;
+
+ ESP_LOGD(TAG, "Install panel IO");
+ esp_lcd_panel_io_spi_config_t io_config = {};
+ io_config.cs_gpio_num = DISPLAY_CS_PIN;
+ io_config.dc_gpio_num = DISPLAY_DC_PIN;
+ io_config.spi_mode = DISPLAY_SPI_MODE;
+ io_config.pclk_hz = 40 * 1000 * 1000;
+ 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(SPI2_HOST, &io_config, &panel_io));
+
+ ESP_LOGD(TAG, "Install LCD driver");
+ const ili9341_vendor_config_t vendor_config = {
+ .init_cmds = &vendor_specific_init[0],
+ .init_cmds_size = sizeof(vendor_specific_init) / sizeof(ili9341_lcd_init_cmd_t),
+ };
+
+ esp_lcd_panel_dev_config_t panel_config = {};
+ panel_config.reset_gpio_num = DISPLAY_RST_PIN;
+ panel_config.rgb_ele_order = DISPLAY_RGB_ORDER;
+ panel_config.bits_per_pixel = 16;
+ panel_config.vendor_config = (void *) &vendor_config;
+ ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(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_set_gap(panel, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y);
+ esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
+ esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
+ ESP_LOGI(TAG, "LCD panel create success, %p", panel);
+
+#ifdef CONFIG_USE_EMOTE_MESSAGE_STYLE
+ display_ = new emote::EmoteDisplay(panel, panel_io, DISPLAY_WIDTH, DISPLAY_HEIGHT);
+#else
+ display_ = new SpiLcdDisplay(panel_io, panel,
+ DISPLAY_WIDTH, DISPLAY_HEIGHT, 0, 0, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
+#endif
+
+ }
+
+public:
+ EspSensairShuttle() : boot_button_(BOOT_BUTTON_GPIO) {
+ InitializeI2c();
+ InitializeCst816dTouchPad();
+ InitializeButtons();
+ InitializeSpi();
+ InitializeLcdDisplay();
+ }
+
+ virtual AudioCodec* GetAudioCodec() override
+ {
+ static AdcPdmAudioCodec audio_codec(
+ AUDIO_INPUT_SAMPLE_RATE,
+ AUDIO_OUTPUT_SAMPLE_RATE,
+ AUDIO_ADC_MIC_CHANNEL,
+ AUDIO_PDM_SPEAK_P_GPIO,
+ AUDIO_PDM_SPEAK_N_GPIO,
+ AUDIO_PA_CTL_GPIO);
+ return &audio_codec;
+ }
+
+ virtual Display* GetDisplay() override
+ {
+ return display_;
+ }
+
+ Cst816d* GetTouchpad()
+ {
+ return cst816d_;
+ }
+};
+
+DECLARE_BOARD(EspSensairShuttle);