diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 94276a4729..e03d2b100b 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -192,6 +192,10 @@ elseif(CONFIG_BOARD_TYPE_XMINI_C3) set(BOARD_TYPE "xmini-c3") set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) set(BUILTIN_ICON_FONT font_awesome_14_1) +elseif(CONFIG_BOARD_TYPE_XMINI_C3_SUPERMINI) + set(BOARD_TYPE "xmini-c3-supermini") + set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) + set(BUILTIN_ICON_FONT font_awesome_14_1) elseif(CONFIG_BOARD_TYPE_ESP32S3_KORVO2_V3) set(BOARD_TYPE "esp32s3-korvo2-v3") set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) @@ -518,6 +522,17 @@ elseif(CONFIG_BOARD_TYPE_WTP4C5MP07S) set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) set(BUILTIN_ICON_FONT font_awesome_30_4) set(DEFAULT_EMOJI_COLLECTION twemoji_64) +elseif(CONFIG_BOARD_TYPE_DOGEPET_V2) + # Folder name for DogePetV2 board + set(BOARD_TYPE "dogepeV2") + 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_DOGEPET) + set(BOARD_TYPE "dogepet") + set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) + set(BUILTIN_ICON_FONT font_awesome_16_4) + set(DEFAULT_EMOJI_COLLECTION twemoji_32) endif() file(GLOB BOARD_SOURCES @@ -532,11 +547,19 @@ if(CONFIG_USE_AUDIO_PROCESSOR) else() list(APPEND SOURCES "audio/processors/no_audio_processor.cc") endif() +# Include wake word sources only for the selected implementation and target +# Note: WAKE_WORD_TYPE is a Kconfig choice; only one of these will be set if(CONFIG_IDF_TARGET_ESP32S3 OR CONFIG_IDF_TARGET_ESP32P4) - list(APPEND SOURCES "audio/wake_words/afe_wake_word.cc") - list(APPEND SOURCES "audio/wake_words/custom_wake_word.cc") + if(CONFIG_USE_AFE_WAKE_WORD) + list(APPEND SOURCES "audio/wake_words/afe_wake_word.cc") + endif() + if(CONFIG_USE_CUSTOM_WAKE_WORD) + list(APPEND SOURCES "audio/wake_words/custom_wake_word.cc") + endif() else() - list(APPEND SOURCES "audio/wake_words/esp_wake_word.cc") + if(CONFIG_USE_ESP_WAKE_WORD) + list(APPEND SOURCES "audio/wake_words/esp_wake_word.cc") + endif() endif() # Select language directory according to Kconfig @@ -662,7 +685,6 @@ endif() idf_component_register(SRCS ${SOURCES} EMBED_FILES ${LANG_SOUNDS} ${COMMON_SOUNDS} INCLUDE_DIRS ${INCLUDE_DIRS} - WHOLE_ARCHIVE ) # Use target_compile_definitions to define BOARD_TYPE, BOARD_NAME diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 78e16d49f8..0c7003a619 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -144,6 +144,9 @@ choice BOARD_TYPE config BOARD_TYPE_XMINI_C3 bool "Xmini C3" depends on IDF_TARGET_ESP32C3 + config BOARD_TYPE_XMINI_C3_SUPERMINI + bool "Xmini C3 SuperMini (MAX98357 + INMP441 + OLED 128x64)" + depends on IDF_TARGET_ESP32C3 config BOARD_TYPE_ESP32S3_KORVO2_V3 bool "ESP32S3 KORVO2 V3" depends on IDF_TARGET_ESP32S3 @@ -421,6 +424,12 @@ choice BOARD_TYPE config BOARD_TYPE_WTP4C5MP07S bool "Wireless-Tag WTP4C5MP07S" depends on IDF_TARGET_ESP32P4 + config BOARD_TYPE_DOGEPET + bool "DogePet (ESP32-S3 SuperMini)" + depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_DOGEPET_V2 + bool "DogePetV2 (ESP32-S3 SuperMini)" + depends on IDF_TARGET_ESP32S3 endchoice choice @@ -468,8 +477,10 @@ choice DISPLAY_OLED_TYPE endchoice choice DISPLAY_LCD_TYPE - depends on BOARD_TYPE_BREAD_COMPACT_WIFI_LCD || BOARD_TYPE_BREAD_COMPACT_ESP32_LCD || BOARD_TYPE_ESP32_CGC || BOARD_TYPE_ESP32P4_NANO || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC || BOARD_TYPE_BREAD_COMPACT_WIFI_CAM + depends on BOARD_TYPE_BREAD_COMPACT_WIFI_LCD || BOARD_TYPE_BREAD_COMPACT_ESP32_LCD || BOARD_TYPE_ESP32_CGC || BOARD_TYPE_ESP32P4_NANO || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC || BOARD_TYPE_BREAD_COMPACT_WIFI_CAM || BOARD_TYPE_DOGEBOARD || BOARD_TYPE_DOGEPET || BOARD_TYPE_DOGEPET_V2 prompt "LCD Type" + default LCD_ST7789_240X240 if BOARD_TYPE_DOGEPET + default LCD_ST7789_240X280 if BOARD_TYPE_DOGEPET_V2 default LCD_ST7789_240X320 help LCD Display Type diff --git a/main/audio/audio_service.cc b/main/audio/audio_service.cc index c5d3ed77d9..248ff47040 100644 --- a/main/audio/audio_service.cc +++ b/main/audio/audio_service.cc @@ -8,10 +8,15 @@ #include "processors/no_audio_processor.h" #endif -#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4 +#if (CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4) +#if CONFIG_USE_AFE_WAKE_WORD #include "wake_words/afe_wake_word.h" +#endif +#if CONFIG_USE_CUSTOM_WAKE_WORD #include "wake_words/custom_wake_word.h" -#else +#endif +#endif +#if CONFIG_USE_ESP_WAKE_WORD #include "wake_words/esp_wake_word.h" #endif @@ -652,20 +657,32 @@ void AudioService::CheckAndUpdateAudioPowerState() { void AudioService::SetModelsList(srmodel_list_t* models_list) { models_list_ = models_list; -#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4 +#if (CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4) +#if CONFIG_USE_CUSTOM_WAKE_WORD if (esp_srmodel_filter(models_list_, ESP_MN_PREFIX, NULL) != nullptr) { wake_word_ = std::make_unique(); - } else if (esp_srmodel_filter(models_list_, ESP_WN_PREFIX, NULL) != nullptr) { + } else { + wake_word_ = nullptr; + } +#elif CONFIG_USE_AFE_WAKE_WORD + if (esp_srmodel_filter(models_list_, ESP_WN_PREFIX, NULL) != nullptr) { wake_word_ = std::make_unique(); } else { wake_word_ = nullptr; } #else + wake_word_ = nullptr; +#endif +#else +#if CONFIG_USE_ESP_WAKE_WORD if (esp_srmodel_filter(models_list_, ESP_WN_PREFIX, NULL) != nullptr) { wake_word_ = std::make_unique(); } else { wake_word_ = nullptr; } +#else + wake_word_ = nullptr; +#endif #endif if (wake_word_) { @@ -678,7 +695,7 @@ void AudioService::SetModelsList(srmodel_list_t* models_list) { } bool AudioService::IsAfeWakeWord() { -#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4 +#if (CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4) && CONFIG_USE_AFE_WAKE_WORD return wake_word_ != nullptr && dynamic_cast(wake_word_.get()) != nullptr; #else return false; diff --git a/main/audio/codecs/no_audio_codec.cc b/main/audio/codecs/no_audio_codec.cc index 4c839c8756..a998f7f6cf 100644 --- a/main/audio/codecs/no_audio_codec.cc +++ b/main/audio/codecs/no_audio_codec.cc @@ -74,6 +74,69 @@ NoAudioCodecDuplex::NoAudioCodecDuplex(int input_sample_rate, int output_sample_ ESP_LOGI(TAG, "Duplex channels created"); } +NoAudioCodecDuplex::NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, i2s_std_slot_mask_t rx_slot_mask) { + duplex_ = true; + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = 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, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + #ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, + #endif + + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_MONO, + .slot_mask = I2S_STD_SLOT_LEFT, + .ws_width = I2S_DATA_BIT_WIDTH_32BIT, + .ws_pol = false, + .bit_shift = true, + #ifdef I2S_HW_VERSION_2 + .left_align = true, + .big_endian = false, + .bit_order_lsb = false + #endif + + }, + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + + // Configure RX channel with specified slot mask (e.g., I2S_STD_SLOT_RIGHT for INMP441) + std_cfg.slot_cfg.slot_mask = rx_slot_mask; + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_LOGI(TAG, "Duplex channels created (TX: LEFT, RX: %s)", + rx_slot_mask == I2S_STD_SLOT_RIGHT ? "RIGHT" : "LEFT"); +} + NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din) { duplex_ = false; diff --git a/main/audio/codecs/no_audio_codec.h b/main/audio/codecs/no_audio_codec.h index 098ee00175..f51219d3c0 100644 --- a/main/audio/codecs/no_audio_codec.h +++ b/main/audio/codecs/no_audio_codec.h @@ -21,6 +21,7 @@ class NoAudioCodec : public AudioCodec { class NoAudioCodecDuplex : public NoAudioCodec { public: NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); + NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, i2s_std_slot_mask_t rx_slot_mask); }; class NoAudioCodecSimplex : public NoAudioCodec { diff --git a/main/boards/common/mpu6050_sensor.cc b/main/boards/common/mpu6050_sensor.cc new file mode 100644 index 0000000000..89f444b478 --- /dev/null +++ b/main/boards/common/mpu6050_sensor.cc @@ -0,0 +1,249 @@ +#include "mpu6050_sensor.h" +#include +#include +#include +#include +#include "settings.h" +#include +#include +#include + +static const char *TAG_MPU = "MPU6050"; + +// MPU6050 registers +static constexpr uint8_t REG_PWR_MGMT_1 = 0x6B; +static constexpr uint8_t REG_SMPLRT_DIV = 0x19; +static constexpr uint8_t REG_CONFIG = 0x1A; +static constexpr uint8_t REG_GYRO_CONFIG = 0x1B; +static constexpr uint8_t REG_ACCEL_CONFIG = 0x1C; +static constexpr uint8_t REG_ACCEL_XOUT_H = 0x3B; + +Mpu6050Sensor::Mpu6050Sensor(i2c_port_t port, gpio_num_t sda, gpio_num_t scl, uint8_t addr, int hz) + : port_(port), sda_(sda), scl_(scl), addr_(addr), hz_(hz) {} + +Mpu6050Sensor::~Mpu6050Sensor() { + if (dev_) { + i2c_master_bus_rm_device(dev_); + dev_ = nullptr; + } + if (bus_) { + i2c_del_master_bus(bus_); + bus_ = nullptr; + } +} + +bool Mpu6050Sensor::Initialize() { + i2c_master_bus_config_t bus_cfg = { + .i2c_port = port_, + .sda_io_num = sda_, + .scl_io_num = scl_, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .intr_priority = 0, + .trans_queue_depth = 0, + .flags = {.enable_internal_pullup = 1}, + }; + if (i2c_new_master_bus(&bus_cfg, &bus_) != ESP_OK) { + ESP_LOGE(TAG_MPU, "Failed to init I2C bus"); + return false; + } + + i2c_device_config_t dev_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = addr_, + .scl_speed_hz = static_cast(hz_), + }; + if (i2c_master_bus_add_device(bus_, &dev_cfg, &dev_) != ESP_OK) { + ESP_LOGE(TAG_MPU, "Failed to add I2C device @0x%02x", addr_); + return false; + } + + // Wake up device + if (!WriteReg(REG_PWR_MGMT_1, 0x00)) return false; + // Set sample rate divider + WriteReg(REG_SMPLRT_DIV, 0x07); // ~1kHz/(1+7)=125Hz base (then filtered) + // DLPF config (5Hz accel, 5Hz gyro) + WriteReg(REG_CONFIG, 0x06); + // Gyro full-scale +/-250 deg/s + WriteReg(REG_GYRO_CONFIG, 0x00); + // Accel full-scale +/-2g + WriteReg(REG_ACCEL_CONFIG, 0x00); + ESP_LOGI(TAG_MPU, "MPU6050 initialized"); + return true; +} + +bool Mpu6050Sensor::WriteReg(uint8_t reg, uint8_t val) { + uint8_t buf[2] = {reg, val}; + if (i2c_master_transmit(dev_, buf, sizeof(buf), 100) != ESP_OK) { + ESP_LOGE(TAG_MPU, "I2C write 0x%02x=0x%02x failed", reg, val); + return false; + } + return true; +} + +bool Mpu6050Sensor::ReadRegs(uint8_t reg, uint8_t *buf, size_t len) { + if (i2c_master_transmit_receive(dev_, ®, 1, buf, len, 100) != ESP_OK) { + ESP_LOGE(TAG_MPU, "I2C read 0x%02x len %u failed", reg, (unsigned)len); + return false; + } + return true; +} + +bool Mpu6050Sensor::Read(Sample &out) { + uint8_t raw[14]; + if (!ReadRegs(REG_ACCEL_XOUT_H, raw, sizeof(raw))) { + return false; + } + auto rd16 = [&](int idx) -> int16_t { + return (int16_t)((raw[idx] << 8) | raw[idx + 1]); + }; + + int16_t ax = rd16(0), ay = rd16(2), az = rd16(4); + int16_t gx = rd16(8), gy = rd16(10), gz = rd16(12); + + // scale + out.ax = ax / 16384.0f; // 2g + out.ay = ay / 16384.0f; + out.az = az / 16384.0f; + out.gx = gx / 131.0f; // 250 dps + out.gy = gy / 131.0f; + out.gz = gz / 131.0f; + + // pitch/roll (simple from accel) + out.roll = atan2f(out.ay, out.az) * 180.0f / (float)M_PI; + out.pitch = atan2f(-out.ax, sqrtf(out.ay*out.ay + out.az*out.az)) * 180.0f / (float)M_PI; + return true; +} + +bool Mpu6050Sensor::ReadFiltered(Sample &out) { + if (!Read(out)) return false; + + // Apply calibration offsets/biases if available + if (calib_.valid) { + out.ax -= calib_.ax_off; + out.ay -= calib_.ay_off; + out.az -= calib_.az_off; // az_off should be ~ (avg_az - 1.0) + out.gx -= calib_.gx_bias; + out.gy -= calib_.gy_bias; + out.gz -= calib_.gz_bias; + } + + // Compute accel-only angles again after offset correction + float roll_acc = atan2f(out.ay, out.az) * 180.0f / (float)M_PI; + float pitch_acc = atan2f(-out.ax, sqrtf(out.ay*out.ay + out.az*out.az)) * 180.0f / (float)M_PI; + + // Complementary filter: integrate gyro, blend with accel + int64_t now = esp_timer_get_time(); + if (!filter_initialized_ || last_us_ == 0) { + roll_filt_ = roll_acc; + pitch_filt_ = pitch_acc; + filter_initialized_ = true; + } else { + float dt = (now - last_us_) / 1e6f; // seconds + // Integrate gyro (deg/s) + float roll_gyro = roll_filt_ + out.gx * dt; + float pitch_gyro = pitch_filt_ + out.gy * dt; + // Blend + roll_filt_ = alpha_ * roll_gyro + (1.0f - alpha_) * roll_acc; + pitch_filt_ = alpha_ * pitch_gyro + (1.0f - alpha_) * pitch_acc; + } + last_us_ = now; + + out.roll = roll_filt_; + out.pitch = pitch_filt_; + return true; +} + +bool Mpu6050Sensor::Calibrate(int samples, int sample_delay_ms) { + // Average many samples while stationary + float ax_sum = 0, ay_sum = 0, az_sum = 0; + float gx_sum = 0, gy_sum = 0, gz_sum = 0; + int count = 0; + for (int i = 0; i < samples; ++i) { + Sample s{}; + if (Read(s)) { + ax_sum += s.ax; + ay_sum += s.ay; + az_sum += s.az; + gx_sum += s.gx; + gy_sum += s.gy; + gz_sum += s.gz; + count++; + } + vTaskDelay(pdMS_TO_TICKS(sample_delay_ms)); + } + if (count < samples / 2) { + ESP_LOGE(TAG_MPU, "Calibration failed: insufficient samples (%d)", count); + return false; + } + float ax_avg = ax_sum / count; + float ay_avg = ay_sum / count; + float az_avg = az_sum / count; + float gx_avg = gx_sum / count; + float gy_avg = gy_sum / count; + float gz_avg = gz_sum / count; + + // When the board is face-up and still: ax≈0, ay≈0, az≈+1g + calib_.ax_off = ax_avg; + calib_.ay_off = ay_avg; + calib_.az_off = az_avg - 1.0f; + calib_.gx_bias = gx_avg; + calib_.gy_bias = gy_avg; + calib_.gz_bias = gz_avg; + calib_.valid = true; + ESP_LOGI(TAG_MPU, "Calib OK: a_off(%.3f,%.3f,%.3f) g_bias(%.3f,%.3f,%.3f)", + calib_.ax_off, calib_.ay_off, calib_.az_off, calib_.gx_bias, calib_.gy_bias, calib_.gz_bias); + // Reset filter seed to accel angles after calibration + filter_initialized_ = false; + last_us_ = 0; + return true; +} + +bool Mpu6050Sensor::SaveCalibration() { + Settings s("imu", true); + s.SetBool("valid", calib_.valid); + s.SetString("ver", "1"); + s.SetInt("alpha_scaled", (int)(alpha_ * 1000)); + // Store as scaled integers to avoid locale/float issues if needed, but here we can store as strings too. + char buf[64]; + snprintf(buf, sizeof(buf), "%.6f", calib_.ax_off); s.SetString("ax_off", buf); + snprintf(buf, sizeof(buf), "%.6f", calib_.ay_off); s.SetString("ay_off", buf); + snprintf(buf, sizeof(buf), "%.6f", calib_.az_off); s.SetString("az_off", buf); + snprintf(buf, sizeof(buf), "%.6f", calib_.gx_bias); s.SetString("gx_bias", buf); + snprintf(buf, sizeof(buf), "%.6f", calib_.gy_bias); s.SetString("gy_bias", buf); + snprintf(buf, sizeof(buf), "%.6f", calib_.gz_bias); s.SetString("gz_bias", buf); + return true; +} + +bool Mpu6050Sensor::LoadCalibration() { + Settings s("imu", false); + if (!s.GetBool("valid", false)) { + calib_.valid = false; + return false; + } + auto parsef = [&](const std::string &k, float defv) -> float { + std::string v = s.GetString(k, ""); + if (v.empty()) return defv; + return strtof(v.c_str(), nullptr); + }; + calib_.ax_off = parsef("ax_off", 0.0f); + calib_.ay_off = parsef("ay_off", 0.0f); + calib_.az_off = parsef("az_off", 0.0f); + calib_.gx_bias = parsef("gx_bias", 0.0f); + calib_.gy_bias = parsef("gy_bias", 0.0f); + calib_.gz_bias = parsef("gz_bias", 0.0f); + int alpha_scaled = s.GetInt("alpha_scaled", (int)(alpha_ * 1000)); + alpha_ = (float)alpha_scaled / 1000.0f; + calib_.valid = true; + filter_initialized_ = false; + last_us_ = 0; + ESP_LOGI(TAG_MPU, "Calib loaded: a_off(%.3f,%.3f,%.3f) g_bias(%.3f,%.3f,%.3f) alpha=%.2f", + calib_.ax_off, calib_.ay_off, calib_.az_off, calib_.gx_bias, calib_.gy_bias, calib_.gz_bias, alpha_); + return true; +} + +void Mpu6050Sensor::SetFilterAlpha(float a) { + if (a < 0.0f) a = 0.0f; + if (a > 1.0f) a = 1.0f; + alpha_ = a; +} diff --git a/main/boards/common/mpu6050_sensor.h b/main/boards/common/mpu6050_sensor.h new file mode 100644 index 0000000000..55ccde99cc --- /dev/null +++ b/main/boards/common/mpu6050_sensor.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +class Mpu6050Sensor { +public: + struct Sample { + float ax, ay, az; // g + float gx, gy, gz; // deg/s + float pitch, roll; // degrees + }; + + // addr usually 0x68 (AD0 low) or 0x69 (AD0 high) + Mpu6050Sensor(i2c_port_t port, gpio_num_t sda, gpio_num_t scl, uint8_t addr = 0x68, int hz = 400000); + ~Mpu6050Sensor(); + + bool Initialize(); + bool Read(Sample &out); + // Read with bias correction and complementary filter on pitch/roll + bool ReadFiltered(Sample &out); + + // Calibrate by sampling while the device is steady on a flat surface + // - Gyro biases are measured (should be ~0 deg/s when still) + // - Accel offsets are measured so that az ~= +1g when face-up + bool Calibrate(int samples = 300, int sample_delay_ms = 5); + + // Save/Load calibration to NVS under namespace "imu" + bool SaveCalibration(); + bool LoadCalibration(); + + void SetFilterAlpha(float a); // 0..1, higher trusts gyro more + +private: + i2c_port_t port_; + gpio_num_t sda_; + gpio_num_t scl_; + uint8_t addr_; + int hz_; + i2c_master_bus_handle_t bus_ = nullptr; + i2c_master_dev_handle_t dev_ = nullptr; + + // Calibration / filtering state + struct Calibration { + float ax_off = 0.0f, ay_off = 0.0f, az_off = 0.0f; // g offsets + float gx_bias = 0.0f, gy_bias = 0.0f, gz_bias = 0.0f; // deg/s biases + bool valid = false; + } calib_; + float alpha_ = 0.98f; // complementary filter coefficient + bool filter_initialized_ = false; + float pitch_filt_ = 0.0f, roll_filt_ = 0.0f; // degrees + int64_t last_us_ = 0; // for dt integration + + bool WriteReg(uint8_t reg, uint8_t val); + bool ReadRegs(uint8_t reg, uint8_t *buf, size_t len); +}; diff --git a/main/boards/dogepeV2/config.h b/main/boards/dogepeV2/config.h new file mode 100644 index 0000000000..08cc55ddf0 --- /dev/null +++ b/main/boards/dogepeV2/config.h @@ -0,0 +1,74 @@ +#ifndef _DOGEPET_CONFIG_H_ +#define _DOGEPET_CONFIG_H_ + +#include + +// Flash/PSRAM info (documentary; sizing is from sdkconfig) +// 4MB flash, 2MB PSRAM (QUAD) + +// Audio sample rates for duplex I2S +// IMPORTANT: Must match for duplex mode with shared clock pins (like DogePet) +#define AUDIO_INPUT_REFERENCE true +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 +// This matches DogePet's working configuration +#define AUDIO_I2S_METHOD_DUPLEX + +#ifdef AUDIO_I2S_METHOD_DUPLEX + +// Shared I2S clock pins for both microphone and speaker +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_NC +#define AUDIO_I2S_GPIO_WS GPIO_NUM_10 // Shared WS/LRCLK (match DogePet V1) +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_12 // Shared BCLK (match DogePet V1) +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_13 // INMP441 SD pin (outputs on RIGHT channel when L/R=GND) +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11 // MAX98357A DIN pin (match DogePet V1) + +#define AUDIO_PA_CTRL_GPIO GPIO_NUM_NC // PA power control (optional) +#define AUDIO_CODEC_PA_PIN GPIO_NUM_NC // Same as PA_CTRL for compatibility +#endif + +// Buttons +// - BOOT button kept on GPIO0 (wake + long-press Wi‑Fi config) +// - Conversation button: dedicated pin to start/stop AI +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define CONVERSATION_BUTTON_GPIO GPIO_NUM_1 + +// LED +#define BUILTIN_LED_GPIO GPIO_NUM_48 + + +// Pins (fits S3 SuperMini + common ST7789 boards) +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_7 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_MOSI_PIN GPIO_NUM_2 +#define DISPLAY_MISO_PIN GPIO_NUM_NC +#define DISPLAY_CLK_PIN GPIO_NUM_3 +#define DISPLAY_DC_PIN GPIO_NUM_4 +#define DISPLAY_RST_PIN GPIO_NUM_5 +#define DISPLAY_CS_PIN GPIO_NUM_6 + + +// Kconfig-selected LCD variants +#ifdef CONFIG_LCD_ST7789_240X240 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_SPI_MODE 0 +#endif + +#ifdef CONFIG_LCD_ST7789_240X280 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_WIDTH 280 +#define DISPLAY_HEIGHT 240 +#define DISPLAY_MIRROR_X true +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY true +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_OFFSET_X 20 +#define DISPLAY_OFFSET_Y 0 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_SPI_MODE 0 +#endif + +#endif // _DOGEPET_CONFIG_H_ diff --git a/main/boards/dogepeV2/config.json b/main/boards/dogepeV2/config.json new file mode 100644 index 0000000000..13b5a49e57 --- /dev/null +++ b/main/boards/dogepeV2/config.json @@ -0,0 +1,14 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "DogePetV2", + "sdkconfig_append": [ + "CONFIG_PM_ENABLE=y", + "CONFIG_FREERTOS_USE_TICKLESS_IDLE=y", + "CONFIG_WAKE_WORD_DISABLED=y", + "CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y" + ] + } + ] +} diff --git a/main/boards/dogepeV2/dogepetv2.cc b/main/boards/dogepeV2/dogepetv2.cc new file mode 100644 index 0000000000..136c39338c --- /dev/null +++ b/main/boards/dogepeV2/dogepetv2.cc @@ -0,0 +1,178 @@ +#include "wifi_board.h" +#include "audio/codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "led/single_led.h" +#include "power_save_timer.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(LCD_TYPE_ILI9341_SERIAL) +#include "esp_lcd_ili9341.h" +#endif +#if defined(LCD_TYPE_GC9A01_SERIAL) +#include "esp_lcd_gc9a01.h" +#endif + +#define TAG "DogePetV2" + +class DogePetV2 : public WifiBoard { +private: + Button conversation_button_; + LcdDisplay* display_ = nullptr; + PowerSaveTimer* power_save_timer_ = nullptr; + // IMU removed to save space + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI_PIN; + buscfg.miso_io_num = DISPLAY_MISO_PIN; + 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 * DISPLAY_HEIGHT * sizeof(uint16_t); + ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + void InitializeDisplay() { + 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(SPI3_HOST, &io_config, &panel_io)); + + ESP_LOGD(TAG, "Install LCD driver"); + 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; +#if defined(LCD_TYPE_ILI9341_SERIAL) + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel)); +#elif defined(LCD_TYPE_GC9A01_SERIAL) + ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(panel_io, &panel_config, &panel)); +#else + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); +#endif + + esp_lcd_panel_reset(panel); + esp_lcd_panel_init(panel); + // Honor per-panel invert setting when provided, fallback to true for typical ST7789 1.54" panels +#ifdef DISPLAY_INVERT_COLOR + esp_lcd_panel_invert_color(panel, DISPLAY_INVERT_COLOR); +#else + esp_lcd_panel_invert_color(panel, true); +#endif + esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + + display_ = new SpiLcdDisplay(panel_io, panel, + DISPLAY_WIDTH, DISPLAY_HEIGHT, + DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, + DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + } + + void InitializeButtons() { + // Conversation button handles everything: + // - Click: Toggle AI conversation mode with auto VAD detection + // (AI listens continuously and auto-detects when you finish speaking) + // - Long press: enter Wi-Fi configuration + conversation_button_.OnClick([this]() { + if (power_save_timer_) power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + app.ToggleChatState(); // Uses VAD to auto-detect speech end + }); + conversation_button_.OnLongPress([this]() { + if (power_save_timer_) power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + app.SetDeviceState(kDeviceStateWifiConfiguring); + ResetWifiConfiguration(); + }); + } + + // Battery functions removed for this board + + // IMU functions removed + +public: + DogePetV2() : + conversation_button_(CONVERSATION_BUTTON_GPIO) { + InitializeSpi(); + InitializeDisplay(); + InitializeButtons(); + // No battery monitor on this board + // Idle power save: screen dims/sleeps when idle; restore on activity + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + power_save_timer_->OnEnterSleepMode([this]() { + // Friendly goodbye on sleep + if (display_) display_->ShowNotification("BYE"); + Application::GetInstance().PlaySound(Lang::Sounds::OGG_SUCCESS); + if (auto bl = GetBacklight()) bl->SetBrightness(1); + if (auto d = GetDisplay()) d->SetPowerSaveMode(true); + }); + power_save_timer_->OnExitSleepMode([this]() { + if (auto d = GetDisplay()) d->SetPowerSaveMode(false); + if (auto bl = GetBacklight()) bl->RestoreBrightness(); + }); + power_save_timer_->SetEnabled(true); + if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) { + GetBacklight()->RestoreBrightness(); + } + + // IMU-related MCP tools removed to save space + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecDuplex audio_codec( + AUDIO_INPUT_SAMPLE_RATE, + AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, + AUDIO_I2S_GPIO_WS, + AUDIO_I2S_GPIO_DOUT, + AUDIO_I2S_GPIO_DIN, + I2S_STD_SLOT_RIGHT); // INMP441 outputs on RIGHT channel + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual Backlight* GetBacklight() override { + if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) { + static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + return &backlight; + } + return nullptr; + } + + virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override { + // Battery not supported on this board + return false; + } +}; + +DECLARE_BOARD(DogePetV2); diff --git a/main/boards/dogepet/assets/JOY/neutral.gif b/main/boards/dogepet/assets/JOY/neutral.gif new file mode 100644 index 0000000000..5dc7b76c34 Binary files /dev/null and b/main/boards/dogepet/assets/JOY/neutral.gif differ diff --git a/main/boards/dogepet/assets/JOY/neutral.mp4 b/main/boards/dogepet/assets/JOY/neutral.mp4 new file mode 100644 index 0000000000..7d5a94ff12 Binary files /dev/null and b/main/boards/dogepet/assets/JOY/neutral.mp4 differ diff --git a/main/boards/dogepet/config.h b/main/boards/dogepet/config.h new file mode 100644 index 0000000000..7a9225337d --- /dev/null +++ b/main/boards/dogepet/config.h @@ -0,0 +1,95 @@ +#ifndef _DOGEPET_CONFIG_H_ +#define _DOGEPET_CONFIG_H_ + +#include + +// Flash/PSRAM info (documentary; sizing is from sdkconfig) +// 4MB flash, 2MB PSRAM (QUAD) + +// Audio sample rates for duplex I2S +// IMPORTANT: Must match for duplex mode with shared clock pins (like DogePet) +#define AUDIO_INPUT_REFERENCE true +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 +// This matches DogePet's working configuration +#define AUDIO_I2S_METHOD_DUPLEX + +#ifdef AUDIO_I2S_METHOD_DUPLEX + +// Shared I2S clock pins for both microphone and speaker +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_NC +#define AUDIO_I2S_GPIO_WS GPIO_NUM_16 // Shared WS/LRCLK +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_17 // Shared BCLK +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_13 // INMP441 SD pin +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_33 // MAX98357A DIN pin + +#define AUDIO_PA_CTRL_GPIO GPIO_NUM_NC // PA power control (optional) +#define AUDIO_CODEC_PA_PIN GPIO_NUM_NC // Same as PA_CTRL for compatibility +#endif + +// Buttons +#define BOOT_BUTTON_GPIO GPIO_NUM_0 +#define BUTTON_A_GPIO GPIO_NUM_40 +#define BUTTON_B_GPIO GPIO_NUM_41 +#define BUTTON_C_GPIO GPIO_NUM_39 + +// LED +#define BUILTIN_LED_GPIO GPIO_NUM_48 + +// IMU removed to save space + +// VBAT ADC (optional). If connected through divider to a GPIO with ADC1 channel, +// you can wire it here. Example uses ADC1_CH0 on GPIO1 or customize as needed. +#define VBAT_ADC_UNIT ADC_UNIT_2 +#define VBAT_ADC_CH ADC_CHANNEL_4 // GPIO15 on ESP32-S3 +#define VBAT_UPPER_R 10000.0f +#define VBAT_LOWER_R 10000.0f +/* No charge detect pin for now */ + +// SPI TFT ST7789 1.54" 240x240 +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 240 +#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 + +// Pins (fits S3 SuperMini + common ST7789 boards) +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_5 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false +#define DISPLAY_MOSI_PIN GPIO_NUM_2 +#define DISPLAY_MISO_PIN GPIO_NUM_4 +#define DISPLAY_CLK_PIN GPIO_NUM_3 +#define DISPLAY_DC_PIN GPIO_NUM_7 +#define DISPLAY_RST_PIN GPIO_NUM_NC +#define DISPLAY_CS_PIN GPIO_NUM_6 + +// SD card (optional, same SPI bus) +#define SD_CARD_CS_PIN GPIO_NUM_8 + +// Kconfig-selected LCD variants +#ifdef CONFIG_LCD_ST7789_240X240 +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_SPI_MODE 0 +#endif + +/* ST7789 7-pin panels typically require SPI mode 3 */ +#ifdef CONFIG_LCD_ST7789_240X240_7PIN +#define LCD_TYPE_ST7789_SERIAL +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_SPI_MODE 3 +#endif + + +#ifdef CONFIG_LCD_GC9A01_240X240 +#define LCD_TYPE_GC9A01_SERIAL +#define DISPLAY_INVERT_COLOR true +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR +#define DISPLAY_SPI_MODE 0 +#endif + +#endif // _DOGEPET_CONFIG_H_ diff --git a/main/boards/dogepet/config.json b/main/boards/dogepet/config.json new file mode 100644 index 0000000000..17b0cb2552 --- /dev/null +++ b/main/boards/dogepet/config.json @@ -0,0 +1,15 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "DogePet-st7789-240x240", + "sdkconfig_append": [ + "CONFIG_PM_ENABLE=y", + "CONFIG_FREERTOS_USE_TICKLESS_IDLE=y", + "CONFIG_WAKE_WORD_DISABLED=y", + "CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y", + "CONFIG_LCD_ST7789_240X240=y" + ] + } + ] +} diff --git a/main/boards/dogepet/dogepet.cc b/main/boards/dogepet/dogepet.cc new file mode 100644 index 0000000000..d1db3e9b8a --- /dev/null +++ b/main/boards/dogepet/dogepet.cc @@ -0,0 +1,227 @@ +#include "wifi_board.h" +#include "audio/codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "system_reset.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "led/single_led.h" +#include "boards/common/adc_battery_monitor.h" +#include "power_save_timer.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(LCD_TYPE_ILI9341_SERIAL) +#include "esp_lcd_ili9341.h" +#endif +#if defined(LCD_TYPE_GC9A01_SERIAL) +#include "esp_lcd_gc9a01.h" +#endif + +#define TAG "DogePet" + +class DogePet : public WifiBoard { +private: + Button boot_button_; + Button btn_a_; + Button btn_b_; + Button btn_c_; + bool conversation_active_ = false; + LcdDisplay* display_ = nullptr; + AdcBatteryMonitor* adc_batt_ = nullptr; + PowerSaveTimer* power_save_timer_ = nullptr; + // IMU removed to save space + + void InitializeSpi() { + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI_PIN; + buscfg.miso_io_num = DISPLAY_MISO_PIN; + 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 * DISPLAY_HEIGHT * sizeof(uint16_t); + ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + void InitializeDisplay() { + 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(SPI3_HOST, &io_config, &panel_io)); + + ESP_LOGD(TAG, "Install LCD driver"); + 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; +#if defined(LCD_TYPE_ILI9341_SERIAL) + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel)); +#elif defined(LCD_TYPE_GC9A01_SERIAL) + ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(panel_io, &panel_config, &panel)); +#else + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); +#endif + + esp_lcd_panel_reset(panel); + esp_lcd_panel_init(panel); + // Honor per-panel invert setting when provided, fallback to true for typical ST7789 1.54" panels +#ifdef DISPLAY_INVERT_COLOR + esp_lcd_panel_invert_color(panel, DISPLAY_INVERT_COLOR); +#else + esp_lcd_panel_invert_color(panel, true); +#endif + esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + + display_ = new SpiLcdDisplay(panel_io, panel, + DISPLAY_WIDTH, DISPLAY_HEIGHT, + DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, + DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + } + + void InitializeButtons() { + // Boot button: long press enters Wi-Fi config; short press just wakes + boot_button_.OnClick([this]() { + if (power_save_timer_) power_save_timer_->WakeUp(); + }); + boot_button_.OnLongPress([this]() { + if (power_save_timer_) power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + app.SetDeviceState(kDeviceStateWifiConfiguring); + ResetWifiConfiguration(); + }); + + btn_a_.OnClick([this]() { + if (power_save_timer_) power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + int vol = std::min(100, codec->output_volume() + 10); + codec->SetOutputVolume(vol); + if (display_) display_->ShowNotification(std::string("VOL ") + std::to_string(vol/10)); + }); + btn_a_.OnLongPress([this]() { + if (power_save_timer_) power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(100); + if (display_) display_->ShowNotification("MAX VOL"); + }); + + btn_b_.OnClick([this]() { + if (power_save_timer_) power_save_timer_->WakeUp(); + auto codec = GetAudioCodec(); + int vol = std::max(0, codec->output_volume() - 10); + codec->SetOutputVolume(vol); + if (display_) display_->ShowNotification(std::string("VOL ") + std::to_string(vol/10)); + }); + btn_b_.OnLongPress([this]() { + if (power_save_timer_) power_save_timer_->WakeUp(); + GetAudioCodec()->SetOutputVolume(0); + if (display_) display_->ShowNotification("MUTED"); + }); + + // Button C: Toggle conversation mode (click to start/stop) + btn_c_.OnClick([this]() { + if (power_save_timer_) power_save_timer_->WakeUp(); + if (!conversation_active_) { + Application::GetInstance().StartListening(); + conversation_active_ = true; + if (display_) display_->ShowNotification("AI ON"); + } else { + Application::GetInstance().StopListening(); + conversation_active_ = false; + if (display_) display_->ShowNotification("AI OFF"); + } + }); + } + + void InitializeBattery() { + // Create only if a valid ADC channel is defined; no charge-detect pin + adc_batt_ = new AdcBatteryMonitor(VBAT_ADC_UNIT, VBAT_ADC_CH, VBAT_UPPER_R, VBAT_LOWER_R); + } + + // IMU functions removed + +public: + DogePet() : + boot_button_(BOOT_BUTTON_GPIO), + btn_a_(BUTTON_A_GPIO), + btn_b_(BUTTON_B_GPIO), + btn_c_(BUTTON_C_GPIO) { + InitializeSpi(); + InitializeDisplay(); + InitializeButtons(); + InitializeBattery(); + // Idle power save: screen dims/sleeps when idle; restore on activity + power_save_timer_ = new PowerSaveTimer(-1, 60, 300); + power_save_timer_->OnEnterSleepMode([this]() { + // Friendly goodbye on sleep + if (display_) display_->ShowNotification("BYE"); + Application::GetInstance().PlaySound(Lang::Sounds::OGG_SUCCESS); + if (auto bl = GetBacklight()) bl->SetBrightness(1); + if (auto d = GetDisplay()) d->SetPowerSaveMode(true); + }); + power_save_timer_->OnExitSleepMode([this]() { + if (auto d = GetDisplay()) d->SetPowerSaveMode(false); + if (auto bl = GetBacklight()) bl->RestoreBrightness(); + }); + power_save_timer_->SetEnabled(true); + if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) { + GetBacklight()->RestoreBrightness(); + } + + // IMU-related MCP tools removed to save space + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecDuplex audio_codec( + AUDIO_INPUT_SAMPLE_RATE, + AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, + AUDIO_I2S_GPIO_WS, + AUDIO_I2S_GPIO_DOUT, + AUDIO_I2S_GPIO_DIN); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual Backlight* GetBacklight() override { + if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) { + static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); + return &backlight; + } + return nullptr; + } + + virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override { + if (!adc_batt_) return false; + charging = adc_batt_->IsCharging(); + discharging = adc_batt_->IsDischarging(); + level = adc_batt_->GetBatteryLevel(); + return true; + } +}; + +DECLARE_BOARD(DogePet); diff --git a/main/boards/xmini-c3-supermini/config.h b/main/boards/xmini-c3-supermini/config.h new file mode 100644 index 0000000000..e66bb4cb19 --- /dev/null +++ b/main/boards/xmini-c3-supermini/config.h @@ -0,0 +1,55 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +#include +#include + +#define AUDIO_INPUT_REFERENCE true +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 +// This matches DogePet's working configuration +#define AUDIO_I2S_METHOD_DUPLEX + +#ifdef AUDIO_I2S_METHOD_DUPLEX + +// I2S pins for MAX98357 (speaker) and INMP441 (microphone) +// Adjust these to match your wiring on the ESP32-C3 SuperMini +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8 +#define AUDIO_I2S_GPIO_WS GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_5 // To MAX98357 DIN +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 // From INMP441 DOUT + +// Built-in LED and boot button (adjust if your board differs) +#define BUILTIN_LED_GPIO GPIO_NUM_2 +#define BOOT_BUTTON_GPIO GPIO_NUM_0 + +// TFT LCD 1.69" ST7789 240x280 over SPI +// SPI pins for the LCD display +#define DISPLAY_SPI_MODE 3 +#define DISPLAY_CS_PIN GPIO_NUM_10 +#define DISPLAY_MOSI_PIN GPIO_NUM_3 +#define DISPLAY_MISO_PIN GPIO_NUM_NC +#define DISPLAY_CLK_PIN GPIO_NUM_4 +#define DISPLAY_DC_PIN GPIO_NUM_9 +#define DISPLAY_RST_PIN GPIO_NUM_NC + +// Display geometry and orientation +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 280 +#define DISPLAY_SWAP_XY false +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB +#define DISPLAY_INVERT_COLOR true + +// Panel memory window offsets (typical for 1.69" ST7789 modules) +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 20 + +// Backlight control (set to NC if your module lacks a BL pin) +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_1 +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + +#endif // AUDIO_I2S_METHOD_DUPLEX + +#endif // _BOARD_CONFIG_H_ \ No newline at end of file diff --git a/main/boards/xmini-c3-supermini/config.json b/main/boards/xmini-c3-supermini/config.json new file mode 100644 index 0000000000..ebcd0506b8 --- /dev/null +++ b/main/boards/xmini-c3-supermini/config.json @@ -0,0 +1,14 @@ +{ + "target": "esp32c3", + "builds": [ + { + "name": "Xmini-C3-SuperMini-1.69TFT-240x280", + "sdkconfig_append": [ + "CONFIG_WAKE_WORD_DISABLED=y", + "CONFIG_LOG_DEFAULT_LEVEL=2", + "CONFIG_PM_ENABLE=y", + "CONFIG_FREERTOS_USE_TICKLESS_IDLE=y" + ] + } + ] +} diff --git a/main/boards/xmini-c3-supermini/xmini_c3_supermini_board.cc b/main/boards/xmini-c3-supermini/xmini_c3_supermini_board.cc new file mode 100644 index 0000000000..f3018d471d --- /dev/null +++ b/main/boards/xmini-c3-supermini/xmini_c3_supermini_board.cc @@ -0,0 +1,131 @@ +#include "wifi_board.h" +#include "codecs/no_audio_codec.h" +#include "display/lcd_display.h" +#include "application.h" +#include "button.h" +#include "led/single_led.h" +#include "config.h" +#include "power_save_timer.h" + +#include +#include +#include +#include +#include +#include +#include + +#define TAG "XminiC3SuperMini" + +class XminiC3SuperMini : public WifiBoard { +private: + esp_lcd_panel_io_handle_t panel_io_ = nullptr; + esp_lcd_panel_handle_t panel_ = nullptr; + Display* display_ = nullptr; + Button boot_button_; + PowerSaveTimer* power_save_timer_ = nullptr; + + void InitializePowerSaveTimer() { + power_save_timer_ = new PowerSaveTimer(160, 300); + power_save_timer_->OnEnterSleepMode([this]() { + if (auto d = GetDisplay()) d->SetPowerSaveMode(true); + }); + power_save_timer_->OnExitSleepMode([this]() { + if (auto d = GetDisplay()) d->SetPowerSaveMode(false); + }); + power_save_timer_->SetEnabled(true); + } + + void InitializeSpiBus() { + ESP_LOGI(TAG, "Initialize SPI bus for ST7789"); + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI_PIN; + buscfg.miso_io_num = DISPLAY_MISO_PIN; + 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 * DISPLAY_HEIGHT * sizeof(uint16_t); + ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); + } + + void InitializeSt7789Display() { + // Install SPI panel IO + ESP_LOGI(TAG, "Install panel IO (ST7789)"); + 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_)); + + // Install ST7789 panel + ESP_LOGI(TAG, "Install ST7789 panel"); + 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; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(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_swap_xy(panel_, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + + display_ = new SpiLcdDisplay(panel_io_, panel_, + DISPLAY_WIDTH, DISPLAY_HEIGHT, + DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, + DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, + DISPLAY_SWAP_XY); + } + + void InitializeButtons() { + boot_button_.OnClick([this]() { + power_save_timer_->WakeUp(); + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); + }); + } + +public: + XminiC3SuperMini() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeSpiBus(); + InitializeSt7789Display(); + InitializeButtons(); + InitializePowerSaveTimer(); + } + + virtual Led* GetLed() override { + static SingleLed led(BUILTIN_LED_GPIO); + return &led; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual AudioCodec* GetAudioCodec() override { + static NoAudioCodecDuplex audio_codec( + AUDIO_INPUT_SAMPLE_RATE, + AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_I2S_GPIO_BCLK, + AUDIO_I2S_GPIO_WS, + AUDIO_I2S_GPIO_DOUT, + AUDIO_I2S_GPIO_DIN); + return &audio_codec; + } + + virtual void SetPowerSaveMode(bool enabled) override { + if (!enabled) { + power_save_timer_->WakeUp(); + } + WifiBoard::SetPowerSaveMode(enabled); + } +}; + +DECLARE_BOARD(XminiC3SuperMini); diff --git a/partitions/custom/doge4m.csv b/partitions/custom/doge4m.csv new file mode 100644 index 0000000000..5e49ff5cd5 --- /dev/null +++ b/partitions/custom/doge4m.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs,data,nvs,0x9000,0x4000, +otadata,data,ota,0xD000,0x2000, +phy_init,data,phy,0xF000,0x1000, +factory,app,factory,0x10000,0x2A0000, +assets,data,spiffs,0x2B0000,0x150000, \ No newline at end of file diff --git a/partitions/custom/huge4m.csv b/partitions/custom/huge4m.csv new file mode 100644 index 0000000000..71408fb23b --- /dev/null +++ b/partitions/custom/huge4m.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs,data,nvs,0x9000,0x4000, +otadata,data,ota,0xD000,0x2000, +phy_init,data,phy,0xF000,0x1000, +factory,app,factory,0x10000,0x3C0000, +assets,data,spiffs,0x3D0000,0x30000, \ No newline at end of file