add some code

This commit is contained in:
2025-09-05 13:25:11 +08:00
parent 9ff0a99e7a
commit 3cf1229a85
8911 changed files with 2535396 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
# 编译命令
## 一键编译
```bash
python scripts/release.py sensecap-watcher
```
## 手动配置编译
```bash
idf.py set-target esp32s3
```
**配置**
```bash
idf.py menuconfig
```
选择板子
```
Xiaozhi Assistant -> Board Type -> SenseCAP Watcher
```
watcher 中一些额外的配置项如下需要在menuconfig 中选择.
```
CONFIG_BOARD_TYPE_SENSECAP_WATCHER=y
CONFIG_ESPTOOLPY_FLASHSIZE_32MB=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v1/32m.csv"
CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH=y
CONFIG_ESPTOOLPY_FLASH_MODE_AUTO_DETECT=n
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
```
## 编译烧入
```bash
idf.py -DBOARD_NAME=sensecap-watcher build flash
```
注意: 如果当前设备出货之前是SenseCAP 固件(非小智版本),请特别小心处理闪存固件分区地址,以避免错误擦除 SenseCAP Watcher 的自身设备信息EUI 等否则设备即使恢复成SenseCAP固件也无法正确连接到 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
```

View File

@@ -0,0 +1,53 @@
# Build Instructions
## One-click Build
```bash
python scripts/release.py sensecap-watcher -c config_en.json
```
## Manual Configuration and Build
```bash
idf.py set-target esp32s3
```
**Configuration**
```bash
idf.py menuconfig
```
Select the board:
```
Xiaozhi Assistant -> Board Type -> SenseCAP Watcher
```
There are some additional configuration options for the watcher. Please select them in menuconfig:
```
CONFIG_BOARD_TYPE_SENSECAP_WATCHER=y
CONFIG_ESPTOOLPY_FLASHSIZE_32MB=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v1/32m.csv"
CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH=y
CONFIG_ESPTOOLPY_FLASH_MODE_AUTO_DETECT=n
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_LANGUAGE_EN_US=y
CONFIG_SR_WN_WN9_JARVIS_TTS=y
```
## Build and Flash
```bash
idf.py -DBOARD_NAME=sensecap-watcher-en build flash
```
Note: If your device was previously shipped with the SenseCAP firmware (not the Xiaozhi version), please be very careful with the flash partition addresses to avoid accidentally erasing the device information (such as EUI) of the SenseCAP Watcher. Otherwise, even if you restore the SenseCAP firmware, the device may not be able to connect to the SenseCraft server correctly! Therefore, before flashing the firmware, be sure to record the necessary device information to ensure you have a way to recover it!
You can use the following command to back up the factory information:
```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
```

View File

@@ -0,0 +1,152 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
#include "driver/spi_common.h"
#include "esp_io_expander.h"
// SSCMA Client Configuration
#define CONFIG_SSCMA_EVENT_QUEUE_SIZE 1
#define CONFIG_SSCMA_TX_BUFFER_SIZE 8192
#define CONFIG_SSCMA_RX_BUFFER_SIZE 98304
// SSCMA Client Process Task
#define CONFIG_SSCMA_PROCESS_TASK_STACK_SIZE 10240
#define CONFIG_SSCMA_PROCESS_TASK_PRIORITY 5
#define CONFIG_SSCMA_PROCESS_TASK_AFFINITY_CPU1 1
#define CONFIG_SSCMA_PROCESS_TASK_AFFINITY 1
#define CONFIG_SSCMA_PROCESS_TASK_STACK_ALLOC_EXTERNAL 1
// SSCMA Client Monitor Task
#define CONFIG_SSCMA_MONITOR_TASK_STACK_SIZE 10240
#define CONFIG_SSCMA_MONITOR_TASK_PRIORITY 4
#define CONFIG_SSCMA_MONITOR_TASK_AFFINITY_CPU1 1
#define CONFIG_SSCMA_MONITOR_TASK_AFFINITY 1
#define CONFIG_SSCMA_MONITOR_TASK_STACK_ALLOC_EXTERNAL 1
#define CONFIG_SSCMA_ALLOC_SMALL_SHORTTERM_MEM_EXTERNALLY 1
/* 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_40
#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)
#define BSP_KNOB_A_PIN GPIO_NUM_41
#define BSP_KNOB_B_PIN GPIO_NUM_42
/* SPI */
#define BSP_SPI2_HOST_SCLK (GPIO_NUM_4)
#define BSP_SPI2_HOST_MOSI (GPIO_NUM_5)
#define BSP_SPI2_HOST_MISO (GPIO_NUM_6)
/* SD Card */
#define BSP_SD_SPI_NUM (SPI2_HOST)
#define BSP_SD_SPI_CS (GPIO_NUM_46)
#define BSP_SD_GPIO_DET (IO_EXPANDER_PIN_NUM_4)
/* 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
/* Touch */
#define BSP_TOUCH_I2C_NUM (1)
#define BSP_TOUCH_GPIO_INT (IO_EXPANDER_PIN_NUM_5)
#define BSP_TOUCH_I2C_SDA (GPIO_NUM_39)
#define BSP_TOUCH_I2C_SCL (GPIO_NUM_38)
#define BSP_TOUCH_I2C_CLK (400000)
/* 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
/* ADC */
#define BSP_BAT_ADC_CHAN (ADC_CHANNEL_2) // GPIO3
#define BSP_BAT_ADC_ATTEN (ADC_ATTEN_DB_2_5) // 0 ~ 1100 mV
#define BSP_BAT_VOL_RATIO ((62 + 20) / 20)
/* Himax */
#define BSP_SSCMA_CLIENT_RST (IO_EXPANDER_PIN_NUM_7)
#define BSP_SSCMA_CLIENT_RST_USE_EXPANDER (true)
#define BSP_SSCMA_CLIENT_SPI_NUM (SPI2_HOST)
#define BSP_SSCMA_CLIENT_SPI_CS (GPIO_NUM_21)
#define BSP_SSCMA_CLIENT_SPI_SYNC (IO_EXPANDER_PIN_NUM_6)
#define BSP_SSCMA_CLIENT_SPI_SYNC_USE_EXPANDER (true)
#define BSP_SSCMA_CLIENT_SPI_CLK (12 * 1000 * 1000)
#define BSP_SSCMA_FLASHER_UART_NUM (UART_NUM_1)
#define BSP_SSCMA_FLASHER_UART_TX (GPIO_NUM_17)
#define BSP_SSCMA_FLASHER_UART_RX (GPIO_NUM_18)
#define BSP_SSCMA_FLASHER_UART_BAUD_RATE (921600)
#endif // _BOARD_CONFIG_H_

View File

@@ -0,0 +1,16 @@
{
"target": "esp32s3",
"builds": [
{
"name": "sensecap-watcher",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_32MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/32m.csv\"",
"CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH=y",
"CONFIG_ESPTOOLPY_FLASH_MODE_AUTO_DETECT=n",
"CONFIG_IDF_EXPERIMENTAL_FEATURES=y",
"CONFIG_FREERTOS_HZ=1000"
]
}
]
}

View File

@@ -0,0 +1,20 @@
{
"target": "esp32s3",
"builds": [
{
"name": "sensecap-watcher-en",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_32MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/32m.csv\"",
"CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH=y",
"CONFIG_ESPTOOLPY_FLASH_MODE_AUTO_DETECT=n",
"CONFIG_IDF_EXPERIMENTAL_FEATURES=y",
"CONFIG_FREERTOS_HZ=1000",
"CONFIG_LANGUAGE_EN_US=y",
"CONFIG_SR_WN_WN9_JARVIS_TTS=y",
"CONFIG_SR_WN_WN9_SOPHIA_TTS=y",
"CONFIG_SR_WN_WN9_NIHAOXIAOZHI_TTS=n"
]
}
]
}

View File

@@ -0,0 +1,214 @@
#include "sensecap_audio_codec.h"
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <driver/i2s_tdm.h>
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 = AUDIO_CODEC_DMA_DESC_NUM,
.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
.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;
}

View File

@@ -0,0 +1,38 @@
#ifndef _SENSECAP_AUDIO_CODEC_H
#define _SENSECAP_AUDIO_CODEC_H
#include "audio_codec.h"
#include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h>
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

View File

@@ -0,0 +1,606 @@
#include "display/lv_display.h"
#include "misc/lv_event.h"
#include "wifi_board.h"
#include "sensecap_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "knob.h"
#include "config.h"
#include "led/single_led.h"
#include "power_save_timer.h"
#include "sscma_camera.h"
#include <esp_log.h>
#include "esp_check.h"
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_spd2010.h>
#include <esp_adc/adc_oneshot.h>
#include <driver/spi_master.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#include <wifi_station.h>
#include <iot_button.h>
#include <iot_knob.h>
#include <esp_io_expander_tca95xx_16bit.h>
#include <esp_sleep.h>
#include <esp_console.h>
#include <esp_mac.h>
#include <nvs_flash.h>
#include "assets/lang_config.h"
#define TAG "sensecap_watcher"
LV_FONT_DECLARE(font_puhui_30_4);
LV_FONT_DECLARE(font_awesome_20_4);
class CustomLcdDisplay : public SpiLcdDisplay {
public:
CustomLcdDisplay(esp_lcd_panel_io_handle_t io_handle,
esp_lcd_panel_handle_t panel_handle,
int width,
int height,
int offset_x,
int offset_y,
bool mirror_x,
bool mirror_y,
bool swap_xy)
: SpiLcdDisplay(io_handle, panel_handle, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy,
{
.text_font = &font_puhui_30_4,
.icon_font = &font_awesome_20_4,
.emoji_font = font_emoji_64_init(),
}) {
DisplayLockGuard lock(this);
lv_obj_set_size(status_bar_, LV_HOR_RES, fonts_.text_font->line_height * 2 + 10);
lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0);
lv_obj_set_style_pad_top(status_bar_, 10, 0);
lv_obj_set_style_pad_bottom(status_bar_, 1, 0);
// 针对圆形屏幕调整位置
// network battery mute //
// status //
lv_obj_align(battery_label_, LV_ALIGN_TOP_MID, -2.5*fonts_.icon_font->line_height, 0);
lv_obj_align(network_label_, LV_ALIGN_TOP_MID, -0.5*fonts_.icon_font->line_height, 0);
lv_obj_align(mute_label_, LV_ALIGN_TOP_MID, 1.5*fonts_.icon_font->line_height, 0);
lv_obj_align(status_label_, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_flex_grow(status_label_, 0);
lv_obj_set_width(status_label_, LV_HOR_RES * 0.75);
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_align(notification_label_, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_width(notification_label_, LV_HOR_RES * 0.75);
lv_label_set_long_mode(notification_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, -20);
lv_obj_set_style_bg_color(low_battery_popup_, lv_color_hex(0xFF0000), 0);
lv_obj_set_width(low_battery_label_, LV_HOR_RES * 0.75);
lv_label_set_long_mode(low_battery_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
}
};
class SensecapWatcher : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
LcdDisplay* display_;
std::unique_ptr<Knob> knob_;
esp_io_expander_handle_t io_exp_handle;
button_handle_t btns;
PowerSaveTimer* power_save_timer_;
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
esp_lcd_panel_handle_t panel_ = nullptr;
uint32_t long_press_cnt_;
button_driver_t* btn_driver_ = nullptr;
static SensecapWatcher* instance_;
SscmaCamera* camera_ = nullptr;
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(10);
});
power_save_timer_->OnExitSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {
ESP_LOGI(TAG, "Shutting down");
bool is_charging = (IoExpanderGetLevel(BSP_PWR_VBUS_IN_DET) == 0);
if (is_charging) {
ESP_LOGI(TAG, "charging");
GetBacklight()->SetBrightness(0);
} else {
IoExpanderSetLevel(BSP_PWR_SYSTEM, 0);
}
});
power_save_timer_->SetEnabled(true);
}
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_));
// pulldown for lcd i2c
const gpio_config_t io_config = {
.pin_bit_mask = (1ULL << BSP_TOUCH_I2C_SDA) | (1ULL << BSP_TOUCH_I2C_SCL) | (1ULL << BSP_SPI3_HOST_PCLK) | (1ULL << BSP_SPI3_HOST_DATA0) | (1ULL << BSP_SPI3_HOST_DATA1)
| (1ULL << BSP_SPI3_HOST_DATA2) | (1ULL << BSP_SPI3_HOST_DATA3) | (1ULL << BSP_LCD_SPI_CS) | (1UL << DISPLAY_BACKLIGHT_PIN),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_config);
gpio_set_level(BSP_TOUCH_I2C_SDA, 0);
gpio_set_level(BSP_TOUCH_I2C_SCL, 0);
gpio_set_level(BSP_LCD_SPI_CS, 0);
gpio_set_level(DISPLAY_BACKLIGHT_PIN, 0);
gpio_set_level(BSP_SPI3_HOST_PCLK, 0);
gpio_set_level(BSP_SPI3_HOST_DATA0, 0);
gpio_set_level(BSP_SPI3_HOST_DATA1, 0);
gpio_set_level(BSP_SPI3_HOST_DATA2, 0);
gpio_set_level(BSP_SPI3_HOST_DATA3, 0);
}
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);
}
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 OnKnobRotate(bool clockwise) {
auto codec = GetAudioCodec();
int current_volume = codec->output_volume();
int new_volume = current_volume + (clockwise ? -5 : 5);
// 确保音量在有效范围内
if (new_volume > 100) {
new_volume = 100;
ESP_LOGW(TAG, "Volume reached maximum limit: %d", new_volume);
} else if (new_volume < 0) {
new_volume = 0;
ESP_LOGW(TAG, "Volume reached minimum limit: %d", new_volume);
}
codec->SetOutputVolume(new_volume);
ESP_LOGI(TAG, "Volume changed from %d to %d", current_volume, new_volume);
// 显示通知前检查实际变化
if (new_volume != codec->output_volume()) {
ESP_LOGE(TAG, "Failed to set volume! Expected:%d Actual:%d",
new_volume, codec->output_volume());
}
GetDisplay()->ShowNotification(std::string(Lang::Strings::VOLUME) + ": "+std::to_string(codec->output_volume()));
power_save_timer_->WakeUp();
}
void InitializeKnob() {
knob_ = std::make_unique<Knob>(BSP_KNOB_A_PIN, BSP_KNOB_B_PIN);
knob_->OnRotate([this](bool clockwise) {
ESP_LOGD(TAG, "Knob rotation detected. Clockwise:%s", clockwise ? "true" : "false");
OnKnobRotate(clockwise);
});
ESP_LOGI(TAG, "Knob initialized with pins A:%d B:%d", BSP_KNOB_A_PIN, BSP_KNOB_B_PIN);
}
void InitializeButton() {
// 设置静态实例指针
instance_ = this;
// watcher 是通过长按滚轮进行开机的, 需要等待滚轮释放, 否则用户开机松手时可能会误触成单击
ESP_LOGI(TAG, "waiting for knob button release");
while(IoExpanderGetLevel(BSP_KNOB_BTN) == 0) {
vTaskDelay(pdMS_TO_TICKS(50));
}
button_config_t btn_config = {
.long_press_time = 2000,
.short_press_time = 0
};
btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t));
btn_driver_->enable_power_save = false;
btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t {
return !instance_->IoExpanderGetLevel(BSP_KNOB_BTN);
};
ESP_ERROR_CHECK(iot_button_create(&btn_config, btn_driver_, &btns));
iot_button_register_cb(btns, BUTTON_SINGLE_CLICK, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<SensecapWatcher*>(usr_data);
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
self->ResetWifiConfiguration();
}
self->power_save_timer_->WakeUp();
app.ToggleChatState();
}, this);
iot_button_register_cb(btns, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<SensecapWatcher*>(usr_data);
bool is_charging = (self->IoExpanderGetLevel(BSP_PWR_VBUS_IN_DET) == 0);
self->long_press_cnt_ = 0;
if (is_charging) {
ESP_LOGI(TAG, "charging");
} else {
self->IoExpanderSetLevel(BSP_PWR_LCD, 0);
self->IoExpanderSetLevel(BSP_PWR_SYSTEM, 0);
}
}, this);
iot_button_register_cb(btns, BUTTON_LONG_PRESS_HOLD, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<SensecapWatcher*>(usr_data);
self->long_press_cnt_++; // 每隔20ms加一
// 长按10s 恢复出厂设置: 2+0.02*400 = 10
if (self->long_press_cnt_ > 400) {
ESP_LOGI(TAG, "Factory reset");
nvs_flash_erase();
esp_restart();
}
}, this);
}
void InitializeSpi() {
ESP_LOGI(TAG, "Initialize SSCMA SPI bus");
spi_bus_config_t spi_cfg = {0};
spi_cfg.mosi_io_num = BSP_SPI2_HOST_MOSI;
spi_cfg.miso_io_num = BSP_SPI2_HOST_MISO;
spi_cfg.sclk_io_num = BSP_SPI2_HOST_SCLK;
spi_cfg.quadwp_io_num = -1;
spi_cfg.quadhd_io_num = -1;
spi_cfg.isr_cpu_id = ESP_INTR_CPU_AFFINITY_1;
spi_cfg.max_transfer_sz = 4095;
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &spi_cfg, SPI_DMA_CH_AUTO));
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_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, &panel_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(panel_io_, &panel_config, &panel_);
esp_lcd_panel_reset(panel_);
esp_lcd_panel_init(panel_);
esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
esp_lcd_panel_disp_on_off(panel_, true);
display_ = new CustomLcdDisplay(panel_io_, panel_,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
// 使每次刷新的起始列数索引是4的倍数且列数总数是4的倍数以满足SPD2010的要求
lv_display_add_event_cb(lv_display_get_default(), [](lv_event_t *e) {
lv_area_t *area = (lv_area_t *)lv_event_get_param(e);
uint16_t x1 = area->x1;
uint16_t x2 = area->x2;
// round the start of area down to the nearest 4N number
area->x1 = (x1 >> 2) << 2;
// round the end of area up to the nearest 4M+3 number
area->x2 = ((x2 >> 2) << 2) + 3;
}, LV_EVENT_INVALIDATE_AREA, NULL);
}
uint16_t BatterygetVoltage(void) {
static bool initialized = false;
static adc_oneshot_unit_handle_t adc_handle;
static adc_cali_handle_t cali_handle = NULL;
if (!initialized) {
adc_oneshot_unit_init_cfg_t init_config = {
.unit_id = ADC_UNIT_1,
};
adc_oneshot_new_unit(&init_config, &adc_handle);
adc_oneshot_chan_cfg_t ch_config = {
.atten = BSP_BAT_ADC_ATTEN,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
adc_oneshot_config_channel(adc_handle, BSP_BAT_ADC_CHAN, &ch_config);
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = ADC_UNIT_1,
.chan = BSP_BAT_ADC_CHAN,
.atten = BSP_BAT_ADC_ATTEN,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
if (adc_cali_create_scheme_curve_fitting(&cali_config, &cali_handle) == ESP_OK) {
initialized = true;
}
}
if (initialized) {
int raw_value = 0;
int voltage = 0; // mV
adc_oneshot_read(adc_handle, BSP_BAT_ADC_CHAN, &raw_value);
adc_cali_raw_to_voltage(cali_handle, raw_value, &voltage);
voltage = voltage * 82 / 20;
// ESP_LOGI(TAG, "voltage: %dmV", voltage);
return (uint16_t)voltage;
}
return 0;
}
uint8_t BatterygetPercent(bool print = false) {
int voltage = 0;
for (uint8_t i = 0; i < 10; i++) {
voltage += BatterygetVoltage();
}
voltage /= 10;
int percent = (-1 * voltage * voltage + 9016 * voltage - 19189000) / 10000;
percent = (percent > 100) ? 100 : (percent < 0) ? 0 : percent;
if (print) {
printf("voltage: %dmV, percentage: %d%%\r\n", voltage, percent);
}
return (uint8_t)percent;
}
void InitializeCmd() {
esp_console_repl_t *repl = NULL;
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
repl_config.max_cmdline_length = 1024;
repl_config.prompt = "SenseCAP>";
const esp_console_cmd_t cmd1 = {
.command = "reboot",
.help = "reboot the device",
.hint = nullptr,
.func = [](int argc, char** argv) -> int {
esp_restart();
return 0;
},
.argtable = nullptr
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd1));
const esp_console_cmd_t cmd2 = {
.command = "shutdown",
.help = "shutdown the device",
.hint = nullptr,
.func = NULL,
.argtable = NULL,
.func_w_context = [](void *context,int argc, char** argv) -> int {
auto self = static_cast<SensecapWatcher*>(context);
self->GetBacklight()->SetBrightness(0);
self->IoExpanderSetLevel(BSP_PWR_SYSTEM, 0);
return 0;
},
.context =this
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd2));
const esp_console_cmd_t cmd3 = {
.command = "battery",
.help = "get battery percent",
.hint = NULL,
.func = NULL,
.argtable = NULL,
.func_w_context = [](void *context,int argc, char** argv) -> int {
auto self = static_cast<SensecapWatcher*>(context);
self->BatterygetPercent(true);
return 0;
},
.context =this
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd3));
const esp_console_cmd_t cmd4 = {
.command = "factory_reset",
.help = "factory reset and reboot the device",
.hint = NULL,
.func = NULL,
.argtable = NULL,
.func_w_context = [](void *context,int argc, char** argv) -> int {
nvs_flash_erase();
esp_restart();
return 0;
},
.context =this
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd4));
const esp_console_cmd_t cmd5 = {
.command = "read_mac",
.help = "Read mac address",
.hint = NULL,
.func = NULL,
.argtable = NULL,
.func_w_context = [](void *context,int argc, char** argv) -> int {
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
printf("wifi_sta_mac: " MACSTR "\n", MAC2STR(mac));
esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP);
printf("wifi_softap_mac: " MACSTR "\n", MAC2STR(mac));
esp_read_mac(mac, ESP_MAC_BT);
printf("bt_mac: " MACSTR "\n", MAC2STR(mac));
return 0;
},
.context =this
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd5));
esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl));
ESP_ERROR_CHECK(esp_console_start_repl(repl));
}
void InitializeCamera() {
ESP_LOGI(TAG, "Initialize Camera");
// !!!NOTE: SD Card use same SPI bus as sscma client, so we need to disable SD card CS pin first
const gpio_config_t io_config = {
.pin_bit_mask = (1ULL << BSP_SD_SPI_CS),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
esp_err_t ret = gpio_config(&io_config);
if (ret != ESP_OK)
return;
gpio_set_level(BSP_SD_SPI_CS, 1);
camera_ = new SscmaCamera(io_exp_handle);
}
public:
SensecapWatcher() {
ESP_LOGI(TAG, "Initialize Sensecap Watcher");
InitializePowerSaveTimer();
InitializeI2c();
InitializeSpi();
InitializeExpander();
InitializeCmd(); //工厂生产测试使用
InitializeButton();
InitializeKnob();
Initializespd2010Display();
GetBacklight()->RestoreBrightness(); // 对于不带摄像头的版本InitializeCamera需要3s, 所以先恢复背光亮度
InitializeCamera();
}
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_;
}
virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight;
}
// 根据 https://github.com/Seeed-Studio/OSHW-SenseCAP-Watcher/blob/main/Hardware/SenseCAP_Watcher_v1.0_SCH.pdf
// RGB LED型号为 ws2813 mini, 连接在GPIO 40供电电压 3.3v, 没有连接 BIN 双信号线
// 可以直接兼容SingleLED采用的ws2812
virtual Led* GetLed() override {
static SingleLed led(BUILTIN_LED_GPIO);
return &led;
}
virtual void SetPowerSaveMode(bool enabled) override {
if (!enabled) {
power_save_timer_->WakeUp();
}
WifiBoard::SetPowerSaveMode(enabled);
}
virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override {
static bool last_discharging = false;
charging = (IoExpanderGetLevel(BSP_PWR_VBUS_IN_DET) == 0);
discharging = !charging;
level = (int)BatterygetPercent(false);
if (discharging != last_discharging) {
power_save_timer_->SetEnabled(discharging);
last_discharging = discharging;
}
if (level <= 1 && discharging) {
ESP_LOGI(TAG, "Battery level is low, shutting down");
IoExpanderSetLevel(BSP_PWR_SYSTEM, 0);
}
return true;
}
virtual Camera* GetCamera() override {
return camera_;
}
};
DECLARE_BOARD(SensecapWatcher);
// 定义静态成员变量
SensecapWatcher* SensecapWatcher::instance_ = nullptr;

View File

@@ -0,0 +1,343 @@
#include "sscma_camera.h"
#include "mcp_server.h"
#include "display.h"
#include "board.h"
#include "system_info.h"
#include "config.h"
#include <esp_log.h>
#include <esp_heap_caps.h>
#include <img_converters.h>
#include <cstring>
#define TAG "SscmaCamera"
#define IMG_JPEG_BUF_SIZE 48 * 1024
SscmaCamera::SscmaCamera(esp_io_expander_handle_t io_exp_handle) {
sscma_client_io_spi_config_t spi_io_config = {0};
spi_io_config.sync_gpio_num = BSP_SSCMA_CLIENT_SPI_SYNC;
spi_io_config.cs_gpio_num = BSP_SSCMA_CLIENT_SPI_CS;
spi_io_config.pclk_hz = BSP_SSCMA_CLIENT_SPI_CLK;
spi_io_config.spi_mode = 0;
spi_io_config.wait_delay = 10; //两个transfer之间至少延时4ms,但当前 FREERTOS_HZ=100, 延时精度只能达到10ms,
spi_io_config.user_ctx = NULL;
spi_io_config.io_expander = io_exp_handle;
spi_io_config.flags.sync_use_expander = BSP_SSCMA_CLIENT_RST_USE_EXPANDER;
sscma_client_new_io_spi_bus((sscma_client_spi_bus_handle_t)BSP_SSCMA_CLIENT_SPI_NUM, &spi_io_config, &sscma_client_io_handle_);
sscma_client_config_t sscma_client_config = SSCMA_CLIENT_CONFIG_DEFAULT();
sscma_client_config.event_queue_size = CONFIG_SSCMA_EVENT_QUEUE_SIZE;
sscma_client_config.tx_buffer_size = CONFIG_SSCMA_TX_BUFFER_SIZE;
sscma_client_config.rx_buffer_size = CONFIG_SSCMA_RX_BUFFER_SIZE;
sscma_client_config.process_task_stack = CONFIG_SSCMA_PROCESS_TASK_STACK_SIZE;
sscma_client_config.process_task_affinity = CONFIG_SSCMA_PROCESS_TASK_AFFINITY;
sscma_client_config.process_task_priority = CONFIG_SSCMA_PROCESS_TASK_PRIORITY;
sscma_client_config.monitor_task_stack = CONFIG_SSCMA_MONITOR_TASK_STACK_SIZE;
sscma_client_config.monitor_task_affinity = CONFIG_SSCMA_MONITOR_TASK_AFFINITY;
sscma_client_config.monitor_task_priority = CONFIG_SSCMA_MONITOR_TASK_PRIORITY;
sscma_client_config.reset_gpio_num = BSP_SSCMA_CLIENT_RST;
sscma_client_config.io_expander = io_exp_handle;
sscma_client_config.flags.reset_use_expander = BSP_SSCMA_CLIENT_RST_USE_EXPANDER;
sscma_client_new(sscma_client_io_handle_, &sscma_client_config, &sscma_client_handle_);
sscma_data_queue_ = xQueueCreate(1, sizeof(SscmaData));
sscma_client_callback_t callback = {0};
callback.on_event = [](sscma_client_handle_t client, const sscma_client_reply_t *reply, void *user_ctx) {
SscmaCamera* self = static_cast<SscmaCamera*>(user_ctx);
if (!self) return;
char *img = NULL;
int img_size = 0;
if (sscma_utils_fetch_image_from_reply(reply, &img, &img_size) == ESP_OK)
{
ESP_LOGI(TAG, "image_size: %d\n", img_size);
// 将数据通过队列发送出去
SscmaData data;
data.img = (uint8_t*)img;
data.len = img_size;
// 清空队列,保证只保存最新的数据
SscmaData dummy;
while (xQueueReceive(self->sscma_data_queue_, &dummy, 0) == pdPASS) {
if (dummy.img) {
heap_caps_free(dummy.img);
}
}
xQueueSend(self->sscma_data_queue_, &data, 0);
// 注意img 的释放由接收方负责
}
};
callback.on_connect = [](sscma_client_handle_t client, const sscma_client_reply_t *reply, void *user_ctx) {
ESP_LOGI(TAG, "SSCMA client connected");
};
callback.on_log = [](sscma_client_handle_t client, const sscma_client_reply_t *reply, void *user_ctx) {
ESP_LOGI(TAG, "log: %s\n", reply->data);
};
sscma_client_register_callback(sscma_client_handle_, &callback, this);
sscma_client_init(sscma_client_handle_);
ESP_LOGI(TAG, "SSCMA client initialized");
// 设置分辨率
// 3 = 640x480
if (sscma_client_set_sensor(sscma_client_handle_, 1, 3, true)) {
ESP_LOGE(TAG, "Failed to set sensor");
sscma_client_del(sscma_client_handle_);
sscma_client_handle_ = NULL;
return;
}
// 获取设备信息
sscma_client_info_t *info;
if (sscma_client_get_info(sscma_client_handle_, &info, true) == ESP_OK) {
ESP_LOGI(TAG, "Device Info - ID: %s, Name: %s",
info->id ? info->id : "NULL",
info->name ? info->name : "NULL");
}
// 初始化JPEG数据的内存
jpeg_data_.len = 0;
jpeg_data_.buf = (uint8_t*)heap_caps_malloc(IMG_JPEG_BUF_SIZE, MALLOC_CAP_SPIRAM);;
if ( jpeg_data_.buf == nullptr ) {
ESP_LOGE(TAG, "Failed to allocate memory for JPEG buffer");
return;
}
//初始化JPEG解码
jpeg_error_t err;
jpeg_dec_config_t config = { .output_type = JPEG_PIXEL_FORMAT_RGB565_LE, .rotate = JPEG_ROTATE_0D };
err = jpeg_dec_open(&config, &jpeg_dec_);
if ( err != JPEG_ERR_OK ) {
ESP_LOGE(TAG, "Failed to open JPEG decoder");
return;
}
jpeg_io_ = (jpeg_dec_io_t*)heap_caps_malloc(sizeof(jpeg_dec_io_t), MALLOC_CAP_SPIRAM);
if (!jpeg_io_) {
ESP_LOGE(TAG, "Failed to allocate memory for JPEG IO");
jpeg_dec_close(jpeg_dec_);
return;
}
memset(jpeg_io_, 0, sizeof(jpeg_dec_io_t));
jpeg_out_ = (jpeg_dec_header_info_t*)heap_caps_aligned_alloc(16, sizeof(jpeg_dec_header_info_t), MALLOC_CAP_SPIRAM);
if (!jpeg_out_) {
ESP_LOGE(TAG, "Failed to allocate memory for JPEG output header");
heap_caps_free(jpeg_io_);
jpeg_dec_close(jpeg_dec_);
return;
}
memset(jpeg_out_, 0, sizeof(jpeg_dec_header_info_t));
// 初始化预览图片的内存
memset(&preview_image_, 0, sizeof(preview_image_));
preview_image_.header.magic = LV_IMAGE_HEADER_MAGIC;
preview_image_.header.cf = LV_COLOR_FORMAT_RGB565;
preview_image_.header.flags = LV_IMAGE_FLAGS_ALLOCATED | LV_IMAGE_FLAGS_MODIFIABLE;
preview_image_.header.w = 640;
preview_image_.header.h = 480;
preview_image_.header.stride = preview_image_.header.w * 2;
preview_image_.data_size = preview_image_.header.w * preview_image_.header.h * 2;
preview_image_.data = (uint8_t*)heap_caps_malloc(preview_image_.data_size, MALLOC_CAP_SPIRAM);
if (preview_image_.data == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory for preview image");
return;
}
}
SscmaCamera::~SscmaCamera() {
if (preview_image_.data) {
heap_caps_free((void*)preview_image_.data);
preview_image_.data = nullptr;
}
if (sscma_client_handle_) {
sscma_client_del(sscma_client_handle_);
}
if (sscma_data_queue_) {
vQueueDelete(sscma_data_queue_);
}
if (jpeg_data_.buf) {
heap_caps_free(jpeg_data_.buf);
jpeg_data_.buf = nullptr;
}
if (jpeg_dec_) {
jpeg_dec_close(jpeg_dec_);
jpeg_dec_ = nullptr;
}
if (jpeg_io_) {
heap_caps_free(jpeg_io_);
jpeg_io_ = nullptr;
}
if (jpeg_out_) {
heap_caps_free(jpeg_out_);
jpeg_out_ = nullptr;
}
}
void SscmaCamera::SetExplainUrl(const std::string& url, const std::string& token) {
explain_url_ = url;
explain_token_ = token;
}
bool SscmaCamera::Capture() {
SscmaData data;
int ret = 0;
if (sscma_client_handle_ == nullptr) {
ESP_LOGE(TAG, "SSCMA client handle is not initialized");
return false;
}
ESP_LOGI(TAG, "Capturing image...");
// himax 有缓存数据,需要拍两张照片, 只获取最新的照片即可.
if (sscma_client_sample(sscma_client_handle_, 2) ) {
ESP_LOGE(TAG, "Failed to capture image from SSCMA client");
return false;
}
vTaskDelay(pdMS_TO_TICKS(500)); // 等待SSCMA客户端处理数据
if (xQueueReceive(sscma_data_queue_, &data, pdMS_TO_TICKS(1000)) != pdPASS) {
ESP_LOGE(TAG, "Failed to receive JPEG data from SSCMA client");
return false;
}
if (jpeg_data_.buf == nullptr) {
heap_caps_free(data.img);
return false;
}
ret = mbedtls_base64_decode(jpeg_data_.buf, IMG_JPEG_BUF_SIZE, &jpeg_data_.len, data.img, data.len);
if (ret != 0 || jpeg_data_.len == 0) {
ESP_LOGE(TAG, "Failed to decode base64 image data, ret: %d, output_len: %zu", ret, jpeg_data_.len);
heap_caps_free(data.img);
return false;
}
heap_caps_free(data.img);
//DECODE JPEG
if (!jpeg_dec_ || !jpeg_io_ || !jpeg_out_ || !preview_image_.data) {
return true;
}
jpeg_io_->inbuf = jpeg_data_.buf;
jpeg_io_->inbuf_len = jpeg_data_.len;
ret = jpeg_dec_parse_header(jpeg_dec_, jpeg_io_, jpeg_out_);
if (ret < 0) {
ESP_LOGE(TAG, "Failed to parse JPEG header, ret: %d", ret);
return true;
}
jpeg_io_->outbuf = (unsigned char*)preview_image_.data;
int inbuf_consumed = jpeg_io_->inbuf_len - jpeg_io_->inbuf_remain;
jpeg_io_->inbuf = jpeg_data_.buf + inbuf_consumed;
jpeg_io_->inbuf_len = jpeg_io_->inbuf_remain;
ret = jpeg_dec_process(jpeg_dec_, jpeg_io_);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to decode JPEG image, ret: %d", ret);
return true;
}
// 显示预览图片
auto display = Board::GetInstance().GetDisplay();
if (display != nullptr) {
display->SetPreviewImage(&preview_image_);
}
return true;
}
bool SscmaCamera::SetHMirror(bool enabled) {
return false;
}
bool SscmaCamera::SetVFlip(bool enabled) {
return false;
}
/**
* @brief 将摄像头捕获的图像发送到远程服务器进行AI分析和解释
*
* 该函数将当前摄像头缓冲区中的图像编码为JPEG格式并通过HTTP POST请求
* 以multipart/form-data的形式发送到指定的解释服务器。服务器将根据提供的
* 问题对图像进行AI分析并返回结果。
*
* @param question 要向AI提出的关于图像的问题将作为表单字段发送
* @return std::string 服务器返回的JSON格式响应字符串
* 成功时包含AI分析结果失败时包含错误信息
* 格式示例:{"success": true, "result": "分析结果"}
* {"success": false, "message": "错误信息"}
*
* @note 调用此函数前必须先调用SetExplainUrl()设置服务器URL
* @note 函数会等待之前的编码线程完成后再开始新的处理
* @warning 如果摄像头缓冲区为空或网络连接失败,将返回错误信息
*/
std::string SscmaCamera::Explain(const std::string& question) {
if (explain_url_.empty()) {
return "{\"success\": false, \"message\": \"Image explain URL or token is not set\"}";
}
auto network = Board::GetInstance().GetNetwork();
auto http = network->CreateHttp(3);
// 构造multipart/form-data请求体
std::string boundary = "----ESP32_CAMERA_BOUNDARY";
// 构造question字段
std::string question_field;
question_field += "--" + boundary + "\r\n";
question_field += "Content-Disposition: form-data; name=\"question\"\r\n";
question_field += "\r\n";
question_field += question + "\r\n";
// 构造文件字段头部
std::string file_header;
file_header += "--" + boundary + "\r\n";
file_header += "Content-Disposition: form-data; name=\"file\"; filename=\"camera.jpg\"\r\n";
file_header += "Content-Type: image/jpeg\r\n";
file_header += "\r\n";
// 构造尾部
std::string multipart_footer;
multipart_footer += "\r\n--" + boundary + "--\r\n";
// 配置HTTP客户端使用分块传输编码
http->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
http->SetHeader("Client-Id", Board::GetInstance().GetUuid().c_str());
if (!explain_token_.empty()) {
http->SetHeader("Authorization", "Bearer " + explain_token_);
}
http->SetHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
http->SetHeader("Transfer-Encoding", "chunked");
if (!http->Open("POST", explain_url_)) {
ESP_LOGE(TAG, "Failed to connect to explain URL");
return "{\"success\": false, \"message\": \"Failed to connect to explain URL\"}";
}
// 第一块question字段
http->Write(question_field.c_str(), question_field.size());
// 第二块:文件字段头部
http->Write(file_header.c_str(), file_header.size());
// 第三块JPEG数据
http->Write((const char*)jpeg_data_.buf, jpeg_data_.len);
// 第四块multipart尾部
http->Write(multipart_footer.c_str(), multipart_footer.size());
// 结束块
http->Write("", 0);
if (http->GetStatusCode() != 200) {
ESP_LOGE(TAG, "Failed to upload photo, status code: %d", http->GetStatusCode());
return "{\"success\": false, \"message\": \"Failed to upload photo\"}";
}
std::string result = http->ReadAll();
http->Close();
ESP_LOGI(TAG, "Explain image size=%d, question=%s\n%s", jpeg_data_.len, question.c_str(), result.c_str());
return result;
}

View File

@@ -0,0 +1,50 @@
#ifndef SSCMA_CAMERA_H
#define SSCMA_CAMERA_H
#include <lvgl.h>
#include <thread>
#include <memory>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <esp_io_expander_tca95xx_16bit.h>
#include <esp_jpeg_dec.h>
#include <mbedtls/base64.h>
#include "sscma_client.h"
#include "camera.h"
struct SscmaData {
uint8_t* img;
size_t len;
};
struct JpegData {
uint8_t* buf;
size_t len;
};
class SscmaCamera : public Camera {
private:
lv_img_dsc_t preview_image_;
std::string explain_url_;
std::string explain_token_;
sscma_client_io_handle_t sscma_client_io_handle_;
sscma_client_handle_t sscma_client_handle_;
QueueHandle_t sscma_data_queue_;
JpegData jpeg_data_;
jpeg_dec_handle_t jpeg_dec_;
jpeg_dec_io_t *jpeg_io_;
jpeg_dec_header_info_t *jpeg_out_;
public:
SscmaCamera(esp_io_expander_handle_t io_exp_handle);
~SscmaCamera();
virtual void SetExplainUrl(const std::string& url, const std::string& token);
virtual bool Capture();
// 翻转控制函数
virtual bool SetHMirror(bool enabled) override;
virtual bool SetVFlip(bool enabled) override;
virtual std::string Explain(const std::string& question);
};
#endif // ESP32_CAMERA_H