Compare commits

...

19 Commits

Author SHA1 Message Date
espressif2022
cd23e0f155 feat: add emote_gfx UI for EchoEar (#1022)
* feat: add emote_gfx UI for EchoEar

* feat: delete local assets
2025-08-01 18:07:13 +08:00
Xiaoxia
26d9ff283f Fix custom wakeword for dual mic (#1018) 2025-08-01 13:30:17 +08:00
Xiaoxia
fb85019c3c change bread-compact-wifi-s3cam test gpio num (#1017) 2025-08-01 05:28:27 +08:00
HonestQiao
4859d57fea 修复esp32-p4配网客户端无法连接、连接获取不到ip或者无法打开配置页面的问题 (#1012) 2025-07-31 05:14:17 +08:00
Xiaoxia
03394fe38d update the sleep time of xmini-c3 with wake word to 300s (#1007) 2025-07-30 15:25:39 +08:00
jake12355
e0e12450c5 jiuchuan-s3修改按键定义取消不对话自动关机修复屏幕显示不全 (#997)
* jiuchuan-s3修改按键定义取消不对话自动关机修复屏幕显示不全

* jiuchuan-s3修改按键定义取消不对话自动关机修复屏幕显示不全
2025-07-29 17:28:40 +08:00
Xiaoxia
e5ac40aac8 fix audio pm (#1004) 2025-07-29 15:25:40 +08:00
Xiaoxia
345c8be467 Add custom wakeword threshold option (#1003) 2025-07-29 10:56:52 +08:00
香草味的纳西妲喵
def6301292 增加其他分辨率表情转换选项 (#987)
* Create requirements.txt

* Update README.md

* add: 增加其他尺寸表情选项
2025-07-26 02:31:09 +08:00
Xiaoxia
df7cbdfcb6 fix esp-hi crashing in esp_codec_dev_close() (#984) 2025-07-25 10:36:56 +08:00
Terrence
d38763d5ef Reduce SRAM usage of audio tasks 2025-07-25 08:27:12 +08:00
LILYGO_L
e90e540933 增强T-CameraPlus-S3麦克风接收音量 (#958)
* Adapt for LilyGO-T-Circle-S3 device

* Adapt for LilyGO-T-Circle-S3 device

* Remove comments and modify the size of the lilygo-t-circle-s3 image

* Modify the code style and format to Google C++

* Modify the code style and format to Google C++

* Fixed bugs in the LILYGO T-Circle-S3 board and added support for two new boards: LILYGO T-Display-S3-Pro-MVSRLora and LILYGO T-Display-S3-Pro-MVSRLora_NO_BATTERY.

* Added support for two new boards: LILYGO T-Display-S3-Pro-MVSRLora and LILYGO T-Display-S3-Pro-MVSRLora_NO_BATTERY.

* Merge branch 'main' of https://github.com/Llgok/xiaozhi-esp32

* Added support for two new boards: LILYGO T-Display-S3-Pro-MVSRLora and LILYGO T-Display-S3-Pro-MVSRLora_NO_BATTERY.

* Added support for two new boards: LILYGO T-Display-S3-Pro-MVSRLora and LILYGO T-Display-S3-Pro-MVSRLora_NO_BATTERY.

* Added support for two new boards: LILYGO T-Display-S3-Pro-MVSRLora and LILYGO T-Display-S3-Pro-MVSRLora_NO_BATTERY.

* Added support for two new boards: LILYGO T-Display-S3-Pro-MVSRLora and LILYGO T-Display-S3-Pro-MVSRLora_NO_BATTERY.

* Fix the color display issue for T-Display-S3-Pro-MVSRLora and LILYGO T-Display-S3-Pro-MVSRLora_NO_BATTERY.

* Update T-CameraPlus-S3_V1.2 Version Xiaozhi Example

* Resolve the issue where the camera on the T-CameraPlus-S3_V1.2 board cannot be used normally.

* Enhance microphone reception volume

* fix the issue where voice wake-up is not working

* fix the issue where voice wake-up is not working
2025-07-23 23:02:45 +08:00
Ky1eYang
656bf3c7fa FIX: 修复双声道声波配网失效, 添加屏幕打印SSID/密码 (#971)
* debug: 添加声波配网的log打印点display

* fix: 修复双声道下声波配网失效的问题

* fix: codec可能为nullptr的问题(需要从单例board获取)

* Update afsk_demod.cc

fix coding style

---------

Co-authored-by: yangkaiyue <yangkaiyue1@tenclass.com>
Co-authored-by: Xiaoxia <terrence@tenclass.com>
2025-07-23 22:59:07 +08:00
Y1hsiaochunnn
ca35b0761b Update README.md (#968)
The README text description is incorrect. It needs to be corrected.
2025-07-23 22:47:19 +08:00
Terrence
b031a829c0 Bump to 1.8.2 2025-07-22 22:43:41 +08:00
Xiaoxia
3c11cceb43 增加自定义唤醒词启动失败的提升 (#965) 2025-07-22 18:57:25 +08:00
Y1hsiaochunnn
15f233e773 Add compatibility for Waveshare ESP32-S3-Touch-AMOLED-2.06 (#960)
* Add Waveshare ESP32-S3-Touch-AMOLED-2.06

* Update some configuration settings

* Add configuration to the configuration file

* Fix the abnormal areas
2025-07-22 18:02:31 +08:00
Forairaaaaa
721b58f8c7 Fix atoms3 backlight control (#959) 2025-07-22 17:55:33 +08:00
Xiaoxia
eb0bba2c89 Update LICENSE 2025-07-22 10:06:11 +08:00
81 changed files with 1804 additions and 824 deletions

3
.gitignore vendored
View File

@@ -15,4 +15,5 @@ main/mmap_generate_emoji.h
.cache
main/mmap_generate_emoji.h
*.pyc
*.bin
*.bin
mmap_generate_*.h

View File

@@ -4,7 +4,7 @@
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
set(PROJECT_VER "1.8.1")
set(PROJECT_VER "1.8.4")
# Add this line to disable the specific warning
add_compile_options(-Wno-missing-field-initializers)

View File

@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2024 Xiaoxia
Copyright (c) 2025 Shenzhen Xinzhi Future Technology Co., Ltd.
Copyright (c) 2025 Project Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -106,6 +106,8 @@ elseif(CONFIG_BOARD_TYPE_ECHOEAR)
set(BOARD_TYPE "echoear")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8)
set(BOARD_TYPE "esp32-s3-touch-amoled-1.8")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06)
set(BOARD_TYPE "waveshare-s3-touch-amoled-2.06")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75)
set(BOARD_TYPE "waveshare-s3-touch-amoled-1.75")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_85C)
@@ -255,6 +257,7 @@ file(GLOB COMMON_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/common/*.p3)
if(CONFIG_IDF_TARGET_ESP32)
list(REMOVE_ITEM SOURCES "audio/codecs/box_audio_codec.cc"
"audio/codecs/es8388_audio_codec.cc"
"audio/codecs/es8389_audio_codec.cc"
"led/gpio_led.cc"
)
endif()
@@ -332,4 +335,25 @@ spiffs_create_partition_assets(
FLASH_IN_PROJECT
MMAP_FILE_SUPPORT_FORMAT ".aaf"
)
endif()
if(CONFIG_BOARD_TYPE_ECHOEAR)
idf_build_get_property(build_components BUILD_COMPONENTS)
foreach(COMPONENT ${build_components})
if(COMPONENT MATCHES "esp_emote_gfx" OR COMPONENT MATCHES "espressif2022__esp_emote_gfx")
set(EMOTE_GFX_COMPONENT ${COMPONENT})
idf_component_get_property(EMOTE_GFX_COMPONENT_PATH ${EMOTE_GFX_COMPONENT} COMPONENT_DIR)
set(SPIFFS_DIR "${EMOTE_GFX_COMPONENT_PATH}/emoji_normal")
break()
endif()
endforeach()
spiffs_create_partition_assets(
assets_A
${SPIFFS_DIR}
FLASH_IN_PROJECT
MMAP_FILE_SUPPORT_FORMAT ".aaf, ttf, bin"
IMPORT_INC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}
)
endif()

View File

@@ -148,6 +148,8 @@ choice BOARD_TYPE
config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8
bool "Waveshare ESP32-S3-Touch-AMOLED-1.8"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06
bool "Waveshare ESP32-S3-Touch-AMOLED-2.06"
config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75
bool "Waveshare ESP32-S3-Touch-AMOLED-1.75"
depends on IDF_TARGET_ESP32S3
@@ -419,19 +421,26 @@ config USE_CUSTOM_WAKE_WORD
config CUSTOM_WAKE_WORD
string "Custom Wake Word"
default "ni hao xiao zhi"
default "xiao tu dou"
depends on USE_CUSTOM_WAKE_WORD
help
自定义唤醒词,用汉语拼音表示
自定义唤醒词,中文用拼音表示,每个字之间用空格隔开
config CUSTOM_WAKE_WORD_DISPLAY
string "Custom Wake Word Display"
default "你好小智"
default "小土豆"
depends on USE_CUSTOM_WAKE_WORD
help
自定义唤醒词对应问候语
唤醒后发送给服务器的问候语
config CUSTOM_WAKE_WORD_THRESHOLD
int "Custom Wake Word Threshold (%)"
default 20
range 1 99
depends on USE_CUSTOM_WAKE_WORD
help
自定义唤醒词阈值范围1-99越小越敏感默认10
config USE_AUDIO_PROCESSOR
bool "Enable Audio Noise Reduction"
default y
@@ -442,7 +451,7 @@ config USE_AUDIO_PROCESSOR
config USE_DEVICE_AEC
bool "Enable Device-Side AEC"
default n
depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE || BOARD_TYPE_LICHUANG_DEV || BOARD_TYPE_ESP32S3_KORVO2_V3 || BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75 || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC)
depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE || BOARD_TYPE_LICHUANG_DEV || BOARD_TYPE_ESP32S3_KORVO2_V3 || BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75 || BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06 || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC)
help
因为性能不够,不建议和微信聊天界面风格同时开启

View File

@@ -97,34 +97,42 @@ void AudioService::Start() {
esp_timer_start_periodic(audio_power_timer_, 1000000);
/* Start the audio input task */
#if CONFIG_USE_AUDIO_PROCESSOR
/* Start the audio input task */
xTaskCreatePinnedToCore([](void* arg) {
AudioService* audio_service = (AudioService*)arg;
audio_service->AudioInputTask();
vTaskDelete(NULL);
}, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_, 1);
#else
xTaskCreate([](void* arg) {
AudioService* audio_service = (AudioService*)arg;
audio_service->AudioInputTask();
vTaskDelete(NULL);
}, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_);
#endif
/* Start the audio output task */
xTaskCreate([](void* arg) {
AudioService* audio_service = (AudioService*)arg;
audio_service->AudioOutputTask();
vTaskDelete(NULL);
}, "audio_output", 4096, this, 3, &audio_output_task_handle_);
}, "audio_output", 2048 * 2, this, 3, &audio_output_task_handle_);
#else
/* Start the audio input task */
xTaskCreate([](void* arg) {
AudioService* audio_service = (AudioService*)arg;
audio_service->AudioInputTask();
vTaskDelete(NULL);
}, "audio_input", 2048 * 2, this, 8, &audio_input_task_handle_);
/* Start the audio output task */
xTaskCreate([](void* arg) {
AudioService* audio_service = (AudioService*)arg;
audio_service->AudioOutputTask();
vTaskDelete(NULL);
}, "audio_output", 2048, this, 3, &audio_output_task_handle_);
#endif
/* Start the opus codec task */
xTaskCreate([](void* arg) {
AudioService* audio_service = (AudioService*)arg;
audio_service->OpusCodecTask();
vTaskDelete(NULL);
}, "opus_codec", 4096 * 7, this, 2, &opus_codec_task_handle_);
}, "opus_codec", 2048 * 13, this, 2, &opus_codec_task_handle_);
}
void AudioService::Stop() {
@@ -468,7 +476,10 @@ void AudioService::EnableWakeWordDetection(bool enable) {
ESP_LOGD(TAG, "%s wake word detection", enable ? "Enabling" : "Disabling");
if (enable) {
if (!wake_word_initialized_) {
wake_word_->Initialize(codec_);
if (!wake_word_->Initialize(codec_)) {
ESP_LOGE(TAG, "Failed to initialize wake word");
return;
}
wake_word_initialized_ = true;
}
wake_word_->Start();
@@ -513,6 +524,11 @@ void AudioService::EnableAudioTesting(bool enable) {
void AudioService::EnableDeviceAec(bool enable) {
ESP_LOGI(TAG, "%s device AEC", enable ? "Enabling" : "Disabling");
if (!audio_processor_initialized_) {
audio_processor_->Initialize(codec_, OPUS_FRAME_DURATION_MS);
audio_processor_initialized_ = true;
}
audio_processor_->EnableDeviceAec(enable);
}

View File

@@ -133,10 +133,8 @@ private:
std::deque<std::unique_ptr<AudioStreamPacket>> audio_testing_queue_;
std::deque<std::unique_ptr<AudioTask>> audio_encode_queue_;
std::deque<std::unique_ptr<AudioTask>> audio_playback_queue_;
// For server AEC
std::deque<uint32_t> timestamp_queue_;
std::mutex timestamp_mutex_;
bool wake_word_initialized_ = false;
bool audio_processor_initialized_ = false;

View File

@@ -11,7 +11,7 @@ class WakeWord {
public:
virtual ~WakeWord() = default;
virtual void Initialize(AudioCodec* codec) = 0;
virtual bool Initialize(AudioCodec* codec) = 0;
virtual void Feed(const std::vector<int16_t>& data) = 0;
virtual void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) = 0;
virtual void Start() = 0;

View File

@@ -1,9 +1,7 @@
#include "afe_wake_word.h"
#include "application.h"
#include "audio_service.h"
#include <esp_log.h>
#include <model_path.h>
#include <arpa/inet.h>
#include <sstream>
#define DETECTION_RUNNING_EVENT 1
@@ -27,23 +25,31 @@ AfeWakeWord::~AfeWakeWord() {
heap_caps_free(wake_word_encode_task_stack_);
}
if (wake_word_encode_task_buffer_ != nullptr) {
heap_caps_free(wake_word_encode_task_buffer_);
}
if (models_ != nullptr) {
esp_srmodel_deinit(models_);
}
vEventGroupDelete(event_group_);
}
void AfeWakeWord::Initialize(AudioCodec* codec) {
bool AfeWakeWord::Initialize(AudioCodec* codec) {
codec_ = codec;
int ref_num = codec_->input_reference() ? 1 : 0;
srmodel_list_t *models = esp_srmodel_init("model");
if (models == nullptr || models->num == -1) {
models_ = esp_srmodel_init("model");
if (models_ == nullptr || models_->num == -1) {
ESP_LOGE(TAG, "Failed to initialize wakenet model");
return;
return false;
}
for (int i = 0; i < models->num; i++) {
ESP_LOGI(TAG, "Model %d: %s", i, models->model_name[i]);
if (strstr(models->model_name[i], ESP_WN_PREFIX) != NULL) {
wakenet_model_ = models->model_name[i];
auto words = esp_srmodel_get_wake_words(models, wakenet_model_);
for (int i = 0; i < models_->num; i++) {
ESP_LOGI(TAG, "Model %d: %s", i, models_->model_name[i]);
if (strstr(models_->model_name[i], ESP_WN_PREFIX) != NULL) {
wakenet_model_ = models_->model_name[i];
auto words = esp_srmodel_get_wake_words(models_, wakenet_model_);
// split by ";" to get all wake words
std::stringstream ss(words);
std::string word;
@@ -60,7 +66,7 @@ void AfeWakeWord::Initialize(AudioCodec* codec) {
for (int i = 0; i < ref_num; i++) {
input_format.push_back('R');
}
afe_config_t* afe_config = afe_config_init(input_format.c_str(), models, AFE_TYPE_SR, AFE_MODE_HIGH_PERF);
afe_config_t* afe_config = afe_config_init(input_format.c_str(), models_, AFE_TYPE_SR, AFE_MODE_HIGH_PERF);
afe_config->aec_init = codec_->input_reference();
afe_config->aec_mode = AEC_MODE_SR_HIGH_PERF;
afe_config->afe_perferred_core = 1;
@@ -75,6 +81,8 @@ void AfeWakeWord::Initialize(AudioCodec* codec) {
this_->AudioDetectionTask();
vTaskDelete(NULL);
}, "audio_detection", 4096, this, 3, nullptr);
return true;
}
void AfeWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) {
@@ -144,10 +152,17 @@ void AfeWakeWord::StoreWakeWordData(const int16_t* data, size_t samples) {
}
void AfeWakeWord::EncodeWakeWordData() {
const size_t stack_size = 4096 * 7;
wake_word_opus_.clear();
if (wake_word_encode_task_stack_ == nullptr) {
wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(4096 * 8, MALLOC_CAP_SPIRAM);
wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(stack_size, MALLOC_CAP_SPIRAM);
assert(wake_word_encode_task_stack_ != nullptr);
}
if (wake_word_encode_task_buffer_ == nullptr) {
wake_word_encode_task_buffer_ = (StaticTask_t*)heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL);
assert(wake_word_encode_task_buffer_ != nullptr);
}
wake_word_encode_task_ = xTaskCreateStatic([](void* arg) {
auto this_ = (AfeWakeWord*)arg;
{
@@ -174,7 +189,7 @@ void AfeWakeWord::EncodeWakeWordData() {
this_->wake_word_cv_.notify_all();
}
vTaskDelete(NULL);
}, "encode_detect_packets", 4096 * 8, this, 2, wake_word_encode_task_stack_, &wake_word_encode_task_buffer_);
}, "encode_wake_word", stack_size, this, 2, wake_word_encode_task_stack_, wake_word_encode_task_buffer_);
}
bool AfeWakeWord::GetWakeWordOpus(std::vector<uint8_t>& opus) {

View File

@@ -7,8 +7,9 @@
#include <esp_afe_sr_models.h>
#include <esp_nsn_models.h>
#include <model_path.h>
#include <list>
#include <deque>
#include <string>
#include <vector>
#include <functional>
@@ -23,7 +24,7 @@ public:
AfeWakeWord();
~AfeWakeWord();
void Initialize(AudioCodec* codec);
bool Initialize(AudioCodec* codec);
void Feed(const std::vector<int16_t>& data);
void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback);
void Start();
@@ -34,6 +35,7 @@ public:
const std::string& GetLastDetectedWakeWord() const { return last_detected_wake_word_; }
private:
srmodel_list_t *models_ = nullptr;
esp_afe_sr_iface_t* afe_iface_ = nullptr;
esp_afe_sr_data_t* afe_data_ = nullptr;
char* wakenet_model_ = NULL;
@@ -44,10 +46,10 @@ private:
std::string last_detected_wake_word_;
TaskHandle_t wake_word_encode_task_ = nullptr;
StaticTask_t wake_word_encode_task_buffer_;
StaticTask_t* wake_word_encode_task_buffer_ = nullptr;
StackType_t* wake_word_encode_task_stack_ = nullptr;
std::list<std::vector<int16_t>> wake_word_pcm_;
std::list<std::vector<uint8_t>> wake_word_opus_;
std::deque<std::vector<int16_t>> wake_word_pcm_;
std::deque<std::vector<uint8_t>> wake_word_opus_;
std::mutex wake_word_mutex_;
std::condition_variable wake_word_cv_;

View File

@@ -1,75 +1,66 @@
#include "custom_wake_word.h"
#include "application.h"
#include "audio_service.h"
#include "system_info.h"
#include <esp_log.h>
#include <model_path.h>
#include <arpa/inet.h>
#include "esp_wn_iface.h"
#include "esp_wn_models.h"
#include "esp_afe_sr_iface.h"
#include "esp_afe_sr_models.h"
#include "esp_mn_iface.h"
#include "esp_mn_models.h"
#include "esp_mn_speech_commands.h"
#include <sstream>
#define DETECTION_RUNNING_EVENT 1
#define TAG "CustomWakeWord"
CustomWakeWord::CustomWakeWord()
: afe_data_(nullptr),
wake_word_pcm_(),
wake_word_opus_() {
event_group_ = xEventGroupCreate();
: wake_word_pcm_(), wake_word_opus_() {
}
CustomWakeWord::~CustomWakeWord() {
if (afe_data_ != nullptr) {
afe_iface_->destroy(afe_data_);
if (multinet_model_data_ != nullptr && multinet_ != nullptr) {
multinet_->destroy(multinet_model_data_);
multinet_model_data_ = nullptr;
}
if (wake_word_encode_task_stack_ != nullptr) {
heap_caps_free(wake_word_encode_task_stack_);
}
vEventGroupDelete(event_group_);
if (wake_word_encode_task_buffer_ != nullptr) {
heap_caps_free(wake_word_encode_task_buffer_);
}
if (models_ != nullptr) {
esp_srmodel_deinit(models_);
}
}
void CustomWakeWord::Initialize(AudioCodec* codec) {
bool CustomWakeWord::Initialize(AudioCodec* codec) {
codec_ = codec;
int ref_num = codec_->input_reference() ? 1 : 0;
models = esp_srmodel_init("model");
if (models == nullptr || models->num == -1) {
models_ = esp_srmodel_init("model");
if (models_ == nullptr || models_->num == -1) {
ESP_LOGE(TAG, "Failed to initialize wakenet model");
return;
}
std::string input_format;
for (int i = 0; i < codec_->input_channels() - ref_num; i++) {
input_format.push_back('M');
}
for (int i = 0; i < ref_num; i++) {
input_format.push_back('R');
return false;
}
afe_config_t* afe_config = afe_config_init(input_format.c_str(), models, AFE_TYPE_SR, AFE_MODE_HIGH_PERF);
afe_config->aec_init = codec_->input_reference();
afe_config->aec_mode = AEC_MODE_SR_HIGH_PERF;
afe_config->afe_perferred_core = 1;
afe_config->afe_perferred_priority = 1;
afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM;
// 初始化 multinet (命令词识别)
mn_name_ = esp_srmodel_filter(models_, ESP_MN_PREFIX, ESP_MN_CHINESE);
if (mn_name_ == nullptr) {
ESP_LOGE(TAG, "Failed to initialize multinet, mn_name is nullptr");
ESP_LOGI(TAG, "Please refer to https://pcn7cs20v8cr.feishu.cn/wiki/CpQjwQsCJiQSWSkYEvrcxcbVnwh to add custom wake word");
return false;
}
ESP_LOGI(TAG, "multinet: %s", mn_name_);
multinet_ = esp_mn_handle_from_name(mn_name_);
multinet_model_data_ = multinet_->create(mn_name_, 3000); // 3 秒超时
multinet_->set_det_threshold(multinet_model_data_, CONFIG_CUSTOM_WAKE_WORD_THRESHOLD / 100.0f);
esp_mn_commands_clear();
esp_mn_commands_add(1, CONFIG_CUSTOM_WAKE_WORD);
esp_mn_commands_update();
afe_iface_ = esp_afe_handle_from_config(afe_config);
afe_data_ = afe_iface_->create_from_config(afe_config);
xTaskCreate([](void* arg) {
auto this_ = (CustomWakeWord*)arg;
this_->AudioDetectionTask();
vTaskDelete(NULL);
}, "audio_detection", 16384, this, 3, nullptr);
multinet_->print_active_speech_commands(multinet_model_data_);
return true;
}
void CustomWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) {
@@ -77,115 +68,65 @@ void CustomWakeWord::OnWakeWordDetected(std::function<void(const std::string& wa
}
void CustomWakeWord::Start() {
xEventGroupSetBits(event_group_, DETECTION_RUNNING_EVENT);
running_ = true;
}
void CustomWakeWord::Stop() {
xEventGroupClearBits(event_group_, DETECTION_RUNNING_EVENT);
if (afe_data_ != nullptr) {
afe_iface_->reset_buffer(afe_data_);
}
running_ = false;
}
void CustomWakeWord::Feed(const std::vector<int16_t>& data) {
if (afe_data_ == nullptr) {
if (multinet_model_data_ == nullptr || !running_) {
return;
}
afe_iface_->feed(afe_data_, data.data());
esp_mn_state_t mn_state;
// If input channels is 2, we need to fetch the left channel data
if (codec_->input_channels() == 2) {
auto mono_data = std::vector<int16_t>(data.size() / 2);
for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) {
mono_data[i] = data[j];
}
StoreWakeWordData(mono_data);
mn_state = multinet_->detect(multinet_model_data_, const_cast<int16_t*>(mono_data.data()));
} else {
StoreWakeWordData(data);
mn_state = multinet_->detect(multinet_model_data_, const_cast<int16_t*>(data.data()));
}
if (mn_state == ESP_MN_STATE_DETECTING) {
return;
} else if (mn_state == ESP_MN_STATE_DETECTED) {
esp_mn_results_t *mn_result = multinet_->get_results(multinet_model_data_);
ESP_LOGI(TAG, "Custom wake word detected: command_id=%d, string=%s, prob=%f",
mn_result->command_id[0], mn_result->string, mn_result->prob[0]);
if (mn_result->command_id[0] == 1) {
last_detected_wake_word_ = CONFIG_CUSTOM_WAKE_WORD_DISPLAY;
}
running_ = false;
if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_);
}
multinet_->clean(multinet_model_data_);
} else if (mn_state == ESP_MN_STATE_TIMEOUT) {
ESP_LOGD(TAG, "Command word detection timeout, cleaning state");
multinet_->clean(multinet_model_data_);
}
}
size_t CustomWakeWord::GetFeedSize() {
if (afe_data_ == nullptr) {
if (multinet_model_data_ == nullptr) {
return 0;
}
return afe_iface_->get_feed_chunksize(afe_data_) * codec_->input_channels();
return multinet_->get_samp_chunksize(multinet_model_data_) * codec_->input_channels();
}
void CustomWakeWord::AudioDetectionTask() {
auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_);
auto feed_size = afe_iface_->get_feed_chunksize(afe_data_);
// 初始化 multinet (命令词识别)
char *mn_name = esp_srmodel_filter(models, ESP_MN_PREFIX, ESP_MN_CHINESE);
ESP_LOGI(TAG, "multinet:%s", mn_name);
esp_mn_iface_t *multinet = esp_mn_handle_from_name(mn_name);
model_iface_data_t *model_data = multinet->create(mn_name, 2000); // 2秒超时
multinet->set_det_threshold(model_data, 0.5);
esp_mn_commands_clear();
esp_mn_commands_add(1, CONFIG_CUSTOM_WAKE_WORD); // 添加自定义唤醒词作为命令词
esp_mn_commands_update();
int mu_chunksize = multinet->get_samp_chunksize(model_data);
assert(mu_chunksize == feed_size);
// 打印所有的命令词
multinet->print_active_speech_commands(model_data);
ESP_LOGI(TAG, "Audio detection task started, feed size: %d fetch size: %d", feed_size, fetch_size);
ESP_LOGI(TAG, "Custom wake word: %s", CONFIG_CUSTOM_WAKE_WORD);
// 禁用wakenet直接使用multinet检测自定义唤醒词
afe_iface_->disable_wakenet(afe_data_);
while (true) {
xEventGroupWaitBits(event_group_, DETECTION_RUNNING_EVENT, pdFALSE, pdTRUE, portMAX_DELAY);
auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY);
if (res == nullptr || res->ret_value == ESP_FAIL) {
ESP_LOGW(TAG, "Fetch failed, continue");
continue;
}
// 存储音频数据用于语音识别
StoreWakeWordData(res->data, res->data_size / sizeof(int16_t));
// 直接使用multinet检测自定义唤醒词
esp_mn_state_t mn_state = multinet->detect(model_data, res->data);
if (mn_state == ESP_MN_STATE_DETECTING) {
// 仍在检测中,继续
continue;
} else if (mn_state == ESP_MN_STATE_DETECTED) {
// 检测到自定义唤醒词
esp_mn_results_t *mn_result = multinet->get_results(model_data);
ESP_LOGI(TAG, "Custom wake word detected: command_id=%d, string=%s, prob=%f",
mn_result->command_id[0], mn_result->string, mn_result->prob[0]);
if (mn_result->command_id[0] == 1) { // 自定义唤醒词
ESP_LOGI(TAG, "Custom wake word '%s' detected successfully!", CONFIG_CUSTOM_WAKE_WORD);
// 停止检测
Stop();
last_detected_wake_word_ = CONFIG_CUSTOM_WAKE_WORD_DISPLAY;
// 调用回调
if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_);
}
// 清理multinet状态准备下次检测
multinet->clean(model_data);
ESP_LOGI(TAG, "Ready for next detection");
}
} else if (mn_state == ESP_MN_STATE_TIMEOUT) {
// 超时,清理状态继续检测
ESP_LOGD(TAG, "Command word detection timeout, cleaning state");
multinet->clean(model_data);
continue;
}
}
// 清理资源
if (model_data) {
multinet->destroy(model_data);
model_data = NULL;
}
ESP_LOGI(TAG, "Audio detection task ended");
}
void CustomWakeWord::StoreWakeWordData(const int16_t* data, size_t samples) {
void CustomWakeWord::StoreWakeWordData(const std::vector<int16_t>& data) {
// store audio data to wake_word_pcm_
wake_word_pcm_.emplace_back(std::vector<int16_t>(data, data + samples));
wake_word_pcm_.push_back(data);
// keep about 2 seconds of data, detect duration is 30ms (sample_rate == 16000, chunksize == 512)
while (wake_word_pcm_.size() > 2000 / 30) {
wake_word_pcm_.pop_front();
@@ -193,10 +134,17 @@ void CustomWakeWord::StoreWakeWordData(const int16_t* data, size_t samples) {
}
void CustomWakeWord::EncodeWakeWordData() {
const size_t stack_size = 4096 * 7;
wake_word_opus_.clear();
if (wake_word_encode_task_stack_ == nullptr) {
wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(4096 * 8, MALLOC_CAP_SPIRAM);
wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(stack_size, MALLOC_CAP_SPIRAM);
assert(wake_word_encode_task_stack_ != nullptr);
}
if (wake_word_encode_task_buffer_ == nullptr) {
wake_word_encode_task_buffer_ = (StaticTask_t*)heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL);
assert(wake_word_encode_task_buffer_ != nullptr);
}
wake_word_encode_task_ = xTaskCreateStatic([](void* arg) {
auto this_ = (CustomWakeWord*)arg;
{
@@ -223,7 +171,7 @@ void CustomWakeWord::EncodeWakeWordData() {
this_->wake_word_cv_.notify_all();
}
vTaskDelete(NULL);
}, "encode_detect_packets", 4096 * 8, this, 2, wake_word_encode_task_stack_, &wake_word_encode_task_buffer_);
}, "encode_wake_word", stack_size, this, 2, wake_word_encode_task_stack_, wake_word_encode_task_buffer_);
}
bool CustomWakeWord::GetWakeWordOpus(std::vector<uint8_t>& opus) {

View File

@@ -1,24 +1,18 @@
#ifndef CUSTOM_WAKE_WORD_H
#define CUSTOM_WAKE_WORD_H
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/event_groups.h>
#include <esp_afe_sr_models.h>
#include <esp_afe_sr_iface.h>
#include <esp_nsn_models.h>
#include <esp_wn_iface.h>
#include <esp_wn_models.h>
#include <esp_attr.h>
#include <esp_mn_iface.h>
#include <esp_mn_models.h>
#include <model_path.h>
#include <list>
#include <deque>
#include <string>
#include <vector>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include "audio_codec.h"
#include "wake_word.h"
@@ -28,7 +22,7 @@ public:
CustomWakeWord();
~CustomWakeWord();
void Initialize(AudioCodec* codec);
bool Initialize(AudioCodec* codec);
void Feed(const std::vector<int16_t>& data);
void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback);
void Start();
@@ -39,27 +33,26 @@ public:
const std::string& GetLastDetectedWakeWord() const { return last_detected_wake_word_; }
private:
esp_afe_sr_iface_t* afe_iface_ = nullptr;
esp_afe_sr_data_t* afe_data_ = nullptr;
srmodel_list_t *models = nullptr;
// multinet 相关成员变量
esp_mn_iface_t* multinet_ = nullptr;
model_iface_data_t* multinet_model_data_ = nullptr;
srmodel_list_t *models_ = nullptr;
char* mn_name_ = nullptr;
char* wakenet_model_ = NULL;
std::vector<std::string> wake_words_;
EventGroupHandle_t event_group_;
std::function<void(const std::string& wake_word)> wake_word_detected_callback_;
AudioCodec* codec_ = nullptr;
std::string last_detected_wake_word_;
std::atomic<bool> running_ = false;
TaskHandle_t wake_word_encode_task_ = nullptr;
StaticTask_t wake_word_encode_task_buffer_;
StaticTask_t* wake_word_encode_task_buffer_ = nullptr;
StackType_t* wake_word_encode_task_stack_ = nullptr;
std::list<std::vector<int16_t>> wake_word_pcm_;
std::list<std::vector<uint8_t>> wake_word_opus_;
std::deque<std::vector<int16_t>> wake_word_pcm_;
std::deque<std::vector<uint8_t>> wake_word_opus_;
std::mutex wake_word_mutex_;
std::condition_variable wake_word_cv_;
void StoreWakeWordData(const int16_t* data, size_t size);
void AudioDetectionTask();
void StoreWakeWordData(const std::vector<int16_t>& data);
};
#endif

View File

@@ -1,17 +1,10 @@
#include "esp_wake_word.h"
#include "application.h"
#include <esp_log.h>
#include <model_path.h>
#include <arpa/inet.h>
#include <sstream>
#define DETECTION_RUNNING_EVENT 1
#define TAG "EspWakeWord"
EspWakeWord::EspWakeWord() {
event_group_ = xEventGroupCreate();
}
EspWakeWord::~EspWakeWord() {
@@ -19,23 +12,21 @@ EspWakeWord::~EspWakeWord() {
wakenet_iface_->destroy(wakenet_data_);
esp_srmodel_deinit(wakenet_model_);
}
vEventGroupDelete(event_group_);
}
void EspWakeWord::Initialize(AudioCodec* codec) {
bool EspWakeWord::Initialize(AudioCodec* codec) {
codec_ = codec;
wakenet_model_ = esp_srmodel_init("model");
if (wakenet_model_ == nullptr || wakenet_model_->num == -1) {
ESP_LOGE(TAG, "Failed to initialize wakenet model");
return;
return false;
}
if(wakenet_model_->num > 1) {
ESP_LOGW(TAG, "More than one model found, using the first one");
} else if (wakenet_model_->num == 0) {
ESP_LOGE(TAG, "No model found");
return;
return false;
}
char *model_name = wakenet_model_->model_name[0];
wakenet_iface_ = (esp_wn_iface_t*)esp_wn_handle_from_name(model_name);
@@ -44,6 +35,8 @@ void EspWakeWord::Initialize(AudioCodec* codec) {
int frequency = wakenet_iface_->get_samp_rate(wakenet_data_);
int audio_chunksize = wakenet_iface_->get_samp_chunksize(wakenet_data_);
ESP_LOGI(TAG, "Wake word(%s),freq: %d, chunksize: %d", model_name, frequency, audio_chunksize);
return true;
}
void EspWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) {
@@ -51,18 +44,22 @@ void EspWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_
}
void EspWakeWord::Start() {
xEventGroupSetBits(event_group_, DETECTION_RUNNING_EVENT);
running_ = true;
}
void EspWakeWord::Stop() {
xEventGroupClearBits(event_group_, DETECTION_RUNNING_EVENT);
running_ = false;
}
void EspWakeWord::Feed(const std::vector<int16_t>& data) {
if (wakenet_data_ == nullptr || !running_) {
return;
}
int res = wakenet_iface_->detect(wakenet_data_, (int16_t *)data.data());
if (res > 0) {
Stop();
last_detected_wake_word_ = wakenet_iface_->get_word_name(wakenet_data_, res);
running_ = false;
if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_);

View File

@@ -1,20 +1,14 @@
#ifndef ESP_WAKE_WORD_H
#define ESP_WAKE_WORD_H
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/event_groups.h>
#include <esp_wn_iface.h>
#include <esp_wn_models.h>
#include <model_path.h>
#include <list>
#include <string>
#include <vector>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include "audio_codec.h"
#include "wake_word.h"
@@ -24,7 +18,7 @@ public:
EspWakeWord();
~EspWakeWord();
void Initialize(AudioCodec* codec);
bool Initialize(AudioCodec* codec);
void Feed(const std::vector<int16_t>& data);
void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback);
void Start();
@@ -38,8 +32,8 @@ private:
esp_wn_iface_t *wakenet_iface_ = nullptr;
model_iface_data_t *wakenet_data_ = nullptr;
srmodel_list_t *wakenet_model_ = nullptr;
EventGroupHandle_t event_group_;
AudioCodec* codec_ = nullptr;
std::atomic<bool> running_ = false;
std::function<void(const std::string& wake_word)> wake_word_detected_callback_;
std::string last_detected_wake_word_;

View File

@@ -139,8 +139,7 @@ private:
power_save_timer_->OnEnterSleepMode([this]() {
power_sleep_ = kDeviceNeutralSleep;
XiaozhiStatus_ = kDevice_join_Sleep;
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
if (LcdStatus_ != kDevicelcdbacklightOff) {
GetBacklight()->SetBrightness(1);
@@ -148,8 +147,7 @@ private:
});
power_save_timer_->OnExitSleepMode([this]() {
power_sleep_ = kDeviceNoSleep;
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
if (XiaozhiStatus_ != kDevice_Exit_Sleep) {
GetBacklight()->RestoreBrightness();

View File

@@ -99,13 +99,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -99,13 +99,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -229,7 +229,7 @@ public:
}
virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT, 256);
return &backlight;
}
};

View File

@@ -303,6 +303,6 @@
// A MCP Test: Control a lamp
#define LAMP_GPIO GPIO_NUM_18
#define LAMP_GPIO GPIO_NUM_14
#endif // _BOARD_CONFIG_H_

View File

@@ -2,6 +2,7 @@
#include <cstring>
#include <algorithm>
#include "esp_log.h"
#include "display.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
@@ -12,7 +13,10 @@ namespace audio_wifi_config
static const char *kLogTag = "AUDIO_WIFI_CONFIG";
void ReceiveWifiCredentialsFromAudio(Application *app,
WifiConfigurationAp *wifi_ap)
WifiConfigurationAp *wifi_ap,
Display *display,
size_t input_channels
)
{
const int kInputSampleRate = 16000; // Input sampling rate
const float kDownsampleStep = static_cast<float>(kInputSampleRate) / static_cast<float>(kAudioSampleRate); // Downsampling step
@@ -35,29 +39,31 @@ namespace audio_wifi_config
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
if (input_channels == 2) { // 如果是双声道输入,转换为单声道
auto mono_data = std::vector<int16_t>(audio_data.size() / 2);
for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) {
mono_data[i] = audio_data[j];
}
audio_data = std::move(mono_data);
}
// Downsample the audio data
std::vector<float> downsampled_data;
size_t last_index = 0;
if (kDownsampleStep > 1.0f)
{
if (kDownsampleStep > 1.0f) {
downsampled_data.reserve(audio_data.size() / static_cast<size_t>(kDownsampleStep));
for (size_t i = 0; i < audio_data.size(); ++i)
{
for (size_t i = 0; i < audio_data.size(); ++i) {
size_t sample_index = static_cast<size_t>(i / kDownsampleStep);
if ((sample_index + 1) > last_index)
{
if ((sample_index + 1) > last_index) {
downsampled_data.push_back(static_cast<float>(audio_data[i]));
last_index = sample_index + 1;
}
}
}
else
{
} else {
downsampled_data.reserve(audio_data.size());
for (int16_t sample : audio_data)
{
for (int16_t sample : audio_data) {
downsampled_data.push_back(static_cast<float>(sample));
}
}
@@ -66,35 +72,28 @@ namespace audio_wifi_config
auto probabilities = signal_processor.ProcessAudioSamples(downsampled_data);
// Feed probability data to the data buffer
if (data_buffer.ProcessProbabilityData(probabilities, 0.5f))
{
if (data_buffer.ProcessProbabilityData(probabilities, 0.5f)) {
// If complete data was received, extract WiFi credentials
if (data_buffer.decoded_text.has_value())
{
if (data_buffer.decoded_text.has_value()) {
ESP_LOGI(kLogTag, "Received text data: %s", data_buffer.decoded_text->c_str());
display->SetChatMessage("system", data_buffer.decoded_text->c_str());
// Split SSID and password by newline character
std::string wifi_ssid, wifi_password;
size_t newline_position = data_buffer.decoded_text->find('\n');
if (newline_position != std::string::npos)
{
if (newline_position != std::string::npos) {
wifi_ssid = data_buffer.decoded_text->substr(0, newline_position);
wifi_password = data_buffer.decoded_text->substr(newline_position + 1);
ESP_LOGI(kLogTag, "WiFi SSID: %s, Password: %s", wifi_ssid.c_str(), wifi_password.c_str());
}
else
{
} else {
ESP_LOGE(kLogTag, "Invalid data format, no newline character found");
continue;
}
if (wifi_ap->ConnectToWifi(wifi_ssid, wifi_password))
{
if (wifi_ap->ConnectToWifi(wifi_ssid, wifi_password)) {
wifi_ap->Save(wifi_ssid, wifi_password); // Save WiFi credentials
esp_restart(); // Restart device to apply new WiFi configuration
}
else
{
} else {
ESP_LOGE(kLogTag, "Failed to connect to WiFi with received credentials");
}
data_buffer.decoded_text.reset(); // Clear processed data
@@ -115,8 +114,7 @@ namespace audio_wifi_config
// FrequencyDetector implementation
FrequencyDetector::FrequencyDetector(float frequency, size_t window_size)
: frequency_(frequency), window_size_(window_size)
{
: frequency_(frequency), window_size_(window_size) {
frequency_bin_ = std::floor(frequency_ * static_cast<float>(window_size_));
angular_frequency_ = 2.0f * M_PI * frequency_;
cos_coefficient_ = std::cos(angular_frequency_);
@@ -128,17 +126,14 @@ namespace audio_wifi_config
state_buffer_.push_back(0.0f);
}
void FrequencyDetector::Reset()
{
void FrequencyDetector::Reset() {
state_buffer_.clear();
state_buffer_.push_back(0.0f);
state_buffer_.push_back(0.0f);
}
void FrequencyDetector::ProcessSample(float sample)
{
if (state_buffer_.size() < 2)
{
void FrequencyDetector::ProcessSample(float sample) {
if (state_buffer_.size() < 2) {
return;
}
@@ -153,10 +148,8 @@ namespace audio_wifi_config
state_buffer_.push_back(s_current); // Add new S[0]
}
float FrequencyDetector::GetAmplitude() const
{
if (state_buffer_.size() < 2)
{
float FrequencyDetector::GetAmplitude() const {
if (state_buffer_.size() < 2) {
return 0.0f;
}
@@ -172,10 +165,8 @@ namespace audio_wifi_config
// AudioSignalProcessor implementation
AudioSignalProcessor::AudioSignalProcessor(size_t sample_rate, size_t mark_frequency, size_t space_frequency,
size_t bit_rate, size_t window_size)
: input_buffer_size_(window_size), output_sample_count_(0)
{
if (sample_rate % bit_rate != 0)
{
: input_buffer_size_(window_size), output_sample_count_(0) {
if (sample_rate % bit_rate != 0) {
// On ESP32 we can continue execution, but log the error
ESP_LOGW(kLogTag, "Sample rate %zu is not divisible by bit rate %zu", sample_rate, bit_rate);
}
@@ -189,28 +180,21 @@ namespace audio_wifi_config
samples_per_bit_ = sample_rate / bit_rate; // Number of samples per bit
}
std::vector<float> AudioSignalProcessor::ProcessAudioSamples(const std::vector<float> &samples)
{
std::vector<float> AudioSignalProcessor::ProcessAudioSamples(const std::vector<float> &samples) {
std::vector<float> result;
for (float sample : samples)
{
if (input_buffer_.size() < input_buffer_size_)
{
for (float sample : samples) {
if (input_buffer_.size() < input_buffer_size_) {
input_buffer_.push_back(sample); // Just add, don't process yet
}
else
{
} else {
// Input buffer is full, process the data
input_buffer_.pop_front(); // Remove oldest sample
input_buffer_.push_back(sample); // Add new sample
output_sample_count_++;
if (output_sample_count_ >= samples_per_bit_)
{
if (output_sample_count_ >= samples_per_bit_) {
// Process all samples in the window using Goertzel algorithm
for (float window_sample : input_buffer_)
{
for (float window_sample : input_buffer_) {
mark_detector_->ProcessSample(window_sample);
space_detector_->ProcessSample(window_sample);
}
@@ -239,8 +223,7 @@ namespace audio_wifi_config
: current_state_(DataReceptionState::kInactive),
start_of_transmission_(kDefaultStartTransmissionPattern),
end_of_transmission_(kDefaultEndTransmissionPattern),
enable_checksum_validation_(true)
{
enable_checksum_validation_(true) {
identifier_buffer_size_ = std::max(start_of_transmission_.size(), end_of_transmission_.size());
max_bit_buffer_size_ = 776; // Preset bit buffer size, 776 bits = (32 + 1 + 63 + 1) * 8 = 776
@@ -252,48 +235,39 @@ namespace audio_wifi_config
: current_state_(DataReceptionState::kInactive),
start_of_transmission_(start_identifier),
end_of_transmission_(end_identifier),
enable_checksum_validation_(enable_checksum)
{
enable_checksum_validation_(enable_checksum) {
identifier_buffer_size_ = std::max(start_of_transmission_.size(), end_of_transmission_.size());
max_bit_buffer_size_ = max_byte_size * 8; // Bit buffer size in bytes
bit_buffer_.reserve(max_bit_buffer_size_);
}
uint8_t AudioDataBuffer::CalculateChecksum(const std::string &text)
{
uint8_t AudioDataBuffer::CalculateChecksum(const std::string &text) {
uint8_t checksum = 0;
for (char character : text)
{
for (char character : text) {
checksum += static_cast<uint8_t>(character);
}
return checksum;
}
void AudioDataBuffer::ClearBuffers()
{
void AudioDataBuffer::ClearBuffers() {
identifier_buffer_.clear();
bit_buffer_.clear();
}
bool AudioDataBuffer::ProcessProbabilityData(const std::vector<float> &probabilities, float threshold)
{
for (float probability : probabilities)
{
bool AudioDataBuffer::ProcessProbabilityData(const std::vector<float> &probabilities, float threshold) {
for (float probability : probabilities) {
uint8_t bit = (probability > threshold) ? 1 : 0;
if (identifier_buffer_.size() >= identifier_buffer_size_)
{
if (identifier_buffer_.size() >= identifier_buffer_size_) {
identifier_buffer_.pop_front(); // Maintain buffer size
}
identifier_buffer_.push_back(bit);
// Process received bit based on state machine
switch (current_state_)
{
switch (current_state_) {
case DataReceptionState::kInactive:
if (identifier_buffer_.size() >= start_of_transmission_.size())
{
if (identifier_buffer_.size() >= start_of_transmission_.size()) {
current_state_ = DataReceptionState::kWaiting; // Enter waiting state
ESP_LOGI(kLogTag, "Entering Waiting state");
}
@@ -301,8 +275,7 @@ namespace audio_wifi_config
case DataReceptionState::kWaiting:
// Waiting state, possibly waiting for transmission end
if (identifier_buffer_.size() >= start_of_transmission_.size())
{
if (identifier_buffer_.size() >= start_of_transmission_.size()) {
std::vector<uint8_t> identifier_snapshot(identifier_buffer_.begin(), identifier_buffer_.end());
if (identifier_snapshot == start_of_transmission_)
{
@@ -315,11 +288,9 @@ namespace audio_wifi_config
case DataReceptionState::kReceiving:
bit_buffer_.push_back(bit);
if (identifier_buffer_.size() >= end_of_transmission_.size())
{
if (identifier_buffer_.size() >= end_of_transmission_.size()) {
std::vector<uint8_t> identifier_snapshot(identifier_buffer_.begin(), identifier_buffer_.end());
if (identifier_snapshot == end_of_transmission_)
{
if (identifier_snapshot == end_of_transmission_) {
current_state_ = DataReceptionState::kInactive; // Enter inactive state
// Convert bits to bytes
@@ -328,22 +299,18 @@ namespace audio_wifi_config
uint8_t received_checksum = 0;
size_t minimum_length = 0;
if (enable_checksum_validation_)
{
if (enable_checksum_validation_) {
// If checksum is required, last byte is checksum
minimum_length = 1 + start_of_transmission_.size() / 8;
if (bytes.size() >= minimum_length)
{
received_checksum = bytes[bytes.size() - start_of_transmission_.size() / 8 - 1];
}
}
else
{
} else {
minimum_length = start_of_transmission_.size() / 8;
}
if (bytes.size() < minimum_length)
{
if (bytes.size() < minimum_length) {
ClearBuffers();
ESP_LOGW(kLogTag, "Data too short, clearing buffer");
return false; // Data too short, return failure
@@ -356,11 +323,9 @@ namespace audio_wifi_config
std::string result(text_bytes.begin(), text_bytes.end());
// Validate checksum if required
if (enable_checksum_validation_)
{
if (enable_checksum_validation_) {
uint8_t calculated_checksum = CalculateChecksum(result);
if (calculated_checksum != received_checksum)
{
if (calculated_checksum != received_checksum) {
// Checksum mismatch
ESP_LOGW(kLogTag, "Checksum mismatch: expected %d, got %d",
received_checksum, calculated_checksum);
@@ -372,9 +337,7 @@ namespace audio_wifi_config
ClearBuffers();
decoded_text = result;
return true; // Return success
}
else if (bit_buffer_.size() >= max_bit_buffer_size_)
{
} else if (bit_buffer_.size() >= max_bit_buffer_size_) {
// If not end identifier and bit buffer is full, reset
ClearBuffers();
ESP_LOGW(kLogTag, "Buffer overflow, clearing buffer");
@@ -388,19 +351,16 @@ namespace audio_wifi_config
return false;
}
std::vector<uint8_t> AudioDataBuffer::ConvertBitsToBytes(const std::vector<uint8_t> &bits) const
{
std::vector<uint8_t> AudioDataBuffer::ConvertBitsToBytes(const std::vector<uint8_t> &bits) const {
std::vector<uint8_t> bytes;
// Ensure number of bits is a multiple of 8
size_t complete_bytes_count = bits.size() / 8;
bytes.reserve(complete_bytes_count);
for (size_t i = 0; i < complete_bytes_count; ++i)
{
for (size_t i = 0; i < complete_bytes_count; ++i) {
uint8_t byte_value = 0;
for (size_t j = 0; j < 8; ++j)
{
for (size_t j = 0; j < 8; ++j) {
byte_value |= bits[i * 8 + j] << (7 - j);
}
bytes.push_back(byte_value);
@@ -408,4 +368,4 @@ namespace audio_wifi_config
return bytes;
}
}
}

View File

@@ -19,7 +19,8 @@ const size_t kWindowSize = 64;
namespace audio_wifi_config
{
// Main function to receive WiFi credentials through audio signal
void ReceiveWifiCredentialsFromAudio(Application *app, WifiConfigurationAp *wifi_ap);
void ReceiveWifiCredentialsFromAudio(Application *app, WifiConfigurationAp *wifi_ap, Display *display,
size_t input_channels = 1);
/**
* Goertzel algorithm implementation for single frequency detection

View File

@@ -81,12 +81,12 @@ void Backlight::OnTransitionTimer() {
}
}
PwmBacklight::PwmBacklight(gpio_num_t pin, bool output_invert) : Backlight() {
PwmBacklight::PwmBacklight(gpio_num_t pin, bool output_invert, uint32_t freq_hz) : Backlight() {
const ledc_timer_config_t backlight_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_10_BIT,
.timer_num = LEDC_TIMER_0,
.freq_hz = 25000, //背光pwm频率需要高一点防止电感啸叫
.freq_hz = freq_hz, //背光pwm频率需要高一点防止电感啸叫
.clk_cfg = LEDC_AUTO_CLK,
.deconfigure = false
};

View File

@@ -29,7 +29,7 @@ protected:
class PwmBacklight : public Backlight {
public:
PwmBacklight(gpio_num_t pin, bool output_invert = false);
PwmBacklight(gpio_num_t pin, bool output_invert = false, uint32_t freq_hz = 25000);
~PwmBacklight();
void SetBrightnessImpl(uint8_t brightness) override;

View File

@@ -62,12 +62,26 @@ void PowerSaveTimer::PowerSaveCheck() {
ticks_++;
if (seconds_to_sleep_ != -1 && ticks_ >= seconds_to_sleep_) {
if (!in_sleep_mode_) {
ESP_LOGI(TAG, "Enabling power save mode");
in_sleep_mode_ = true;
if (on_enter_sleep_mode_) {
on_enter_sleep_mode_();
}
if (cpu_max_freq_ != -1) {
// Disable wake word detection
auto& audio_service = app.GetAudioService();
is_wake_word_running_ = audio_service.IsWakeWordRunning();
if (is_wake_word_running_) {
audio_service.EnableWakeWordDetection(false);
vTaskDelay(pdMS_TO_TICKS(100));
}
// Disable audio input
auto codec = Board::GetInstance().GetAudioCodec();
if (codec) {
codec->EnableInput(false);
}
esp_pm_config_t pm_config = {
.max_freq_mhz = cpu_max_freq_,
.min_freq_mhz = 40,
@@ -85,6 +99,7 @@ void PowerSaveTimer::PowerSaveCheck() {
void PowerSaveTimer::WakeUp() {
ticks_ = 0;
if (in_sleep_mode_) {
ESP_LOGI(TAG, "Exiting power save mode");
in_sleep_mode_ = false;
if (cpu_max_freq_ != -1) {
@@ -94,6 +109,13 @@ void PowerSaveTimer::WakeUp() {
.light_sleep_enable = false,
};
esp_pm_configure(&pm_config);
// Enable wake word detection
auto& app = Application::GetInstance();
auto& audio_service = app.GetAudioService();
if (is_wake_word_running_) {
audio_service.EnableWakeWordDetection(true);
}
}
if (on_exit_sleep_mode_) {

View File

@@ -22,6 +22,7 @@ private:
esp_timer_handle_t power_save_timer_ = nullptr;
bool enabled_ = false;
bool in_sleep_mode_ = false;
bool is_wake_word_running_ = false;
int ticks_ = 0;
int cpu_max_freq_;
int seconds_to_sleep_;

View File

@@ -70,6 +70,13 @@ void SleepTimer::CheckTimer() {
if (on_enter_light_sleep_mode_) {
on_enter_light_sleep_mode_();
}
auto& audio_service = app.GetAudioService();
bool is_wake_word_running = audio_service.IsWakeWordRunning();
if (is_wake_word_running) {
audio_service.EnableWakeWordDetection(false);
vTaskDelay(pdMS_TO_TICKS(100));
}
app.Schedule([this, &app]() {
while (in_light_sleep_mode_) {
@@ -86,12 +93,17 @@ void SleepTimer::CheckTimer() {
lvgl_port_resume();
auto wakeup_reason = esp_sleep_get_wakeup_cause();
ESP_LOGI(TAG, "Wake up from light sleep, wakeup_reason: %d", wakeup_reason);
if (wakeup_reason != ESP_SLEEP_WAKEUP_TIMER) {
break;
}
}
WakeUp();
});
if (is_wake_word_running) {
audio_service.EnableWakeWordDetection(true);
}
}
}
if (seconds_to_deep_sleep_ != -1 && ticks_ >= seconds_to_deep_sleep_) {

View File

@@ -52,7 +52,14 @@ void WifiBoard::EnterWifiConfigMode() {
application.Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "", Lang::Sounds::P3_WIFICONFIG);
#if CONFIG_USE_ACOUSTIC_WIFI_PROVISIONING
audio_wifi_config::ReceiveWifiCredentialsFromAudio(&application, &wifi_ap);
auto display = Board::GetInstance().GetDisplay();
auto codec = Board::GetInstance().GetAudioCodec();
int channel = 1;
if (codec) {
channel = codec->input_channels();
}
ESP_LOGI(TAG, "Start receiving WiFi credentials from audio, input channels: %d", channel);
audio_wifi_config::ReceiveWifiCredentialsFromAudio(&application, &wifi_ap, display, channel);
#endif
// Wait forever until reset after configuration

View File

@@ -47,14 +47,11 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -5,6 +5,7 @@
#include "button.h"
#include "config.h"
#include "backlight.h"
#include "emote_display.h"
#include <wifi_station.h>
#include <esp_log.h>
@@ -25,6 +26,8 @@
#define TAG "EchoEar"
#define USE_LVGL_DEFAULT 0
LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);
temperature_sensor_handle_t temp_sensor = NULL;
@@ -219,26 +222,34 @@ gpio_num_t AUDIO_I2S_GPIO_DIN = AUDIO_I2S_GPIO_DIN_1;
gpio_num_t AUDIO_CODEC_PA_PIN = AUDIO_CODEC_PA_PIN_1;
gpio_num_t QSPI_PIN_NUM_LCD_RST = QSPI_PIN_NUM_LCD_RST_1;
gpio_num_t TOUCH_PAD2 = TOUCH_PAD2_1;
gpio_num_t UART1_TX = UART1_TX_1;
gpio_num_t UART1_RX = UART1_RX_1;
gpio_num_t UART1_TX = UART1_TX_1;
gpio_num_t UART1_RX = UART1_RX_1;
class Charge : public I2cDevice {
public:
Charge(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
Charge(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr)
{
read_buffer_ = new uint8_t[8];
}
~Charge() {
~Charge()
{
delete[] read_buffer_;
}
void Printcharge() {
ReadRegs(0x08, read_buffer_, 2);
void Printcharge()
{
ReadRegs(0x08, read_buffer_, 2);
ReadRegs(0x0c, read_buffer_ + 2, 2);
ESP_ERROR_CHECK(temperature_sensor_get_celsius(temp_sensor, &tsens_value));
int16_t voltage = (uint16_t)(read_buffer_[1] << 8 | read_buffer_[0]);
int16_t current = (int16_t)(read_buffer_[3] << 8 | read_buffer_[2]);
}
static void TaskFunction(void *pvParameters) {
int16_t voltage = static_cast<uint16_t>(read_buffer_[1] << 8 | read_buffer_[0]);
int16_t current = static_cast<int16_t>(read_buffer_[3] << 8 | read_buffer_[2]);
// Use the variables to avoid warnings (can be removed if actual implementation uses them)
(void)voltage;
(void)current;
}
static void TaskFunction(void *pvParameters)
{
Charge* charge = static_cast<Charge*>(pvParameters);
while (true) {
charge->Printcharge();
@@ -250,7 +261,6 @@ private:
uint8_t* read_buffer_ = nullptr;
};
class Cst816s : public I2cDevice {
public:
struct TouchPoint_t {
@@ -258,32 +268,120 @@ public:
int x = -1;
int y = -1;
};
Cst816s(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
enum TouchEvent {
TOUCH_NONE,
TOUCH_PRESS,
TOUCH_RELEASE,
TOUCH_HOLD
};
Cst816s(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr)
{
read_buffer_ = new uint8_t[6];
was_touched_ = false;
press_count_ = 0;
// Create touch interrupt semaphore
touch_isr_mux_ = xSemaphoreCreateBinary();
if (touch_isr_mux_ == NULL) {
ESP_LOGE("EchoEar", "Failed to create touch semaphore");
}
}
~Cst816s() {
~Cst816s()
{
delete[] read_buffer_;
// Delete semaphore if it exists
if (touch_isr_mux_ != NULL) {
vSemaphoreDelete(touch_isr_mux_);
touch_isr_mux_ = NULL;
}
}
void UpdateTouchPoint() {
void UpdateTouchPoint()
{
ReadRegs(0x02, read_buffer_, 6);
tp_.num = read_buffer_[0] & 0x0F;
tp_.x = ((read_buffer_[1] & 0x0F) << 8) | read_buffer_[2];
tp_.y = ((read_buffer_[3] & 0x0F) << 8) | read_buffer_[4];
}
const TouchPoint_t& GetTouchPoint() {
const TouchPoint_t &GetTouchPoint()
{
return tp_;
}
TouchEvent CheckTouchEvent()
{
bool is_touched = (tp_.num > 0);
TouchEvent event = TOUCH_NONE;
if (is_touched && !was_touched_) {
// Press event (transition from not touched to touched)
press_count_++;
event = TOUCH_PRESS;
ESP_LOGI("EchoEar", "TOUCH PRESS - count: %d, x: %d, y: %d", press_count_, tp_.x, tp_.y);
} else if (!is_touched && was_touched_) {
// Release event (transition from touched to not touched)
event = TOUCH_RELEASE;
ESP_LOGI("EchoEar", "TOUCH RELEASE - total presses: %d", press_count_);
} else if (is_touched && was_touched_) {
// Continuous touch (hold)
event = TOUCH_HOLD;
ESP_LOGD("EchoEar", "TOUCH HOLD - x: %d, y: %d", tp_.x, tp_.y);
}
// Update previous state
was_touched_ = is_touched;
return event;
}
int GetPressCount() const
{
return press_count_;
}
void ResetPressCount()
{
press_count_ = 0;
}
// Semaphore management methods
SemaphoreHandle_t GetTouchSemaphore()
{
return touch_isr_mux_;
}
bool WaitForTouchEvent(TickType_t timeout = portMAX_DELAY)
{
if (touch_isr_mux_ != NULL) {
return xSemaphoreTake(touch_isr_mux_, timeout) == pdTRUE;
}
return false;
}
void NotifyTouchEvent()
{
if (touch_isr_mux_ != NULL) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(touch_isr_mux_, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
private:
uint8_t* read_buffer_ = nullptr;
TouchPoint_t tp_;
// Touch state tracking
bool was_touched_;
int press_count_;
// Touch interrupt semaphore
SemaphoreHandle_t touch_isr_mux_;
};
static SemaphoreHandle_t touch_isr_mux = NULL;
static bool touch_event_pending = false;
static int64_t touch_event_time = 0;
class EspS3Cat : public WifiBoard {
private:
@@ -291,13 +389,17 @@ private:
Cst816s* cst816s_;
Charge* charge_;
Button boot_button_;
#if USE_LVGL_DEFAULT
LcdDisplay* display_;
#else
anim::EmoteDisplay* display_ = nullptr;
#endif
PwmBacklight* backlight_ = nullptr;
esp_timer_handle_t touchpad_timer_;
esp_lcd_touch_handle_t tp; // LCD touch handle
void InitializeI2c() {
void InitializeI2c()
{
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = I2C_NUM_0,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
@@ -315,14 +417,15 @@ private:
temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(10, 50);
ESP_ERROR_CHECK(temperature_sensor_install(&temp_sensor_config, &temp_sensor));
ESP_ERROR_CHECK(temperature_sensor_enable(temp_sensor));
}
uint8_t DetectPcbVersion() {
esp_err_t ret = i2c_master_probe(i2c_bus_, 0x18, 100);
uint8_t DetectPcbVersion()
{
esp_err_t ret = i2c_master_probe(i2c_bus_, 0x18, 100);
uint8_t pcb_verison = 0;
if (ret == ESP_OK) {
ESP_LOGI(TAG, "PCB verison V1.0");
pcb_verison = 0;
pcb_verison = 0;
} else {
gpio_config_t gpio_conf = {
.pin_bit_mask = (1ULL << GPIO_NUM_48),
@@ -334,10 +437,10 @@ private:
ESP_ERROR_CHECK(gpio_config(&gpio_conf));
ESP_ERROR_CHECK(gpio_set_level(GPIO_NUM_48, 1));
vTaskDelay(pdMS_TO_TICKS(100));
ret = i2c_master_probe(i2c_bus_, 0x18, 100);
ret = i2c_master_probe(i2c_bus_, 0x18, 100);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "PCB verison V1.2");
pcb_verison = 1;
pcb_verison = 1;
AUDIO_I2S_GPIO_DIN = AUDIO_I2S_GPIO_DIN_2;
AUDIO_CODEC_PA_PIN = AUDIO_CODEC_PA_PIN_2;
QSPI_PIN_NUM_LCD_RST = QSPI_PIN_NUM_LCD_RST_2;
@@ -346,141 +449,87 @@ private:
UART1_RX = UART1_RX_2;
} else {
ESP_LOGE(TAG, "PCB version detection error");
}
}
}
return pcb_verison;
}
static void touchpad_timer_callback(void* arg) {
auto& board = (EspS3Cat&)Board::GetInstance();
auto touchpad = board.GetTouchpad();
static bool was_touched = false;
static int64_t touch_start_time = 0;
const int64_t TOUCH_THRESHOLD_MS = 500;
touchpad->UpdateTouchPoint();
auto touch_point = touchpad->GetTouchPoint();
if (touch_point.num > 0 && !was_touched) {
was_touched = true;
touch_start_time = esp_timer_get_time() / 1000;
}
else if (touch_point.num == 0 && was_touched) {
was_touched = false;
int64_t touch_duration = (esp_timer_get_time() / 1000) - touch_start_time;
if (touch_duration < TOUCH_THRESHOLD_MS) {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting &&
!WifiStation::GetInstance().IsConnected()) {
board.ResetWifiConfiguration();
}
app.ToggleChatState();
}
}
}
static void touchpad_callback(Cst816s::TouchPoint_t touch_point) {
auto& board = (EspS3Cat&)Board::GetInstance();
static bool was_touched = false;
static int64_t touch_start_time = 0;
const int64_t TOUCH_THRESHOLD_MS = 500;
if (touch_point.num > 0 && !was_touched) {
was_touched = true;
touch_start_time = esp_timer_get_time() / 1000;
}
else if (touch_point.num == 0 && was_touched) {
was_touched = false;
int64_t touch_duration = (esp_timer_get_time() / 1000) - touch_start_time;
if (touch_duration < TOUCH_THRESHOLD_MS) {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting &&
!WifiStation::GetInstance().IsConnected()) {
board.ResetWifiConfiguration();
}
app.ToggleChatState();
}
}
}
static void lvgl_port_touch_isr_cb(void* arg)
static void touch_isr_callback(void* arg)
{
int64_t current_time = esp_timer_get_time() / 1000;
static int64_t last_touch_time = 0;
if (current_time - last_touch_time >= 300) {
touch_event_pending = true;
touch_event_time = current_time;
last_touch_time = current_time;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (touch_isr_mux != NULL) {
xSemaphoreGiveFromISR(touch_isr_mux, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
Cst816s* touchpad = static_cast<Cst816s*>(arg);
if (touchpad != nullptr) {
touchpad->NotifyTouchEvent();
}
}
static void touch_event_task(void* arg) {
static void touch_event_task(void* arg)
{
Cst816s* touchpad = static_cast<Cst816s*>(arg);
if (touchpad == nullptr) {
ESP_LOGE(TAG, "Invalid touchpad pointer in touch_event_task");
vTaskDelete(NULL);
return;
}
while (true) {
if (xSemaphoreTake(touch_isr_mux, portMAX_DELAY) == pdTRUE) {
if (touch_event_pending) {
touch_event_pending = false;
if (touchpad->WaitForTouchEvent()) {
auto &app = Application::GetInstance();
auto &board = (EspS3Cat &)Board::GetInstance();
auto& board = (EspS3Cat&)Board::GetInstance();
auto& app = Application::GetInstance();
ESP_LOGI(TAG, "Touch event, TP_PIN_NUM_INT: %d", gpio_get_level(TP_PIN_NUM_INT));
touchpad->UpdateTouchPoint();
auto touch_event = touchpad->CheckTouchEvent();
if (app.GetDeviceState() == kDeviceStateStarting &&
!WifiStation::GetInstance().IsConnected()) {
if (touch_event == Cst816s::TOUCH_RELEASE) {
if (app.GetDeviceState() == kDeviceStateStarting &&
!WifiStation::GetInstance().IsConnected()) {
board.ResetWifiConfiguration();
} else {
app.ToggleChatState();
}
app.ToggleChatState();
}
}
}
}
void InitializeCharge() {
void InitializeCharge()
{
charge_ = new Charge(i2c_bus_, 0x55);
xTaskCreatePinnedToCore(Charge::TaskFunction, "batterydecTask", 3 * 1024, charge_, 6, NULL, 0);
}
void InitializeCst816sTouchPad() {
void InitializeCst816sTouchPad()
{
cst816s_ = new Cst816s(i2c_bus_, 0x15);
touch_isr_mux = xSemaphoreCreateBinary();
if (touch_isr_mux == NULL) {
ESP_LOGE(TAG, "Failed to create touch semaphore");
return;
}
xTaskCreatePinnedToCore(touch_event_task, "touch_task", 4 * 1024, cst816s_, 5, NULL, 1);
xTaskCreatePinnedToCore(touch_event_task, "touch_task", 4 * 1024, NULL, 5, NULL, 1);
const gpio_config_t int_gpio_config = {
.pin_bit_mask = (1ULL << TP_PIN_NUM_INT),
.mode = GPIO_MODE_INPUT,
.intr_type = GPIO_INTR_NEGEDGE
// .intr_type = GPIO_INTR_NEGEDGE
.intr_type = GPIO_INTR_ANYEDGE
};
gpio_config(&int_gpio_config);
gpio_install_isr_service(0);
gpio_intr_enable(TP_PIN_NUM_INT);
gpio_isr_handler_add(TP_PIN_NUM_INT, EspS3Cat::lvgl_port_touch_isr_cb, NULL);
gpio_isr_handler_add(TP_PIN_NUM_INT, EspS3Cat::touch_isr_callback, cst816s_);
}
void InitializeSpi() {
void InitializeSpi()
{
const spi_bus_config_t bus_config = TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(QSPI_PIN_NUM_LCD_PCLK,
QSPI_PIN_NUM_LCD_DATA0,
QSPI_PIN_NUM_LCD_DATA1,
QSPI_PIN_NUM_LCD_DATA2,
QSPI_PIN_NUM_LCD_DATA3,
QSPI_LCD_H_RES * 80 * sizeof(uint16_t));
QSPI_PIN_NUM_LCD_DATA0,
QSPI_PIN_NUM_LCD_DATA1,
QSPI_PIN_NUM_LCD_DATA2,
QSPI_PIN_NUM_LCD_DATA3,
QSPI_LCD_H_RES * 80 * sizeof(uint16_t));
ESP_ERROR_CHECK(spi_bus_initialize(QSPI_LCD_HOST, &bus_config, SPI_DMA_CH_AUTO));
}
void Initializest77916Display(uint8_t pcb_verison) {
void Initializest77916Display(uint8_t pcb_verison)
{
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
@@ -511,73 +560,83 @@ private:
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
#if USE_LVGL_DEFAULT
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,
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = font_emoji_64_init(),
});
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY, {
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = font_emoji_64_init(),
});
#else
display_ = new anim::EmoteDisplay(panel, panel_io);
#endif
backlight_ = new PwmBacklight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
backlight_->RestoreBrightness();
}
void InitializeButtons() {
void InitializeButtons()
{
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
auto &app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ESP_LOGI(TAG, "Boot button pressed, enter WiFi configuration mode");
ResetWifiConfiguration();
}
app.ToggleChatState();
});
gpio_config_t power_gpio_config = {
.pin_bit_mask = (BIT64(POWER_CTRL) ),
gpio_config_t power_gpio_config = {
.pin_bit_mask = (BIT64(POWER_CTRL)),
.mode = GPIO_MODE_OUTPUT,
};
};
ESP_ERROR_CHECK(gpio_config(&power_gpio_config));
gpio_set_level(POWER_CTRL, 0);
}
public:
EspS3Cat() : boot_button_(BOOT_BUTTON_GPIO) {
EspS3Cat() : boot_button_(BOOT_BUTTON_GPIO)
{
InitializeI2c();
uint8_t pcb_verison = DetectPcbVersion();
InitializeCharge();
InitializeCst816sTouchPad();
InitializeSpi();
Initializest77916Display(pcb_verison);
InitializeButtons();
}
virtual AudioCodec* GetAudioCodec() override {
virtual AudioCodec* GetAudioCodec() override
{
static BoxAudioCodec audio_codec(
i2c_bus_,
AUDIO_INPUT_SAMPLE_RATE,
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_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_ES7210_ADDR,
AUDIO_CODEC_PA_PIN,
AUDIO_CODEC_ES8311_ADDR,
AUDIO_CODEC_ES7210_ADDR,
AUDIO_INPUT_REFERENCE);
return &audio_codec;
}
virtual Display* GetDisplay() override {
virtual Display* GetDisplay() override
{
return display_;
}
Cst816s* GetTouchpad() {
Cst816s* GetTouchpad()
{
return cst816s_;
}
virtual Backlight* GetBacklight() override {
virtual Backlight* GetBacklight() override
{
return backlight_;
}
};

View File

@@ -24,8 +24,43 @@ idf.py menuconfig
分别配置如下选项:
### 基本配置
- `Xiaozhi Assistant``Board Type` → 选择 `EchoEar`
### 分区表配置
- `Partition Table``Partition Table` → 选择 `Custom partition table CSV`
- `Partition Table``Custom partition CSV file` → 输入 `partitions/v1/16m_echoear.csv`
### UI风格选择
EchoEar 支持两种不同的UI显示风格通过修改代码中的宏定义来选择
#### 自定义表情显示系统 (推荐)
```c
#define USE_LVGL_DEFAULT 0
```
- **特点**: 使用自定义的 `EmoteDisplay` 表情显示系统
- **功能**: 支持丰富的表情动画、眼睛动画、状态图标显示
- **适用**: 智能助手场景,提供更生动的人机交互体验
- **类**: `anim::EmoteDisplay` + `anim::EmoteEngine`
#### LVGL默认显示系统
```c
#define USE_LVGL_DEFAULT 1
```
- **特点**: 使用标准LVGL图形库的显示系统
- **功能**: 传统的文本和图标显示界面
- **适用**: 需要标准GUI控件的应用场景
- **类**: `SpiLcdDisplay`
#### 如何修改
1. 打开 `main/boards/echoear/EchoEar.cc` 文件
2. 找到第29行的宏定义`#define USE_LVGL_DEFAULT 0`
3. 修改为想要的值0或1
4. 重新编译项目
> **说明**: EchoEar 使用16MB Flash需要使用专门的分区表配置来合理分配存储空间给应用程序、OTA更新、资源文件等。
`S` 保存,按 `Q` 退出。
**编译**

View File

@@ -0,0 +1,419 @@
#include "emote_display.h"
#include <cstring>
#include <memory>
#include <unordered_map>
#include <tuple>
#include <esp_log.h>
#include <esp_lcd_panel_io.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <sys/time.h>
#include <time.h>
#include "display/lcd_display.h"
#include "mmap_generate_emoji_normal.h"
#include "config.h"
#include "gfx.h"
namespace anim {
static const char* TAG = "emoji";
// UI element management
static gfx_obj_t* obj_label_tips = nullptr;
static gfx_obj_t* obj_label_time = nullptr;
static gfx_obj_t* obj_anim_eye = nullptr;
static gfx_obj_t* obj_anim_mic = nullptr;
static gfx_obj_t* obj_img_icon = nullptr;
static gfx_image_dsc_t icon_img_dsc;
// Track current icon to determine when to show time
static int current_icon_type = MMAP_EMOJI_NORMAL_ICON_BATTERY_BIN;
enum class UIDisplayMode : uint8_t {
SHOW_ANIM_TOP = 1, // Show obj_anim_mic
SHOW_TIME = 2, // Show obj_label_time
SHOW_TIPS = 3 // Show obj_label_tips
};
static void SetUIDisplayMode(UIDisplayMode mode)
{
gfx_obj_set_visible(obj_anim_mic, false);
gfx_obj_set_visible(obj_label_time, false);
gfx_obj_set_visible(obj_label_tips, false);
// Show the selected control
switch (mode) {
case UIDisplayMode::SHOW_ANIM_TOP:
gfx_obj_set_visible(obj_anim_mic, true);
break;
case UIDisplayMode::SHOW_TIME:
gfx_obj_set_visible(obj_label_time, true);
break;
case UIDisplayMode::SHOW_TIPS:
gfx_obj_set_visible(obj_label_tips, true);
break;
}
}
static void clock_tm_callback(void* user_data)
{
// Only display time when battery icon is shown
if (current_icon_type == MMAP_EMOJI_NORMAL_ICON_BATTERY_BIN) {
time_t now;
struct tm timeinfo;
time(&now);
setenv("TZ", "GMT+0", 1);
tzset();
localtime_r(&now, &timeinfo);
char time_str[6];
snprintf(time_str, sizeof(time_str), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
gfx_label_set_text(obj_label_time, time_str);
SetUIDisplayMode(UIDisplayMode::SHOW_TIME);
}
}
static void InitializeAssets(mmap_assets_handle_t* assets_handle)
{
const mmap_assets_config_t assets_cfg = {
.partition_label = "assets_A",
.max_files = MMAP_EMOJI_NORMAL_FILES,
.checksum = MMAP_EMOJI_NORMAL_CHECKSUM,
.flags = {.mmap_enable = true, .full_check = true}
};
mmap_assets_new(&assets_cfg, assets_handle);
}
static void InitializeGraphics(esp_lcd_panel_handle_t panel, gfx_handle_t* engine_handle)
{
gfx_core_config_t gfx_cfg = {
.flush_cb = EmoteEngine::OnFlush,
.user_data = panel,
.flags = {
.swap = true,
.double_buffer = true,
.buff_dma = true,
},
.h_res = DISPLAY_WIDTH,
.v_res = DISPLAY_HEIGHT,
.fps = 30,
.buffers = {
.buf1 = nullptr,
.buf2 = nullptr,
.buf_pixels = DISPLAY_WIDTH * 16,
},
.task = GFX_EMOTE_INIT_CONFIG()
};
gfx_cfg.task.task_stack_caps = MALLOC_CAP_DEFAULT;
gfx_cfg.task.task_affinity = 0;
gfx_cfg.task.task_priority = 5;
gfx_cfg.task.task_stack = 20 * 1024;
*engine_handle = gfx_emote_init(&gfx_cfg);
}
static void InitializeEyeAnimation(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle)
{
obj_anim_eye = gfx_anim_create(engine_handle);
const void* anim_data = mmap_assets_get_mem(assets_handle, MMAP_EMOJI_NORMAL_IDLE_ONE_AAF);
size_t anim_size = mmap_assets_get_size(assets_handle, MMAP_EMOJI_NORMAL_IDLE_ONE_AAF);
gfx_anim_set_src(obj_anim_eye, anim_data, anim_size);
gfx_obj_align(obj_anim_eye, GFX_ALIGN_LEFT_MID, 10, -20);
gfx_anim_set_mirror(obj_anim_eye, true, (DISPLAY_WIDTH - (173 + 10) * 2));
gfx_anim_set_segment(obj_anim_eye, 0, 0xFFFF, 20, false);
gfx_anim_start(obj_anim_eye);
}
static void InitializeFont(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle)
{
gfx_font_t font;
gfx_label_cfg_t font_cfg = {
.name = "DejaVuSans.ttf",
.mem = mmap_assets_get_mem(assets_handle, MMAP_EMOJI_NORMAL_KAITI_TTF),
.mem_size = static_cast<size_t>(mmap_assets_get_size(assets_handle, MMAP_EMOJI_NORMAL_KAITI_TTF)),
};
gfx_label_new_font(engine_handle, &font_cfg, &font);
ESP_LOGI(TAG, "stack: %d", uxTaskGetStackHighWaterMark(nullptr));
}
static void InitializeLabels(gfx_handle_t engine_handle)
{
// Initialize tips label
obj_label_tips = gfx_label_create(engine_handle);
gfx_obj_align(obj_label_tips, GFX_ALIGN_TOP_MID, 0, 45);
gfx_obj_set_size(obj_label_tips, 160, 40);
gfx_label_set_text(obj_label_tips, "启动中...");
gfx_label_set_font_size(obj_label_tips, 20);
gfx_label_set_color(obj_label_tips, GFX_COLOR_HEX(0xFFFFFF));
gfx_label_set_text_align(obj_label_tips, GFX_TEXT_ALIGN_LEFT);
gfx_label_set_long_mode(obj_label_tips, GFX_LABEL_LONG_SCROLL);
gfx_label_set_scroll_speed(obj_label_tips, 20);
gfx_label_set_scroll_loop(obj_label_tips, true);
// Initialize time label
obj_label_time = gfx_label_create(engine_handle);
gfx_obj_align(obj_label_time, GFX_ALIGN_TOP_MID, 0, 30);
gfx_obj_set_size(obj_label_time, 160, 50);
gfx_label_set_text(obj_label_time, "--:--");
gfx_label_set_font_size(obj_label_time, 40);
gfx_label_set_color(obj_label_time, GFX_COLOR_HEX(0xFFFFFF));
gfx_label_set_text_align(obj_label_time, GFX_TEXT_ALIGN_CENTER);
}
static void InitializeMicAnimation(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle)
{
obj_anim_mic = gfx_anim_create(engine_handle);
gfx_obj_align(obj_anim_mic, GFX_ALIGN_TOP_MID, 0, 25);
const void* anim_data = mmap_assets_get_mem(assets_handle, MMAP_EMOJI_NORMAL_LISTEN_AAF);
size_t anim_size = mmap_assets_get_size(assets_handle, MMAP_EMOJI_NORMAL_LISTEN_AAF);
gfx_anim_set_src(obj_anim_mic, anim_data, anim_size);
gfx_anim_start(obj_anim_mic);
gfx_obj_set_visible(obj_anim_mic, false);
}
static void InitializeIcon(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle)
{
obj_img_icon = gfx_img_create(engine_handle);
gfx_obj_align(obj_img_icon, GFX_ALIGN_TOP_MID, -100, 38);
SetupImageDescriptor(assets_handle, &icon_img_dsc, MMAP_EMOJI_NORMAL_ICON_WIFI_FAILED_BIN);
gfx_img_set_src(obj_img_icon, static_cast<void*>(&icon_img_dsc));
}
static void RegisterCallbacks(esp_lcd_panel_io_handle_t panel_io, gfx_handle_t engine_handle)
{
const esp_lcd_panel_io_callbacks_t cbs = {
.on_color_trans_done = EmoteEngine::OnFlushIoReady,
};
esp_lcd_panel_io_register_event_callbacks(panel_io, &cbs, engine_handle);
}
void SetupImageDescriptor(mmap_assets_handle_t assets_handle,
gfx_image_dsc_t* img_dsc,
int asset_id)
{
const void* img_data = mmap_assets_get_mem(assets_handle, asset_id);
size_t img_size = mmap_assets_get_size(assets_handle, asset_id);
std::memcpy(&img_dsc->header, img_data, sizeof(gfx_image_header_t));
img_dsc->data = static_cast<const uint8_t*>(img_data) + sizeof(gfx_image_header_t);
img_dsc->data_size = img_size - sizeof(gfx_image_header_t);
}
EmoteEngine::EmoteEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io)
{
ESP_LOGI(TAG, "Create EmoteEngine, panel: %p, panel_io: %p", panel, panel_io);
InitializeAssets(&assets_handle_);
InitializeGraphics(panel, &engine_handle_);
gfx_emote_lock(engine_handle_);
gfx_emote_set_bg_color(engine_handle_, GFX_COLOR_HEX(0x000000));
// Initialize all UI components
InitializeEyeAnimation(engine_handle_, assets_handle_);
InitializeFont(engine_handle_, assets_handle_);
InitializeLabels(engine_handle_);
InitializeMicAnimation(engine_handle_, assets_handle_);
InitializeIcon(engine_handle_, assets_handle_);
current_icon_type = MMAP_EMOJI_NORMAL_ICON_WIFI_FAILED_BIN;
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
gfx_timer_create(engine_handle_, clock_tm_callback, 1000, obj_label_tips);
gfx_emote_unlock(engine_handle_);
RegisterCallbacks(panel_io, engine_handle_);
}
EmoteEngine::~EmoteEngine()
{
if (engine_handle_) {
gfx_emote_deinit(engine_handle_);
engine_handle_ = nullptr;
}
if (assets_handle_) {
mmap_assets_del(assets_handle_);
assets_handle_ = nullptr;
}
}
void EmoteEngine::setEyes(int aaf, bool repeat, int fps)
{
if (!engine_handle_) {
return;
}
const void* src_data = mmap_assets_get_mem(assets_handle_, aaf);
size_t src_len = mmap_assets_get_size(assets_handle_, aaf);
Lock();
gfx_anim_set_src(obj_anim_eye, src_data, src_len);
gfx_anim_set_segment(obj_anim_eye, 0, 0xFFFF, fps, repeat);
gfx_anim_start(obj_anim_eye);
Unlock();
}
void EmoteEngine::stopEyes()
{
// Implementation if needed
}
void EmoteEngine::Lock()
{
if (engine_handle_) {
gfx_emote_lock(engine_handle_);
}
}
void EmoteEngine::Unlock()
{
if (engine_handle_) {
gfx_emote_unlock(engine_handle_);
}
}
void EmoteEngine::SetIcon(int asset_id)
{
if (!engine_handle_) {
return;
}
Lock();
SetupImageDescriptor(assets_handle_, &icon_img_dsc, asset_id);
gfx_img_set_src(obj_img_icon, static_cast<void*>(&icon_img_dsc));
current_icon_type = asset_id;
Unlock();
}
bool EmoteEngine::OnFlushIoReady(esp_lcd_panel_io_handle_t panel_io,
esp_lcd_panel_io_event_data_t* edata,
void* user_ctx)
{
return true;
}
void EmoteEngine::OnFlush(gfx_handle_t handle, int x_start, int y_start,
int x_end, int y_end, const void* color_data)
{
auto* panel = static_cast<esp_lcd_panel_handle_t>(gfx_emote_get_user_data(handle));
if (panel) {
esp_lcd_panel_draw_bitmap(panel, x_start, y_start, x_end, y_end, color_data);
}
gfx_emote_flush_ready(handle, true);
}
// EmoteDisplay implementation
EmoteDisplay::EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io)
{
InitializeEngine(panel, panel_io);
}
EmoteDisplay::~EmoteDisplay() = default;
void EmoteDisplay::SetEmotion(const char* emotion)
{
if (!engine_) {
return;
}
using EmotionParam = std::tuple<int, bool, int>;
static const std::unordered_map<std::string, EmotionParam> emotion_map = {
{"happy", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"laughing", {MMAP_EMOJI_NORMAL_ENJOY_ONE_AAF, true, 20}},
{"funny", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"loving", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"embarrassed", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"confident", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"delicious", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"sad", {MMAP_EMOJI_NORMAL_SAD_ONE_AAF, true, 20}},
{"crying", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"sleepy", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"silly", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"angry", {MMAP_EMOJI_NORMAL_ANGRY_ONE_AAF, true, 20}},
{"surprised", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"shocked", {MMAP_EMOJI_NORMAL_SHOCKED_ONE_AAF, true, 20}},
{"thinking", {MMAP_EMOJI_NORMAL_THINKING_ONE_AAF, true, 20}},
{"winking", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"relaxed", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"confused", {MMAP_EMOJI_NORMAL_DIZZY_ONE_AAF, true, 20}},
{"neutral", {MMAP_EMOJI_NORMAL_IDLE_ONE_AAF, false, 20}},
{"idle", {MMAP_EMOJI_NORMAL_IDLE_ONE_AAF, false, 20}},
};
auto it = emotion_map.find(emotion);
if (it != emotion_map.end()) {
int aaf = std::get<0>(it->second);
bool repeat = std::get<1>(it->second);
int fps = std::get<2>(it->second);
engine_->setEyes(aaf, repeat, fps);
}
}
void EmoteDisplay::SetChatMessage(const char* role, const char* content)
{
engine_->Lock();
if (content && strlen(content) > 0) {
gfx_label_set_text(obj_label_tips, content);
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
}
engine_->Unlock();
}
void EmoteDisplay::SetStatus(const char* status)
{
if (!engine_) {
return;
}
if (std::strcmp(status, "聆听中...") == 0) {
SetUIDisplayMode(UIDisplayMode::SHOW_ANIM_TOP);
engine_->setEyes(MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20);
engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_MIC_BIN);
} else if (std::strcmp(status, "待命") == 0) {
SetUIDisplayMode(UIDisplayMode::SHOW_TIME);
engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_BATTERY_BIN);
} else if (std::strcmp(status, "说话中...") == 0) {
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_SPEAKER_ZZZ_BIN);
} else if (std::strcmp(status, "错误") == 0) {
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_WIFI_FAILED_BIN);
}
engine_->Lock();
if (std::strcmp(status, "连接中...") != 0) {
gfx_label_set_text(obj_label_tips, status);
}
engine_->Unlock();
}
void EmoteDisplay::InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io)
{
engine_ = std::make_unique<EmoteEngine>(panel, panel_io);
}
bool EmoteDisplay::Lock(int timeout_ms)
{
return true;
}
void EmoteDisplay::Unlock()
{
// Implementation if needed
}
} // namespace anim

View File

@@ -0,0 +1,66 @@
#pragma once
#include "display/lcd_display.h"
#include <memory>
#include <functional>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include "mmap_generate_emoji_normal.h"
#include "gfx.h"
namespace anim {
// Helper function for setting up image descriptors
void SetupImageDescriptor(mmap_assets_handle_t assets_handle, gfx_image_dsc_t* img_dsc, int asset_id);
class EmoteEngine;
using FlushIoReadyCallback = std::function<bool(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void*)>;
using FlushCallback = std::function<void(gfx_handle_t, int, int, int, int, const void*)>;
class EmoteEngine {
public:
EmoteEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io);
~EmoteEngine();
void setEyes(int aaf, bool repeat, int fps);
void stopEyes();
void Lock();
void Unlock();
void SetIcon(int asset_id);
mmap_assets_handle_t GetAssetsHandle() const { return assets_handle_; }
// Callback functions (public to be accessible from static helper functions)
static bool OnFlushIoReady(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx);
static void OnFlush(gfx_handle_t handle, int x_start, int y_start, int x_end, int y_end, const void *color_data);
private:
gfx_handle_t engine_handle_;
mmap_assets_handle_t assets_handle_;
};
class EmoteDisplay : public Display {
public:
EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io);
virtual ~EmoteDisplay();
virtual void SetEmotion(const char* emotion) override;
virtual void SetStatus(const char* status) override;
virtual void SetChatMessage(const char* role, const char* content) override;
anim::EmoteEngine* GetEngine()
{
return engine_.get();
}
private:
void InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io);
virtual bool Lock(int timeout_ms = 0) override;
virtual void Unlock() override;
std::unique_ptr<anim::EmoteEngine> engine_;
};
} // namespace anim

View File

@@ -141,7 +141,8 @@ void AdcPdmAudioCodec::EnableInput(bool enable) {
};
ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
} else {
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
// ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
return;
}
AudioCodec::EnableInput(enable);
}

View File

@@ -69,14 +69,11 @@ void InitializePowerManager() {
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});

View File

@@ -135,16 +135,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(20);
});
power_save_timer_->OnExitSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -120,16 +120,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(20);
});
power_save_timer_->OnExitSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -69,23 +69,13 @@ private:
}
void InitializePowerSaveTimer() {
// 第一个参数不为 -1 时,进入睡眠会关闭音频输入
power_save_timer_ = new PowerSaveTimer(240, 60);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
auto codec = GetAudioCodec();
codec->EnableInput(false);
GetDisplay()->SetPowerSaveMode(true);
});
power_save_timer_->OnExitSleepMode([this]() {
auto codec = GetAudioCodec();
codec->EnableInput(true);
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
});
power_save_timer_->SetEnabled(true);
}

View File

@@ -22,12 +22,35 @@
#include <driver/rtc_io.h>
#include <esp_sleep.h>
#define TAG "JiuchuanDevBoard"
#define BOARD_TAG "JiuchuanDevBoard"
#define __USER_GPIO_PWRDOWN__
LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);
// 自定义LCD显示器类用于圆形屏幕适配
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,
DisplayFonts fonts)
: SpiLcdDisplay(io_handle, panel_handle, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy, fonts)
{
DisplayLockGuard lock(this);
lv_obj_set_style_pad_left(status_bar_, LV_HOR_RES * 0.167, 0);
lv_obj_set_style_pad_right(status_bar_, LV_HOR_RES * 0.167, 0);
}
};
class JiuchuanDevBoard : public WifiBoard {
private:
i2c_master_bus_handle_t codec_i2c_bus_;
@@ -40,6 +63,17 @@ private:
PowerManager* power_manager_;
esp_lcd_panel_io_handle_t panel_io = NULL;
esp_lcd_panel_handle_t panel = NULL;
// 音量映射函数:将内部音量(0-80)映射为显示音量(0-100%)
int MapVolumeForDisplay(int internal_volume) {
// 确保输入在有效范围内
if (internal_volume < 0) internal_volume = 0;
if (internal_volume > 80) internal_volume = 80;
// 将0-80映射到0-100
// 公式: 显示音量 = (内部音量 / 80) * 100
return (internal_volume * 100) / 80;
}
void InitializePowerManager() {
power_manager_ = new PowerManager(PWR_ADC_GPIO);
@@ -81,17 +115,14 @@ private:
}
#endif
//一分钟进入浅睡眠5分钟进入深睡眠关机
power_save_timer_ = new PowerSaveTimer(-1, (60*10), (60*30));
power_save_timer_ = new PowerSaveTimer(-1, (60*5), -1);
// power_save_timer_ = new PowerSaveTimer(-1, 6, 10);//test
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {
@@ -131,6 +162,11 @@ private:
}
void InitializeButtons() {
static bool pwrbutton_unreleased = false;
if (gpio_get_level(GPIO_NUM_3) == 1) {
pwrbutton_unreleased = true;
}
// 配置GPIO
ESP_LOGI(TAG, "Configuring power button GPIO");
GpioManager::Config(GPIO_NUM_3, GpioManager::GpioMode::INPUT_PULLDOWN);
@@ -142,10 +178,19 @@ private:
// 检查电源按钮初始状态
ESP_LOGI(TAG, "Power button initial state: %d", GpioManager::GetLevel(PWR_BUTTON_GPIO));
// 高电平有效长按关机逻辑
pwr_button_.OnLongPress([this]() {
ESP_LOGI(TAG, "Power button long press detected (high-active)");
pwr_button_.OnPressDown([this]() {
pwrbutton_unreleased = false;
});
pwr_button_.OnLongPress([this]()
{
ESP_LOGI(TAG, "Power button long press detected (high-active)");
if (pwrbutton_unreleased){
ESP_LOGI(TAG, "开机后电源键未松开,取消关机");
return;
}
// 高电平有效防抖确认
for (int i = 0; i < 5; i++) {
@@ -160,75 +205,126 @@ private:
}
ESP_LOGI(TAG, "Confirmed power button pressed - initiating shutdown");
power_manager_->SetPowerState(PowerState::SHUTDOWN);
});
power_manager_->SetPowerState(PowerState::SHUTDOWN); });
wifi_button.OnClick([this]() {
ESP_LOGI(TAG, "Wifi button clicked");
power_save_timer_->WakeUp();
//单击切换状态
pwr_button_.OnClick([this]()
{
// 获取当前应用实例和状态
auto &app = Application::GetInstance();
auto current_state = app.GetDeviceState();
ESP_LOGI(TAG, "当前设备状态: %d", current_state);
if (current_state == kDeviceStateIdle) {
// 如果当前是待命状态,切换到聆听状态
ESP_LOGI(TAG, "从待命状态切换到聆听状态");
app.ToggleChatState(); // 切换到聆听状态
} else if (current_state == kDeviceStateListening) {
// 如果当前是聆听状态,切换到待命状态
ESP_LOGI(TAG, "从聆听状态切换到待命状态");
app.ToggleChatState(); // 切换到待命状态
} else if (current_state == kDeviceStateSpeaking) {
// 如果当前是说话状态,终止说话并切换到待命状态
ESP_LOGI(TAG, "从说话状态切换到待命状态");
app.ToggleChatState(); // 终止说话
} else {
// 其他状态下只唤醒设备
ESP_LOGI(TAG, "唤醒设备");
power_save_timer_->WakeUp();
} });
ESP_LOGI(TAG, "Resetting WiFi configuration");
GpioManager::SetLevel(PWR_EN_GPIO, 1);
ResetWifiConfiguration();
});
cmd_button.OnClick([this]() {
ESP_LOGI(TAG, "Command button clicked");
// 电源键三击重置WiFi
pwr_button_.OnMultipleClick([this]()
{
ESP_LOGI(TAG, "Power button triple click: 重置WiFi");
power_save_timer_->WakeUp();
Application::GetInstance().ToggleChatState();
});
ResetWifiConfiguration(); }, 3);
wifi_button.OnPressDown([this]()
{
ESP_LOGI(TAG, "Volume up button pressed");
power_save_timer_->WakeUp();
auto codec = GetAudioCodec();
int current_vol = codec->output_volume(); // 获取实际当前音量
current_vol = (current_vol + 8 > 80) ? 80 : current_vol + 8;
codec->SetOutputVolume(current_vol);
ESP_LOGI(TAG, "Current volume: %d", current_vol);
int display_volume = MapVolumeForDisplay(current_vol);
GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(display_volume) + "%");});
cmd_button.OnPressDown([this]()
{
ESP_LOGI(TAG, "Volume down button pressed");
power_save_timer_->WakeUp();
auto codec = GetAudioCodec();
int current_vol = codec->output_volume(); // 获取实际当前音量
current_vol = (current_vol - 8 < 0) ? 0 : current_vol - 8;
codec->SetOutputVolume(current_vol);
ESP_LOGI(TAG, "Current volume: %d", current_vol);
if (current_vol == 0) {
GetDisplay()->ShowNotification(Lang::Strings::MUTED);
} else {
int display_volume = MapVolumeForDisplay(current_vol);
GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(display_volume) + "%");
}});
}
void InitializeGC9301isplay() {
// 液晶屏控制IO初始化
ESP_LOGI(TAG, "test Install panel IO");
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN;
buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN;
buscfg.miso_io_num = GPIO_NUM_NC;
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));
// 初始化SPI总线
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN;
io_config.dc_gpio_num = DISPLAY_DC_PIN;
io_config.spi_mode = 3;
io_config.pclk_hz = 80 * 1000 * 1000;
io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io);
void InitializeGC9301isplay()
{
// 液晶屏控制IO初始化
ESP_LOGI(TAG, "test Install panel IO");
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN;
buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN;
buscfg.miso_io_num = GPIO_NUM_NC;
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));
// 初始化液晶屏驱动芯片9309
ESP_LOGI(TAG, "Install LCD driver");
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_NC;
panel_config.rgb_ele_order = LCD_RGB_ENDIAN_BGR;
panel_config.bits_per_pixel = 16;
esp_lcd_new_panel_gc9309na(panel_io, &panel_config, &panel);
// 初始化SPI总线
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN;
io_config.dc_gpio_num = DISPLAY_DC_PIN;
io_config.spi_mode = 3;
io_config.pclk_hz = 80 * 1000 * 1000;
io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io);
esp_lcd_panel_reset(panel);
// 初始化液晶屏驱动芯片9309
ESP_LOGI(TAG, "Install LCD driver");
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_NC;
panel_config.rgb_ele_order = LCD_RGB_ENDIAN_BGR;
panel_config.bits_per_pixel = 16;
esp_lcd_new_panel_gc9309na(panel_io, &panel_config, &panel);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, false);
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,
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, false);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
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,
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
#if CONFIG_USE_WECHAT_MESSAGE_STYLE
.emoji_font = font_emoji_32_init(),
.emoji_font = font_emoji_32_init(),
#else
.emoji_font = font_emoji_64_init(),
.emoji_font = font_emoji_64_init(),
#endif
});
});
}
public:
@@ -298,4 +394,4 @@ public:
}
};
DECLARE_BOARD(JiuchuanDevBoard);
DECLARE_BOARD(JiuchuanDevBoard);

View File

@@ -4,15 +4,16 @@
#include <driver/gpio.h>
#include "pin_config.h"
#define AUDIO_INPUT_REFERENCE true
#define AUDIO_INPUT_SAMPLE_RATE 24000
#define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
#ifdef CONFIG_BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3_V1_0_V1_1
#define AUDIO_INPUT_REFERENCE true
#define AUDIO_MIC_I2S_GPIO_BCLK static_cast<gpio_num_t>(MSM261_BCLK)
#define AUDIO_MIC_I2S_GPIO_WS static_cast<gpio_num_t>(MSM261_WS)
#define AUDIO_MIC_I2S_GPIO_DATA static_cast<gpio_num_t>(MSM261_DATA)
#elif defined CONFIG_BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3_V1_2
#define AUDIO_INPUT_REFERENCE false
#define AUDIO_MIC_I2S_GPIO_BCLK GPIO_NUM_NC
#define AUDIO_MIC_I2S_GPIO_WS static_cast<gpio_num_t>(MP34DT05TR_LRCLK)
#define AUDIO_MIC_I2S_GPIO_DATA static_cast<gpio_num_t>(MP34DT05TR_DATA)

View File

@@ -82,16 +82,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, -1);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(10);
});
power_save_timer_->OnExitSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -140,10 +140,17 @@ void Tcamerapluss3AudioCodec::EnableOutput(bool enable) {
AudioCodec::EnableOutput(enable);
}
int Tcamerapluss3AudioCodec::Read(int16_t *dest, int samples){
if (input_enabled_){
int Tcamerapluss3AudioCodec::Read(int16_t *dest, int samples) {
if (input_enabled_) {
size_t bytes_read;
i2s_channel_read(rx_handle_, dest, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY);
// 麦克风接收音量放大20倍限制在 int16_t 范围内防止溢出)
int16_t *ptr = dest;
for (int i = 0; i < samples; i++) {
int32_t amplified = *ptr * 20;
*ptr++ = (amplified > 32767) ? 32767 : (amplified < -32768) ? -32768 : amplified;
}
}
return samples;
}

View File

@@ -64,16 +64,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(10);
});
power_save_timer_->OnExitSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->SetEnabled(true);

View File

@@ -83,16 +83,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(10);
});
power_save_timer_->OnExitSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->SetEnabled(true);

View File

@@ -138,16 +138,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(10);
});
power_save_timer_->OnExitSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -87,14 +87,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(240, 60, -1);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});

View File

@@ -102,14 +102,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(240, 60, -1);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});

View File

@@ -102,22 +102,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(160);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(10);
auto codec = GetAudioCodec();
codec->EnableInput(false);
});
power_save_timer_->OnExitSleepMode([this]() {
auto codec = GetAudioCodec();
codec->EnableInput(true);
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->SetEnabled(true);

View File

@@ -66,22 +66,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(160);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(10);
auto codec = GetAudioCodec();
codec->EnableInput(false);
});
power_save_timer_->OnExitSleepMode([this]() {
auto codec = GetAudioCodec();
codec->EnableInput(true);
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->SetEnabled(true);

View File

@@ -61,14 +61,11 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -102,16 +102,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(10);
});
power_save_timer_->OnExitSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -113,14 +113,11 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 290);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -98,14 +98,11 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -76,14 +76,12 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, -1);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling modem-sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
esp_wifi_set_ps(WIFI_PS_NONE); // 关闭Wi-Fi省电恢复正常
// esp_lcd_panel_disp_on_off(panel_, true); // 重新打开显示

View File

@@ -67,20 +67,10 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(240, 60, -1);
power_save_timer_->OnEnterSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
auto codec = GetAudioCodec();
codec->EnableInput(false);
GetDisplay()->SetPowerSaveMode(true);
});
power_save_timer_->OnExitSleepMode([this]() {
auto codec = GetAudioCodec();
codec->EnableInput(true);
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
});
power_save_timer_->SetEnabled(true);
}

View File

@@ -161,15 +161,10 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(20); });
power_save_timer_->OnExitSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness(); });
power_save_timer_->OnShutdownRequest([this](){
pmic_->PowerOff(); });

View File

@@ -0,0 +1,11 @@
# Waveshare ESP32-S3-Touch-AMOLED-2.06
[ESP32-S3-Touch-AMOLED-2.06](https://www.waveshare.com/esp32-s3-touch-amoled-2.06.htm) is a high-performance, wearable watch-style development board designed by Waveshare. Based on the ESP32-S3R8 microcontroller, it integrates a 2.06inch AMOLED capacitive touch display, 6-axis IMU, RTC chip, audio codec chip, power management IC, and so on. Comes with a custom-designed case with a smartwatch-like appearance, making it ideal for prototyping and functional verification of wearable applications.
## images
| [ESP32-S3-Touch-AMOLED-2.06](https://www.waveshare.com/esp32-s3-touch-amoled-2.06.htm) |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <img style="width: 150px; height: auto; display: block; margin: 0 auto;" src= "https://www.waveshare.com/media/catalog/product/cache/1/image/800x800/9df78eab33525d08d6e5fb8d27136e95/e/s/esp32-s3-touch-amoled-2.06-1.jpg"> |

View File

@@ -0,0 +1,43 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
#define AUDIO_INPUT_SAMPLE_RATE 24000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
#define AUDIO_INPUT_REFERENCE true
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_16
#define AUDIO_I2S_GPIO_WS GPIO_NUM_45
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_41
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_42
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_40
#define AUDIO_CODEC_PA_PIN GPIO_NUM_46
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_15
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_14
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define EXAMPLE_PIN_NUM_LCD_CS GPIO_NUM_12
#define EXAMPLE_PIN_NUM_LCD_PCLK GPIO_NUM_11
#define EXAMPLE_PIN_NUM_LCD_DATA0 GPIO_NUM_4
#define EXAMPLE_PIN_NUM_LCD_DATA1 GPIO_NUM_5
#define EXAMPLE_PIN_NUM_LCD_DATA2 GPIO_NUM_6
#define EXAMPLE_PIN_NUM_LCD_DATA3 GPIO_NUM_7
#define EXAMPLE_PIN_NUM_LCD_RST GPIO_NUM_8
#define DISPLAY_WIDTH 410
#define DISPLAY_HEIGHT 502
#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_NC
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
#endif // _BOARD_CONFIG_H_

View File

@@ -0,0 +1,12 @@
{
"target": "esp32s3",
"builds": [
{
"name": "waveshare-s3-touch-amoled-2.06",
"sdkconfig_append": [
"CONFIG_USE_WECHAT_MESSAGE_STYLE=n",
"CONFIG_USE_DEVICE_AEC=y"
]
}
]
}

View File

@@ -0,0 +1,361 @@
#include "wifi_board.h"
#include "display/lcd_display.h"
#include "esp_lcd_sh8601.h"
#include "font_awesome_symbols.h"
#include "codecs/box_audio_codec.h"
#include "application.h"
#include "button.h"
#include "led/single_led.h"
#include "mcp_server.h"
#include "config.h"
#include "power_save_timer.h"
#include "axp2101.h"
#include "i2c_device.h"
#include <wifi_station.h>
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <driver/i2c_master.h>
#include <driver/spi_master.h>
#include "settings.h"
#include <esp_lcd_touch_ft5x06.h>
#include <esp_lvgl_port.h>
#include <lvgl.h>
#define TAG "WaveshareEsp32s3TouchAMOLED2inch06"
LV_FONT_DECLARE(font_puhui_30_4);
LV_FONT_DECLARE(font_awesome_30_4);
class Pmic : public Axp2101 {
public:
Pmic(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : Axp2101(i2c_bus, addr) {
WriteReg(0x22, 0b110); // PWRON > OFFLEVEL as POWEROFF Source enable
WriteReg(0x27, 0x10); // hold 4s to power off
// Disable All DCs but DC1
WriteReg(0x80, 0x01);
// Disable All LDOs
WriteReg(0x90, 0x00);
WriteReg(0x91, 0x00);
// Set DC1 to 3.3V
WriteReg(0x82, (3300 - 1500) / 100);
// Set ALDO1 to 3.3V
WriteReg(0x92, (3300 - 500) / 100);
WriteReg(0x93, (3300 - 500) / 100);
// Enable ALDO1(MIC)
WriteReg(0x90, 0x03);
WriteReg(0x64, 0x02); // CV charger voltage setting to 4.1V
WriteReg(0x61, 0x02); // set Main battery precharge current to 50mA
WriteReg(0x62, 0x0A); // set Main battery charger current to 400mA ( 0x08-200mA, 0x09-300mA, 0x0A-400mA )
WriteReg(0x63, 0x01); // set Main battery term charge current to 25mA
}
};
#define LCD_OPCODE_WRITE_CMD (0x02ULL)
#define LCD_OPCODE_READ_CMD (0x03ULL)
#define LCD_OPCODE_WRITE_COLOR (0x32ULL)
static const sh8601_lcd_init_cmd_t vendor_specific_init[] = {
// set display to qspi mode
{0x11, (uint8_t []){0x00}, 0, 120},
{0xC4, (uint8_t []){0x80}, 1, 0},
{0x44, (uint8_t []){0x01, 0xD1}, 2, 0},
{0x35, (uint8_t []){0x00}, 1, 0},
{0x53, (uint8_t []){0x20}, 1, 10},
{0x63, (uint8_t []){0xFF}, 1, 10},
{0x51, (uint8_t []){0x00}, 1, 10},
{0x2A, (uint8_t []){0x00,0x16,0x01,0xAF}, 4, 0},
{0x2B, (uint8_t []){0x00,0x00,0x01,0xF5}, 4, 0},
{0x29, (uint8_t []){0x00}, 0, 10},
{0x51, (uint8_t []){0xFF}, 1, 0},
};
// 在waveshare_amoled_2_06类之前添加新的显示类
class CustomLcdDisplay : public SpiLcdDisplay {
public:
static void rounder_event_cb(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;
uint16_t y1 = area->y1;
uint16_t y2 = area->y2;
// round the start of coordinate down to the nearest 2M number
area->x1 = (x1 >> 1) << 1;
area->y1 = (y1 >> 1) << 1;
// round the end of coordinate up to the nearest 2N+1 number
area->x2 = ((x2 >> 1) << 1) + 1;
area->y2 = ((y2 >> 1) << 1) + 1;
}
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_30_4,
#if CONFIG_USE_WECHAT_MESSAGE_STYLE
.emoji_font = font_emoji_32_init(),
#else
.emoji_font = font_emoji_64_init(),
#endif
})
{
DisplayLockGuard lock(this);
lv_obj_set_style_pad_left(status_bar_, LV_HOR_RES* 0.1, 0);
lv_obj_set_style_pad_right(status_bar_, LV_HOR_RES* 0.1, 0);
lv_display_add_event_cb(display_, rounder_event_cb, LV_EVENT_INVALIDATE_AREA, NULL);
}
};
class CustomBacklight : public Backlight {
public:
CustomBacklight(esp_lcd_panel_io_handle_t panel_io) : Backlight(), panel_io_(panel_io) {}
protected:
esp_lcd_panel_io_handle_t panel_io_;
virtual void SetBrightnessImpl(uint8_t brightness) override {
auto display = Board::GetInstance().GetDisplay();
DisplayLockGuard lock(display);
uint8_t data[1] = {((uint8_t)((255* brightness) / 100))};
int lcd_cmd = 0x51;
lcd_cmd &= 0xff;
lcd_cmd <<= 8;
lcd_cmd |= LCD_OPCODE_WRITE_CMD << 24;
esp_lcd_panel_io_tx_param(panel_io_, lcd_cmd, &data, sizeof(data));
}
};
class WaveshareEsp32s3TouchAMOLED2inch06 : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
Pmic* pmic_ = nullptr;
Button boot_button_;
CustomLcdDisplay* display_;
CustomBacklight* backlight_;
PowerSaveTimer* power_save_timer_;
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(20); });
power_save_timer_->OnExitSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness(); });
power_save_timer_->OnShutdownRequest([this](){
pmic_->PowerOff(); });
power_save_timer_->SetEnabled(true);
}
void InitializeCodecI2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = I2C_NUM_0,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
}
void InitializeAxp2101() {
ESP_LOGI(TAG, "Init AXP2101");
pmic_ = new Pmic(i2c_bus_, 0x34);
}
void InitializeSpi() {
spi_bus_config_t buscfg = {};
buscfg.sclk_io_num = EXAMPLE_PIN_NUM_LCD_PCLK;
buscfg.data0_io_num = EXAMPLE_PIN_NUM_LCD_DATA0;
buscfg.data1_io_num = EXAMPLE_PIN_NUM_LCD_DATA1;
buscfg.data2_io_num = EXAMPLE_PIN_NUM_LCD_DATA2;
buscfg.data3_io_num = EXAMPLE_PIN_NUM_LCD_DATA3;
buscfg.max_transfer_sz = DISPLAY_WIDTH* DISPLAY_HEIGHT* sizeof(uint16_t);
buscfg.flags = SPICOMMON_BUSFLAG_QUAD;
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
app.ToggleChatState();
});
#if CONFIG_USE_DEVICE_AEC
boot_button_.OnDoubleClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateIdle) {
app.SetAecMode(app.GetAecMode() == kAecOff ? kAecOnDeviceSide : kAecOff);
}
});
#endif
}
void InitializeSH8601Display() {
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
// 液晶屏控制IO初始化
ESP_LOGD(TAG, "Install panel IO");
esp_lcd_panel_io_spi_config_t io_config = SH8601_PANEL_IO_QSPI_CONFIG(
EXAMPLE_PIN_NUM_LCD_CS,
nullptr,
nullptr);
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io));
// 初始化液晶屏驱动芯片
ESP_LOGD(TAG, "Install LCD driver");
const sh8601_vendor_config_t vendor_config = {
.init_cmds = &vendor_specific_init[0],
.init_cmds_size = sizeof(vendor_specific_init) / sizeof(sh8601_lcd_init_cmd_t),
.flags = {
.use_qspi_interface = 1,
}};
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = EXAMPLE_PIN_NUM_LCD_RST;
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
panel_config.bits_per_pixel = 16;
panel_config.vendor_config = (void* )&vendor_config;
ESP_ERROR_CHECK(esp_lcd_new_panel_sh8601(panel_io, &panel_config, &panel));
esp_lcd_panel_set_gap(panel, 0x16, 0);
esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, false);
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);
backlight_ = new CustomBacklight(panel_io);
backlight_->RestoreBrightness();
}
void InitializeTouch() {
esp_lcd_touch_handle_t tp;
esp_lcd_touch_config_t tp_cfg = {
.x_max = DISPLAY_WIDTH - 1,
.y_max = DISPLAY_HEIGHT - 1,
.rst_gpio_num = GPIO_NUM_9,
.int_gpio_num = GPIO_NUM_38,
.levels = {
.reset = 0,
.interrupt = 0,
},
.flags = {
.swap_xy = 0,
.mirror_x = 0,
.mirror_y = 0,
},
};
esp_lcd_panel_io_handle_t tp_io_handle = NULL;
esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_FT5x06_CONFIG();
tp_io_config.scl_speed_hz = 400* 1000;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus_, &tp_io_config, &tp_io_handle));
ESP_LOGI(TAG, "Initialize touch controller");
ESP_ERROR_CHECK(esp_lcd_touch_new_i2c_ft5x06(tp_io_handle, &tp_cfg, &tp));
const lvgl_port_touch_cfg_t touch_cfg = {
.disp = lv_display_get_default(),
.handle = tp,
};
lvgl_port_add_touch(&touch_cfg);
ESP_LOGI(TAG, "Touch panel initialized successfully");
}
// 初始化工具
void InitializeTools() {
auto &mcp_server = McpServer::GetInstance();
mcp_server.AddTool("self.system.reconfigure_wifi",
"Reboot the device and enter WiFi configuration mode.\n"
"**CAUTION** You must ask the user to confirm this action.",
PropertyList(), [this](const PropertyList& properties) {
ResetWifiConfiguration();
return true;
});
}
public:
WaveshareEsp32s3TouchAMOLED2inch06() : boot_button_(BOOT_BUTTON_GPIO) {
InitializePowerSaveTimer();
InitializeCodecI2c();
InitializeAxp2101();
InitializeSpi();
InitializeSH8601Display();
InitializeTouch();
InitializeButtons();
InitializeTools();
}
virtual AudioCodec* GetAudioCodec() override {
static BoxAudioCodec 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_ES7210_ADDR,
AUDIO_INPUT_REFERENCE);
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
virtual Backlight* GetBacklight() override {
return backlight_;
}
virtual bool GetBatteryLevel(int &level, bool &charging, bool &discharging) override {
static bool last_discharging = false;
charging = pmic_->IsCharging();
discharging = pmic_->IsDischarging();
if (discharging != last_discharging)
{
power_save_timer_->SetEnabled(discharging);
last_discharging = discharging;
}
level = pmic_->GetBatteryLevel();
return true;
}
virtual void SetPowerSaveMode(bool enabled) override {
if (!enabled)
{
power_save_timer_->WakeUp();
}
WifiBoard::SetPowerSaveMode(enabled);
}
};
DECLARE_BOARD(WaveshareEsp32s3TouchAMOLED2inch06);

View File

@@ -122,16 +122,11 @@ private:
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(20);
});
power_save_timer_->OnExitSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -112,14 +112,11 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -112,14 +112,11 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -54,15 +54,10 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
});
power_save_timer_->OnExitSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
});
power_save_timer_->OnShutdownRequest([this]() {
ESP_LOGI(TAG, "Shutting down");

View File

@@ -55,15 +55,10 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
});
power_save_timer_->OnExitSleepMode([this]() {
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
});
power_save_timer_->OnShutdownRequest([this]() {
ESP_LOGI(TAG, "Shutting down");

View File

@@ -52,14 +52,11 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -52,14 +52,11 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {

View File

@@ -48,14 +48,14 @@ private:
void InitializePowerSaveTimer() {
#if CONFIG_USE_ESP_WAKE_WORD
sleep_timer_ = new SleepTimer(600);
sleep_timer_ = new SleepTimer(300);
#else
sleep_timer_ = new SleepTimer(30);
#endif
sleep_timer_->OnEnterLightSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
// Show the standby screen
GetDisplay()->ShowStandbyScreen(true);
GetDisplay()->SetPowerSaveMode(true);
// Enable sleep mode, and sleep in 1 second after DTR is set to high
modem_->SetSleepMode(true, 1);
// Set the DTR pin to high to make the modem enter sleep mode
@@ -65,7 +65,7 @@ private:
// Set the DTR pin to low to make the modem wake up
modem_->GetAtUart()->SetDtrPin(false);
// Hide the standby screen
GetDisplay()->ShowStandbyScreen(false);
GetDisplay()->SetPowerSaveMode(false);
});
sleep_timer_->SetEnabled(true);
}

View File

@@ -6,6 +6,7 @@
"sdkconfig_append": [
"CONFIG_PM_ENABLE=y",
"CONFIG_FREERTOS_USE_TICKLESS_IDLE=y",
"CONFIG_USE_ESP_WAKE_WORD=y",
"CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y"
]
}

View File

@@ -7,7 +7,7 @@
#include "mcp_server.h"
#include "settings.h"
#include "config.h"
#include "sleep_timer.h"
#include "power_save_timer.h"
#include "font_awesome_symbols.h"
#include "adc_battery_monitor.h"
@@ -32,44 +32,33 @@ private:
Display* display_ = nullptr;
Button boot_button_;
bool press_to_talk_enabled_ = false;
SleepTimer* sleep_timer_ = nullptr;
PowerSaveTimer* power_save_timer_ = nullptr;
AdcBatteryMonitor* adc_battery_monitor_ = nullptr;
void InitializePowerManager() {
adc_battery_monitor_ = new AdcBatteryMonitor(ADC_UNIT_1, ADC_CHANNEL_3, 100000, 100000, GPIO_NUM_12);
adc_battery_monitor_->OnChargingStatusChanged([this](bool is_charging) {
if (is_charging) {
sleep_timer_->SetEnabled(false);
power_save_timer_->SetEnabled(false);
} else {
sleep_timer_->SetEnabled(true);
power_save_timer_->SetEnabled(true);
}
});
}
void InitializePowerSaveTimer() {
#if CONFIG_USE_ESP_WAKE_WORD
sleep_timer_ = new SleepTimer(600);
power_save_timer_ = new PowerSaveTimer(160, 300);
#else
sleep_timer_ = new SleepTimer(30);
power_save_timer_ = new PowerSaveTimer(160, 60);
#endif
sleep_timer_->OnEnterLightSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
auto codec = GetAudioCodec();
codec->EnableInput(false);
power_save_timer_->OnEnterSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(true);
});
sleep_timer_->OnExitLightSleepMode([this]() {
auto codec = GetAudioCodec();
codec->EnableInput(true);
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
power_save_timer_->OnExitSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(false);
});
sleep_timer_->SetEnabled(true);
power_save_timer_->SetEnabled(true);
}
void InitializeCodecI2c() {
@@ -156,8 +145,8 @@ private:
}
});
boot_button_.OnPressDown([this]() {
if (sleep_timer_) {
sleep_timer_->WakeUp();
if (power_save_timer_) {
power_save_timer_->WakeUp();
}
if (press_to_talk_enabled_) {
Application::GetInstance().StartListening();
@@ -241,7 +230,7 @@ public:
virtual void SetPowerSaveMode(bool enabled) override {
if (!enabled) {
sleep_timer_->WakeUp();
power_save_timer_->WakeUp();
}
WifiBoard::SetPowerSaveMode(enabled);
}

View File

@@ -34,26 +34,15 @@ private:
void InitializePowerSaveTimer() {
#if CONFIG_USE_ESP_WAKE_WORD
power_save_timer_ = new PowerSaveTimer(160, 600);
power_save_timer_ = new PowerSaveTimer(160, 300);
#else
power_save_timer_ = new PowerSaveTimer(160, 60);
#endif
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("sleepy");
auto codec = GetAudioCodec();
codec->EnableInput(false);
GetDisplay()->SetPowerSaveMode(true);
});
power_save_timer_->OnExitSleepMode([this]() {
auto codec = GetAudioCodec();
codec->EnableInput(true);
auto display = GetDisplay();
display->SetChatMessage("system", "");
display->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
});
power_save_timer_->SetEnabled(true);
}

View File

@@ -55,14 +55,11 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->SetEnabled(true);

View File

@@ -59,14 +59,11 @@ private:
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() {
ESP_LOGI(TAG, "Enabling sleep mode");
display_->SetChatMessage("system", "");
display_->SetEmotion("sleepy");
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1);
});
power_save_timer_->OnExitSleepMode([this]() {
display_->SetChatMessage("system", "");
display_->SetEmotion("neutral");
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->SetEnabled(true);

View File

@@ -269,8 +269,8 @@ void Display::SetTheme(const std::string& theme_name) {
settings.SetString("theme", theme_name);
}
void Display::ShowStandbyScreen(bool show) {
if (show) {
void Display::SetPowerSaveMode(bool on) {
if (on) {
SetChatMessage("system", "");
SetEmotion("sleepy");
} else {

View File

@@ -30,7 +30,7 @@ public:
virtual void SetTheme(const std::string& theme_name);
virtual std::string GetTheme() { return current_theme_name_; }
virtual void UpdateStatusBar(bool update_all = false);
virtual void ShowStandbyScreen(bool show);
virtual void SetPowerSaveMode(bool on);
inline int width() const { return width_; }
inline int height() const { return height_; }

View File

@@ -22,6 +22,7 @@ OledDisplay::OledDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handl
ESP_LOGI(TAG, "Initialize LVGL");
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
port_cfg.task_priority = 1;
port_cfg.task_stack = 6144;
port_cfg.timer_period_ms = 50;
lvgl_port_init(&port_cfg);

View File

@@ -15,10 +15,10 @@ dependencies:
78/esp_lcd_nv3023: ~1.0.0
78/esp-wifi-connect: ~2.4.3
78/esp-opus-encoder: ~2.4.0
78/esp-ml307: ~3.1.1
78/esp-ml307: ~3.2.4
78/xiaozhi-fonts: ~1.3.2
espressif/led_strip: ^2.5.5
espressif/esp_codec_dev: ~1.3.2
espressif/esp_codec_dev: ~1.3.6
espressif/esp-sr: ~2.1.1
espressif/button: ~4.1.3
espressif/knob: ^1.0.0
@@ -31,6 +31,7 @@ dependencies:
esp_lvgl_port: ~2.6.0
espressif/esp_io_expander_tca95xx_16bit: ^2.0.0
espressif2022/image_player: ==1.1.0~1
espressif2022/esp_emote_gfx: ^1.0.0
espressif/adc_mic: ^0.2.0
espressif/esp_mmap_assets: '>=1.2'
txp666/otto-emoji-gif-component: ~1.0.2
@@ -63,6 +64,10 @@ dependencies:
version: ^1.0.1
rules:
- if: target in [esp32p4]
espressif/esp_hosted:
version: '2.0.17'
rules:
- if: target in [esp32h2, esp32p4]
espressif/esp_wifi_remote:
version: '*'
rules:

View File

@@ -0,0 +1,9 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
model, data, spiffs, 0x10000, 0xF0000,
ota_0, app, ota_0, 0x100000, 5M,
ota_1, app, ota_1, 0x700000, 5M,
assets_A, data, spiffs, , 4000K,
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000, 0x4000,
4 otadata, data, ota, 0xd000, 0x2000,
5 phy_init, data, phy, 0xf000, 0x1000,
6 model, data, spiffs, 0x10000, 0xF0000,
7 ota_0, app, ota_0, 0x100000, 5M,
8 ota_1, app, ota_1, 0x700000, 5M,
9 assets_A, data, spiffs, , 4000K,

View File

@@ -20,14 +20,26 @@
### 使用方法
安装Pillow
创建虚拟环境
```bash
pip install Pillow # 处理图像需要
# 创建 venv
python -m venv venv
# 激活环境
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
```
安装依赖
```bash
pip install -r requirements.txt
```
运行转换工具
```bash
# 激活环境
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
# 运行
python lvgl_tools_gui.py
```

View File

@@ -51,7 +51,7 @@ class ImageConverterApp:
# 分辨率设置
ttk.Label(settings_frame, text="分辨率:").grid(row=0, column=0, padx=2)
ttk.Combobox(settings_frame, textvariable=self.resolution,
values=["128x128", "64x64", "32x32"], width=8).grid(row=0, column=1, padx=2)
values=["512x512", "256x256", "128x128", "64x64", "32x32"], width=8).grid(row=0, column=1, padx=2)
# 颜色格式
ttk.Label(settings_frame, text="颜色格式:").grid(row=0, column=2, padx=2)
@@ -64,7 +64,7 @@ class ImageConverterApp:
values=["NONE", "RLE"], width=8).grid(row=0, column=5, padx=2)
# 文件操作框架
file_frame = ttk.LabelFrame(self.root, text="输入文件")
file_frame = ttk.LabelFrame(self.root, text="选取文件")
file_frame.grid(row=1, column=0, padx=10, pady=5, sticky="nsew")
# 文件操作按钮
@@ -77,7 +77,7 @@ class ImageConverterApp:
# 文件列表Treeview
self.tree = ttk.Treeview(file_frame, columns=("selected", "filename"),
show="headings", height=10)
self.tree.heading("selected", text="", anchor=tk.W)
self.tree.heading("selected", text="", anchor=tk.W)
self.tree.heading("filename", text="文件名", anchor=tk.W)
self.tree.column("selected", width=60, anchor=tk.W)
self.tree.column("filename", width=600, anchor=tk.W)
@@ -250,4 +250,4 @@ class ImageConverterApp:
if __name__ == "__main__":
root = tk.Tk()
app = ImageConverterApp(root)
root.mainloop()
root.mainloop()

View File

@@ -0,0 +1,3 @@
lz4==4.4.4
Pillow==11.3.0
pypng==0.20220715.0

View File

@@ -36,6 +36,9 @@ CONFIG_CODEC_I2C_BACKWARD_COMPATIBLE=n
# Fix ML307 FIFO Overflow
CONFIG_UART_ISR_IN_IRAM=y
# Fix ESP_SSL error
CONFIG_MBEDTLS_SSL_RENEGOTIATION=n
# LVGL 9.2.2
CONFIG_LV_OS_NONE=y