From 2eb8cac140dfec81dd5c3a2948143476ffe87970 Mon Sep 17 00:00:00 2001 From: virgil Date: Tue, 4 Mar 2025 20:38:53 +0800 Subject: [PATCH] Add SenseCAP Watcher (#241) * feat: add sensecap watcher board. * feat(sensecap): Add button function. * fix: fix compilation error. * style:Modify code style. * fix: fix wake bug * fix: fix compilation error & Enable click to toggle state. * style: move sensecap_audio_codec files to sensecap board * fix: Optimize shutdown. --- README.md | 4 + docs/v1/sensecap_watcher.jpg | Bin 0 -> 39233 bytes main/CMakeLists.txt | 2 + main/Kconfig.projbuild | 2 + main/boards/sensecap-watcher/README.md | 34 +++ main/boards/sensecap-watcher/config.h | 93 +++++++ main/boards/sensecap-watcher/config.json | 12 + .../sensecap-watcher/sensecap_audio_codec.cc | 214 +++++++++++++++ .../sensecap-watcher/sensecap_audio_codec.h | 38 +++ .../sensecap-watcher/sensecap_watcher.cc | 246 ++++++++++++++++++ main/idf_component.yml | 1 + partitions_32M_sensecap.csv | 9 + 12 files changed, 655 insertions(+) create mode 100644 docs/v1/sensecap_watcher.jpg create mode 100644 main/boards/sensecap-watcher/README.md create mode 100644 main/boards/sensecap-watcher/config.h create mode 100644 main/boards/sensecap-watcher/config.json create mode 100644 main/boards/sensecap-watcher/sensecap_audio_codec.cc create mode 100644 main/boards/sensecap-watcher/sensecap_audio_codec.h create mode 100644 main/boards/sensecap-watcher/sensecap_watcher.cc create mode 100644 partitions_32M_sensecap.csv diff --git a/README.md b/README.md index c4adfcc6..0045eef2 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ - Moji 小智AI衍生版 - 无名科技Nologo-星智-1.54TFT - 无名科技Nologo-星智-0.96TFT +- SenseCAP Watcher
@@ -96,6 +97,9 @@ + + +
## 固件部分 diff --git a/docs/v1/sensecap_watcher.jpg b/docs/v1/sensecap_watcher.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b1d7e4cea08aae2a65287e8449c02c9caf4598c3 GIT binary patch literal 39233 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 Board Type -> SenseCAP Watcher +``` + +**编译烧入:** + +```bash +idf.py build flash +``` +注意: 请特别小心处理闪存固件分区地址,以避免错误擦除 SenseCAP Watcher 的自身设备信息(EUI 等),否则设备可能无法正确连接到 SenseCraft 服务器!在刷写固件之前,请务必记录设备的相关必要信息,以确保有恢复的方法! + +您可以使用以下命令备份生产信息 + +```bash +# firstly backup the factory information partition which contains the credentials for connecting the SenseCraft server +esptool.py --chip esp32s3 --baud 2000000 --before default_reset --after hard_reset --no-stub read_flash 0x9000 204800 nvsfactory.bin + +``` \ No newline at end of file diff --git a/main/boards/sensecap-watcher/config.h b/main/boards/sensecap-watcher/config.h new file mode 100644 index 00000000..39dbd4ff --- /dev/null +++ b/main/boards/sensecap-watcher/config.h @@ -0,0 +1,93 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include +#include "esp_io_expander.h" + +/* General I2C */ +#define BSP_GENERAL_I2C_NUM (I2C_NUM_0) +#define BSP_GENERAL_I2C_SDA (GPIO_NUM_47) +#define BSP_GENERAL_I2C_SCL (GPIO_NUM_48) + +/* Audio */ +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_INPUT_REFERENCE false + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_12 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_11 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_15 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_16 + + +#define AUDIO_CODEC_PA_PIN GPIO_NUM_NC +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7243E_ADDR (0x14) + + + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +/* Expander */ +#define BSP_IO_EXPANDER_INT (GPIO_NUM_2) +#define DRV_IO_EXP_INPUT_MASK (0x20ff) // P0.0 ~ P0.7 | P1.3 +#define DRV_IO_EXP_OUTPUT_MASK (0xDf00) // P1.0 ~ P1.7 & ~P1.3 + +/* Expander IO PIN */ +#define BSP_PWR_CHRG_DET (IO_EXPANDER_PIN_NUM_0) +#define BSP_PWR_STDBY_DET (IO_EXPANDER_PIN_NUM_1) +#define BSP_PWR_VBUS_IN_DET (IO_EXPANDER_PIN_NUM_2) +#define BSP_PWR_SDCARD (IO_EXPANDER_PIN_NUM_8) +#define BSP_PWR_LCD (IO_EXPANDER_PIN_NUM_9) +#define BSP_PWR_SYSTEM (IO_EXPANDER_PIN_NUM_10) +#define BSP_PWR_AI_CHIP (IO_EXPANDER_PIN_NUM_11) +#define BSP_PWR_CODEC_PA (IO_EXPANDER_PIN_NUM_12) +#define BSP_PWR_BAT_DET (IO_EXPANDER_PIN_NUM_13) +#define BSP_PWR_GROVE (IO_EXPANDER_PIN_NUM_14) +#define BSP_PWR_BAT_ADC (IO_EXPANDER_PIN_NUM_15) + +#define BSP_PWR_START_UP (BSP_PWR_SDCARD | BSP_PWR_LCD | BSP_PWR_SYSTEM | BSP_PWR_AI_CHIP | BSP_PWR_CODEC_PA | BSP_PWR_GROVE | BSP_PWR_BAT_ADC) + +#define BSP_KNOB_BTN (IO_EXPANDER_PIN_NUM_3) + + +/* QSPI */ +#define BSP_SPI3_HOST_PCLK (GPIO_NUM_7) +#define BSP_SPI3_HOST_DATA0 (GPIO_NUM_9) +#define BSP_SPI3_HOST_DATA1 (GPIO_NUM_1) +#define BSP_SPI3_HOST_DATA2 (GPIO_NUM_14) +#define BSP_SPI3_HOST_DATA3 (GPIO_NUM_13) + +/* LCD */ +#define BSP_LCD_SPI_NUM (SPI3_HOST) +#define BSP_LCD_SPI_CS (GPIO_NUM_45) +#define BSP_LCD_GPIO_RST (GPIO_NUM_NC) +#define BSP_LCD_GPIO_DC (GPIO_NUM_1) + +#define DISPLAY_WIDTH 412 +#define DISPLAY_HEIGHT 412 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_8 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +/* Settings */ +#define DRV_LCD_PIXEL_CLK_HZ (40 * 1000 * 1000) +#define DRV_LCD_CMD_BITS (32) +#define DRV_LCD_PARAM_BITS (8) +#define DRV_LCD_RGB_ELEMENT_ORDER (LCD_RGB_ELEMENT_ORDER_RGB) +#define DRV_LCD_BITS_PER_PIXEL (16) + +#define CONFIG_BSP_LCD_SPI_DMA_SIZE_DIV 16 + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/sensecap-watcher/config.json b/main/boards/sensecap-watcher/config.json new file mode 100644 index 00000000..c73e6f3f --- /dev/null +++ b/main/boards/sensecap-watcher/config.json @@ -0,0 +1,12 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "sensecap-watcher", + "sdkconfig_append": [ + "CONFIG_ESPTOOLPY_FLASHSIZE_32MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME='partitions_32M_sensecap.csv'" + ] + } + ] +} \ No newline at end of file diff --git a/main/boards/sensecap-watcher/sensecap_audio_codec.cc b/main/boards/sensecap-watcher/sensecap_audio_codec.cc new file mode 100644 index 00000000..c2ade566 --- /dev/null +++ b/main/boards/sensecap-watcher/sensecap_audio_codec.cc @@ -0,0 +1,214 @@ +#include "sensecap_audio_codec.h" + +#include +#include +#include + +static const char TAG[] = "SensecapAudioCodec"; + +SensecapAudioCodec::SensecapAudioCodec(void* i2c_master_handle, 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 es8311_addr, uint8_t es7243e_addr, bool input_reference) { + duplex_ = true; // 是否双工 + input_reference_ = input_reference; // 是否使用参考输入,实现回声消除 + input_channels_ = input_reference_ ? 2 : 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_t)0, + .addr = es8311_addr, + .bus_handle = i2c_master_handle, + }; + out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(out_ctrl_if_ != NULL); + + gpio_if_ = audio_codec_new_gpio(); + assert(gpio_if_ != NULL); + + es8311_codec_cfg_t es8311_cfg = {}; + es8311_cfg.ctrl_if = out_ctrl_if_; + es8311_cfg.gpio_if = gpio_if_; + es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC; + es8311_cfg.pa_pin = pa_pin; + es8311_cfg.use_mclk = true; + es8311_cfg.hw_gain.pa_voltage = 5.0; + es8311_cfg.hw_gain.codec_dac_voltage = 3.3; + out_codec_if_ = es8311_codec_new(&es8311_cfg); + assert(out_codec_if_ != NULL); + + esp_codec_dev_cfg_t dev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = out_codec_if_, + .data_if = data_if_, + }; + output_dev_ = esp_codec_dev_new(&dev_cfg); + assert(output_dev_ != NULL); + + // Input + i2c_cfg.addr = es7243e_addr << 1; + in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(in_ctrl_if_ != NULL); + + es7243e_codec_cfg_t es7243e_cfg = {}; + es7243e_cfg.ctrl_if = in_ctrl_if_; + in_codec_if_ = es7243e_codec_new(&es7243e_cfg); + assert(in_codec_if_ != NULL); + + dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; + dev_cfg.codec_if = in_codec_if_; + input_dev_ = esp_codec_dev_new(&dev_cfg); + assert(input_dev_ != NULL); + + esp_codec_set_disable_when_closed(output_dev_, false); + esp_codec_set_disable_when_closed(input_dev_, false); + + ESP_LOGI(TAG, "SensecapAudioDevice initialized"); +} + +SensecapAudioCodec::~SensecapAudioCodec() { + 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(in_codec_if_); + audio_codec_delete_ctrl_if(in_ctrl_if_); + audio_codec_delete_codec_if(out_codec_if_); + audio_codec_delete_ctrl_if(out_ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); +} + +void SensecapAudioCodec::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, + .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_MONO, + .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(tx_handle_, &std_cfg)); + + std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_RIGHT; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Duplex channels created"); +} + +void SensecapAudioCodec::SetOutputVolume(int volume) { + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); + AudioCodec::SetOutputVolume(volume); +} + +void SensecapAudioCodec::EnableInput(bool enable) { + if (enable == input_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 2, + .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1), + .sample_rate = (uint32_t)output_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_, 27.0)); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + } + AudioCodec::EnableInput(enable); +} + +void SensecapAudioCodec::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_)); + if (pa_pin_ != GPIO_NUM_NC) { + gpio_set_level(pa_pin_, 1); + } + } + else { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + if (pa_pin_ != GPIO_NUM_NC) { + gpio_set_level(pa_pin_, 0); + } + } + AudioCodec::EnableOutput(enable); +} + +int SensecapAudioCodec::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 SensecapAudioCodec::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/boards/sensecap-watcher/sensecap_audio_codec.h b/main/boards/sensecap-watcher/sensecap_audio_codec.h new file mode 100644 index 00000000..794a4d74 --- /dev/null +++ b/main/boards/sensecap-watcher/sensecap_audio_codec.h @@ -0,0 +1,38 @@ +#ifndef _SENSECAP_AUDIO_CODEC_H +#define _SENSECAP_AUDIO_CODEC_H + +#include "audio_codec.h" + +#include +#include + +class SensecapAudioCodec : public AudioCodec { +private: + const audio_codec_data_if_t* data_if_ = nullptr; + const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr; + const audio_codec_if_t* out_codec_if_ = nullptr; + const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr; + const audio_codec_if_t* in_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; + gpio_num_t pa_pin_ = GPIO_NUM_NC; + + 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: + SensecapAudioCodec(void* i2c_master_handle, 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 es8311_addr, uint8_t es7210_addr, bool input_reference); + virtual ~SensecapAudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _SENSECAP_AUDIO_CODEC_H diff --git a/main/boards/sensecap-watcher/sensecap_watcher.cc b/main/boards/sensecap-watcher/sensecap_watcher.cc new file mode 100644 index 00000000..e1a572f2 --- /dev/null +++ b/main/boards/sensecap-watcher/sensecap_watcher.cc @@ -0,0 +1,246 @@ +#include "wifi_board.h" +#include "sensecap_audio_codec.h" +#include "display/lcd_display.h" +#include "font_awesome_symbols.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "iot/thing_manager.h" + +#include +#include "esp_check.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "esp_io_expander_tca95xx_16bit.h" + +#define TAG "sensecap_watcher" + + +LV_FONT_DECLARE(font_puhui_30_4); +LV_FONT_DECLARE(font_awesome_30_4); + +class SensecapWatcher : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + LcdDisplay* display_; + esp_io_expander_handle_t io_exp_handle; + button_handle_t btns; + + void InitializeI2c() { + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = (i2c_port_t)0, + .sda_io_num = BSP_GENERAL_I2C_SDA, + .scl_io_num = BSP_GENERAL_I2C_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_)); + } + esp_err_t IoExpanderSetLevel(uint16_t pin_mask, uint8_t level) + { + return esp_io_expander_set_level(io_exp_handle, pin_mask, level); + } + + uint8_t IoExpanderGetLevel(uint16_t pin_mask) { + uint32_t pin_val = 0; + esp_io_expander_get_level(io_exp_handle, DRV_IO_EXP_INPUT_MASK, &pin_val); + pin_mask &= DRV_IO_EXP_INPUT_MASK; + return (uint8_t)((pin_val & pin_mask) ? 1 : 0); + } + static uint8_t KnobBtnGetValue(void *param) + { + SensecapWatcher* obj = static_cast(param); + return obj->IoExpanderGetLevel(BSP_KNOB_BTN); + } + static void KnobBtnClickHandler(void* button_handle, void* usr_data) + { + ESP_LOGI(TAG, "Button clicked"); + SensecapWatcher* obj = static_cast(usr_data); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + obj->ResetWifiConfiguration(); + } + app.ToggleChatState(); + } + + static void KnobBtnDownHandler(void* button_handle, void* usr_data) + { + ESP_LOGI(TAG, "Button down"); + Application::GetInstance().StartListening(); + } + static void KnobBtnUpHandler(void* button_handle, void* usr_data) + { + ESP_LOGI(TAG, "Button up"); + Application::GetInstance().StopListening(); + } + + static void KnobBtnLongPressHandler(void* button_handle, void* usr_data) { + ESP_LOGI(TAG, "Button long pressed"); + SensecapWatcher* obj = static_cast(usr_data); + bool is_charging = (obj->IoExpanderGetLevel(BSP_PWR_VBUS_IN_DET) == 0); + if (is_charging) { + ESP_LOGI(TAG, "charging"); + } else { + obj->IoExpanderSetLevel(BSP_PWR_SYSTEM, 0); + obj->IoExpanderSetLevel(BSP_PWR_LCD, 0); + } + } + + void InitializeExpander() { + esp_err_t ret = ESP_OK; + esp_io_expander_new_i2c_tca95xx_16bit(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_001, &io_exp_handle); + + ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_INPUT_MASK, IO_EXPANDER_INPUT); + ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_OUTPUT_MASK, IO_EXPANDER_OUTPUT); + ret |= esp_io_expander_set_level(io_exp_handle, DRV_IO_EXP_OUTPUT_MASK, 0); + ret |= esp_io_expander_set_level(io_exp_handle, BSP_PWR_SYSTEM, 1); + vTaskDelay(100 / portTICK_PERIOD_MS); + ret |= esp_io_expander_set_level(io_exp_handle, BSP_PWR_START_UP, 1); + vTaskDelay(50 / portTICK_PERIOD_MS); + + uint32_t pin_val = 0; + ret |= esp_io_expander_get_level(io_exp_handle, DRV_IO_EXP_INPUT_MASK, &pin_val); + ESP_LOGI(TAG, "IO expander initialized: %x", DRV_IO_EXP_OUTPUT_MASK | (uint16_t)pin_val); + + assert(ret == ESP_OK); + } + + void InitializeButton() { + + button_config_t btn_config = { + .type = BUTTON_TYPE_CUSTOM, + .long_press_time = 1000, + .short_press_time = 200, + .custom_button_config = { + .active_level = 0, + .button_custom_init =nullptr, + .button_custom_get_key_value = KnobBtnGetValue, + .button_custom_deinit = nullptr, + .priv = this, + }, + }; + btns = iot_button_create(&btn_config); + iot_button_register_cb(btns, BUTTON_SINGLE_CLICK, KnobBtnClickHandler, (void *)this); + iot_button_register_cb(btns, BUTTON_LONG_PRESS_START, KnobBtnLongPressHandler, (void *)this); + // iot_button_register_cb(btns, BUTTON_PRESS_DOWN, KnobBtnDownHandler, (void *)this); + // iot_button_register_cb(btns, BUTTON_PRESS_UP, KnobBtnUpHandler, (void *)this); + } + + void InitializeSpi() { + + ESP_LOGI(TAG, "Initialize QSPI bus"); + + spi_bus_config_t qspi_cfg = {0}; + + qspi_cfg.sclk_io_num = BSP_SPI3_HOST_PCLK; + qspi_cfg.data0_io_num = BSP_SPI3_HOST_DATA0; + qspi_cfg.data1_io_num = BSP_SPI3_HOST_DATA1; + qspi_cfg.data2_io_num = BSP_SPI3_HOST_DATA2; + qspi_cfg.data3_io_num = BSP_SPI3_HOST_DATA3; + qspi_cfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * DRV_LCD_BITS_PER_PIXEL / 8 / CONFIG_BSP_LCD_SPI_DMA_SIZE_DIV; + + ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &qspi_cfg, SPI_DMA_CH_AUTO)); + } + + void Initializespd2010Display() { + esp_err_t ret = ESP_OK; + esp_lcd_panel_io_handle_t ret_io; + esp_lcd_panel_handle_t ret_panel; + + ESP_LOGI(TAG, "Install panel IO"); + const esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = BSP_LCD_SPI_CS, + .dc_gpio_num = -1, + .spi_mode = 3, + .pclk_hz = DRV_LCD_PIXEL_CLK_HZ, + .trans_queue_depth = 2, + .lcd_cmd_bits = DRV_LCD_CMD_BITS, + .lcd_param_bits = DRV_LCD_PARAM_BITS, + .flags = { + .quad_mode = true, + }, + }; + spd2010_vendor_config_t vendor_config = { + .flags = { + .use_qspi_interface = 1, + }, + }; + esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)BSP_LCD_SPI_NUM, &io_config, &ret_io); + + ESP_LOGD(TAG, "Install LCD driver"); + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = BSP_LCD_GPIO_RST, // Shared with Touch reset + .rgb_ele_order = DRV_LCD_RGB_ELEMENT_ORDER, + .bits_per_pixel = DRV_LCD_BITS_PER_PIXEL, + .vendor_config = &vendor_config, + }; + esp_lcd_new_panel_spd2010(ret_io, &panel_config, &ret_panel); + + esp_lcd_panel_reset(ret_panel); + esp_lcd_panel_init(ret_panel); + esp_lcd_panel_mirror(ret_panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + esp_lcd_panel_disp_on_off(ret_panel, true); + + //TODO + display_ = new SpiLcdDisplay(ret_io, ret_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_30_4, + .icon_font = &font_awesome_30_4, + .emoji_font = font_emoji_64_init(), + }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot() { + auto& thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + thing_manager.AddThing(iot::CreateThing("Backlight")); + } + +public: + SensecapWatcher(){ + ESP_LOGI(TAG, "Initialize Sensecap Watcher"); + InitializeI2c(); + InitializeSpi(); + InitializeExpander(); + InitializeButton(); + Initializespd2010Display(); + InitializeIot(); + } + + virtual AudioCodec* GetAudioCodec() override { + static SensecapAudioCodec audio_codec( + i2c_bus_, + AUDIO_INPUT_SAMPLE_RATE, + AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_MCLK, + AUDIO_I2S_GPIO_BCLK, + AUDIO_I2S_GPIO_WS, + AUDIO_I2S_GPIO_DOUT, + AUDIO_I2S_GPIO_DIN, + AUDIO_CODEC_PA_PIN, + AUDIO_CODEC_ES8311_ADDR, + AUDIO_CODEC_ES7243E_ADDR, + AUDIO_INPUT_REFERENCE); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } +}; + +DECLARE_BOARD(SensecapWatcher); diff --git a/main/idf_component.yml b/main/idf_component.yml index ca9c6992..a0cbca64 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -18,6 +18,7 @@ dependencies: espressif/button: "^3.3.1" lvgl/lvgl: "~9.2.2" esp_lvgl_port: "~2.4.4" + espressif/esp_io_expander_tca95xx_16bit: "^2.0.0" ## Required IDF version idf: version: ">=5.3" diff --git a/partitions_32M_sensecap.csv b/partitions_32M_sensecap.csv new file mode 100644 index 00000000..33be2eb4 --- /dev/null +++ b/partitions_32M_sensecap.csv @@ -0,0 +1,9 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +nvsfactory, data, nvs, , 200K, +nvs, data, nvs, , 840K, +otadata, data, ota, , 0x2000, +phy_init, data, phy, , 0x1000, +model, data, spiffs, , 1024K, +ota_0, app, ota_0, , 12M, +ota_1, app, ota_1, , 12M,