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 + +## 简介 + +
+ + 开发版文档 + + | + + 传感器 & ShuttleBoard 文档 + +
+ +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);