Compare commits

..

18 Commits

Author SHA1 Message Date
Terrence
564680da92 lichuang c3 use 20px fonts 2025-02-04 00:32:37 +08:00
Terrence
c36e25ce3f upgrade to LVGL 9 2025-02-03 23:43:07 +08:00
Terrence
e434cd1aed improve wifi experience 2025-02-02 19:06:54 +08:00
Terrence
e49c520349 fix websocket mode not responding after close conn 2025-02-02 18:13:34 +08:00
Terrence
5be81638aa edit version upload 2025-02-01 23:09:40 +08:00
Terrence
d840699bbf edit signal display 2025-02-01 23:08:41 +08:00
Terrence
c34cf27ccb fix surprised emotion 2025-02-01 11:06:17 +08:00
ooxxU
d44c01a5a3 支持 ESP32 系列开发板 (#116)
* 1. 支持 ESP32系列开发板: DevKitC / NodeMcu-32S / GoouuuESP32 / ESP32 DoIt / ESP-32S
2. 注意:非ESP32-C3 / 非ESP32-S3

* 补图片: M5Stack  Atom Matrix + ES8311 + NS4150B

* 修订 ESP32 系列开发板 面包板 成品图

* 修订: ESP32系列开发板 README.md

* 修订: 移除对首页修改的这部分内容,跳转链接添加至《百科全书》的目录里。
2025-02-01 11:03:49 +08:00
Terrence
3d3f2ad96d fix tudouzi board compile 2025-01-28 12:58:36 +08:00
Terrence
ce26b8d3a1 toggle and hold mode cannot work together 2025-01-28 09:31:13 +08:00
Kevincoooool
3816f7eb57 add tudouzi board (#85)
* add tudouzi board

* Update Kconfig.projbuild

fix spaces

---------

Co-authored-by: Xiaoxia <terrence@tenclass.com>
2025-01-28 09:23:52 +08:00
MOV
f0a683578a fix: Moji LCD initialization configuration (#114)
* fix:Modify the README and add Moji images

* fix: Moji LCD initialization configuration.
2025-01-28 01:41:30 +08:00
Xiaoxia
c564d24cf1 Fix UI condition racing (#110)
* move display init code to board

* fix ui condition race
2025-01-25 15:35:00 +08:00
Xiaoxia
d2cc2e0090 move display init code to board (#109) 2025-01-25 15:17:37 +08:00
Wiking_Xu
e48370ffcd 增加正点原子两款ESP32S3板子 (#107)
* ESP32S3-BOX
* ESP32S3开发板
2025-01-25 15:11:50 +08:00
Terrence
046ca3e0c9 fix upgrade icon 2025-01-25 12:44:31 +08:00
Terrence
e4d80060a2 fix lcd font error 2025-01-25 00:41:49 +08:00
MOV
fbf90d1b17 fix:Modify the README and add Moji images (#106) 2025-01-24 12:17:49 +08:00
53 changed files with 1630 additions and 426 deletions

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.0.1")
set(PROJECT_VER "1.1.0")
# Add this line to disable the specific warning
add_compile_options(-Wno-missing-field-initializers)

View File

@@ -51,10 +51,12 @@
- <a href="https://github.com/espressif/esp-box" target="_blank" title="乐鑫 ESP32-S3-BOX3">乐鑫 ESP32-S3-BOX3</a>
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">AtomS3R + Echo Base</a>
- <a href="https://docs.m5stack.com/en/core/ATOM%20Matrix" target="_blank" title="AtomMatrix + Echo Base">AtomMatrix + Echo Base</a>
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="神奇按钮 2.4">神奇按钮 2.4</a>
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">微雪电子 ESP32-S3-Touch-AMOLED-1.8</a>
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="虾哥 Mini C3">虾哥 Mini C3</a>
- <a href="https://oshwhub.com/movecall/moji-xiaozhi-ai-derivative-editi" target="_blank" title="Movecall Moji ESP32S3">Moji 小智AI衍生版</a>
<div style="display: flex; justify-content: space-between;">
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="立创·实战派 ESP32-S3 开发板">
@@ -69,6 +71,9 @@
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
<img src="docs/v1/atoms3r.jpg" width="240" />
</a>
<a href="docs/AtomMatrix-echo-base.jpg" target="_blank" title="AtomMatrix-echo-base + Echo Base">
<img src="docs/AtomMatrix-echo-base.jpg" width="240" />
</a>
<a href="docs/v1/magiclick.jpg" target="_blank" title="神奇按钮 2.4">
<img src="docs/v1/magiclick.jpg" width="240" />
</a>
@@ -81,6 +86,9 @@
<a href="docs/xmini-c3.jpg" target="_blank" title="虾哥 Mini C3">
<img src="docs/xmini-c3.jpg" width="240" />
</a>
<a href="docs/v1/movecall-moji-esp32s3.jpg" target="_blank" title="Movecall Moji 小智AI衍生版">
<img src="docs/v1/movecall-moji-esp32s3.jpg" width="240" />
</a>
</div>
## 固件部分

View File

@@ -51,10 +51,12 @@ Breadboard setup shown below:
- <a href="https://github.com/espressif/esp-box" target="_blank" title="Espressif ESP32-S3-BOX3">Espressif ESP32-S3-BOX3</a>
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">AtomS3R + Echo Base</a>
- <a href="https://docs.m5stack.com/en/core/ATOM%20Matrix" target="_blank" title="AtomMatrix + Echo Base">AtomMatrix + Echo Base</a>
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="MagiClick 2.4">MagiClick 2.4</a>
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">Waveshare ESP32-S3-Touch-AMOLED-1.8</a>
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="Xmini C3">Xmini C3</a>
- <a href="https://oshwhub.com/movecall/moji-xiaozhi-ai-derivative-editi" target="_blank" title="Movecall Moji ESP32S3">Movecall Moji ESP32S3</a>
<div style="display: flex; justify-content: space-between;">
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 Development Board">
@@ -69,6 +71,9 @@ Breadboard setup shown below:
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
<img src="docs/v1/atoms3r.jpg" width="240" />
</a>
<a href="docs/AtomMatrix-echo-base.jpg" target="_blank" title="AtomMatrix-echo-base + Echo Base">
<img src="docs/AtomMatrix-echo-base.jpg" width="240" />
</a>
<a href="docs/v1/magiclick.jpg" target="_blank" title="MagiClick 2.4">
<img src="docs/v1/magiclick.jpg" width="240" />
</a>
@@ -81,6 +86,9 @@ Breadboard setup shown below:
<a href="docs/xmini-c3.jpg" target="_blank" title="Xmini C3">
<img src="docs/xmini-c3.jpg" width="240" />
</a>
<a href="docs/v1/movecall-moji-esp32s3.jpg" target="_blank" title="Moji">
<img src="docs/v1/movecall-moji-esp32s3.jpg" width="240" />
</a>
</div>
## Firmware Section

View File

@@ -55,6 +55,7 @@
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">Waveshare ESP32-S3-Touch-AMOLED-1.8</a>
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="Xmini C3">Xmini C3</a>
- <a href="https://oshwhub.com/movecall/moji-xiaozhi-ai-derivative-editi" target="_blank" title="Movecall Moji ESP32S3">Movecall Moji ESP32S3</a>
<div style="display: flex; justify-content: space-between;">
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 開発ボード">
@@ -81,6 +82,9 @@
<a href="docs/xmini-c3.jpg" target="_blank" title="Xmini C3">
<img src="docs/xmini-c3.jpg" width="240" />
</a>
<a href="docs/v1/movecall-moji-esp32s3.jpg" target="_blank" title="Moji">
<img src="docs/v1/movecall-moji-esp32s3.jpg" width="240" />
</a>
</div>
## ファームウェアセクション

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/ESP32-BreadBoard.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -2,6 +2,7 @@ set(SOURCES "audio_codecs/audio_codec.cc"
"audio_codecs/no_audio_codec.cc"
"audio_codecs/box_audio_codec.cc"
"audio_codecs/es8311_audio_codec.cc"
"audio_codecs/es8388_audio_codec.cc"
"audio_codecs/cores3_audio_codec.cc"
"audio_codecs/tcircles3_audio_codec.cc"
"led/single_led.cc"
@@ -40,6 +41,8 @@ if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI)
set(BOARD_TYPE "bread-compact-wifi")
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307)
set(BOARD_TYPE "bread-compact-ml307")
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32)
set(BOARD_TYPE "bread-compact-esp32")
elseif(CONFIG_BOARD_TYPE_ESP_BOX_3)
set(BOARD_TYPE "esp-box-3")
elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_1)
@@ -72,10 +75,16 @@ elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8)
set(BOARD_TYPE "esp32-s3-touch-amoled-1.8")
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD)
set(BOARD_TYPE "bread-compact-wifi-lcd")
elseif(CONFIG_BOARD_TYPE_TUDOUZI)
set(BOARD_TYPE "tudouzi")
elseif(CONFIG_BOARD_TYPE_LILYGO_T_CIRCLE_S3)
set(BOARD_TYPE "lilygo-t-circle-s3")
elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI_ESP32S3)
set(BOARD_TYPE "movecall-moji-esp32s3")
set(BOARD_TYPE "movecall-moji-esp32s3")
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3)
set(BOARD_TYPE "atk-dnesp32s3")
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX)
set(BOARD_TYPE "atk-dnesp32s3-box")
endif()
file(GLOB BOARD_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.cc)
list(APPEND SOURCES ${BOARD_SOURCES})
@@ -88,7 +97,8 @@ endif()
if(CONFIG_IDF_TARGET_ESP32)
# 排除 "audio_codecs/box_audio_codec.cc" 和 "audio_codecs/cores3_audio_codec.cc"
list(REMOVE_ITEM SOURCES "audio_codecs/box_audio_codec.cc"
"audio_codecs/cores3_audio_codec.cc")
"audio_codecs/cores3_audio_codec.cc"
"audio_codecs/es8388_audio_codec.cc")
endif()
idf_component_register(SRCS ${SOURCES}

View File

@@ -42,6 +42,8 @@ choice BOARD_TYPE
bool "面包板新版接线WiFi+ LCD"
config BOARD_TYPE_BREAD_COMPACT_ML307
bool "面包板新版接线ML307 AT"
config BOARD_TYPE_BREAD_COMPACT_ESP32
bool "面包板 ESP32 DevKit"
config BOARD_TYPE_ESP_BOX_3
bool "ESP BOX 3"
config BOARD_TYPE_KEVIN_BOX_1
@@ -72,10 +74,16 @@ choice BOARD_TYPE
bool "ESP-SparkBot开发板"
config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8
bool "Waveshare ESP32-S3-Touch-AMOLED-1.8"
config BOARD_TYPE_TUDOUZI
bool "土豆子"
config BOARD_TYPE_LILYGO_T_CIRCLE_S3
bool "LILYGO T-Circle-S3"
config BOARD_TYPE_MOVECALL_MOJI_ESP32S3
bool "Movecall Moji 小智AI衍生版"
config BOARD_TYPE_ATK_DNESP32S3
bool "正点原子DNESP32S3开发板"
config BOARD_TYPE_ATK_DNESP32S3_BOX
bool "正点原子DNESP32S3-BOX"
endchoice
choice DISPLAY_LCD_TYPE

View File

@@ -77,6 +77,9 @@ void Application::CheckNewVersion() {
display->SetStatus("新版本 " + ota_.GetFirmwareVersion());
board.SetPowerSaveMode(false);
#if CONFIG_IDF_TARGET_ESP32S3
wake_word_detect_.StopDetection();
#endif
// 预先关闭音频输出,避免升级过程有音频操作
board.GetAudioCodec()->EnableOutput(false);
{
@@ -248,74 +251,6 @@ void Application::Start() {
/* Wait for the network to be ready */
board.StartNetwork();
// Check for new firmware version or get the MQTT broker address
xTaskCreate([](void* arg) {
Application* app = (Application*)arg;
app->CheckNewVersion();
vTaskDelete(NULL);
}, "check_new_version", 4096 * 2, this, 1, nullptr);
#if CONFIG_IDF_TARGET_ESP32S3
audio_processor_.Initialize(codec->input_channels(), codec->input_reference());
audio_processor_.OnOutput([this](std::vector<int16_t>&& data) {
background_task_->Schedule([this, data = std::move(data)]() mutable {
opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) {
Schedule([this, opus = std::move(opus)]() {
protocol_->SendAudio(opus);
});
});
});
});
wake_word_detect_.Initialize(codec->input_channels(), codec->input_reference());
wake_word_detect_.OnVadStateChange([this](bool speaking) {
Schedule([this, speaking]() {
if (device_state_ == kDeviceStateListening) {
if (speaking) {
voice_detected_ = true;
} else {
voice_detected_ = false;
}
auto led = Board::GetInstance().GetLed();
led->OnStateChanged();
}
});
});
wake_word_detect_.OnWakeWordDetected([this](const std::string& wake_word) {
Schedule([this, &wake_word]() {
if (device_state_ == kDeviceStateIdle) {
SetDeviceState(kDeviceStateConnecting);
wake_word_detect_.EncodeWakeWordData();
if (!protocol_->OpenAudioChannel()) {
ESP_LOGE(TAG, "Failed to open audio channel");
SetDeviceState(kDeviceStateIdle);
wake_word_detect_.StartDetection();
return;
}
std::vector<uint8_t> opus;
// Encode and send the wake word data to the server
while (wake_word_detect_.GetWakeWordOpus(opus)) {
protocol_->SendAudio(opus);
}
// Set the chat state to wake word detected
protocol_->SendWakeWordDetected(wake_word);
ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
keep_listening_ = true;
SetDeviceState(kDeviceStateListening);
} else if (device_state_ == kDeviceStateSpeaking) {
AbortSpeaking(kAbortReasonWakeWordDetected);
}
// Resume detection
wake_word_detect_.StartDetection();
});
});
wake_word_detect_.StartDetection();
#endif
// Initialize the protocol
display->SetStatus("初始化协议");
#ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET
@@ -380,19 +315,25 @@ void Application::Start() {
auto text = cJSON_GetObjectItem(root, "text");
if (text != NULL) {
ESP_LOGI(TAG, "<< %s", text->valuestring);
display->SetChatMessage("assistant", text->valuestring);
Schedule([this, display, message = std::string(text->valuestring)]() {
display->SetChatMessage("assistant", message);
});
}
}
} else if (strcmp(type->valuestring, "stt") == 0) {
auto text = cJSON_GetObjectItem(root, "text");
if (text != NULL) {
ESP_LOGI(TAG, ">> %s", text->valuestring);
display->SetChatMessage("user", text->valuestring);
Schedule([this, display, message = std::string(text->valuestring)]() {
display->SetChatMessage("user", message);
});
}
} else if (strcmp(type->valuestring, "llm") == 0) {
auto emotion = cJSON_GetObjectItem(root, "emotion");
if (emotion != NULL) {
display->SetEmotion(emotion->valuestring);
Schedule([this, display, emotion_str = std::string(emotion->valuestring)]() {
display->SetEmotion(emotion_str);
});
}
} else if (strcmp(type->valuestring, "iot") == 0) {
auto commands = cJSON_GetObjectItem(root, "commands");
@@ -406,12 +347,83 @@ void Application::Start() {
}
});
// Check for new firmware version or get the MQTT broker address
xTaskCreate([](void* arg) {
Application* app = (Application*)arg;
app->CheckNewVersion();
vTaskDelete(NULL);
}, "check_new_version", 4096 * 2, this, 1, nullptr);
#if CONFIG_IDF_TARGET_ESP32S3
audio_processor_.Initialize(codec->input_channels(), codec->input_reference());
audio_processor_.OnOutput([this](std::vector<int16_t>&& data) {
background_task_->Schedule([this, data = std::move(data)]() mutable {
opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) {
Schedule([this, opus = std::move(opus)]() {
protocol_->SendAudio(opus);
});
});
});
});
wake_word_detect_.Initialize(codec->input_channels(), codec->input_reference());
wake_word_detect_.OnVadStateChange([this](bool speaking) {
Schedule([this, speaking]() {
if (device_state_ == kDeviceStateListening) {
if (speaking) {
voice_detected_ = true;
} else {
voice_detected_ = false;
}
auto led = Board::GetInstance().GetLed();
led->OnStateChanged();
}
});
});
wake_word_detect_.OnWakeWordDetected([this](const std::string& wake_word) {
Schedule([this, &wake_word]() {
if (device_state_ == kDeviceStateIdle) {
SetDeviceState(kDeviceStateConnecting);
wake_word_detect_.EncodeWakeWordData();
if (!protocol_->OpenAudioChannel()) {
ESP_LOGE(TAG, "Failed to open audio channel");
SetDeviceState(kDeviceStateIdle);
wake_word_detect_.StartDetection();
return;
}
std::vector<uint8_t> opus;
// Encode and send the wake word data to the server
while (wake_word_detect_.GetWakeWordOpus(opus)) {
protocol_->SendAudio(opus);
}
// Set the chat state to wake word detected
protocol_->SendWakeWordDetected(wake_word);
ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
keep_listening_ = true;
SetDeviceState(kDeviceStateListening);
} else if (device_state_ == kDeviceStateSpeaking) {
AbortSpeaking(kAbortReasonWakeWordDetected);
}
// Resume detection
wake_word_detect_.StartDetection();
});
});
wake_word_detect_.StartDetection();
#endif
SetDeviceState(kDeviceStateIdle);
}
void Application::Schedule(std::function<void()> callback) {
std::lock_guard<std::mutex> lock(mutex_);
main_tasks_.push_back(std::move(callback));
{
std::lock_guard<std::mutex> lock(mutex_);
main_tasks_.push_back(std::move(callback));
}
xEventGroupSetBits(event_group_, SCHEDULE_EVENT);
}
@@ -431,9 +443,9 @@ void Application::MainLoop() {
OutputAudio();
}
if (bits & SCHEDULE_EVENT) {
mutex_.lock();
std::unique_lock<std::mutex> lock(mutex_);
std::list<std::function<void()>> tasks = std::move(main_tasks_);
mutex_.unlock();
lock.unlock();
for (auto& task : tasks) {
task();
}
@@ -609,6 +621,7 @@ void Application::SetDecodeSampleRate(int sample_rate) {
}
opus_decode_sample_rate_ = sample_rate;
opus_decoder_.reset();
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(opus_decode_sample_rate_, 1);
auto codec = Board::GetInstance().GetAudioCodec();

View File

@@ -144,7 +144,7 @@ void Es8311AudioCodec::EnableInput(bool enable) {
.bits_per_sample = 16,
.channel = 1,
.channel_mask = 0,
.sample_rate = (uint32_t)output_sample_rate_,
.sample_rate = (uint32_t)input_sample_rate_,
.mclk_multiple = 0,
};
ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));

View File

@@ -0,0 +1,197 @@
#include "es8388_audio_codec.h"
#include <esp_log.h>
static const char TAG[] = "Es8388AudioCodec";
Es8388AudioCodec::Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
gpio_num_t pa_pin, uint8_t es8388_addr) {
duplex_ = true; // 是否双工
input_reference_ = false; // 是否使用参考输入,实现回声消除
input_channels_ = 1; // 输入通道数
input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate;
pa_pin_ = pa_pin; CreateDuplexChannels(mclk, bclk, ws, dout, din);
// Do initialize of related interface: data_if, ctrl_if and gpio_if
audio_codec_i2s_cfg_t i2s_cfg = {
.port = I2S_NUM_0,
.rx_handle = rx_handle_,
.tx_handle = tx_handle_,
};
data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
assert(data_if_ != NULL);
// Output
audio_codec_i2c_cfg_t i2c_cfg = {
.port = i2c_port,
.addr = es8388_addr,
.bus_handle = i2c_master_handle,
};
ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
assert(ctrl_if_ != NULL);
gpio_if_ = audio_codec_new_gpio();
assert(gpio_if_ != NULL);
es8388_codec_cfg_t es8388_cfg = {};
es8388_cfg.ctrl_if = ctrl_if_;
es8388_cfg.gpio_if = gpio_if_;
es8388_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
es8388_cfg.master_mode = true;
es8388_cfg.pa_pin = pa_pin;
es8388_cfg.pa_reverted = false;
es8388_cfg.hw_gain.pa_voltage = 5.0;
es8388_cfg.hw_gain.codec_dac_voltage = 3.3;
codec_if_ = es8388_codec_new(&es8388_cfg);
assert(codec_if_ != NULL);
esp_codec_dev_cfg_t outdev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_OUT,
.codec_if = codec_if_,
.data_if = data_if_,
};
output_dev_ = esp_codec_dev_new(&outdev_cfg);
assert(output_dev_ != NULL);
esp_codec_dev_cfg_t indev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_IN,
.codec_if = codec_if_,
.data_if = data_if_,
};
input_dev_ = esp_codec_dev_new(&indev_cfg);
assert(input_dev_ != NULL);
esp_codec_set_disable_when_closed(output_dev_, false);
esp_codec_set_disable_when_closed(input_dev_, false);
ESP_LOGI(TAG, "Es8388AudioCodec initialized");
}
Es8388AudioCodec::~Es8388AudioCodec() {
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
esp_codec_dev_delete(output_dev_);
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
esp_codec_dev_delete(input_dev_);
audio_codec_delete_codec_if(codec_if_);
audio_codec_delete_ctrl_if(ctrl_if_);
audio_codec_delete_gpio_if(gpio_if_);
audio_codec_delete_data_if(data_if_);
}
void Es8388AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din){
assert(input_sample_rate_ == output_sample_rate_);
i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0,
.role = I2S_ROLE_MASTER,
.dma_desc_num = 6,
.dma_frame_num = 240,
.auto_clear_after_cb = true,
.auto_clear_before_cb = false,
.intr_priority = 0,
};
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
i2s_std_config_t std_cfg = {
.clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT,
.ext_clk_freq_hz = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256
},
.slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = I2S_STD_SLOT_BOTH,
.ws_width = I2S_DATA_BIT_WIDTH_16BIT,
.ws_pol = false,
.bit_shift = true,
.left_align = true,
.big_endian = false,
.bit_order_lsb = false
},
.gpio_cfg = {
.mclk = mclk,
.bclk = bclk,
.ws = ws,
.dout = dout,
.din = din,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false
}
}
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
ESP_LOGI(TAG, "Duplex channels created");
}
void Es8388AudioCodec::SetOutputVolume(int volume) {
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
AudioCodec::SetOutputVolume(volume);
}
void Es8388AudioCodec::EnableInput(bool enable) {
if (enable == input_enabled_) {
return;
}
if (enable) {
esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16,
.channel = 1,
.channel_mask = 0,
.sample_rate = (uint32_t)input_sample_rate_,
.mclk_multiple = 0,
};
ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 40.0));
} else {
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
}
AudioCodec::EnableInput(enable);
}
void Es8388AudioCodec::EnableOutput(bool enable) {
if (enable == output_enabled_) {
return;
}
if (enable) {
esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16,
.channel = 1,
.channel_mask = 0,
.sample_rate = (uint32_t)output_sample_rate_,
.mclk_multiple = 0,
};
ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
if (pa_pin_ != GPIO_NUM_NC) {
gpio_set_level(pa_pin_, 1);
}
} else {
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
if (pa_pin_ != GPIO_NUM_NC) {
gpio_set_level(pa_pin_, 0);
}
}
AudioCodec::EnableOutput(enable);
}
int Es8388AudioCodec::Read(int16_t* dest, int samples) {
if (input_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
}
return samples;
}
int Es8388AudioCodec::Write(const int16_t* data, int samples) {
if (output_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
}
return samples;
}

View File

@@ -0,0 +1,37 @@
#ifndef _ES8388_AUDIO_CODEC_H
#define _ES8388_AUDIO_CODEC_H
#include "audio_codec.h"
#include <driver/i2c.h>
#include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h>
class Es8388AudioCodec : public AudioCodec {
private:
const audio_codec_data_if_t* data_if_ = nullptr;
const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
const audio_codec_if_t* codec_if_ = nullptr;
const audio_codec_gpio_if_t* gpio_if_ = nullptr;
esp_codec_dev_handle_t output_dev_ = nullptr;
esp_codec_dev_handle_t input_dev_ = nullptr;
gpio_num_t pa_pin_ = GPIO_NUM_NC;
void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
virtual int Read(int16_t* dest, int samples) override;
virtual int Write(const int16_t* data, int samples) override;
public:
Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
gpio_num_t pa_pin, uint8_t es8388_addr);
virtual ~Es8388AudioCodec();
virtual void SetOutputVolume(int volume) override;
virtual void EnableInput(bool enable) override;
virtual void EnableOutput(bool enable) override;
};
#endif // _ES8388_AUDIO_CODEC_H

View File

@@ -25,7 +25,7 @@ NoAudioCodecDuplex::NoAudioCodecDuplex(int input_sample_rate, int output_sample_
.role = I2S_ROLE_MASTER,
.dma_desc_num = 6,
.dma_frame_num = 240,
.auto_clear_after_cb = false,
.auto_clear_after_cb = true,
.auto_clear_before_cb = false,
.intr_priority = 0,
};
@@ -74,6 +74,64 @@ NoAudioCodecDuplex::NoAudioCodecDuplex(int input_sample_rate, int output_sample_
ESP_LOGI(TAG, "Duplex channels created");
}
ATK_NoAudioCodecDuplex::ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
duplex_ = true;
input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate;
i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0,
.role = I2S_ROLE_MASTER,
.dma_desc_num = 6,
.dma_frame_num = 240,
.auto_clear_after_cb = true,
.auto_clear_before_cb = false,
.intr_priority = 0,
};
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
i2s_std_config_t std_cfg = {
.clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT,
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
#ifdef I2S_HW_VERSION_2
.ext_clk_freq_hz = 0,
#endif
},
.slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = I2S_STD_SLOT_BOTH,
.ws_width = I2S_DATA_BIT_WIDTH_16BIT,
.ws_pol = false,
.bit_shift = true,
#ifdef I2S_HW_VERSION_2
.left_align = true,
.big_endian = false,
.bit_order_lsb = false
#endif
},
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = bclk,
.ws = ws,
.dout = dout,
.din = din,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false
}
}
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
ESP_LOGI(TAG, "Duplex channels created");
}
NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din) {
duplex_ = false;
input_sample_rate_ = input_sample_rate;

View File

@@ -20,6 +20,11 @@ public:
NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
};
class ATK_NoAudioCodecDuplex : public NoAudioCodec {
public:
ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
};
class NoAudioCodecSimplex : public NoAudioCodec {
public:
NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din);

View File

@@ -0,0 +1,206 @@
#include "wifi_board.h"
#include "audio_codec.h"
#include "audio_codecs/no_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "led/single_led.h"
#include "iot/thing_manager.h"
#include "i2c_device.h"
#include <wifi_station.h>
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/timers.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_vendor.h>
#include <esp_lcd_panel_ops.h>
#define TAG "atk_dnesp32s3_box"
LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);
class XL9555 : public I2cDevice {
public:
XL9555(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
WriteReg(0x06, 0x1B);
WriteReg(0x07, 0xFE);
}
void SetOutputState(uint8_t bit, uint8_t level) {
uint16_t data;
if (bit < 8) {
data = ReadReg(0x02);
} else {
data = ReadReg(0x03);
bit -= 8;
}
data = (data & ~(1 << bit)) | (level << bit);
if (bit < 8) {
WriteReg(0x02, data);
} else {
WriteReg(0x03, data);
}
}
};
class atk_dnesp32s3_box : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
i2c_master_dev_handle_t xl9555_handle_;
Button boot_button_;
LcdDisplay* display_;
XL9555* xl9555_;
void InitializeI2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)0,
.sda_io_num = GPIO_NUM_48,
.scl_io_num = GPIO_NUM_45,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
// Initialize XL9555
xl9555_ = new XL9555(i2c_bus_, 0x20);
}
void InitializeATK_ST7789_80_Display() {
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
/* 配置RD引脚 */
gpio_config_t gpio_init_struct;
gpio_init_struct.intr_type = GPIO_INTR_DISABLE;
gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT;
gpio_init_struct.pin_bit_mask = 1ull << LCD_NUM_RD;
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&gpio_init_struct);
gpio_set_level(LCD_NUM_RD, 1);
esp_lcd_i80_bus_handle_t i80_bus = NULL;
esp_lcd_i80_bus_config_t bus_config = {
.dc_gpio_num = LCD_NUM_DC,
.wr_gpio_num = LCD_NUM_WR,
.clk_src = LCD_CLK_SRC_DEFAULT,
.data_gpio_nums = {
GPIO_LCD_D0,
GPIO_LCD_D1,
GPIO_LCD_D2,
GPIO_LCD_D3,
GPIO_LCD_D4,
GPIO_LCD_D5,
GPIO_LCD_D6,
GPIO_LCD_D7,
},
.bus_width = 8,
.max_transfer_bytes = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t),
.psram_trans_align = 64,
.sram_trans_align = 4,
};
ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus));
esp_lcd_panel_io_i80_config_t io_config = {
.cs_gpio_num = LCD_NUM_CS,
.pclk_hz = (10 * 1000 * 1000),
.trans_queue_depth = 10,
.on_color_trans_done = nullptr,
.user_ctx = nullptr,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.dc_levels = {
.dc_idle_level = 0,
.dc_cmd_level = 0,
.dc_dummy_level = 0,
.dc_data_level = 1,
},
.flags = {
.swap_color_bytes = 0,
},
};
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &panel_io));
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = LCD_NUM_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = 16,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, true);
esp_lcd_panel_set_gap(panel, 0, 0);
uint8_t data0[] = {0x00};
uint8_t data1[] = {0x65};
esp_lcd_panel_io_tx_param(panel_io, 0x36, data0, 1);
esp_lcd_panel_io_tx_param(panel_io, 0x3A, data1, 1);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true));
display_ = new LcdDisplay(panel_io, panel, DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY,
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = font_emoji_64_init(),
});
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
});
boot_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
});
boot_button_.OnPressUp([this]() {
Application::GetInstance().StopListening();
});
}
// 物联网初始化,添加对 AI 可见设备
void InitializeIot() {
auto& thing_manager = iot::ThingManager::GetInstance();
thing_manager.AddThing(iot::CreateThing("Speaker"));
}
public:
atk_dnesp32s3_box() : boot_button_(BOOT_BUTTON_GPIO) {
InitializeI2c();
InitializeATK_ST7789_80_Display();
xl9555_->SetOutputState(5, 1);
xl9555_->SetOutputState(7, 1);
InitializeButtons();
InitializeIot();
}
virtual AudioCodec* GetAudioCodec() override {
static ATK_NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN);
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
};
DECLARE_BOARD(atk_dnesp32s3_box);

View File

@@ -0,0 +1,45 @@
#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_I2S_GPIO_WS GPIO_NUM_13
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_21
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_47
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_14
#define BUILTIN_LED_GPIO GPIO_NUM_4
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
#define DISPLAY_SWAP_XY true
#define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
// Pin Definitions
#define LCD_NUM_CS GPIO_NUM_1
#define LCD_NUM_DC GPIO_NUM_2
#define LCD_NUM_RD GPIO_NUM_41
#define LCD_NUM_WR GPIO_NUM_42
#define LCD_NUM_RST GPIO_NUM_NC
#define GPIO_LCD_D0 GPIO_NUM_40
#define GPIO_LCD_D1 GPIO_NUM_39
#define GPIO_LCD_D2 GPIO_NUM_38
#define GPIO_LCD_D3 GPIO_NUM_12
#define GPIO_LCD_D4 GPIO_NUM_11
#define GPIO_LCD_D5 GPIO_NUM_10
#define GPIO_LCD_D6 GPIO_NUM_9
#define GPIO_LCD_D7 GPIO_NUM_46
#endif // _BOARD_CONFIG_H_

View File

@@ -0,0 +1,181 @@
#include "wifi_board.h"
#include "es8388_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "i2c_device.h"
#include "iot/thing_manager.h"
#include "led/single_led.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#include <wifi_station.h>
#define TAG "atk_dnesp32s3"
LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);
class XL9555 : public I2cDevice {
public:
XL9555(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
WriteReg(0x06, 0x03);
WriteReg(0x07, 0xF0);
}
void SetOutputState(uint8_t bit, uint8_t level) {
uint16_t data;
if (bit < 8) {
data = ReadReg(0x02);
} else {
data = ReadReg(0x03);
bit -= 8;
}
data = (data & ~(1 << bit)) | (level << bit);
if (bit < 8) {
WriteReg(0x02, data);
} else {
WriteReg(0x03, data);
}
}
};
class atk_dnesp32s3 : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
Button boot_button_;
LcdDisplay* display_;
XL9555* xl9555_;
void InitializeI2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)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,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
// Initialize XL9555
xl9555_ = new XL9555(i2c_bus_, 0x20);
}
// Initialize spi peripheral
void InitializeSpi() {
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = LCD_MOSI_PIN;
buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = LCD_SCLK_PIN;
buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
});
boot_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
});
boot_button_.OnPressUp([this]() {
Application::GetInstance().StopListening();
});
}
void InitializeSt7789Display() {
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
ESP_LOGD(TAG, "Install panel IO");
// 液晶屏控制IO初始化
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = LCD_CS_PIN;
io_config.dc_gpio_num = LCD_DC_PIN;
io_config.spi_mode = 0;
io_config.pclk_hz = 20 * 1000 * 1000;
io_config.trans_queue_depth = 7;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io);
// 初始化液晶屏驱动芯片ST7789
ESP_LOGD(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_ELEMENT_ORDER_RGB;
panel_config.bits_per_pixel = 16;
panel_config.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel);
esp_lcd_panel_reset(panel);
xl9555_->SetOutputState(8, 1);
xl9555_->SetOutputState(2, 0);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, true);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
display_ = new LcdDisplay(panel_io, panel, DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY,
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = font_emoji_64_init(),
});
}
// 物联网初始化,添加对 AI 可见设备
void InitializeIot() {
auto& thing_manager = iot::ThingManager::GetInstance();
thing_manager.AddThing(iot::CreateThing("Speaker"));
}
public:
atk_dnesp32s3() : boot_button_(BOOT_BUTTON_GPIO) {
InitializeI2c();
InitializeSpi();
InitializeSt7789Display();
InitializeButtons();
InitializeIot();
}
virtual Led* GetLed() override {
static SingleLed led(BUILTIN_LED_GPIO);
return &led;
}
virtual AudioCodec* GetAudioCodec() override {
static Es8388AudioCodec* audio_codec = nullptr;
if (audio_codec == nullptr) {
audio_codec = new Es8388AudioCodec(i2c_bus_, I2C_NUM_0, 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,
GPIO_NUM_NC, AUDIO_CODEC_ES8388_ADDR);
audio_codec->SetOutputVolume(AUDIO_DEFAULT_OUTPUT_VOLUME); //设置默认音量
}
return audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
};
DECLARE_BOARD(atk_dnesp32s3);

View File

@@ -0,0 +1,44 @@
#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_DEFAULT_OUTPUT_VOLUME 90
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_3
#define AUDIO_I2S_GPIO_WS GPIO_NUM_9
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_46
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_14
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_10
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_41
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_42
#define AUDIO_CODEC_ES8388_ADDR ES8388_CODEC_DEFAULT_ADDR
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define BUILTIN_LED_GPIO GPIO_NUM_1
#define LCD_SCLK_PIN GPIO_NUM_12
#define LCD_MOSI_PIN GPIO_NUM_11
#define LCD_MISO_PIN GPIO_NUM_13
#define LCD_DC_PIN GPIO_NUM_40
#define LCD_CS_PIN GPIO_NUM_21
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
#define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY true
#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

@@ -200,7 +200,7 @@ private:
{
.text_font = &font_puhui_16_4,
.icon_font = &font_awesome_16_4,
.emoji_font = emoji_font_init(),
.emoji_font = font_emoji_32_init(),
});
}

View File

@@ -0,0 +1,37 @@
# 编译配置命令
**配置编译目标为 ESP32**
```bash
idf.py set-target esp32
```
**打开 menuconfig**
```bash
idf.py menuconfig
```
**选择板子:**
```
Xiaozhi Assistant -> Board Type -> 面包板 ESP32 DevKit
```
**修改 flash 大小:**
```
Serial flasher config -> Flash size -> 4 MB
```
**修改分区表:**
```
Partition Table -> Custom partition CSV file -> partitions_4M.csv
```
**编译:**
```bash
idf.py build
```

View File

@@ -0,0 +1,36 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
#define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
// 如果使用 Duplex I2S 模式,请注释下面一行
#define AUDIO_I2S_METHOD_SIMPLEX
#ifdef AUDIO_I2S_METHOD_SIMPLEX
#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_25
#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_26
#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_32
#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_33
#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_14
#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_27
#else
#define AUDIO_I2S_GPIO_WS GPIO_NUM_4
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_7
#endif
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define TOUCH_BUTTON_GPIO GPIO_NUM_5
#define BUILTIN_LED_GPIO GPIO_NUM_2
#endif // _BOARD_CONFIG_H_

View File

@@ -0,0 +1,80 @@
#include "wifi_board.h"
#include "audio_codecs/no_audio_codec.h"
#include "system_reset.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "iot/thing_manager.h"
#include "led/single_led.h"
#include <wifi_station.h>
#include <esp_log.h>
#include <driver/i2c_master.h>
#define TAG "ESP32-MarsbearSupport"
class CompactWifiBoard : public WifiBoard {
private:
Button boot_button_;
Button touch_button_;
void InitializeButtons() {
// 配置 GPIO
gpio_config_t io_conf = {
.pin_bit_mask = 1ULL << BUILTIN_LED_GPIO, // 设置需要配置的 GPIO 引脚
.mode = GPIO_MODE_OUTPUT, // 设置为输出模式
.pull_up_en = GPIO_PULLUP_DISABLE, // 禁用上拉
.pull_down_en = GPIO_PULLDOWN_DISABLE, // 禁用下拉
.intr_type = GPIO_INTR_DISABLE // 禁用中断
};
gpio_config(&io_conf); // 应用配置
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
gpio_set_level(BUILTIN_LED_GPIO, 1);
app.ToggleChatState();
});
touch_button_.OnPressDown([this]() {
gpio_set_level(BUILTIN_LED_GPIO, 1);
Application::GetInstance().StartListening();
});
touch_button_.OnPressUp([this]() {
gpio_set_level(BUILTIN_LED_GPIO, 0);
Application::GetInstance().StopListening();
});
}
// 物联网初始化,添加对 AI 可见设备
void InitializeIot() {
auto& thing_manager = iot::ThingManager::GetInstance();
thing_manager.AddThing(iot::CreateThing("Speaker"));
thing_manager.AddThing(iot::CreateThing("Lamp"));
}
public:
CompactWifiBoard() : boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO)
{
InitializeButtons();
InitializeIot();
}
virtual AudioCodec* GetAudioCodec() override
{
#ifdef AUDIO_I2S_METHOD_SIMPLEX
static NoAudioCodecSimplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT, AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN);
#else
static NoAudioCodecDuplex audio_codec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN);
#endif
return &audio_codec;
}
};
DECLARE_BOARD(CompactWifiBoard);

View File

@@ -20,8 +20,8 @@
#define TAG "CompactWifiBoardLCD"
LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);
LV_FONT_DECLARE(font_puhui_16_4);
LV_FONT_DECLARE(font_awesome_16_4);
class CompactWifiBoardLCD : public WifiBoard {
private:
@@ -76,7 +76,11 @@ private:
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
display_ = new LcdDisplay(panel_io, panel, DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY,
&font_puhui_20_4, &font_awesome_20_4);
{
.text_font = &font_puhui_16_4,
.icon_font = &font_awesome_16_4,
.emoji_font = DISPLAY_HEIGHT >= 240 ? font_emoji_64_init() : font_emoji_32_init(),
});
}
@@ -94,8 +98,7 @@ private:
// 物联网初始化,添加对 AI 可见设备
void InitializeIot() {
auto& thing_manager = iot::ThingManager::GetInstance();
thing_manager.AddThing(iot::CreateThing("Lamp"));
//thing_manager.AddThing(iot::CreateThing("Lamp"));
thing_manager.AddThing(iot::CreateThing("Speaker"));
}
public:

View File

@@ -112,16 +112,14 @@ const char* Ml307Board::GetNetworkStateIcon() {
int csq = modem_.GetCsq();
if (csq == -1) {
return FONT_AWESOME_SIGNAL_OFF;
} else if (csq >= 0 && csq <= 9) {
} else if (csq >= 0 && csq <= 14) {
return FONT_AWESOME_SIGNAL_1;
} else if (csq >= 10 && csq <= 14) {
return FONT_AWESOME_SIGNAL_2;
} else if (csq >= 15 && csq <= 19) {
return FONT_AWESOME_SIGNAL_3;
return FONT_AWESOME_SIGNAL_2;
} else if (csq >= 20 && csq <= 24) {
return FONT_AWESOME_SIGNAL_4;
return FONT_AWESOME_SIGNAL_3;
} else if (csq >= 25 && csq <= 31) {
return FONT_AWESOME_SIGNAL_FULL;
return FONT_AWESOME_SIGNAL_4;
}
ESP_LOGW(TAG, "Invalid CSQ: %d", csq);

View File

@@ -165,9 +165,9 @@ const char* WifiBoard::GetNetworkStateIcon() {
return FONT_AWESOME_WIFI_OFF;
}
int8_t rssi = wifi_station.GetRssi();
if (rssi >= -55) {
if (rssi >= -60) {
return FONT_AWESOME_WIFI;
} else if (rssi >= -65) {
} else if (rssi >= -70) {
return FONT_AWESOME_WIFI_FAIR;
} else {
return FONT_AWESOME_WIFI_WEAK;

View File

@@ -126,7 +126,7 @@ private:
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = emoji_font_64_lite_init(),
.emoji_font = font_emoji_64_init(),
});
}

View File

@@ -118,7 +118,7 @@ private:
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = emoji_font_64_lite_init(),
.emoji_font = font_emoji_64_init(),
});
}

View File

@@ -52,7 +52,7 @@ public:
{
.text_font = &font_puhui_30_4,
.icon_font = &font_awesome_30_4,
.emoji_font = emoji_font_64_lite_init(),
.emoji_font = font_emoji_64_init(),
}) {
DisplayLockGuard lock(this);
@@ -105,12 +105,6 @@ private:
}
app.ToggleChatState();
});
boot_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
});
boot_button_.OnPressUp([this]() {
Application::GetInstance().StopListening();
});
}
void InitializeSH8601Display() {

View File

@@ -102,7 +102,7 @@ private:
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = emoji_font_64_lite_init(),
.emoji_font = font_emoji_64_init(),
});
}

View File

@@ -15,8 +15,8 @@
#define TAG "LichuangC3DevBoard"
LV_FONT_DECLARE(font_puhui_16_4);
LV_FONT_DECLARE(font_awesome_16_4);
LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);
class LichuangC3DevBoard : public WifiBoard {
private:
@@ -60,12 +60,6 @@ private:
}
app.ToggleChatState();
});
boot_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
});
boot_button_.OnPressUp([this]() {
Application::GetInstance().StopListening();
});
}
void InitializeSt7789Display() {
@@ -100,9 +94,9 @@ private:
display_ = new LcdDisplay(panel_io, panel, DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY,
{
.text_font = &font_puhui_16_4,
.icon_font = &font_awesome_16_4,
.emoji_font = emoji_font_64_lite_init(),
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = font_emoji_32_init(),
});
}

View File

@@ -78,7 +78,6 @@ private:
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
app.ToggleChatState();
});
boot_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
@@ -123,7 +122,7 @@ private:
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = emoji_font_64_lite_init(),
.emoji_font = font_emoji_64_init(),
});
}

View File

@@ -175,7 +175,7 @@ private:
{
.text_font = &font_puhui_16_4,
.icon_font = &font_awesome_16_4,
.emoji_font = emoji_font_init(),
.emoji_font = font_emoji_32_init(),
});
gpio_config_t config;

View File

@@ -251,7 +251,7 @@ private:
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = emoji_font_64_lite_init(),
.emoji_font = font_emoji_64_init(),
});
}

View File

@@ -29,7 +29,7 @@ public:
{
.text_font = &font_puhui_16_4,
.icon_font = &font_awesome_16_4,
.emoji_font = emoji_font_init(),
.emoji_font = font_emoji_32_init(),
}) {
DisplayLockGuard lock(this);

View File

@@ -30,7 +30,7 @@ public:
{
.text_font = &font_puhui_16_4,
.icon_font = &font_awesome_16_4,
.emoji_font = emoji_font_init(),
.emoji_font = font_emoji_32_init(),
}) {
DisplayLockGuard lock(this);

View File

@@ -34,5 +34,12 @@
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_3
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
#define DISPLAY_SPI_SCLK_PIN GPIO_NUM_16
#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_17
#define DISPLAY_SPI_CS_PIN GPIO_NUM_15
#define DISPLAY_SPI_DC_PIN GPIO_NUM_7
#define DISPLAY_SPI_RESET_PIN GPIO_NUM_18
#define DISPLAY_SPI_SCLK_HZ (40 * 1000 * 1000)
#endif // _BOARD_CONFIG_H_

View File

@@ -25,59 +25,6 @@
LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);
static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = {
// {cmd, { data }, data_size, delay_ms}
{0xef, (uint8_t[]){0x00}, 0, 0},
{0xeb, (uint8_t[]){0x14}, 0, 0},
{0xfe, (uint8_t[]){0x00}, 0, 0},
{0xef, (uint8_t[]){0x00}, 0, 0},
{0xEB, (uint8_t[]){0x14}, 1, 0},
{0x84, (uint8_t[]){0x40}, 1, 0},
{0x85, (uint8_t[]){0xFF}, 1, 0},
{0x86, (uint8_t[]){0xFF}, 1, 0},
{0x87, (uint8_t[]){0xFF}, 1, 0},
{0x88, (uint8_t[]){0x0A}, 1, 0},
{0x89, (uint8_t[]){0x21}, 1, 0},
{0x8A, (uint8_t[]){0x00}, 1, 0},
{0x8B, (uint8_t[]){0x80}, 1, 0},
{0x8C, (uint8_t[]){0x01}, 1, 0},
{0x8D, (uint8_t[]){0x01}, 1, 0},
{0x8E, (uint8_t[]){0xFF}, 1, 0},
{0x8F, (uint8_t[]){0xFF}, 1, 0},
{0xB6, (uint8_t[]){0x00, 0x20,}, 2, 0},
{0x3A, (uint8_t[]){0x05}, 1, 0},
{0x90, (uint8_t[]){0x08, 0x08, 0x08, 0x08}, 4, 0},
{0xBD, (uint8_t[]){0x06}, 1, 0},
{0xBC, (uint8_t[]){0x00}, 1, 0},
{0xFF, (uint8_t[]){0x60, 0x01, 0x04}, 3, 0},
{0xC3, (uint8_t[]){0x13}, 1, 0},
{0xC4, (uint8_t[]){0x13}, 1, 0},
{0xC9, (uint8_t[]){0x22}, 1, 0},
{0xBE, (uint8_t[]){0x11}, 1, 0},
{0xE1, (uint8_t[]){0x10, 0x0E}, 2, 0},
{0xDF, (uint8_t[]){0x21, 0x0c, 0x02}, 3, 0},
{0xF0, (uint8_t[]){0x45, 0x09, 0x08, 0x08, 0x26, 0x2A}, 6, 0},
{0xF1, (uint8_t[]){0x43, 0x70, 0x72, 0x36, 0x37, 0x6F}, 6, 0},
{0xF2, (uint8_t[]){0x45, 0x09, 0x08, 0x08, 0x26, 0x2A}, 6, 0},
{0xF3, (uint8_t[]){0x43, 0x70, 0x72, 0x36, 0x37, 0x6F}, 6, 0},
{0xED, (uint8_t[]){0x1B, 0x0B}, 2, 0},
{0xAE, (uint8_t[]){0x77}, 1, 0},
{0xCD, (uint8_t[]){0x63}, 1, 0},
{0x70, (uint8_t[]){0x07, 0x07, 0x04, 0x0E, 0x0F, 0x09, 0x07, 0x08, 0x03}, 9, 0},
{0xE8, (uint8_t[]){0x34}, 1, 0},
{0x62, (uint8_t[]){0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70}, 12, 0},
{0x63, (uint8_t[]){0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70}, 12, 0},
{0x64, (uint8_t[]){0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07}, 7, 0},
{0x66, (uint8_t[]){0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00}, 10, 0},
{0x67, (uint8_t[]){0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98}, 10, 0},
{0x74, (uint8_t[]){0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00}, 7, 0},
{0x98, (uint8_t[]){0x3e, 0x07}, 2, 0},
{0x35, (uint8_t[]){0x00}, 0, 0},
{0x11, (uint8_t[]){0x00}, 0, 120},
{0x29, (uint8_t[]){0x00}, 0, 20},
// GC9A01_INVON
{0x21, (uint8_t[]){0x00}, 0, 20},
};
class CustomLcdDisplay : public LcdDisplay {
public:
@@ -97,7 +44,7 @@ public:
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = emoji_font_64_lite_init(),
.emoji_font = font_emoji_64_init(),
}) {
DisplayLockGuard lock(this);
@@ -133,47 +80,33 @@ private:
// SPI初始化
void InitializeSpi() {
ESP_LOGI(TAG, "Initialize SPI bus");
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = GPIO_NUM_17;
buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = GPIO_NUM_16;
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);
spi_bus_config_t buscfg = GC9A01_PANEL_BUS_SPI_CONFIG(DISPLAY_SPI_SCLK_PIN, DISPLAY_SPI_MOSI_PIN,
DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t));
ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
// GC9107初始化
void InitializeGc9107Display() {
ESP_LOGI(TAG, "Init GC9107 display");
// GC9A01初始化
void InitializeGc9a01Display() {
ESP_LOGI(TAG, "Init GC9A01 display");
ESP_LOGI(TAG, "Install panel IO");
esp_lcd_panel_io_handle_t io_handle = NULL;
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = GPIO_NUM_15;
io_config.dc_gpio_num = GPIO_NUM_7;
io_config.spi_mode = 0;
io_config.pclk_hz = 40 * 1000 * 1000;
io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
esp_lcd_panel_io_spi_config_t io_config = GC9A01_PANEL_IO_SPI_CONFIG(DISPLAY_SPI_CS_PIN, DISPLAY_SPI_DC_PIN, NULL, NULL);
io_config.pclk_hz = DISPLAY_SPI_SCLK_HZ;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &io_handle));
ESP_LOGI(TAG, "Install GC9A01 panel driver");
esp_lcd_panel_handle_t panel_handle = NULL;
gc9a01_vendor_config_t gc9107_vendor_config = {
.init_cmds = gc9107_lcd_init_cmds,
.init_cmds_size = sizeof(gc9107_lcd_init_cmds) / sizeof(gc9a01_lcd_init_cmd_t),
};
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_18; // Set to -1 if not use
panel_config.rgb_endian = LCD_RGB_ELEMENT_ORDER_BGR;//LCD_RGB_ENDIAN_RGB;
panel_config.bits_per_pixel = 16; // Implemented by LCD command `3Ah` (16/18)
panel_config.vendor_config = &gc9107_vendor_config;
panel_config.reset_gpio_num = DISPLAY_SPI_RESET_PIN; // Set to -1 if not use
panel_config.rgb_endian = LCD_RGB_ENDIAN_BGR; //LCD_RGB_ENDIAN_RGB;
panel_config.bits_per_pixel = 16; // Implemented by LCD command `3Ah` (16/18)
ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true));
ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, true, false));
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
display_ = new LcdDisplay(io_handle, panel_handle, DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT,
@@ -181,7 +114,7 @@ private:
{
.text_font = &font_puhui_20_4,
.icon_font = &font_awesome_20_4,
.emoji_font = emoji_font_64_lite_init(),
.emoji_font = font_emoji_64_init(),
});
}
@@ -193,18 +126,12 @@ private:
}
app.ToggleChatState();
});
boot_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
});
boot_button_.OnPressUp([this]() {
Application::GetInstance().StopListening();
});
}
// 物联网初始化,添加对 AI 可见设备
void InitializeIot() {
auto& thing_manager = iot::ThingManager::GetInstance();
thing_manager.AddThing(iot::CreateThing("Speaker"));
thing_manager.AddThing(iot::CreateThing("Speaker"));
}
public:
@@ -212,7 +139,7 @@ public:
InitializeCodecI2c();
InitializeSpi();
InitializeGc9107Display();
InitializeGc9a01Display();
InitializeButtons();
InitializeIot();
}

View File

@@ -0,0 +1,59 @@
#include "axp2101.h"
#include "board.h"
#include "display.h"
#include <esp_log.h>
#define TAG "Axp2101"
Axp2101::Axp2101(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
// ** EFUSE defaults **
WriteReg(0x22, 0b110); // PWRON > OFFLEVEL as POWEROFF Source enable
WriteReg(0x27, 0x10); // hold 4s to power off
WriteReg(0x93, 0x1C); // 配置 aldo2 输出为 3.3V
uint8_t value = ReadReg(0x90); // XPOWERS_AXP2101_LDO_ONOFF_CTRL0
value = value | 0x02; // set bit 1 (ALDO2)
WriteReg(0x90, value); // and power channels now enabled
WriteReg(0x64, 0x03); // CV charger voltage setting to 4.2V
WriteReg(0x61, 0x05); // set Main battery precharge current to 125mA
WriteReg(0x62, 0x0A); // set Main battery charger current to 400mA ( 0x08-200mA, 0x09-300mA, 0x0A-400mA )
WriteReg(0x63, 0x15); // set Main battery term charge current to 125mA
WriteReg(0x14, 0x00); // set minimum system voltage to 4.1V (default 4.7V), for poor USB cables
WriteReg(0x15, 0x00); // set input voltage limit to 3.88v, for poor USB cables
WriteReg(0x16, 0x05); // set input current limit to 2000mA
WriteReg(0x24, 0x01); // set Vsys for PWROFF threshold to 3.2V (default - 2.6V and kill battery)
WriteReg(0x50, 0x14); // set TS pin to EXTERNAL input (not temperature)
}
int Axp2101::GetBatteryCurrentDirection() {
return (ReadReg(0x01) & 0b01100000) >> 5;
}
bool Axp2101::IsCharging() {
return GetBatteryCurrentDirection() == 1;
}
bool Axp2101::IsDischarging() {
return GetBatteryCurrentDirection() == 2;
}
bool Axp2101::IsChargingDone() {
uint8_t value = ReadReg(0x01);
return (value & 0b00000111) == 0b00000100;
}
int Axp2101::GetBatteryLevel() {
return ReadReg(0xA4);
}
void Axp2101::PowerOff() {
uint8_t value = ReadReg(0x10);
value = value | 0x01;
WriteReg(0x10, value);
}

View File

@@ -0,0 +1,19 @@
#ifndef __AXP2101_H__
#define __AXP2101_H__
#include "i2c_device.h"
class Axp2101 : public I2cDevice {
public:
Axp2101(i2c_master_bus_handle_t i2c_bus, uint8_t addr);
bool IsCharging();
bool IsDischarging();
bool IsChargingDone();
int GetBatteryLevel();
void PowerOff();
private:
int GetBatteryCurrentDirection();
};
#endif

View File

@@ -0,0 +1,41 @@
#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_40
#define AUDIO_I2S_GPIO_WS GPIO_NUM_47
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_38
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_39
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_48
#define AUDIO_CODEC_PA_PIN GPIO_NUM_9
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_42
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_41
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR
#define BUILTIN_LED_GPIO GPIO_NUM_3
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_1
#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_2
#define DISPLAY_SDA_PIN GPIO_NUM_7
#define DISPLAY_SCL_PIN GPIO_NUM_8
#define DISPLAY_WIDTH 128
#define DISPLAY_HEIGHT 64
#define DISPLAY_MIRROR_X false
#define DISPLAY_MIRROR_Y false
#define ML307_RX_PIN GPIO_NUM_5
#define ML307_TX_PIN GPIO_NUM_6
#define AXP2101_I2C_ADDR 0x34
#endif // _BOARD_CONFIG_H_

View File

@@ -0,0 +1,217 @@
#include "ml307_board.h"
#include "audio_codecs/box_audio_codec.h"
#include "display/ssd1306_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "axp2101.h"
#include "iot/thing_manager.h"
#include "led/single_led.h"
#include <esp_log.h>
#include <esp_spiffs.h>
#include <driver/gpio.h>
#include <driver/i2c_master.h>
#include <esp_timer.h>
#define TAG "KevinBoxBoard"
LV_FONT_DECLARE(font_puhui_14_1);
LV_FONT_DECLARE(font_awesome_14_1);
class KevinBoxBoard : public Ml307Board {
private:
i2c_master_bus_handle_t display_i2c_bus_;
i2c_master_bus_handle_t codec_i2c_bus_;
Axp2101* axp2101_ = nullptr;
Button boot_button_;
Button volume_up_button_;
Button volume_down_button_;
uint8_t _data_buffer[2];
esp_timer_handle_t power_save_timer_ = nullptr;
void InitializePowerSaveTimer() {
esp_timer_create_args_t power_save_timer_args = {
.callback = [](void *arg) {
auto board = static_cast<KevinBoxBoard*>(arg);
board->PowerSaveCheck();
},
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "Power Save Timer",
.skip_unhandled_events = false,
};
ESP_ERROR_CHECK(esp_timer_create(&power_save_timer_args, &power_save_timer_));
ESP_ERROR_CHECK(esp_timer_start_periodic(power_save_timer_, 1000000));
}
void PowerSaveCheck() {
// 电池放电模式下,如果待机超过一定时间,则自动关机
const int seconds_to_shutdown = 600;
static int seconds = 0;
if (Application::GetInstance().GetDeviceState() != kDeviceStateIdle) {
seconds = 0;
return;
}
if (!axp2101_->IsDischarging()) {
seconds = 0;
return;
}
if (seconds >= seconds_to_shutdown) {
axp2101_->PowerOff();
}
}
void MountStorage() {
// Mount the storage partition
esp_vfs_spiffs_conf_t conf = {
.base_path = "/storage",
.partition_label = "storage",
.max_files = 5,
.format_if_mount_failed = true,
};
esp_vfs_spiffs_register(&conf);
}
void Enable4GModule() {
// Make GPIO HIGH to enable the 4G module
gpio_config_t ml307_enable_config = {
.pin_bit_mask = (1ULL << 4),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&ml307_enable_config);
gpio_set_level(GPIO_NUM_4, 1);
}
void InitializeDisplayI2c() {
i2c_master_bus_config_t bus_config = {
.i2c_port = (i2c_port_t)0,
.sda_io_num = DISPLAY_SDA_PIN,
.scl_io_num = DISPLAY_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &display_i2c_bus_));
}
void InitializeCodecI2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)1,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_));
}
void InitializeButtons() {
boot_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
});
boot_button_.OnPressUp([this]() {
Application::GetInstance().StopListening();
});
volume_up_button_.OnClick([this]() {
auto codec = GetAudioCodec();
auto volume = codec->output_volume() + 10;
if (volume > 100) {
volume = 100;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("音量 " + std::to_string(volume));
});
volume_up_button_.OnLongPress([this]() {
GetAudioCodec()->SetOutputVolume(100);
GetDisplay()->ShowNotification("最大音量");
});
volume_down_button_.OnClick([this]() {
auto codec = GetAudioCodec();
auto volume = codec->output_volume() - 10;
if (volume < 0) {
volume = 0;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("音量 " + std::to_string(volume));
});
volume_down_button_.OnLongPress([this]() {
GetAudioCodec()->SetOutputVolume(0);
GetDisplay()->ShowNotification("已静音");
});
}
// 物联网初始化,添加对 AI 可见设备
void InitializeIot() {
auto& thing_manager = iot::ThingManager::GetInstance();
thing_manager.AddThing(iot::CreateThing("Speaker"));
}
public:
KevinBoxBoard() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN, 4096),
boot_button_(BOOT_BUTTON_GPIO),
volume_up_button_(VOLUME_UP_BUTTON_GPIO),
volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) {
InitializeDisplayI2c();
InitializeCodecI2c();
axp2101_ = new Axp2101(codec_i2c_bus_, AXP2101_I2C_ADDR);
MountStorage();
Enable4GModule();
InitializeButtons();
InitializePowerSaveTimer();
InitializeIot();
}
virtual Led* GetLed() override {
static SingleLed led(BUILTIN_LED_GPIO);
return &led;
}
virtual AudioCodec* GetAudioCodec() override {
static BoxAudioCodec audio_codec(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 {
static Ssd1306Display display(display_i2c_bus_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y,
&font_puhui_14_1, &font_awesome_14_1);
return &display;
}
virtual bool GetBatteryLevel(int &level, bool& charging) override {
static int last_level = 0;
static bool last_charging = false;
level = axp2101_->GetBatteryLevel();
charging = axp2101_->IsCharging();
if (level != last_level || charging != last_charging) {
last_level = level;
last_charging = charging;
ESP_LOGI(TAG, "Battery level: %d, charging: %d", level, charging);
}
return true;
}
};
DECLARE_BOARD(KevinBoxBoard);

View File

@@ -36,17 +36,21 @@ Display::Display() {
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "Update Display Timer",
.skip_unhandled_events = false,
.skip_unhandled_events = true,
};
ESP_ERROR_CHECK(esp_timer_create(&update_display_timer_args, &update_timer_));
ESP_ERROR_CHECK(esp_timer_start_periodic(update_timer_, 1000000));
}
Display::~Display() {
esp_timer_stop(notification_timer_);
esp_timer_stop(update_timer_);
esp_timer_delete(notification_timer_);
esp_timer_delete(update_timer_);
if (notification_timer_ != nullptr) {
esp_timer_stop(notification_timer_);
esp_timer_delete(notification_timer_);
}
if (update_timer_ != nullptr) {
esp_timer_stop(update_timer_);
esp_timer_delete(update_timer_);
}
if (network_label_ != nullptr) {
lv_obj_del(network_label_);
@@ -54,6 +58,7 @@ Display::~Display() {
lv_obj_del(status_label_);
lv_obj_del(mute_label_);
lv_obj_del(battery_label_);
lv_obj_del(emotion_label_);
}
}
@@ -88,14 +93,16 @@ void Display::Update() {
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
DisplayLockGuard lock(this);
// 如果静音状态改变,则更新图标
if (codec->output_volume() == 0 && !muted_) {
muted_ = true;
lv_label_set_text(mute_label_, FONT_AWESOME_VOLUME_MUTE);
} else if (codec->output_volume() > 0 && muted_) {
muted_ = false;
lv_label_set_text(mute_label_, "");
{
DisplayLockGuard lock(this);
// 如果静音状态改变,则更新图标
if (codec->output_volume() == 0 && !muted_) {
muted_ = true;
lv_label_set_text(mute_label_, FONT_AWESOME_VOLUME_MUTE);
} else if (codec->output_volume() > 0 && muted_) {
muted_ = false;
lv_label_set_text(mute_label_, "");
}
}
// 更新电池图标
@@ -117,16 +124,24 @@ void Display::Update() {
icon = levels[battery_level / 20];
}
if (battery_icon_ != icon) {
DisplayLockGuard lock(this);
battery_icon_ = icon;
lv_label_set_text(battery_label_, battery_icon_);
}
}
// 仅在聊天状态为空闲时,读取网络状态避免升级时占用 UART 资源
// 升级固件时,读取 4G 网络状态避免占用 UART 资源
auto device_state = Application::GetInstance().GetDeviceState();
if (device_state == kDeviceStateIdle || device_state == kDeviceStateStarting) {
static const std::vector<DeviceState> allowed_states = {
kDeviceStateIdle,
kDeviceStateStarting,
kDeviceStateWifiConfiguring,
kDeviceStateListening,
};
if (std::find(allowed_states.begin(), allowed_states.end(), device_state) != allowed_states.end()) {
icon = board.GetNetworkStateIcon();
if (network_icon_ != icon) {
DisplayLockGuard lock(this);
network_icon_ = icon;
lv_label_set_text(network_label_, network_icon_);
}
@@ -167,13 +182,12 @@ void Display::SetEmotion(const std::string &emotion) {
{FONT_AWESOME_EMOJI_SILLY, "silly"},
{FONT_AWESOME_EMOJI_CONFUSED, "confused"}
};
DisplayLockGuard lock(this);
// 查找匹配的表情
auto it = std::find_if(emotions.begin(), emotions.end(),
[&emotion](const Emotion& e) { return e.text == emotion; });
DisplayLockGuard lock(this);
// 如果找到匹配的表情就显示对应图标否则显示默认的neutral表情
if (it != emotions.end()) {
lv_label_set_text(emotion_label_, it->icon);

View File

@@ -3,6 +3,7 @@
#include <lvgl.h>
#include <esp_timer.h>
#include <esp_log.h>
#include <string>
@@ -30,7 +31,7 @@ protected:
int width_ = 0;
int height_ = 0;
lv_disp_t *disp_ = nullptr;
lv_display_t *display_ = nullptr;
lv_obj_t *emotion_label_ = nullptr;
lv_obj_t *network_label_ = nullptr;
@@ -56,7 +57,9 @@ protected:
class DisplayLockGuard {
public:
DisplayLockGuard(Display *display) : display_(display) {
display_->Lock();
if (!display_->Lock(3000)) {
ESP_LOGE("Display", "Failed to lock display");
}
}
~DisplayLockGuard() {
display_->Unlock();

View File

@@ -1,102 +1,27 @@
#include "lcd_display.h"
#include "font_awesome_symbols.h"
#include <font_awesome_symbols.h>
#include <esp_log.h>
#include <esp_err.h>
#include <driver/ledc.h>
#include <vector>
#include <esp_lvgl_port.h>
#include "board.h"
#define TAG "LcdDisplay"
#define LCD_LEDC_CH LEDC_CHANNEL_0
#define LCD_LVGL_TICK_PERIOD_MS 2
#define LCD_LVGL_TASK_MAX_DELAY_MS 60
#define LCD_LVGL_TASK_MIN_DELAY_MS 1
#define LCD_LVGL_TASK_STACK_SIZE (4 * 1024)
#define LCD_LVGL_TASK_PRIORITY 1
LV_FONT_DECLARE(font_awesome_30_4);
static lv_disp_drv_t disp_drv;
static void lcd_lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map)
{
esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t)drv->user_data;
int offsetx1 = area->x1;
int offsetx2 = area->x2;
int offsety1 = area->y1;
int offsety2 = area->y2;
// copy a buffer's content to a specific area of the display
esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, color_map);
lv_disp_flush_ready(&disp_drv);
}
/* Rotate display and touch, when rotated screen in LVGL. Called when driver parameters are updated. */
static void lcd_lvgl_port_update_callback(lv_disp_drv_t *drv)
{
esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t)drv->user_data;
switch (drv->rotated)
{
case LV_DISP_ROT_NONE:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, false);
esp_lcd_panel_mirror(panel_handle, true, false);
break;
case LV_DISP_ROT_90:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, true);
esp_lcd_panel_mirror(panel_handle, true, true);
break;
case LV_DISP_ROT_180:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, false);
esp_lcd_panel_mirror(panel_handle, false, true);
break;
case LV_DISP_ROT_270:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, true);
esp_lcd_panel_mirror(panel_handle, false, false);
break;
}
}
void LcdDisplay::LvglTask() {
ESP_LOGI(TAG, "Starting LVGL task");
uint32_t task_delay_ms = LCD_LVGL_TASK_MAX_DELAY_MS;
while (1)
{
// Lock the mutex due to the LVGL APIs are not thread-safe
if (Lock())
{
task_delay_ms = lv_timer_handler();
Unlock();
}
if (task_delay_ms > LCD_LVGL_TASK_MAX_DELAY_MS)
{
task_delay_ms = LCD_LVGL_TASK_MAX_DELAY_MS;
}
else if (task_delay_ms < LCD_LVGL_TASK_MIN_DELAY_MS)
{
task_delay_ms = LCD_LVGL_TASK_MIN_DELAY_MS;
}
vTaskDelay(pdMS_TO_TICKS(task_delay_ms));
}
}
LcdDisplay::LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
gpio_num_t backlight_pin, bool backlight_output_invert,
int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy,
DisplayFonts fonts)
: panel_io_(panel_io), panel_(panel), backlight_pin_(backlight_pin), backlight_output_invert_(backlight_output_invert),
mirror_x_(mirror_x), mirror_y_(mirror_y), swap_xy_(swap_xy),
fonts_(fonts) {
width_ = width;
height_ = height;
offset_x_ = offset_x;
offset_y_ = offset_y;
InitializeBacklight(backlight_pin);
@@ -112,50 +37,45 @@ LcdDisplay::LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_
ESP_LOGI(TAG, "Initialize LVGL library");
lv_init();
// alloc draw buffers used by LVGL
static lv_disp_draw_buf_t disp_buf; // contains internal graphic buffer(s) called draw buffer(s)
// it's recommended to choose the size of the draw buffer(s) to be at least 1/10 screen sized
lv_color_t *buf1 = (lv_color_t *)heap_caps_malloc(width_ * 10 * sizeof(lv_color_t), MALLOC_CAP_DMA);
assert(buf1);
lv_color_t *buf2 = (lv_color_t *)heap_caps_malloc(width_ * 10 * sizeof(lv_color_t), MALLOC_CAP_DMA);
assert(buf2);
// initialize LVGL draw buffers
lv_disp_draw_buf_init(&disp_buf, buf1, buf2, width_ * 10);
ESP_LOGI(TAG, "Register display driver to LVGL");
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = width_;
disp_drv.ver_res = height_;
disp_drv.offset_x = offset_x_;
disp_drv.offset_y = offset_y_;
disp_drv.flush_cb = lcd_lvgl_flush_cb;
disp_drv.drv_update_cb = lcd_lvgl_port_update_callback;
disp_drv.draw_buf = &disp_buf;
disp_drv.user_data = panel_;
ESP_LOGI(TAG, "Initialize LVGL port");
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
lvgl_port_init(&port_cfg);
lv_disp_drv_register(&disp_drv);
ESP_LOGI(TAG, "Install LVGL tick timer");
// Tick interface for LVGL (using esp_timer to generate 2ms periodic event)
const esp_timer_create_args_t lvgl_tick_timer_args = {
.callback = [](void* arg) {
lv_tick_inc(LCD_LVGL_TICK_PERIOD_MS);
ESP_LOGI(TAG, "Adding LCD screen");
const lvgl_port_display_cfg_t display_cfg = {
.io_handle = panel_io_,
.panel_handle = panel_,
.control_handle = nullptr,
.buffer_size = static_cast<uint32_t>(width_ * 10),
.double_buffer = true,
.trans_size = 0,
.hres = static_cast<uint32_t>(width_),
.vres = static_cast<uint32_t>(height_),
.monochrome = false,
.rotation = {
.swap_xy = swap_xy,
.mirror_x = mirror_x,
.mirror_y = mirror_y,
},
.color_format = LV_COLOR_FORMAT_RGB565,
.flags = {
.buff_dma = 1,
.buff_spiram = 0,
.sw_rotate = 0,
.swap_bytes = 1,
.full_refresh = 0,
.direct_mode = 0,
},
.arg = NULL,
.dispatch_method = ESP_TIMER_TASK,
.name = "LVGL Tick Timer",
.skip_unhandled_events = false
};
ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer_));
ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer_, LCD_LVGL_TICK_PERIOD_MS * 1000));
lvgl_mutex_ = xSemaphoreCreateRecursiveMutex();
assert(lvgl_mutex_ != nullptr);
ESP_LOGI(TAG, "Create LVGL task");
xTaskCreate([](void *arg) {
static_cast<LcdDisplay*>(arg)->LvglTask();
vTaskDelete(NULL);
}, "LVGL", LCD_LVGL_TASK_STACK_SIZE, this, LCD_LVGL_TASK_PRIORITY, NULL);
display_ = lvgl_port_add_disp(&display_cfg);
if (display_ == nullptr) {
ESP_LOGE(TAG, "Failed to add display");
return;
}
lv_display_set_offset(display_, offset_x, offset_y);
SetBacklight(100);
@@ -163,9 +83,7 @@ LcdDisplay::LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_
}
LcdDisplay::~LcdDisplay() {
ESP_ERROR_CHECK(esp_timer_stop(lvgl_tick_timer_));
ESP_ERROR_CHECK(esp_timer_delete(lvgl_tick_timer_));
// 然后再清理 LVGL 对象
if (content_ != nullptr) {
lv_obj_del(content_);
}
@@ -178,6 +96,9 @@ LcdDisplay::~LcdDisplay() {
if (container_ != nullptr) {
lv_obj_del(container_);
}
if (display_ != nullptr) {
lv_display_delete(display_);
}
if (panel_ != nullptr) {
esp_lcd_panel_del(panel_);
@@ -185,7 +106,6 @@ LcdDisplay::~LcdDisplay() {
if (panel_io_ != nullptr) {
esp_lcd_panel_io_del(panel_io_);
}
vSemaphoreDelete(lvgl_mutex_);
}
void LcdDisplay::InitializeBacklight(gpio_num_t backlight_pin) {
@@ -236,14 +156,11 @@ void LcdDisplay::SetBacklight(uint8_t brightness) {
}
bool LcdDisplay::Lock(int timeout_ms) {
// Convert timeout in milliseconds to FreeRTOS ticks
// If `timeout_ms` is set to 0, the program will block until the condition is met
const TickType_t timeout_ticks = (timeout_ms == 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
return xSemaphoreTakeRecursive(lvgl_mutex_, timeout_ticks) == pdTRUE;
return lvgl_port_lock(timeout_ms);
}
void LcdDisplay::Unlock() {
xSemaphoreGiveRecursive(lvgl_mutex_);
lvgl_port_unlock();
}
void LcdDisplay::SetupUI() {
@@ -340,7 +257,7 @@ void LcdDisplay::SetEmotion(const std::string &emotion) {
static const std::vector<Emotion> emotions = {
{"😶", "neutral"},
{"😊", "happy"},
{"🙂", "happy"},
{"😆", "laughing"},
{"😂", "funny"},
{"😔", "sad"},
@@ -348,32 +265,39 @@ void LcdDisplay::SetEmotion(const std::string &emotion) {
{"😭", "crying"},
{"😍", "loving"},
{"😳", "embarrassed"},
{"😲", "surprised"},
{"😨", "shocked"},
{"😯", "surprised"},
{"😱", "shocked"},
{"🤔", "thinking"},
{"😉", "winking"},
{"😎", "cool"},
{"😌", "relaxed"},
{"😋", "delicious"},
{"🤤", "delicious"},
{"😘", "kissy"},
{"😏", "confident"},
{"😴", "sleepy"},
{"🤪", "silly"},
{"😕", "confused"}
{"😜", "silly"},
{"🙄", "confused"}
};
DisplayLockGuard lock(this);
// 查找匹配的表情
auto it = std::find_if(emotions.begin(), emotions.end(),
[&emotion](const Emotion& e) { return e.text == emotion; });
DisplayLockGuard lock(this);
// 如果找到匹配的表情就显示对应图标否则显示默认的neutral表情
lv_obj_set_style_text_font(emotion_label_, fonts_.emoji_font, 0);
if (it != emotions.end()) {
lv_label_set_text(emotion_label_, it->icon);
} else {
lv_label_set_text(emotion_label_, FONT_AWESOME_EMOJI_NEUTRAL);
lv_label_set_text(emotion_label_, "😶");
}
}
void LcdDisplay::SetIcon(const char* icon) {
if (emotion_label_ == nullptr) {
return;
}
DisplayLockGuard lock(this);
lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_4, 0);
lv_label_set_text(emotion_label_, icon);
}

View File

@@ -5,11 +5,14 @@
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <driver/gpio.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <esp_timer.h>
#include <emoji_font.h>
#include <font_emoji.h>
#include <atomic>
class LcdDisplay : public Display {
protected:
@@ -17,14 +20,8 @@ protected:
esp_lcd_panel_handle_t panel_ = nullptr;
gpio_num_t backlight_pin_ = GPIO_NUM_NC;
bool backlight_output_invert_ = false;
bool mirror_x_ = false;
bool mirror_y_ = false;
bool swap_xy_ = false;
int offset_x_ = 0;
int offset_y_ = 0;
SemaphoreHandle_t lvgl_mutex_ = nullptr;
esp_timer_handle_t lvgl_tick_timer_ = nullptr;
lv_draw_buf_t draw_buf_;
lv_obj_t* status_bar_ = nullptr;
lv_obj_t* content_ = nullptr;
lv_obj_t* container_ = nullptr;
@@ -35,7 +32,6 @@ protected:
void InitializeBacklight(gpio_num_t backlight_pin);
void SetBacklight(uint8_t brightness);
void LvglTask();
virtual void SetupUI();
virtual bool Lock(int timeout_ms = 0) override;
@@ -50,6 +46,7 @@ public:
void SetChatMessage(const std::string &role, const std::string &content) override;
void SetEmotion(const std::string &emotion) override;
void SetIcon(const char* icon) override;
};
#endif // LCD_DISPLAY_H

View File

@@ -13,7 +13,7 @@ LV_FONT_DECLARE(font_awesome_30_1);
Ssd1306Display::Ssd1306Display(void* i2c_master_handle, int width, int height, bool mirror_x, bool mirror_y,
const lv_font_t* text_font, const lv_font_t* icon_font)
: mirror_x_(mirror_x), mirror_y_(mirror_y), text_font_(text_font), icon_font_(icon_font) {
: text_font_(text_font), icon_font_(icon_font) {
width_ = width;
height_ = height;
@@ -76,8 +76,8 @@ Ssd1306Display::Ssd1306Display(void* i2c_master_handle, int width, int height, b
.monochrome = true,
.rotation = {
.swap_xy = false,
.mirror_x = mirror_x_,
.mirror_y = mirror_y_,
.mirror_x = mirror_x,
.mirror_y = mirror_y,
},
.flags = {
.buff_dma = 1,
@@ -88,9 +88,8 @@ Ssd1306Display::Ssd1306Display(void* i2c_master_handle, int width, int height, b
},
};
disp_ = lvgl_port_add_disp(&display_cfg);
if (disp_ == nullptr) {
display_ = lvgl_port_add_disp(&display_cfg);
if (display_ == nullptr) {
ESP_LOGE(TAG, "Failed to add display");
return;
}
@@ -136,7 +135,7 @@ void Ssd1306Display::Unlock() {
void Ssd1306Display::SetupUI_128x64() {
DisplayLockGuard lock(this);
auto screen = lv_disp_get_scr_act(disp_);
auto screen = lv_disp_get_scr_act(display_);
lv_obj_set_style_text_font(screen, text_font_, 0);
lv_obj_set_style_text_color(screen, lv_color_black(), 0);
@@ -198,7 +197,7 @@ void Ssd1306Display::SetupUI_128x64() {
void Ssd1306Display::SetupUI_128x32() {
DisplayLockGuard lock(this);
auto screen = lv_disp_get_scr_act(disp_);
auto screen = lv_disp_get_scr_act(display_);
lv_obj_set_style_text_font(screen, text_font_, 0);
/* Container */

View File

@@ -10,8 +10,6 @@ class Ssd1306Display : public Display {
private:
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
esp_lcd_panel_handle_t panel_ = nullptr;
bool mirror_x_ = false;
bool mirror_y_ = false;
lv_obj_t* status_bar_ = nullptr;
lv_obj_t* content_ = nullptr;

View File

@@ -6,17 +6,16 @@ dependencies:
espressif/esp_lcd_ili9341: "==1.2.0"
espressif/esp_lcd_gc9a01: "^2.0.1"
78/esp_lcd_nv3023: "~1.0.0"
78/esp-wifi-connect: "~2.0.2"
78/esp-wifi-connect: "~2.1.0"
78/esp-opus-encoder: "~2.1.0"
78/esp-ml307: "~1.7.1"
78/xiaozhi-fonts: "~1.2.0"
78/emoji_font: "~1.1.0"
78/xiaozhi-fonts: "~1.3.2"
espressif/led_strip: "^2.4.1"
espressif/esp_codec_dev: "~1.3.2"
espressif/esp-sr: "^1.9.0"
espressif/button: "^3.3.1"
lvgl/lvgl: "~8.4.0"
esp_lvgl_port: "~2.4.1"
lvgl/lvgl: "~9.2.2"
esp_lvgl_port: "~2.4.4"
## Required IDF version
idf:
version: ">=5.3"

View File

@@ -40,7 +40,7 @@ void WebsocketProtocol::SendText(const std::string& text) {
}
bool WebsocketProtocol::IsAudioChannelOpened() const {
return websocket_ != nullptr;
return websocket_ != nullptr && websocket_->IsConnected();
}
void WebsocketProtocol::CloseAudioChannel() {

View File

@@ -7,6 +7,8 @@ import struct
import zipfile
import oss2
import json
import requests
from requests.exceptions import RequestException
# 切换到项目根目录
os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -67,13 +69,13 @@ def get_board_name(folder):
return "bread-compact-wifi"
elif "KevinBox1" in basename:
return "kevin-box-1"
if basename.startswith("v0.7") or basename.startswith("v0.8") or basename.startswith("v0.9"):
if basename.startswith("v0.7") or basename.startswith("v0.8") or basename.startswith("v0.9") or basename.startswith("v1."):
return basename.split("_")[1]
raise Exception(f"Unknown board name: {basename}")
def read_binary(dir_path):
merged_bin_path = os.path.join(dir_path, "merged-binary.bin")
data = open(merged_bin_path, "rb").read()[0x200000:]
data = open(merged_bin_path, "rb").read()[0x100000:]
if data[0] != 0xE9:
print(dir_path, "is not a valid image")
return
@@ -105,6 +107,7 @@ def read_binary(dir_path):
"flash_size": flash_size,
"board": get_board_name(dir_path),
"application": desc,
"firmware_size": len(data),
}
def extract_zip(zip_path, extract_path):
@@ -122,45 +125,73 @@ def upload_dir_to_oss(source_dir, target_dir):
print('uploading', oss_key)
bucket.put_object(oss_key, open(os.path.join(source_dir, filename), 'rb'))
def post_info_to_server(info):
"""
将固件信息发送到服务器
Args:
info: 包含固件信息的字典
"""
try:
# 从环境变量获取服务器URL和token
server_url = os.environ.get('VERSIONS_SERVER_URL')
server_token = os.environ.get('VERSIONS_TOKEN')
if not server_url or not server_token:
raise Exception("Missing SERVER_URL or TOKEN in environment variables")
# 准备请求头和数据
headers = {
'Authorization': f'Bearer {server_token}',
'Content-Type': 'application/json'
}
# 发送POST请求
response = requests.post(
server_url,
headers=headers,
json={'jsonData': json.dumps(info)}
)
# 检查响应状态
response.raise_for_status()
print(f"Successfully uploaded version info for tag: {info['tag']}")
except RequestException as e:
if hasattr(e.response, 'json'):
error_msg = e.response.json().get('error', str(e))
else:
error_msg = str(e)
print(f"Failed to upload version info: {error_msg}")
raise
except Exception as e:
print(f"Error uploading version info: {str(e)}")
raise
def main():
release_dir = "releases"
versions = []
# look for zip files startswith "v"
for name in os.listdir(release_dir):
if name.startswith("v") and name.endswith(".zip"):
tag = name[:-4]
folder = os.path.join(release_dir, tag)
if not os.path.exists(folder):
os.makedirs(folder)
extract_zip(os.path.join(release_dir, name), folder)
info_path = os.path.join(folder, "info.json")
if not os.path.exists(info_path):
if not os.path.exists(folder):
os.makedirs(folder)
extract_zip(os.path.join(release_dir, name), folder)
info = read_binary(folder)
target_dir = os.path.join("firmwares", tag)
info["tag"] = tag
info["url"] = os.path.join(os.environ['OSS_BUCKET_URL'], target_dir, "xiaozhi.bin")
open(os.path.join(folder, "info.json"), "w").write(json.dumps(info, indent=4))
open(info_path, "w").write(json.dumps(info, indent=4))
# upload all file to oss
upload_dir_to_oss(folder, target_dir)
# read info.json
info = json.load(open(os.path.join(folder, "info.json")))
versions.append(info)
# sort versions by version
versions.sort(key=lambda x: x["tag"], reverse=True)
# write versions to file
versions_path = os.path.join(release_dir, "versions.json")
open(versions_path, "w").write(json.dumps(versions, indent=4))
print(f"Versions written to {versions_path}")
# copy versions.json to server
versions_config_path = os.environ.get('VERSIONS_CONFIG_PATH')
if not versions_config_path:
print("VERSIONS_CONFIG_PATH is not set")
exit(1)
ret = os.system(f'scp {versions_path} {versions_config_path}')
if ret != 0:
print(f'Failed to copy versions.json to server')
exit(1)
print(f'Copied versions.json to server: {versions_config_path}')
# read info.json
info = json.load(open(info_path))
# post info.json to server
post_info_to_server(info)

View File

@@ -17,21 +17,26 @@ ESP_TASK_WDT_TIMEOUT_S=10
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y
CONFIG_LV_COLOR_16_SWAP=y
CONFIG_LV_MEM_CUSTOM=y
CONFIG_MBEDTLS_DYNAMIC_BUFFER=y
CONFIG_ESP_WIFI_IRAM_OPT=n
CONFIG_ESP_WIFI_RX_IRAM_OPT=n
CONFIG_CODEC_I2C_BACKWARD_COMPATIBLE=n
# LVGL 9.2.2
CONFIG_LV_OS_FREERTOS=y
CONFIG_LV_USE_OS=2
CONFIG_LV_USE_FREERTOS_TASK_NOTIFY=y
CONFIG_LV_USE_CLIB_MALLOC=y
CONFIG_LV_USE_CLIB_STRING=y
CONFIG_LV_USE_CLIB_SPRINTF=y
CONFIG_LV_USE_IMGFONT=y
# Use compressed font
CONFIG_LV_FONT_FMT_TXT_LARGE=y
CONFIG_LV_USE_FONT_COMPRESSED=y
CONFIG_LV_USE_FONT_SUBPX=y
CONFIG_LV_USE_FONT_PLACEHOLDER=y
# Disable extra widgets to save flash size
CONFIG_LV_USE_ANIMIMG=n
@@ -39,18 +44,17 @@ CONFIG_LV_USE_CALENDAR=n
CONFIG_LV_USE_CALENDAR_HEADER_ARROW=n
CONFIG_LV_USE_CALENDAR_HEADER_DROPDOWN=n
CONFIG_LV_USE_CHART=n
CONFIG_LV_USE_COLORWHEEL=n
CONFIG_LV_USE_IMGBTN=n
CONFIG_LV_USE_KEYBOARD=n
CONFIG_LV_USE_LED=n
CONFIG_LV_USE_LIST=n
CONFIG_LV_USE_MENU=n
CONFIG_LV_USE_METER=n
CONFIG_LV_USE_MSGBOX=n
CONFIG_LV_USE_SPAN=n
CONFIG_LV_SPAN_SNIPPET_STACK_SIZE=64
CONFIG_LV_USE_SPINBOX=n
CONFIG_LV_USE_SPINNER=n
CONFIG_LV_USE_TABVIEW=n
CONFIG_LV_USE_TILEVIEW=n
CONFIG_LV_USE_WIN=n
CONFIG_LV_BUILD_EXAMPLES=n

View File

@@ -9,7 +9,7 @@ CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=49152
CONFIG_SPIRAM_MEMTEST=n
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y