Compare commits

...

17 Commits

Author SHA1 Message Date
Xiaoxia
e9de52647c Merge pull request #20 from 78/ui
v0.9.2
2024-11-25 04:48:43 +08:00
Terrence
ff28586c35 add hold to talk test 2024-11-25 04:44:27 +08:00
Terrence
e4382faee3 charger current set to 400mA 2024-11-25 04:33:06 +08:00
Terrence
b07ec1a148 连接wifi时按下boot重置wifi 2024-11-25 02:27:21 +08:00
Terrence
472219d5bf update protocol to support manual response mode 2024-11-25 00:59:03 +08:00
Terrence
aa806f676e fix duplex bug 2024-11-23 16:14:24 +08:00
Xiaoxia
2d7a127c27 Merge pull request #18 from 78/ui
add network error callback
2024-11-20 03:29:43 +08:00
Terrence
c79d6cf4d8 add network error callback 2024-11-20 03:28:52 +08:00
Xiaoxia
bb43cf5876 Merge pull request #17 from 78/ui
add power save timer
2024-11-19 08:56:22 +08:00
Terrence
874adc80b8 add power save timer 2024-11-19 08:50:47 +08:00
Xiaoxia
6dcc64459f Merge pull request #16 from 78/ui
加入中文UI
2024-11-18 20:41:27 +08:00
Terrence
6bfe2719a8 加入中文UI 2024-11-18 06:17:39 +08:00
Terrence
794e6f4bef add websocket protocol 2024-11-16 05:49:35 +08:00
Xiaoxia
cabb29a1bb Merge pull request #15 from 78/mqtt
adjust board structure
2024-11-16 03:27:11 +08:00
Terrence
a494c41367 adjust board structure 2024-11-16 03:25:55 +08:00
Xiaoxia
384426e03a Merge pull request #14 from 78/mqtt
set power safe mode
2024-11-15 23:18:57 +08:00
Terrence
15891f5840 set power safe mode 2024-11-15 23:07:20 +08:00
68 changed files with 59736 additions and 793 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 "0.8.1")
set(PROJECT_VER "0.9.2")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(xiaozhi)

View File

@@ -21,7 +21,7 @@
- Wi-Fi 配网
- 支持 BOOT 键唤醒和打断
- 离线语音唤醒(乐鑫方案)
- 流式语音对话WebSocket 协议)
- 流式语音对话WebSocket 或 UDP 协议)
- 支持国语、粤语、英语、日语、韩语 5 种语言识别SenseVoice 方案)
- 声纹识别(识别是谁在喊 AI 的名字,[3D Speaker 项目](https://github.com/modelscope/3D-Speaker)
- 使用大模型 TTS火山引擎与 CosyVoice 方案)

View File

@@ -1,25 +1,31 @@
set(SOURCES "audio_codec.cc"
set(SOURCES "audio_codecs/audio_codec.cc"
"audio_codecs/no_audio_codec.cc"
"audio_codecs/box_audio_codec.cc"
"display.cc"
"display/display.cc"
"display/no_display.cc"
"display/st7789_display.cc"
"display/ssd1306_display.cc"
"board.cc"
"boards/wifi_board.cc"
"boards/ml307_board.cc"
"protocol.cc"
"protocols/protocol.cc"
"protocols/mqtt_protocol.cc"
"protocols/websocket_protocol.cc"
"system_info.cc"
"system_reset.cc"
"application.cc"
"button.cc"
"led.cc"
"ota.cc"
"settings.cc"
"main.cc"
)
set(INCLUDE_DIRS ".")
set(INCLUDE_DIRS "." "display" "audio_codecs" "protocols")
# 字体
file(GLOB FONT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/fonts/*.c)
list(APPEND SOURCES ${FONT_SOURCES})
list(APPEND INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/fonts)
# 添加板级公共文件
file(GLOB BOARD_COMMON_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/common/*.cc)
list(APPEND SOURCES ${BOARD_COMMON_SOURCES})
list(APPEND INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/boards/common)
# 根据 BOARD_TYPE 配置添加对应的板级文件
if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI)
@@ -28,8 +34,6 @@ elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307)
set(BOARD_TYPE "bread-compact-ml307")
elseif(CONFIG_BOARD_TYPE_ESP_BOX_3)
set(BOARD_TYPE "esp-box-3")
elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_0)
set(BOARD_TYPE "kevin-box-0")
elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_1)
set(BOARD_TYPE "kevin-box-1")
elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_2)

View File

@@ -6,13 +6,26 @@ config OTA_VERSION_URL
help
The application will access this URL to check for updates.
choice CONNECTION_TYPE
prompt "Connection Type"
default CONNECTION_TYPE_MQTT_UDP
help
网络数据传输协议
config CONNECTION_TYPE_MQTT_UDP
bool "MQTT + UDP"
config CONNECTION_TYPE_WEBSOCKET
bool "Websocket"
endchoice
config WEBSOCKET_URL
depends on CONNECTION_TYPE_WEBSOCKET
string "Websocket URL"
default "wss://api.tenclass.net/xiaozhi/v1/"
help
Communication with the server through websocket after wake up.
config WEBSOCKET_ACCESS_TOKEN
depends on CONNECTION_TYPE_WEBSOCKET
string "Websocket Access Token"
default "test-token"
help
@@ -29,8 +42,6 @@ choice BOARD_TYPE
bool "面包板新版接线ML307 AT"
config BOARD_TYPE_ESP_BOX_3
bool "ESP BOX 3"
config BOARD_TYPE_KEVIN_BOX_0
bool "Kevin Box 0"
config BOARD_TYPE_KEVIN_BOX_1
bool "Kevin Box 1"
config BOARD_TYPE_KEVIN_BOX_2

View File

@@ -2,7 +2,9 @@
#include "system_info.h"
#include "ml307_ssl_transport.h"
#include "audio_codec.h"
#include "protocols/mqtt_protocol.h"
#include "mqtt_protocol.h"
#include "websocket_protocol.h"
#include "font_awesome_symbols.h"
#include <cstring>
#include <esp_log.h>
@@ -42,23 +44,31 @@ Application::~Application() {
}
void Application::CheckNewVersion() {
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
// Check if there is a new firmware version available
ota_.SetPostData(Board::GetInstance().GetJson());
ota_.SetPostData(board.GetJson());
while (true) {
if (ota_.CheckVersion()) {
if (ota_.HasNewVersion()) {
// Wait for the chat state to be idle
while (chat_state_ != kChatStateIdle) {
vTaskDelay(100);
}
do {
vTaskDelay(pdMS_TO_TICKS(3000));
} while (GetChatState() != kChatStateIdle);
SetChatState(kChatStateUpgrading);
ota_.StartUpgrade([](int progress, size_t speed) {
display->SetIcon(FONT_AWESOME_DOWNLOAD);
display->SetStatus("新版本 " + ota_.GetFirmwareVersion());
// 预先关闭音频输出,避免升级过程有音频操作
board.GetAudioCodec()->EnableOutput(false);
ota_.StartUpgrade([display](int progress, size_t speed) {
char buffer[64];
snprintf(buffer, sizeof(buffer), "Upgrading...\n %d%% %zuKB/s", progress, speed / 1024);
auto display = Board::GetInstance().GetDisplay();
display->SetText(buffer);
snprintf(buffer, sizeof(buffer), "%d%% %zuKB/s", progress, speed / 1024);
display->SetStatus(buffer);
});
// If upgrade success, the device will reboot and never reach here
@@ -66,6 +76,7 @@ void Application::CheckNewVersion() {
SetChatState(kChatStateIdle);
} else {
ota_.MarkCurrentVersionValid();
display->ShowNotification("版本 " + ota_.GetCurrentVersion());
}
return;
}
@@ -76,9 +87,9 @@ void Application::CheckNewVersion() {
}
void Application::Alert(const std::string&& title, const std::string&& message) {
ESP_LOGE(TAG, "Alert: %s, %s", title.c_str(), message.c_str());
ESP_LOGW(TAG, "Alert: %s, %s", title.c_str(), message.c_str());
auto display = Board::GetInstance().GetDisplay();
display->ShowNotification(std::string(title + "\n" + message));
display->ShowNotification(message);
if (message == "PIN is not ready") {
PlayLocalFile(p3_err_pin_start, p3_err_pin_end - p3_err_pin_start);
@@ -112,20 +123,52 @@ void Application::ToggleChatState() {
Schedule([this]() {
if (chat_state_ == kChatStateIdle) {
SetChatState(kChatStateConnecting);
if (protocol_->OpenAudioChannel()) {
opus_encoder_.ResetState();
SetChatState(kChatStateListening);
} else {
if (!protocol_->OpenAudioChannel()) {
ESP_LOGE(TAG, "Failed to open audio channel");
SetChatState(kChatStateIdle);
return;
}
keep_listening_ = true;
protocol_->SendStartListening(kListeningModeAutoStop);
SetChatState(kChatStateListening);
} else if (chat_state_ == kChatStateSpeaking) {
AbortSpeaking();
AbortSpeaking(kAbortReasonNone);
} else if (chat_state_ == kChatStateListening) {
protocol_->CloseAudioChannel();
}
});
}
void Application::StartListening() {
Schedule([this]() {
keep_listening_ = false;
if (chat_state_ == kChatStateIdle) {
if (!protocol_->IsAudioChannelOpened()) {
SetChatState(kChatStateConnecting);
if (!protocol_->OpenAudioChannel()) {
SetChatState(kChatStateIdle);
ESP_LOGE(TAG, "Failed to open audio channel");
return;
}
}
protocol_->SendStartListening(kListeningModeManualStop);
SetChatState(kChatStateListening);
} else if (chat_state_ == kChatStateSpeaking) {
AbortSpeaking(kAbortReasonNone);
protocol_->SendStartListening(kListeningModeManualStop);
SetChatState(kChatStateListening);
}
});
}
void Application::StopListening() {
Schedule([this]() {
protocol_->SendStopListening();
SetChatState(kChatStateIdle);
});
}
void Application::Start() {
auto& board = Board::GetInstance();
board.Initialize();
@@ -136,7 +179,6 @@ void Application::Start() {
/* Setup the display */
auto display = board.GetDisplay();
display->SetupUI();
/* Setup the audio codec */
auto codec = board.GetAudioCodec();
@@ -229,35 +271,40 @@ void Application::Start() {
auto builtin_led = Board::GetInstance().GetBuiltinLed();
if (chat_state_ == kChatStateListening) {
if (speaking) {
builtin_led->SetRed(32);
builtin_led->SetRed(HIGH_BRIGHTNESS);
} else {
builtin_led->SetRed(8);
builtin_led->SetRed(LOW_BRIGHTNESS);
}
builtin_led->TurnOn();
}
});
});
wake_word_detect_.OnWakeWordDetected([this]() {
Schedule([this]() {
wake_word_detect_.OnWakeWordDetected([this](const std::string& wake_word) {
Schedule([this, &wake_word]() {
if (chat_state_ == kChatStateIdle) {
SetChatState(kChatStateConnecting);
wake_word_detect_.EncodeWakeWordData();
if (protocol_->OpenAudioChannel()) {
std::string opus;
// Encode and send the wake word data to the server
while (wake_word_detect_.GetWakeWordOpus(opus)) {
protocol_->SendAudio(opus);
}
opus_encoder_.ResetState();
// Send a ready message to indicate the server that the wake word data is sent
SetChatState(kChatStateWakeWordDetected);
} else {
if (!protocol_->OpenAudioChannel()) {
ESP_LOGE(TAG, "Failed to open audio channel");
SetChatState(kChatStateIdle);
wake_word_detect_.StartDetection();
return;
}
std::string 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;
SetChatState(kChatStateListening);
} else if (chat_state_ == kChatStateSpeaking) {
AbortSpeaking();
AbortSpeaking(kAbortReasonWakeWordDetected);
}
// Resume detection
@@ -268,68 +315,87 @@ void Application::Start() {
#endif
// Initialize the protocol
display->SetText("Starting\nProtocol...");
display->SetStatus("初始化协议");
#ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET
protocol_ = new WebsocketProtocol();
#else
protocol_ = new MqttProtocol();
#endif
protocol_->OnNetworkError([this](const std::string& message) {
Alert("Error", std::move(message));
});
protocol_->OnIncomingAudio([this](const std::string& data) {
std::lock_guard<std::mutex> lock(mutex_);
audio_decode_queue_.emplace_back(std::move(data));
cv_.notify_all();
});
protocol_->OnAudioChannelOpened([this, codec]() {
if (protocol_->GetServerSampleRate() != codec->output_sample_rate()) {
protocol_->OnAudioChannelOpened([this, codec, &board]() {
if (protocol_->server_sample_rate() != codec->output_sample_rate()) {
ESP_LOGW(TAG, "服务器的音频采样率 %d 与设备输出的采样率 %d 不一致,重采样后可能会失真",
protocol_->GetServerSampleRate(), codec->output_sample_rate());
protocol_->server_sample_rate(), codec->output_sample_rate());
}
SetDecodeSampleRate(protocol_->GetServerSampleRate());
SetDecodeSampleRate(protocol_->server_sample_rate());
board.SetPowerSaveMode(false);
});
protocol_->OnAudioChannelClosed([this]() {
protocol_->OnAudioChannelClosed([this, &board]() {
Schedule([this]() {
SetChatState(kChatStateIdle);
});
board.SetPowerSaveMode(true);
});
protocol_->OnIncomingJson([this](const cJSON* root) {
protocol_->OnIncomingJson([this, display](const cJSON* root) {
// Parse JSON data
auto type = cJSON_GetObjectItem(root, "type");
if (strcmp(type->valuestring, "tts") == 0) {
auto state = cJSON_GetObjectItem(root, "state");
if (strcmp(state->valuestring, "start") == 0) {
Schedule([this]() {
skip_to_end_ = false;
SetChatState(kChatStateSpeaking);
if (chat_state_ == kChatStateIdle || chat_state_ == kChatStateListening) {
skip_to_end_ = false;
opus_decoder_ctl(opus_decoder_, OPUS_RESET_STATE);
SetChatState(kChatStateSpeaking);
}
});
} else if (strcmp(state->valuestring, "stop") == 0) {
Schedule([this]() {
auto codec = Board::GetInstance().GetAudioCodec();
codec->WaitForOutputDone();
if (chat_state_ == kChatStateSpeaking) {
SetChatState(kChatStateListening);
if (keep_listening_) {
protocol_->SendStartListening(kListeningModeAutoStop);
SetChatState(kChatStateListening);
} else {
SetChatState(kChatStateIdle);
}
}
});
} else if (strcmp(state->valuestring, "sentence_start") == 0) {
auto text = cJSON_GetObjectItem(root, "text");
if (text != NULL) {
ESP_LOGI(TAG, ">> %s", text->valuestring);
ESP_LOGI(TAG, "<< %s", text->valuestring);
display->SetChatMessage("assistant", text->valuestring);
}
}
} 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);
}
} else if (strcmp(type->valuestring, "llm") == 0) {
auto emotion = cJSON_GetObjectItem(root, "emotion");
if (emotion != NULL) {
ESP_LOGD(TAG, "EMOTION: %s", emotion->valuestring);
display->SetEmotion(emotion->valuestring);
}
}
});
// Blink the LED to indicate the device is running
display->SetStatus("待命");
builtin_led->SetGreen();
builtin_led->BlinkOnce();
SetChatState(kChatStateIdle);
display->UpdateDisplay();
}
void Application::Schedule(std::function<void()> callback) {
@@ -354,9 +420,9 @@ void Application::MainLoop() {
}
}
void Application::AbortSpeaking() {
void Application::AbortSpeaking(AbortReason reason) {
ESP_LOGI(TAG, "Abort speaking");
protocol_->SendAbort();
protocol_->SendAbortSpeaking(reason);
skip_to_end_ = true;
auto codec = Board::GetInstance().GetAudioCodec();
@@ -370,8 +436,6 @@ void Application::SetChatState(ChatState state) {
"connecting",
"listening",
"speaking",
"wake_word_detected",
"testing",
"upgrading",
"invalid_state"
};
@@ -379,16 +443,15 @@ void Application::SetChatState(ChatState state) {
// No need to update the state
return;
}
chat_state_ = state;
ESP_LOGI(TAG, "STATE: %s", state_str[chat_state_]);
auto display = Board::GetInstance().GetDisplay();
auto builtin_led = Board::GetInstance().GetBuiltinLed();
switch (chat_state_) {
switch (state) {
case kChatStateUnknown:
case kChatStateIdle:
builtin_led->TurnOff();
display->SetText("I'm\nIdle.");
display->SetStatus("待命");
display->SetEmotion("neutral");
#ifdef CONFIG_USE_AFE_SR
audio_processor_.Stop();
#endif
@@ -396,12 +459,14 @@ void Application::SetChatState(ChatState state) {
case kChatStateConnecting:
builtin_led->SetBlue();
builtin_led->TurnOn();
display->SetText("I'm\nConnecting...");
display->SetStatus("连接中...");
break;
case kChatStateListening:
builtin_led->SetRed();
builtin_led->TurnOn();
display->SetText("I'm\nListening...");
display->SetStatus("聆听中...");
display->SetEmotion("neutral");
opus_encoder_.ResetState();
#ifdef CONFIG_USE_AFE_SR
audio_processor_.Start();
#endif
@@ -409,22 +474,22 @@ void Application::SetChatState(ChatState state) {
case kChatStateSpeaking:
builtin_led->SetGreen();
builtin_led->TurnOn();
display->SetText("I'm\nSpeaking...");
display->SetStatus("说话中...");
#ifdef CONFIG_USE_AFE_SR
audio_processor_.Stop();
#endif
break;
case kChatStateWakeWordDetected:
builtin_led->SetBlue();
builtin_led->TurnOn();
break;
case kChatStateUpgrading:
builtin_led->SetGreen();
builtin_led->StartContinuousBlink(100);
break;
default:
ESP_LOGE(TAG, "Invalid chat state: %d", chat_state_);
return;
}
protocol_->SendState(state_str[chat_state_]);
chat_state_ = state;
ESP_LOGI(TAG, "STATE: %s", state_str[chat_state_]);
}
void Application::AudioEncodeTask() {

View File

@@ -22,12 +22,6 @@
#include "audio_processor.h"
#endif
struct BinaryProtocol3 {
uint8_t type;
uint8_t reserved;
uint16_t payload_size;
uint8_t payload[];
} __attribute__((packed));
enum ChatState {
kChatStateUnknown,
@@ -35,7 +29,6 @@ enum ChatState {
kChatStateConnecting,
kChatStateListening,
kChatStateSpeaking,
kChatStateWakeWordDetected,
kChatStateUpgrading
};
@@ -47,17 +40,19 @@ public:
static Application instance;
return instance;
}
// 删除拷贝构造函数和赋值运算符
Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
void Start();
ChatState GetChatState() const { return chat_state_; }
void Schedule(std::function<void()> callback);
void SetChatState(ChatState state);
void Alert(const std::string&& title, const std::string&& message);
void AbortSpeaking();
void AbortSpeaking(AbortReason reason);
void ToggleChatState();
// 删除拷贝构造函数和赋值运算符
Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
void StartListening();
void StopListening();
private:
Application();
@@ -74,6 +69,7 @@ private:
Protocol* protocol_ = nullptr;
EventGroupHandle_t event_group_;
volatile ChatState chat_state_ = kChatStateUnknown;
bool keep_listening_ = false;
bool skip_to_end_ = false;
// Audio encode / decode

View File

@@ -23,6 +23,8 @@ public:
virtual ~AudioCodec();
virtual void SetOutputVolume(int volume);
virtual void EnableInput(bool enable);
virtual void EnableOutput(bool enable);
void Start();
void OnInputData(std::function<void(std::vector<int16_t>&& data)> callback);
@@ -67,8 +69,6 @@ protected:
virtual int Read(int16_t* dest, int samples) = 0;
virtual int Write(const int16_t* data, int samples) = 0;
virtual void EnableInput(bool enable);
virtual void EnableOutput(bool enable);
};
#endif // _AUDIO_CODEC_H

View File

@@ -22,8 +22,6 @@ private:
virtual int Read(int16_t* dest, int samples) override;
virtual int Write(const int16_t* data, int samples) override;
virtual void EnableInput(bool enable) override;
virtual void EnableOutput(bool enable) override;
public:
BoxAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate,
@@ -32,6 +30,8 @@ public:
virtual ~BoxAudioCodec();
virtual void SetOutputVolume(int volume) override;
virtual void EnableInput(bool enable) override;
virtual void EnableOutput(bool enable) override;
};
#endif // _BOX_AUDIO_CODEC_H

View File

@@ -65,8 +65,6 @@ NoAudioCodec::NoAudioCodec(int input_sample_rate, int output_sample_rate, gpio_n
};
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_ERROR_CHECK(i2s_channel_enable(tx_handle_));
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_));
ESP_LOGI(TAG, "Duplex channels created");
}

View File

@@ -1,4 +1,4 @@
#include "boards/ml307_board.h"
#include "ml307_board.h"
#include "audio_codecs/no_audio_codec.h"
#include "display/ssd1306_display.h"
#include "system_reset.h"
@@ -18,6 +18,7 @@ private:
Button boot_button_;
Button volume_up_button_;
Button volume_down_button_;
SystemReset system_reset_;
void InitializeDisplayI2c() {
i2c_master_bus_config_t bus_config = {
@@ -47,13 +48,13 @@ private:
volume = 100;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("Volume\n" + std::to_string(volume));
GetDisplay()->ShowNotification("音量 " + std::to_string(volume));
});
volume_up_button_.OnLongPress([this]() {
auto codec = GetAudioCodec();
codec->SetOutputVolume(100);
GetDisplay()->ShowNotification("Volume\n100");
GetDisplay()->ShowNotification("最大音量");
});
volume_down_button_.OnClick([this]() {
@@ -63,13 +64,13 @@ private:
volume = 0;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("Volume\n" + std::to_string(volume));
GetDisplay()->ShowNotification("音量 " + std::to_string(volume));
});
volume_down_button_.OnLongPress([this]() {
auto codec = GetAudioCodec();
codec->SetOutputVolume(0);
GetDisplay()->ShowNotification("Volume\n0");
GetDisplay()->ShowNotification("已静音");
});
}
@@ -77,13 +78,14 @@ public:
CompactMl307Board() : 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) {
volume_down_button_(VOLUME_DOWN_BUTTON_GPIO),
system_reset_(RESET_NVS_BUTTON_GPIO, RESET_FACTORY_BUTTON_GPIO) {
}
virtual void Initialize() override {
ESP_LOGI(TAG, "Initializing CompactMl307Board");
// Check if the reset button is pressed
SystemReset::GetInstance().CheckButtons();
system_reset_.CheckButtons();
InitializeDisplayI2c();
InitializeButtons();

View File

@@ -31,6 +31,8 @@
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40
#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39
#define RESET_NVS_BUTTON_GPIO GPIO_NUM_1
#define RESET_FACTORY_BUTTON_GPIO GPIO_NUM_2
#define DISPLAY_SDA_PIN GPIO_NUM_41
#define DISPLAY_SCL_PIN GPIO_NUM_42

View File

@@ -1,4 +1,4 @@
#include "boards/wifi_board.h"
#include "wifi_board.h"
#include "audio_codecs/no_audio_codec.h"
#include "display/ssd1306_display.h"
#include "system_reset.h"
@@ -7,6 +7,7 @@
#include "led.h"
#include "config.h"
#include <wifi_station.h>
#include <esp_log.h>
#include <driver/i2c_master.h>
@@ -18,6 +19,7 @@ private:
Button boot_button_;
Button volume_up_button_;
Button volume_down_button_;
SystemReset system_reset_;
void InitializeDisplayI2c() {
i2c_master_bus_config_t bus_config = {
@@ -37,7 +39,11 @@ private:
void InitializeButtons() {
boot_button_.OnClick([this]() {
Application::GetInstance().ToggleChatState();
auto& app = Application::GetInstance();
if (app.GetChatState() == kChatStateUnknown && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
app.ToggleChatState();
});
volume_up_button_.OnClick([this]() {
@@ -47,13 +53,13 @@ private:
volume = 100;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("Volume\n" + std::to_string(volume));
GetDisplay()->ShowNotification("音量 " + std::to_string(volume));
});
volume_up_button_.OnLongPress([this]() {
auto codec = GetAudioCodec();
codec->SetOutputVolume(100);
GetDisplay()->ShowNotification("Volume\n100");
GetDisplay()->ShowNotification("最大音量");
});
volume_down_button_.OnClick([this]() {
@@ -63,13 +69,13 @@ private:
volume = 0;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("Volume\n" + std::to_string(volume));
GetDisplay()->ShowNotification("音量 " + std::to_string(volume));
});
volume_down_button_.OnLongPress([this]() {
auto codec = GetAudioCodec();
codec->SetOutputVolume(0);
GetDisplay()->ShowNotification("Volume\n0");
GetDisplay()->ShowNotification("已静音");
});
}
@@ -77,13 +83,14 @@ public:
CompactWifiBoard() :
boot_button_(BOOT_BUTTON_GPIO),
volume_up_button_(VOLUME_UP_BUTTON_GPIO),
volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) {
volume_down_button_(VOLUME_DOWN_BUTTON_GPIO),
system_reset_(RESET_NVS_BUTTON_GPIO, RESET_FACTORY_BUTTON_GPIO) {
}
virtual void Initialize() override {
ESP_LOGI(TAG, "Initializing CompactWifiBoard");
// Check if the reset button is pressed
SystemReset::GetInstance().CheckButtons();
system_reset_.CheckButtons();
InitializeDisplayI2c();
InitializeButtons();

View File

@@ -32,6 +32,8 @@
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40
#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39
#define RESET_NVS_BUTTON_GPIO GPIO_NUM_1
#define RESET_FACTORY_BUTTON_GPIO GPIO_NUM_2
#define DISPLAY_SDA_PIN GPIO_NUM_41
#define DISPLAY_SCL_PIN GPIO_NUM_42

View File

@@ -41,8 +41,10 @@ public:
virtual Mqtt* CreateMqtt() = 0;
virtual Udp* CreateUdp() = 0;
virtual bool GetNetworkState(std::string& network_name, int& signal_quality, std::string& signal_quality_text) = 0;
virtual const char* GetNetworkStateIcon() = 0;
virtual bool GetBatteryLevel(int &level, bool& charging);
virtual std::string GetJson();
virtual void SetPowerSaveMode(bool enabled) = 0;
};
#define DECLARE_BOARD(BOARD_CLASS_NAME) \

View File

@@ -30,15 +30,28 @@ Button::~Button() {
}
}
void Button::OnPress(std::function<void()> callback) {
void Button::OnPressDown(std::function<void()> callback) {
if (button_handle_ == nullptr) {
return;
}
on_press_ = callback;
on_press_down_ = callback;
iot_button_register_cb(button_handle_, BUTTON_PRESS_DOWN, [](void* handle, void* usr_data) {
Button* button = static_cast<Button*>(usr_data);
if (button->on_press_) {
button->on_press_();
if (button->on_press_down_) {
button->on_press_down_();
}
}, this);
}
void Button::OnPressUp(std::function<void()> callback) {
if (button_handle_ == nullptr) {
return;
}
on_press_up_ = callback;
iot_button_register_cb(button_handle_, BUTTON_PRESS_UP, [](void* handle, void* usr_data) {
Button* button = static_cast<Button*>(usr_data);
if (button->on_press_up_) {
button->on_press_up_();
}
}, this);
}

View File

@@ -10,7 +10,8 @@ public:
Button(gpio_num_t gpio_num);
~Button();
void OnPress(std::function<void()> callback);
void OnPressDown(std::function<void()> callback);
void OnPressUp(std::function<void()> callback);
void OnLongPress(std::function<void()> callback);
void OnClick(std::function<void()> callback);
void OnDoubleClick(std::function<void()> callback);
@@ -19,7 +20,8 @@ private:
button_handle_t button_handle_;
std::function<void()> on_press_;
std::function<void()> on_press_down_;
std::function<void()> on_press_up_;
std::function<void()> on_long_press_;
std::function<void()> on_click_;
std::function<void()> on_double_click_;

View File

@@ -0,0 +1,31 @@
#include "i2c_device.h"
#include <esp_log.h>
#define TAG "I2cDevice"
I2cDevice::I2cDevice(i2c_master_bus_handle_t i2c_bus, uint8_t addr) {
i2c_device_config_t i2c_device_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = addr,
.scl_speed_hz = 100000,
.scl_wait_us = 0,
.flags = {
.disable_ack_check = 0,
},
};
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus, &i2c_device_cfg, &i2c_device_));
assert(i2c_device_ != NULL);
}
void I2cDevice::WriteReg(uint8_t reg, uint8_t value) {
uint8_t buffer[2] = {reg, value};
ESP_ERROR_CHECK(i2c_master_transmit(i2c_device_, buffer, 2, 100));
}
uint8_t I2cDevice::ReadReg(uint8_t reg) {
uint8_t buffer[1];
ESP_ERROR_CHECK(i2c_master_transmit_receive(i2c_device_, &reg, 1, buffer, 1, 100));
return buffer[0];
}

View File

@@ -0,0 +1,17 @@
#ifndef I2C_DEVICE_H
#define I2C_DEVICE_H
#include <driver/i2c_master.h>
class I2cDevice {
public:
I2cDevice(i2c_master_bus_handle_t i2c_bus, uint8_t addr);
protected:
i2c_master_dev_handle_t i2c_device_;
void WriteReg(uint8_t reg, uint8_t value);
uint8_t ReadReg(uint8_t reg);
};
#endif // I2C_DEVICE_H

View File

@@ -11,7 +11,9 @@
#define BLINK_TASK_STOPPED_BIT BIT0
#define BLINK_TASK_RUNNING_BIT BIT1
#define DEFAULT_BRIGHTNESS 16
#define DEFAULT_BRIGHTNESS 4
#define HIGH_BRIGHTNESS 16
#define LOW_BRIGHTNESS 2
class Led {
public:

View File

@@ -1,5 +1,6 @@
#include "ml307_board.h"
#include "application.h"
#include "font_awesome_symbols.h"
#include <esp_log.h>
#include <esp_timer.h>
@@ -34,7 +35,7 @@ Ml307Board::Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, size_t rx_buffer_si
void Ml307Board::StartNetwork() {
auto display = Board::GetInstance().GetDisplay();
display->SetText(std::string("Starting modem"));
display->SetStatus("初始化模块");
modem_.SetDebug(false);
modem_.SetBaudRate(921600);
@@ -54,7 +55,7 @@ void Ml307Board::StartNetwork() {
void Ml307Board::WaitForNetworkReady() {
auto& application = Application::GetInstance();
auto display = Board::GetInstance().GetDisplay();
display->SetText(std::string("Wait for network\n"));
display->SetStatus("等待网络...");
int result = modem_.WaitForNetworkReady();
if (result == -1) {
application.Alert("Error", "PIN is not ready");
@@ -103,6 +104,29 @@ bool Ml307Board::GetNetworkState(std::string& network_name, int& signal_quality,
return signal_quality != -1;
}
const char* Ml307Board::GetNetworkStateIcon() {
if (!modem_.network_ready()) {
return FONT_AWESOME_SIGNAL_OFF;
}
int csq = modem_.GetCsq();
if (csq == -1) {
return FONT_AWESOME_SIGNAL_OFF;
} else if (csq >= 0 && csq <= 9) {
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;
} else if (csq >= 20 && csq <= 24) {
return FONT_AWESOME_SIGNAL_4;
} else if (csq >= 25 && csq <= 31) {
return FONT_AWESOME_SIGNAL_FULL;
}
ESP_LOGW(TAG, "Invalid CSQ: %d", csq);
return FONT_AWESOME_SIGNAL_OFF;
}
std::string Ml307Board::GetBoardJson() {
// Set the board type for OTA
std::string board_type = BOARD_TYPE;
@@ -114,3 +138,7 @@ std::string Ml307Board::GetBoardJson() {
board_json += "\"iccid\":\"" + modem_.GetIccid() + "\"}";
return board_json;
}
void Ml307Board::SetPowerSaveMode(bool enabled) {
// TODO: Implement power save mode for ML307
}

View File

@@ -20,6 +20,8 @@ public:
virtual Mqtt* CreateMqtt() override;
virtual Udp* CreateUdp() override;
virtual bool GetNetworkState(std::string& network_name, int& signal_quality, std::string& signal_quality_text) override;
virtual const char* GetNetworkStateIcon() override;
virtual void SetPowerSaveMode(bool enabled) override;
};
#endif // ML307_BOARD_H

View File

@@ -11,12 +11,12 @@
#define TAG "SystemReset"
SystemReset::SystemReset() {
SystemReset::SystemReset(gpio_num_t reset_nvs_pin, gpio_num_t reset_factory_pin) : reset_nvs_pin_(reset_nvs_pin), reset_factory_pin_(reset_factory_pin) {
// Configure GPIO1, GPIO2 as INPUT, reset NVS flash if the button is pressed
gpio_config_t io_conf;
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pin_bit_mask = (1ULL << GPIO_NUM_1) | (1ULL << GPIO_NUM_2);
io_conf.pin_bit_mask = (1ULL << reset_nvs_pin_) | (1ULL << reset_factory_pin_);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&io_conf);
@@ -24,13 +24,13 @@ SystemReset::SystemReset() {
void SystemReset::CheckButtons() {
if (gpio_get_level(GPIO_NUM_2) == 0) {
if (gpio_get_level(reset_factory_pin_) == 0) {
ESP_LOGI(TAG, "Button is pressed, reset to factory");
ResetNvsFlash();
ResetToFactory();
}
if (gpio_get_level(GPIO_NUM_1) == 0) {
if (gpio_get_level(reset_nvs_pin_) == 0) {
ESP_LOGI(TAG, "Button is pressed, reset NVS flash");
ResetNvsFlash();
}

View File

@@ -0,0 +1,21 @@
#ifndef _SYSTEM_RESET_H
#define _SYSTEM_RESET_H
#include <driver/gpio.h>
class SystemReset {
public:
SystemReset(gpio_num_t reset_nvs_pin, gpio_num_t reset_factory_pin); // 构造函数私有化
void CheckButtons();
private:
gpio_num_t reset_nvs_pin_;
gpio_num_t reset_factory_pin_;
void ResetNvsFlash();
void ResetToFactory();
void RestartInSeconds(int seconds);
};
#endif

View File

@@ -1,6 +1,8 @@
#include "wifi_board.h"
#include "application.h"
#include "system_info.h"
#include "font_awesome_symbols.h"
#include "settings.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
@@ -38,7 +40,7 @@ void WifiBoard::StartNetwork() {
// Try to connect to WiFi, if failed, launch the WiFi configuration AP
auto& wifi_station = WifiStation::GetInstance();
display->SetText(std::string("Connect to WiFi\n") + wifi_station.GetSsid());
display->SetStatus(std::string("正在连接 ") + wifi_station.GetSsid());
wifi_station.Start();
if (!wifi_station.IsConnected()) {
builtin_led->SetBlue();
@@ -46,10 +48,18 @@ void WifiBoard::StartNetwork() {
auto& wifi_ap = WifiConfigurationAp::GetInstance();
wifi_ap.SetSsidPrefix("Xiaozhi");
wifi_ap.Start();
// 播报配置 WiFi 的提示
application.Alert("Info", "Configuring WiFi");
// 显示 WiFi 配置 AP 的 SSID 和 Web 服务器 URL
display->SetText(wifi_ap.GetSsid() + "\n" + wifi_ap.GetWebServerUrl());
std::string hint = "请在手机上连接热点 ";
hint += wifi_ap.GetSsid();
hint += ",然后打开浏览器访问 ";
hint += wifi_ap.GetWebServerUrl();
display->SetStatus(hint);
// Wait forever until reset after configuration
while (true) {
vTaskDelay(pdMS_TO_TICKS(1000));
@@ -66,12 +76,15 @@ Http* WifiBoard::CreateHttp() {
}
WebSocket* WifiBoard::CreateWebSocket() {
#ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET
std::string url = CONFIG_WEBSOCKET_URL;
if (url.find("wss://") == 0) {
return new WebSocket(new TlsTransport());
} else {
return new WebSocket(new TcpTransport());
}
return new WebSocket(new TlsTransport());
} else {
return new WebSocket(new TcpTransport());
}
#endif
return nullptr;
}
Mqtt* WifiBoard::CreateMqtt() {
@@ -100,6 +113,24 @@ bool WifiBoard::GetNetworkState(std::string& network_name, int& signal_quality,
return signal_quality != -1;
}
const char* WifiBoard::GetNetworkStateIcon() {
if (wifi_config_mode_) {
return FONT_AWESOME_WIFI;
}
auto& wifi_station = WifiStation::GetInstance();
if (!wifi_station.IsConnected()) {
return FONT_AWESOME_WIFI_OFF;
}
int8_t rssi = wifi_station.GetRssi();
if (rssi >= -55) {
return FONT_AWESOME_WIFI;
} else if (rssi >= -65) {
return FONT_AWESOME_WIFI_FAIR;
} else {
return FONT_AWESOME_WIFI_WEAK;
}
}
std::string WifiBoard::GetBoardJson() {
// Set the board type for OTA
auto& wifi_station = WifiStation::GetInstance();
@@ -114,3 +145,20 @@ std::string WifiBoard::GetBoardJson() {
board_json += "\"mac\":\"" + SystemInfo::GetMacAddress() + "\"}";
return board_json;
}
void WifiBoard::SetPowerSaveMode(bool enabled) {
auto& wifi_station = WifiStation::GetInstance();
wifi_station.SetPowerSaveMode(enabled);
}
void WifiBoard::ResetWifiConfiguration() {
// Reset the wifi station
{
Settings settings("wifi", true);
settings.EraseAll();
}
GetDisplay()->ShowNotification("已重置 WiFi...");
vTaskDelay(pdMS_TO_TICKS(1000));
// Reboot the device
esp_restart();
}

View File

@@ -17,6 +17,9 @@ public:
virtual Mqtt* CreateMqtt() override;
virtual Udp* CreateUdp() override;
virtual bool GetNetworkState(std::string& network_name, int& signal_quality, std::string& signal_quality_text) override;
virtual const char* GetNetworkStateIcon() override;
virtual void SetPowerSaveMode(bool enabled) override;
virtual void ResetWifiConfiguration();
};
#endif // WIFI_BOARD_H

View File

@@ -1,4 +1,4 @@
#include "boards/wifi_board.h"
#include "wifi_board.h"
#include "audio_codecs/box_audio_codec.h"
#include "display/no_display.h"
#include "application.h"

View File

@@ -1,39 +0,0 @@
#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_0
#define AUDIO_I2S_GPIO_WS GPIO_NUM_47
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_48
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_45
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_21
#define AUDIO_CODEC_PA_PIN GPIO_NUM_40
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_39
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_38
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR
#define BUILTIN_LED_GPIO GPIO_NUM_8
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_6
#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_7
#define DISPLAY_SDA_PIN GPIO_NUM_4
#define DISPLAY_SCL_PIN GPIO_NUM_5
#define DISPLAY_WIDTH 128
#define DISPLAY_HEIGHT 64
#define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y true
#define ML307_RX_PIN GPIO_NUM_17
#define ML307_TX_PIN GPIO_NUM_16
#endif // _BOARD_CONFIG_H_

View File

@@ -1,156 +0,0 @@
#include "boards/ml307_board.h"
#include "audio_codecs/box_audio_codec.h"
#include "display/ssd1306_display.h"
#include "application.h"
#include "button.h"
#include "led.h"
#include "config.h"
#include <esp_log.h>
#include <esp_spiffs.h>
#include <driver/gpio.h>
#include <driver/i2c_master.h>
static const char *TAG = "KevinBoxBoard";
class KevinBoxBoard : public Ml307Board {
private:
i2c_master_bus_handle_t display_i2c_bus_;
i2c_master_bus_handle_t codec_i2c_bus_;
Button boot_button_;
Button volume_up_button_;
Button volume_down_button_;
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 GPIO15 HIGH to enable the 4G module
gpio_config_t ml307_enable_config = {
.pin_bit_mask = (1ULL << 15),
.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_15, 1);
}
void InitializeDisplayI2c() {
i2c_master_bus_config_t bus_config = {
.i2c_port = I2C_NUM_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_NUM_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_.OnClick([this]() {
Application::GetInstance().ToggleChatState();
});
volume_up_button_.OnClick([this]() {
auto codec = GetAudioCodec();
auto volume = codec->output_volume() + 10;
if (volume > 100) {
volume = 100;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("Volume\n" + std::to_string(volume));
});
volume_up_button_.OnLongPress([this]() {
auto codec = GetAudioCodec();
codec->SetOutputVolume(100);
GetDisplay()->ShowNotification("Volume\n100");
});
volume_down_button_.OnClick([this]() {
auto codec = GetAudioCodec();
auto volume = codec->output_volume() - 10;
if (volume < 0) {
volume = 0;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("Volume\n" + std::to_string(volume));
});
volume_down_button_.OnLongPress([this]() {
auto codec = GetAudioCodec();
codec->SetOutputVolume(0);
GetDisplay()->ShowNotification("Volume\n0");
});
}
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) {
}
virtual void Initialize() override {
ESP_LOGI(TAG, "Initializing KevinBoxBoard");
InitializeDisplayI2c();
InitializeCodecI2c();
MountStorage();
Enable4GModule();
InitializeButtons();
Ml307Board::Initialize();
}
virtual Led* GetBuiltinLed() override {
static Led 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);
return &display;
}
};
DECLARE_BOARD(KevinBoxBoard);

View File

@@ -1,4 +1,4 @@
#include "boards/ml307_board.h"
#include "ml307_board.h"
#include "audio_codecs/box_audio_codec.h"
#include "display/ssd1306_display.h"
#include "application.h"
@@ -91,13 +91,13 @@ private:
volume = 100;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("Volume\n" + std::to_string(volume));
GetDisplay()->ShowNotification("音量 " + std::to_string(volume));
});
volume_up_button_.OnLongPress([this]() {
auto codec = GetAudioCodec();
codec->SetOutputVolume(100);
GetDisplay()->ShowNotification("Volume\n100");
GetDisplay()->ShowNotification("最大音量");
});
volume_down_button_.OnClick([this]() {
@@ -107,13 +107,13 @@ private:
volume = 0;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("Volume\n" + std::to_string(volume));
GetDisplay()->ShowNotification("音量 " + std::to_string(volume));
});
volume_down_button_.OnLongPress([this]() {
auto codec = GetAudioCodec();
codec->SetOutputVolume(0);
GetDisplay()->ShowNotification("Volume\n0");
GetDisplay()->ShowNotification("已静音");
});
}

View File

@@ -1,70 +1,46 @@
#include "axp2101.h"
#include "board.h"
#include "display.h"
#include <esp_log.h>
static const char *TAG = "AXP2101";
#define TAG "Axp2101"
bool Axp2101::Initialize(i2c_master_bus_handle_t i2c_bus, int i2c_device_address) {
i2c_device_config_t axp2101_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = (uint16_t)i2c_device_address,
.scl_speed_hz = 100000,
.scl_wait_us = 0,
.flags = {
.disable_ack_check = 0,
},
};
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus, &axp2101_cfg, &i2c_device_));
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
WriteReg(0x93, 0x1C); // 配置 aldo2 输出为 3.3V
uint8_t value = ReadReg(0x90); // XPOWERS_AXP2101_LDO_ONOFF_CTRL0
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 42V
value = ReadReg(0x62);
ESP_LOGI(TAG, "axp2101 read 0x62 get: 0x%X", value);
WriteReg(0x64, 0x03); // CV charger voltage setting to 4.2V
WriteReg(0x61, 0x05); // set Main battery precharge current to 125mA
WriteReg(0x62, 0x10); // set Main battery charger current to 900mA ( 0x08-200mA, 0x09-300mA, 0x0A-400mA )
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
value = ReadReg(0x62);
ESP_LOGI(TAG, "axp2101 read 0x62 get: 0x%X", value);
value = ReadReg(0x18);
ESP_LOGI(TAG, "axp2101 read 0x18 get: 0x%X", value);
value = value & 0b11100000;
value = value | 0b00001110;
WriteReg(0x18, value);
value = ReadReg(0x18);
ESP_LOGI(TAG, "axp2101 read 0x18 get: 0x%X", value);
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 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)
return true;
}
void Axp2101::WriteReg(uint8_t reg, uint8_t value) {
uint8_t buffer[2];
buffer[0] = reg;
buffer[1] = value;
ESP_ERROR_CHECK(i2c_master_transmit(i2c_device_, buffer, 2, 100));
}
uint8_t Axp2101::ReadReg(uint8_t reg) {
uint8_t buffer[1];
ESP_ERROR_CHECK(i2c_master_transmit_receive(i2c_device_, &reg, 1, buffer, 1, 100));
return buffer[0];
int Axp2101::GetBatteryCurrentDirection() {
return (ReadReg(0x01) & 0b01100000) >> 5;
}
bool Axp2101::IsCharging() {
uint8_t value = ReadReg(0x01);
return (value & 0b01100000) == 0b00100000;
return GetBatteryCurrentDirection() == 1;
}
bool Axp2101::IsDischarging() {
return GetBatteryCurrentDirection() == 2;
}
bool Axp2101::IsChargingDone() {
@@ -73,8 +49,7 @@ bool Axp2101::IsChargingDone() {
}
int Axp2101::GetBatteryLevel() {
uint8_t value = ReadReg(0xA4);
return value;
return ReadReg(0xA4);
}
void Axp2101::PowerOff() {

View File

@@ -1,22 +1,19 @@
#ifndef __AXP2101_H__
#define __AXP2101_H__
#include <driver/i2c_master.h>
#include "i2c_device.h"
class Axp2101 {
class Axp2101 : public I2cDevice {
public:
Axp2101() = default;
bool Initialize(i2c_master_bus_handle_t i2c_bus, int i2c_device_address);
Axp2101(i2c_master_bus_handle_t i2c_bus, uint8_t addr);
bool IsCharging();
bool IsDischarging();
bool IsChargingDone();
int GetBatteryLevel();
void PowerOff();
private:
i2c_master_dev_handle_t i2c_device_ = nullptr;
void WriteReg(uint8_t reg, uint8_t value);
uint8_t ReadReg(uint8_t reg);
int GetBatteryCurrentDirection();
};
#endif

View File

@@ -1,4 +1,4 @@
#include "boards/ml307_board.h"
#include "ml307_board.h"
#include "audio_codecs/box_audio_codec.h"
#include "display/ssd1306_display.h"
#include "application.h"
@@ -11,6 +11,7 @@
#include <esp_spiffs.h>
#include <driver/gpio.h>
#include <driver/i2c_master.h>
#include <esp_timer.h>
static const char *TAG = "KevinBoxBoard";
@@ -18,11 +19,46 @@ class KevinBoxBoard : public Ml307Board {
private:
i2c_master_bus_handle_t display_i2c_bus_;
i2c_master_bus_handle_t codec_i2c_bus_;
Axp2101 axp2101_;
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().GetChatState() != kChatStateIdle) {
seconds = 0;
return;
}
if (!axp2101_->IsDischarging()) {
seconds = 0;
return;
}
seconds++;
if (seconds >= seconds_to_shutdown) {
axp2101_->PowerOff();
}
}
void MountStorage() {
// Mount the storage partition
@@ -82,8 +118,15 @@ private:
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
Application::GetInstance().ToggleChatState();
// 测试按住说话
// boot_button_.OnClick([this]() {
// Application::GetInstance().ToggleChatState();
// });
boot_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
});
boot_button_.OnPressUp([this]() {
Application::GetInstance().StopListening();
});
volume_up_button_.OnClick([this]() {
@@ -93,13 +136,13 @@ private:
volume = 100;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("Volume\n" + std::to_string(volume));
GetDisplay()->ShowNotification("音量 " + std::to_string(volume));
});
volume_up_button_.OnLongPress([this]() {
auto codec = GetAudioCodec();
codec->SetOutputVolume(100);
GetDisplay()->ShowNotification("Volume\n100");
GetDisplay()->ShowNotification("最大音量");
});
volume_down_button_.OnClick([this]() {
@@ -109,13 +152,13 @@ private:
volume = 0;
}
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification("Volume\n" + std::to_string(volume));
GetDisplay()->ShowNotification("音量 " + std::to_string(volume));
});
volume_down_button_.OnLongPress([this]() {
auto codec = GetAudioCodec();
codec->SetOutputVolume(0);
GetDisplay()->ShowNotification("Volume\n0");
GetDisplay()->ShowNotification("已静音");
});
}
@@ -130,12 +173,13 @@ public:
ESP_LOGI(TAG, "Initializing KevinBoxBoard");
InitializeDisplayI2c();
InitializeCodecI2c();
axp2101_.Initialize(codec_i2c_bus_, AXP2101_I2C_ADDR);
axp2101_ = new Axp2101(codec_i2c_bus_, AXP2101_I2C_ADDR);
MountStorage();
Enable4GModule();
InitializeButtons();
InitializePowerSaveTimer();
Ml307Board::Initialize();
}
@@ -158,9 +202,15 @@ public:
}
virtual bool GetBatteryLevel(int &level, bool& charging) override {
level = axp2101_.GetBatteryLevel();
charging = axp2101_.IsCharging();
ESP_LOGI(TAG, "Battery level: %d, Charging: %d", level, charging);
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;
}
};

View File

@@ -30,6 +30,10 @@
#define DISPLAY_HEIGHT 240
#define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY true
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_42
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
#endif // _BOARD_CONFIG_H_

View File

@@ -1,22 +1,43 @@
#include "boards/wifi_board.h"
#include "wifi_board.h"
#include "audio_codecs/box_audio_codec.h"
#include "display/st7789_display.h"
#include "application.h"
#include "button.h"
#include "led.h"
#include "config.h"
#include "i2c_device.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 "LichuangDevBoard"
class Pca9557 : public I2cDevice {
public:
Pca9557(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
WriteReg(0x01, 0x03);
WriteReg(0x03, 0xf8);
}
void SetOutputState(uint8_t bit, uint8_t level) {
uint8_t data = ReadReg(0x01);
data = (data & ~(1 << bit)) | (level << bit);
WriteReg(0x01, data);
}
};
class LichuangDevBoard : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
i2c_master_dev_handle_t pca9557_handle_;
Button boot_button_;
St7789Display* display_;
Pca9557* pca9557_;
void InitializeI2c() {
// Initialize I2C peripheral
@@ -33,39 +54,9 @@ private:
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
}
void Pca9557ReadRegister(uint8_t addr, uint8_t* data) {
uint8_t tmp[1] = {addr};
ESP_ERROR_CHECK(i2c_master_transmit_receive(pca9557_handle_, tmp, 1, data, 1, 100));
}
void Pca9557WriteRegister(uint8_t addr, uint8_t data) {
uint8_t tmp[2] = {addr, data};
ESP_ERROR_CHECK(i2c_master_transmit(pca9557_handle_, tmp, 2, 100));
}
void Pca9557SetOutputState(uint8_t bit, uint8_t level) {
uint8_t data;
Pca9557ReadRegister(0x01, &data);
data = (data & ~(1 << bit)) | (level << bit);
Pca9557WriteRegister(0x01, data);
}
void InitializePca9557() {
i2c_device_config_t pca9557_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = 0x19,
.scl_speed_hz = 100000,
.scl_wait_us = 0,
.flags = {
.disable_ack_check = 0,
},
};
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus_, &pca9557_cfg, &pca9557_handle_));
assert(pca9557_handle_ != NULL);
Pca9557WriteRegister(0x01, 0x03);
Pca9557WriteRegister(0x03, 0xf8);
// Initialize PCA9557
pca9557_ = new Pca9557(i2c_bus_, 0x19);
}
void InitializeSpi() {
@@ -81,10 +72,48 @@ private:
void InitializeButtons() {
boot_button_.OnClick([this]() {
Application::GetInstance().ToggleChatState();
auto& app = Application::GetInstance();
if (app.GetChatState() == kChatStateUnknown && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
app.ToggleChatState();
});
}
void InitializeSt7789Display() {
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 = {};
io_config.cs_gpio_num = GPIO_NUM_NC;
io_config.dc_gpio_num = GPIO_NUM_39;
io_config.spi_mode = 2;
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_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_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;
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel);
pca9557_->SetOutputState(0, 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 St7789Display(panel_io, panel, DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
}
public:
LichuangDevBoard() : boot_button_(BOOT_BUTTON_GPIO) {
}
@@ -92,8 +121,8 @@ public:
virtual void Initialize() override {
ESP_LOGI(TAG, "Initializing LichuangDevBoard");
InitializeI2c();
InitializePca9557();
InitializeSpi();
InitializeSt7789Display();
InitializeButtons();
WifiBoard::Initialize();
}
@@ -115,40 +144,7 @@ public:
}
virtual Display* GetDisplay() override {
static St7789Display* display = nullptr;
if (display == nullptr) {
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 = {};
io_config.cs_gpio_num = GPIO_NUM_NC;
io_config.dc_gpio_num = GPIO_NUM_39;
io_config.spi_mode = 2;
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_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_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;
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel);
Pca9557SetOutputState(0, 0);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, true);
esp_lcd_panel_swap_xy(panel, true);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
display = new St7789Display(panel_io, panel, GPIO_NUM_42, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
}
return display;
return display_;
}
};

View File

@@ -1,136 +0,0 @@
#include <esp_log.h>
#include <esp_err.h>
#include <string>
#include <cstdlib>
#include "display.h"
#include "board.h"
#include "application.h"
#define TAG "Display"
void Display::SetupUI() {
if (disp_ == nullptr) {
return;
}
ESP_LOGI(TAG, "Setting up UI");
Lock();
label_ = lv_label_create(lv_disp_get_scr_act(disp_));
// lv_obj_set_style_text_font(label_, font_, 0);
lv_obj_set_style_text_color(label_, lv_color_black(), 0);
lv_label_set_text(label_, "Initializing...");
lv_obj_set_width(label_, disp_->driver->hor_res);
lv_obj_set_height(label_, disp_->driver->ver_res);
notification_ = lv_label_create(lv_disp_get_scr_act(disp_));
// lv_obj_set_style_text_font(notification_, font_, 0);
lv_obj_set_style_text_color(notification_, lv_color_black(), 0);
lv_label_set_text(notification_, "Notification\nTest");
lv_obj_set_width(notification_, disp_->driver->hor_res);
lv_obj_set_height(notification_, disp_->driver->ver_res);
lv_obj_set_style_opa(notification_, LV_OPA_MIN, 0);
Unlock();
// Create a timer to update the display every 10 seconds
esp_timer_create_args_t update_display_timer_args = {
.callback = [](void *arg) {
Display* display = static_cast<Display*>(arg);
display->UpdateDisplay();
},
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "UpdateDisplay",
.skip_unhandled_events = false,
};
ESP_ERROR_CHECK(esp_timer_create(&update_display_timer_args, &update_display_timer_));
ESP_ERROR_CHECK(esp_timer_start_periodic(update_display_timer_, 10 * 1000000));
}
Display::~Display() {
if (notification_timer_ != nullptr) {
esp_timer_stop(notification_timer_);
esp_timer_delete(notification_timer_);
}
if (update_display_timer_ != nullptr) {
esp_timer_stop(update_display_timer_);
esp_timer_delete(update_display_timer_);
}
if (label_ != nullptr) {
lv_obj_del(label_);
lv_obj_del(notification_);
}
if (font_ != nullptr) {
lv_font_free(font_);
}
}
void Display::SetText(const std::string &text) {
if (label_ != nullptr) {
text_ = text;
Lock();
// Change the text of the label
lv_label_set_text(label_, text_.c_str());
Unlock();
}
}
void Display::ShowNotification(const std::string &text) {
if (notification_ != nullptr) {
Lock();
lv_label_set_text(notification_, text.c_str());
lv_obj_set_style_opa(notification_, LV_OPA_MAX, 0);
lv_obj_set_style_opa(label_, LV_OPA_MIN, 0);
Unlock();
if (notification_timer_ != nullptr) {
esp_timer_stop(notification_timer_);
esp_timer_delete(notification_timer_);
}
esp_timer_create_args_t timer_args = {
.callback = [](void *arg) {
Display *display = static_cast<Display*>(arg);
display->Lock();
lv_obj_set_style_opa(display->notification_, LV_OPA_MIN, 0);
lv_obj_set_style_opa(display->label_, LV_OPA_MAX, 0);
display->Unlock();
},
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "Notification Timer",
.skip_unhandled_events = false,
};
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &notification_timer_));
ESP_ERROR_CHECK(esp_timer_start_once(notification_timer_, 3000000));
}
}
void Display::UpdateDisplay() {
auto chat_state = Application::GetInstance().GetChatState();
if (chat_state == kChatStateIdle) {
std::string text;
auto& board = Board::GetInstance();
std::string network_name;
int signal_quality;
std::string signal_quality_text;
if (!board.GetNetworkState(network_name, signal_quality, signal_quality_text)) {
text = "No network";
} else {
text = network_name + "\n" + signal_quality_text;
if (std::abs(signal_quality) != 99) {
text += " (" + std::to_string(signal_quality) + ")";
}
}
int battery_level;
bool charging;
if (board.GetBatteryLevel(battery_level, charging)) {
text += "\nPower " + std::to_string(battery_level) + "%";
if (charging) {
text += " (Charging)";
}
}
SetText(text);
}
}

View File

@@ -1,39 +0,0 @@
#ifndef DISPLAY_H
#define DISPLAY_H
#include <lvgl.h>
#include <esp_timer.h>
#include <string>
class Display {
public:
virtual ~Display();
void SetupUI();
void SetText(const std::string &text);
void ShowNotification(const std::string &text);
void UpdateDisplay();
int width() const { return width_; }
int height() const { return height_; }
protected:
lv_disp_t *disp_ = nullptr;
lv_font_t *font_ = nullptr;
lv_obj_t *label_ = nullptr;
lv_obj_t *notification_ = nullptr;
esp_timer_handle_t notification_timer_ = nullptr;
esp_timer_handle_t update_display_timer_ = nullptr;
int width_ = 0;
int height_ = 0;
std::string text_;
virtual void Lock() = 0;
virtual void Unlock() = 0;
};
#endif

192
main/display/display.cc Normal file
View File

@@ -0,0 +1,192 @@
#include <esp_log.h>
#include <esp_err.h>
#include <string>
#include <cstdlib>
#include "display.h"
#include "board.h"
#include "application.h"
#include "font_awesome_symbols.h"
#include "audio_codec.h"
#define TAG "Display"
Display::Display() {
// Notification timer
esp_timer_create_args_t notification_timer_args = {
.callback = [](void *arg) {
Display *display = static_cast<Display*>(arg);
DisplayLockGuard lock(display);
lv_obj_add_flag(display->notification_label_, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(display->status_label_, LV_OBJ_FLAG_HIDDEN);
},
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "Notification Timer",
.skip_unhandled_events = false,
};
ESP_ERROR_CHECK(esp_timer_create(&notification_timer_args, &notification_timer_));
// Update display timer
esp_timer_create_args_t update_display_timer_args = {
.callback = [](void *arg) {
Display *display = static_cast<Display*>(arg);
display->Update();
},
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "Update Display Timer",
.skip_unhandled_events = false,
};
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 (network_label_ != nullptr) {
lv_obj_del(network_label_);
lv_obj_del(notification_label_);
lv_obj_del(status_label_);
lv_obj_del(mute_label_);
lv_obj_del(battery_label_);
}
}
void Display::SetStatus(const std::string &status) {
if (status_label_ == nullptr) {
return;
}
DisplayLockGuard lock(this);
lv_label_set_text(status_label_, status.c_str());
}
void Display::ShowNotification(const std::string &notification, int duration_ms) {
if (notification_label_ == nullptr) {
return;
}
DisplayLockGuard lock(this);
lv_label_set_text(notification_label_, notification.c_str());
lv_obj_clear_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(status_label_, LV_OBJ_FLAG_HIDDEN);
esp_timer_stop(notification_timer_);
ESP_ERROR_CHECK(esp_timer_start_once(notification_timer_, duration_ms * 1000));
}
void Display::Update() {
if (mute_label_ == nullptr) {
return;
}
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_, "");
}
// 更新电池图标
int battery_level;
bool charging;
const char* icon = nullptr;
if (board.GetBatteryLevel(battery_level, charging)) {
if (charging) {
icon = FONT_AWESOME_BATTERY_CHARGING;
} else {
const char* levels[] = {
FONT_AWESOME_BATTERY_EMPTY, // 0-19%
FONT_AWESOME_BATTERY_1, // 20-39%
FONT_AWESOME_BATTERY_2, // 40-59%
FONT_AWESOME_BATTERY_3, // 60-79%
FONT_AWESOME_BATTERY_FULL, // 80-99%
FONT_AWESOME_BATTERY_FULL, // 100%
};
icon = levels[battery_level / 20];
}
if (battery_icon_ != icon) {
battery_icon_ = icon;
lv_label_set_text(battery_label_, battery_icon_);
}
}
// 仅在聊天状态为空闲时,读取网络状态(避免升级时占用 UART 资源)
auto chat_state = Application::GetInstance().GetChatState();
if (chat_state == kChatStateIdle || chat_state == kChatStateUnknown) {
icon = board.GetNetworkStateIcon();
if (network_icon_ != icon) {
network_icon_ = icon;
lv_label_set_text(network_label_, network_icon_);
}
}
}
void Display::SetEmotion(const std::string &emotion) {
if (emotion_label_ == nullptr) {
return;
}
struct Emotion {
const char* icon;
const char* text;
};
static const std::vector<Emotion> emotions = {
{FONT_AWESOME_EMOJI_NEUTRAL, "neutral"},
{FONT_AWESOME_EMOJI_HAPPY, "happy"},
{FONT_AWESOME_EMOJI_LAUGHING, "laughing"},
{FONT_AWESOME_EMOJI_FUNNY, "funny"},
{FONT_AWESOME_EMOJI_SAD, "sad"},
{FONT_AWESOME_EMOJI_ANGRY, "angry"},
{FONT_AWESOME_EMOJI_CRYING, "crying"},
{FONT_AWESOME_EMOJI_LOVING, "loving"},
{FONT_AWESOME_EMOJI_EMBARRASSED, "embarrassed"},
{FONT_AWESOME_EMOJI_SURPRISED, "surprised"},
{FONT_AWESOME_EMOJI_SHOCKED, "shocked"},
{FONT_AWESOME_EMOJI_THINKING, "thinking"},
{FONT_AWESOME_EMOJI_WINKING, "winking"},
{FONT_AWESOME_EMOJI_COOL, "cool"},
{FONT_AWESOME_EMOJI_RELAXED, "relaxed"},
{FONT_AWESOME_EMOJI_DELICIOUS, "delicious"},
{FONT_AWESOME_EMOJI_KISSY, "kissy"},
{FONT_AWESOME_EMOJI_CONFIDENT, "confident"},
{FONT_AWESOME_EMOJI_SLEEPY, "sleepy"},
{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; });
// 如果找到匹配的表情就显示对应图标否则显示默认的neutral表情
if (it != emotions.end()) {
lv_label_set_text(emotion_label_, it->icon);
} else {
lv_label_set_text(emotion_label_, FONT_AWESOME_EMOJI_NEUTRAL);
}
}
void Display::SetIcon(const char* icon) {
if (emotion_label_ == nullptr) {
return;
}
DisplayLockGuard lock(this);
lv_label_set_text(emotion_label_, icon);
}
void Display::SetChatMessage(const std::string &role, const std::string &content) {
}

64
main/display/display.h Normal file
View File

@@ -0,0 +1,64 @@
#ifndef DISPLAY_H
#define DISPLAY_H
#include <lvgl.h>
#include <esp_timer.h>
#include <string>
class Display {
public:
Display();
virtual ~Display();
virtual void SetStatus(const std::string &status);
virtual void ShowNotification(const std::string &notification, int duration_ms = 3000);
virtual void SetEmotion(const std::string &emotion);
virtual void SetChatMessage(const std::string &role, const std::string &content);
virtual void SetIcon(const char* icon);
int width() const { return width_; }
int height() const { return height_; }
protected:
int width_ = 0;
int height_ = 0;
lv_disp_t *disp_ = nullptr;
lv_obj_t *emotion_label_ = nullptr;
lv_obj_t *network_label_ = nullptr;
lv_obj_t *status_label_ = nullptr;
lv_obj_t *notification_label_ = nullptr;
lv_obj_t *mute_label_ = nullptr;
lv_obj_t *battery_label_ = nullptr;
const char* battery_icon_ = nullptr;
const char* network_icon_ = nullptr;
bool muted_ = false;
esp_timer_handle_t notification_timer_ = nullptr;
esp_timer_handle_t update_timer_ = nullptr;
friend class DisplayLockGuard;
virtual void Lock() = 0;
virtual void Unlock() = 0;
virtual void Update();
};
class DisplayLockGuard {
public:
DisplayLockGuard(Display *display) : display_(display) {
display_->Lock();
}
~DisplayLockGuard() {
display_->Unlock();
}
private:
Display *display_;
};
#endif

View File

@@ -1,4 +1,5 @@
#include "ssd1306_display.h"
#include "font_awesome_symbols.h"
#include <esp_log.h>
#include <esp_err.h>
@@ -8,6 +9,10 @@
#define TAG "Ssd1306Display"
LV_FONT_DECLARE(font_puhui_14_1);
LV_FONT_DECLARE(font_awesome_30_1);
LV_FONT_DECLARE(font_awesome_14_1);
Ssd1306Display::Ssd1306Display(void* i2c_master_handle, int width, int height, bool mirror_x, bool mirror_y)
: mirror_x_(mirror_x), mirror_y_(mirror_y) {
width_ = width;
@@ -85,9 +90,33 @@ Ssd1306Display::Ssd1306Display(void* i2c_master_handle, int width, int height, b
};
disp_ = lvgl_port_add_disp(&display_cfg);
if (disp_ == nullptr) {
ESP_LOGE(TAG, "Failed to add display");
return;
}
if (height_ == 64) {
SetupUI_128x64();
} else {
SetupUI_128x32();
}
}
Ssd1306Display::~Ssd1306Display() {
if (content_ != nullptr) {
lv_obj_del(content_);
}
if (status_bar_ != nullptr) {
lv_obj_del(status_bar_);
}
if (side_bar_ != nullptr) {
lv_obj_del(side_bar_);
}
if (container_ != nullptr) {
lv_obj_del(container_);
}
if (panel_ != nullptr) {
esp_lcd_panel_del(panel_);
}
@@ -104,3 +133,136 @@ void Ssd1306Display::Lock() {
void Ssd1306Display::Unlock() {
lvgl_port_unlock();
}
void Ssd1306Display::SetupUI_128x64() {
DisplayLockGuard lock(this);
auto screen = lv_disp_get_scr_act(disp_);
lv_obj_set_style_text_font(screen, &font_puhui_14_1, 0);
lv_obj_set_style_text_color(screen, lv_color_black(), 0);
/* Container */
container_ = lv_obj_create(screen);
lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES);
lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(container_, 0, 0);
lv_obj_set_style_border_width(container_, 0, 0);
lv_obj_set_style_pad_row(container_, 0, 0);
/* Status bar */
status_bar_ = lv_obj_create(container_);
lv_obj_set_size(status_bar_, LV_HOR_RES, 18);
lv_obj_set_style_radius(status_bar_, 0, 0);
/* Content */
content_ = lv_obj_create(container_);
lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_radius(status_bar_, 0, 0);
lv_obj_set_width(content_, LV_HOR_RES);
lv_obj_set_flex_grow(content_, 1);
emotion_label_ = lv_label_create(content_);
lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_1, 0);
lv_label_set_text(emotion_label_, FONT_AWESOME_AI_CHIP);
lv_obj_center(emotion_label_);
/* Status bar */
lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_all(status_bar_, 0, 0);
lv_obj_set_style_border_width(status_bar_, 0, 0);
lv_obj_set_style_pad_column(status_bar_, 0, 0);
network_label_ = lv_label_create(status_bar_);
lv_label_set_text(network_label_, "");
lv_obj_set_style_text_font(network_label_, &font_awesome_14_1, 0);
notification_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(notification_label_, 1);
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_label_set_text(notification_label_, "通知");
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
status_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(status_label_, 1);
lv_label_set_text(status_label_, "正在初始化");
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
mute_label_ = lv_label_create(status_bar_);
lv_label_set_text(mute_label_, "");
lv_obj_set_style_text_font(mute_label_, &font_awesome_14_1, 0);
battery_label_ = lv_label_create(status_bar_);
lv_label_set_text(battery_label_, "");
lv_obj_set_style_text_font(battery_label_, &font_awesome_14_1, 0);
}
void Ssd1306Display::SetupUI_128x32() {
DisplayLockGuard lock(this);
auto screen = lv_disp_get_scr_act(disp_);
lv_obj_set_style_text_font(screen, &font_puhui_14_1, 0);
/* Container */
container_ = lv_obj_create(screen);
lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES);
lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_all(container_, 0, 0);
lv_obj_set_style_border_width(container_, 0, 0);
lv_obj_set_style_pad_column(container_, 0, 0);
/* Left side */
side_bar_ = lv_obj_create(container_);
lv_obj_set_flex_grow(side_bar_, 1);
lv_obj_set_flex_flow(side_bar_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(side_bar_, 0, 0);
lv_obj_set_style_border_width(side_bar_, 0, 0);
lv_obj_set_style_radius(side_bar_, 0, 0);
lv_obj_set_style_pad_row(side_bar_, 0, 0);
/* Emotion label on the right side */
content_ = lv_obj_create(container_);
lv_obj_set_size(content_, 32, 32);
lv_obj_set_style_pad_all(content_, 0, 0);
lv_obj_set_style_border_width(content_, 0, 0);
lv_obj_set_style_radius(content_, 0, 0);
emotion_label_ = lv_label_create(content_);
lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_1, 0);
lv_label_set_text(emotion_label_, FONT_AWESOME_AI_CHIP);
lv_obj_center(emotion_label_);
/* Status bar */
status_bar_ = lv_obj_create(side_bar_);
lv_obj_set_size(status_bar_, LV_SIZE_CONTENT, 16);
lv_obj_set_style_radius(status_bar_, 0, 0);
lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_all(status_bar_, 0, 0);
lv_obj_set_style_border_width(status_bar_, 0, 0);
lv_obj_set_style_pad_column(status_bar_, 0, 0);
network_label_ = lv_label_create(status_bar_);
lv_label_set_text(network_label_, "");
lv_obj_set_style_text_font(network_label_, &font_awesome_14_1, 0);
mute_label_ = lv_label_create(status_bar_);
lv_label_set_text(mute_label_, "");
lv_obj_set_style_text_font(mute_label_, &font_awesome_14_1, 0);
battery_label_ = lv_label_create(status_bar_);
lv_label_set_text(battery_label_, "");
lv_obj_set_style_text_font(battery_label_, &font_awesome_14_1, 0);
status_label_ = lv_label_create(side_bar_);
lv_obj_set_flex_grow(status_label_, 1);
lv_obj_set_width(status_label_, width_ - 32);
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_label_set_text(status_label_, "正在初始化");
notification_label_ = lv_label_create(side_bar_);
lv_obj_set_flex_grow(notification_label_, 1);
lv_obj_set_width(notification_label_, width_ - 32);
lv_label_set_long_mode(notification_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_label_set_text(notification_label_, "通知");
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
}

View File

@@ -13,9 +13,17 @@ private:
bool mirror_x_ = false;
bool mirror_y_ = false;
lv_obj_t* status_bar_ = nullptr;
lv_obj_t* content_ = nullptr;
lv_obj_t* container_ = nullptr;
lv_obj_t* side_bar_ = nullptr;
virtual void Lock() override;
virtual void Unlock() override;
void SetupUI_128x64();
void SetupUI_128x32();
public:
Ssd1306Display(void* i2c_master_handle, int width, int height, bool mirror_x = false, bool mirror_y = false);
~Ssd1306Display();

View File

@@ -1,4 +1,5 @@
#include "st7789_display.h"
#include "font_awesome_symbols.h"
#include <esp_log.h>
#include <esp_err.h>
@@ -9,9 +10,15 @@
#define TAG "St7789Display"
#define LCD_LEDC_CH LEDC_CHANNEL_0
St7789Display::St7789Display(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, gpio_num_t backlight_pin,
int width, int height, bool mirror_x, bool mirror_y)
: panel_io_(panel_io), panel_(panel), mirror_x_(mirror_x), mirror_y_(mirror_y) {
LV_FONT_DECLARE(font_puhui_14_1);
LV_FONT_DECLARE(font_awesome_30_1);
LV_FONT_DECLARE(font_awesome_14_1);
St7789Display::St7789Display(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, bool mirror_x, bool mirror_y, bool swap_xy)
: 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) {
width_ = width;
height_ = height;
@@ -43,7 +50,7 @@ St7789Display::St7789Display(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_h
.vres = static_cast<uint32_t>(height_),
.monochrome = false,
.rotation = {
.swap_xy = true,
.swap_xy = swap_xy_,
.mirror_x = mirror_x_,
.mirror_y = mirror_y_,
},
@@ -59,9 +66,24 @@ St7789Display::St7789Display(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_h
disp_ = lvgl_port_add_disp(&display_cfg);
SetBacklight(100);
SetupUI();
}
St7789Display::~St7789Display() {
if (content_ != nullptr) {
lv_obj_del(content_);
}
if (status_bar_ != nullptr) {
lv_obj_del(status_bar_);
}
if (side_bar_ != nullptr) {
lv_obj_del(side_bar_);
}
if (container_ != nullptr) {
lv_obj_del(container_);
}
if (panel_ != nullptr) {
esp_lcd_panel_del(panel_);
}
@@ -86,7 +108,7 @@ void St7789Display::InitializeBacklight(gpio_num_t backlight_pin) {
.duty = 0,
.hpoint = 0,
.flags = {
.output_invert = true
.output_invert = backlight_output_invert_,
}
};
const ledc_timer_config_t backlight_timer = {
@@ -103,6 +125,10 @@ void St7789Display::InitializeBacklight(gpio_num_t backlight_pin) {
}
void St7789Display::SetBacklight(uint8_t brightness) {
if (backlight_pin_ == GPIO_NUM_NC) {
return;
}
if (brightness > 100) {
brightness = 100;
}
@@ -121,3 +147,65 @@ void St7789Display::Lock() {
void St7789Display::Unlock() {
lvgl_port_unlock();
}
void St7789Display::SetupUI() {
DisplayLockGuard lock(this);
auto screen = lv_disp_get_scr_act(disp_);
lv_obj_set_style_text_font(screen, &font_puhui_14_1, 0);
lv_obj_set_style_text_color(screen, lv_color_black(), 0);
/* Container */
container_ = lv_obj_create(screen);
lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES);
lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(container_, 0, 0);
lv_obj_set_style_border_width(container_, 0, 0);
lv_obj_set_style_pad_row(container_, 0, 0);
/* Status bar */
status_bar_ = lv_obj_create(container_);
lv_obj_set_size(status_bar_, LV_HOR_RES, 18);
lv_obj_set_style_radius(status_bar_, 0, 0);
/* Content */
content_ = lv_obj_create(container_);
lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_radius(content_, 0, 0);
lv_obj_set_width(content_, LV_HOR_RES);
lv_obj_set_flex_grow(content_, 1);
emotion_label_ = lv_label_create(content_);
lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_1, 0);
lv_label_set_text(emotion_label_, FONT_AWESOME_AI_CHIP);
lv_obj_center(emotion_label_);
/* Status bar */
lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_all(status_bar_, 0, 0);
lv_obj_set_style_border_width(status_bar_, 0, 0);
lv_obj_set_style_pad_column(status_bar_, 0, 0);
network_label_ = lv_label_create(status_bar_);
lv_label_set_text(network_label_, "");
lv_obj_set_style_text_font(network_label_, &font_awesome_14_1, 0);
notification_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(notification_label_, 1);
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_label_set_text(notification_label_, "通知");
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
status_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(status_label_, 1);
lv_label_set_text(status_label_, "正在初始化");
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
mute_label_ = lv_label_create(status_bar_);
lv_label_set_text(mute_label_, "");
lv_obj_set_style_text_font(mute_label_, &font_awesome_14_1, 0);
battery_label_ = lv_label_create(status_bar_);
lv_label_set_text(battery_label_, "");
lv_obj_set_style_text_font(battery_label_, &font_awesome_14_1, 0);
}

View File

@@ -11,18 +11,28 @@ class St7789Display : public Display {
private:
esp_lcd_panel_io_handle_t panel_io_ = nullptr;
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;
lv_obj_t* status_bar_ = nullptr;
lv_obj_t* content_ = nullptr;
lv_obj_t* container_ = nullptr;
lv_obj_t* side_bar_ = nullptr;
void InitializeBacklight(gpio_num_t backlight_pin);
void SetBacklight(uint8_t brightness);
void SetupUI();
virtual void Lock() override;
virtual void Unlock() override;
public:
St7789Display(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, gpio_num_t backlight_pin,
int width, int height, bool mirror_x, bool mirror_y);
St7789Display(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, bool mirror_x, bool mirror_y, bool swap_xy);
~St7789Display();
};

7515
main/fonts/GB2312.TXT Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,565 @@
/*******************************************************************************
* Size: 14 px
* Bpp: 1
* Opts: --no-compress --no-prefilter --force-fast-kern-format --font fa-regular-400.ttf --format lvgl --lv-include lvgl.h --bpp 1 -o font_awesome_14_1.c --size 14 -r 0xf5a4,0xf118,0xf59b,0xf588,0xe384,0xf556,0xf5b3,0xf584,0xf579,0xe36b,0xe375,0xe39b,0xf4da,0xe398,0xe392,0xe372,0xf598,0xe409,0xe38d,0xe3a4,0xe36d,0xf240,0xf241,0xf242,0xf243,0xf244,0xf377,0xf376,0xf1eb,0xf6ab,0xf6aa,0xf6ac,0xf012,0xf68f,0xf68e,0xf68d,0xf68c,0xf695,0xf028,0xf6a8,0xf027,0xf6a9,0xf001,0xf00c,0xf00d,0xf011,0xf013,0xf1f8,0xf015,0xf03e,0xf044,0xf048,0xf051,0xf04b,0xf04c,0xf04d,0xf060,0xf061,0xf062,0xf063,0xf071,0xf0f3,0xf3c5,0xf0ac,0xf124,0xf7c2,0xf293,0xf075,0xe1ec,0xf007,0xe04b,0xf019
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
#include "lvgl.h"
#else
#include "lvgl.h"
#endif
#ifndef FONT_AWESOME_14_1
#define FONT_AWESOME_14_1 1
#endif
#if FONT_AWESOME_14_1
/*-----------------
* BITMAPS
*----------------*/
/*Store the image of the glyphs*/
static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
/* U+E04B "" */
0x2, 0x0, 0x20, 0x1f, 0x8a, 0x5, 0xad, 0x5a,
0x5, 0xaa, 0x53, 0xf8, 0x0, 0x0, 0x0, 0x7f,
0xec, 0x3, 0x8b, 0x1f, 0xff,
/* U+E1EC "" */
0x9, 0x40, 0x25, 0x3, 0xff, 0x8, 0x4, 0xe0,
0x1c, 0x89, 0x4e, 0x55, 0xcb, 0xd4, 0x68, 0x5b,
0x80, 0x72, 0x1, 0xf, 0xfc, 0x9, 0x40, 0x25,
0x0,
/* U+E36B "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x16, 0x32, 0xe0,
0x2e, 0x23, 0x18, 0x8c, 0x60, 0x1, 0x87, 0x7,
0x1c, 0x34, 0x70, 0x88, 0x4, 0x18, 0x60, 0x3f,
0x0,
/* U+E36D "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x10, 0x2, 0xc8,
0xce, 0x23, 0x18, 0x0, 0x60, 0x1, 0x83, 0xc7,
0x30, 0x34, 0x80, 0x88, 0x4, 0x18, 0x60, 0x3f,
0x0,
/* U+E372 "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x10, 0x2, 0x8c,
0xc6, 0x40, 0x98, 0x0, 0x60, 0x1, 0x80, 0x26,
0x21, 0xb4, 0x7e, 0x88, 0x2a, 0x18, 0xa0, 0x38,
0x80,
/* U+E375 "" */
0xf, 0xc0, 0xe1, 0xc7, 0x8f, 0x98, 0x6, 0xfc,
0xee, 0x95, 0x5b, 0xd5, 0x66, 0x39, 0x80, 0x6,
0x1c, 0x14, 0xf8, 0x88, 0x4, 0x10, 0x20, 0x3f,
0x0,
/* U+E384 "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x12, 0x12, 0xd8,
0x6e, 0x80, 0x58, 0x0, 0x60, 0x1, 0x9c, 0xe7,
0x0, 0x34, 0x78, 0x88, 0x4, 0x18, 0x60, 0x3f,
0x0,
/* U+E38D "" */
0x7, 0x9c, 0x7f, 0x33, 0x1, 0xd8, 0x38, 0xc0,
0x42, 0x3, 0xb8, 0x0, 0x60, 0x1, 0x9c, 0xe6,
0x0, 0x1c, 0x30, 0x50, 0xc2, 0x20, 0x18, 0x60,
0xc0, 0xfc, 0x0,
/* U+E392 "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x10, 0x2, 0xc0,
0xe, 0x73, 0x98, 0x0, 0x60, 0x1, 0x80, 0x7,
0x21, 0x34, 0x78, 0x88, 0x4, 0x18, 0x60, 0x3f,
0x0,
/* U+E398 "" */
0xf, 0xc0, 0x40, 0x82, 0x1, 0x1f, 0xfe, 0xff,
0xff, 0xf3, 0xfb, 0xcf, 0x60, 0x1, 0x80, 0x7,
0x21, 0x34, 0x78, 0x88, 0x4, 0x18, 0x60, 0x3f,
0x0,
/* U+E39B "" */
0xf, 0xc0, 0x41, 0x82, 0x81, 0x15, 0x82, 0x88,
0xce, 0x3, 0x18, 0x0, 0x63, 0x1, 0x82, 0x7,
0x84, 0x36, 0xf0, 0x9f, 0x4, 0x7c, 0x60, 0xee,
0x0,
/* U+E3A4 "" */
0xf, 0xc0, 0xe1, 0x87, 0x1d, 0x18, 0x76, 0xd9,
0x4f, 0xd2, 0x3f, 0xc7, 0x76, 0x3d, 0xc3, 0xef,
0x15, 0xa6, 0xc5, 0x8d, 0x1c, 0x1c, 0x60, 0x3f,
0x0,
/* U+E409 "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x16, 0x32, 0xed,
0x6e, 0x94, 0x9b, 0xde, 0x60, 0x1, 0x9f, 0xe7,
0x7c, 0xb4, 0xe4, 0x89, 0xe4, 0x18, 0x60, 0x3f,
0x0,
/* U+F001 "" */
0x0, 0xc, 0x3, 0xf0, 0x7c, 0x43, 0x1, 0x8,
0x3c, 0x2f, 0x90, 0xf0, 0x42, 0x1, 0x8, 0x3c,
0x21, 0x1f, 0x84, 0x62, 0xe, 0x88, 0x1, 0xc0,
0x0,
/* U+F007 "" */
0xf, 0x1, 0x98, 0x10, 0x81, 0x8, 0x10, 0x81,
0xf8, 0xf, 0x0, 0x0, 0x1f, 0x86, 0x6, 0x40,
0x2c, 0x3, 0x80, 0x1f, 0xff,
/* U+F00C "" */
0x0, 0x10, 0x3, 0x0, 0x60, 0xc, 0xc1, 0x86,
0x30, 0x36, 0x1, 0xc0, 0x8, 0x0,
/* U+F00D "" */
0x81, 0xe1, 0x99, 0x87, 0x81, 0x81, 0xe1, 0x99,
0x86, 0x81, 0x80,
/* U+F011 "" */
0x2, 0x1, 0x11, 0x18, 0x8c, 0x84, 0x2c, 0x21,
0xc1, 0x6, 0x8, 0x30, 0x1, 0x80, 0xa, 0x0,
0x98, 0xc, 0x60, 0xc0, 0xf8, 0x0,
/* U+F012 "" */
0x0, 0x2, 0x0, 0x4, 0x0, 0x48, 0x0, 0x90,
0x1, 0x20, 0x22, 0x40, 0x44, 0x80, 0x89, 0x9,
0x12, 0x12, 0x26, 0x24, 0x4c, 0x48, 0x98, 0x91,
0x31, 0x22, 0x40,
/* U+F013 "" */
0xf, 0x80, 0x44, 0x1e, 0x3c, 0xa0, 0xac, 0x21,
0xe6, 0xcd, 0x22, 0x49, 0x32, 0xcf, 0x9e, 0x10,
0xd4, 0x14, 0xf1, 0xe0, 0x88, 0x3, 0x80,
/* U+F015 "" */
0x0, 0x0, 0x1, 0x80, 0x7, 0xc0, 0xc, 0x30,
0x18, 0x18, 0x30, 0xc, 0x60, 0x6, 0xe0, 0x7,
0x23, 0xc4, 0x22, 0x44, 0x22, 0x44, 0x22, 0x44,
0x22, 0x44, 0x22, 0x44, 0x1f, 0xf8,
/* U+F019 "" */
0x2, 0x0, 0x10, 0x0, 0x80, 0x4, 0x0, 0x20,
0x19, 0x30, 0x6b, 0x1, 0xf0, 0xf7, 0x7c, 0x10,
0x60, 0xb, 0x0, 0x58, 0x0, 0xff, 0xfc,
/* U+F027 "" */
0x3, 0x0, 0x70, 0xd, 0x7, 0x90, 0xf1, 0x38,
0x11, 0x81, 0x1f, 0x13, 0x79, 0x0, 0xd0, 0x7,
0x0, 0x30,
/* U+F028 "" */
0x0, 0x0, 0x1, 0x83, 0x1, 0xc0, 0xc1, 0xa1,
0xa7, 0x90, 0x6f, 0x89, 0x96, 0x4, 0x4b, 0x2,
0x25, 0xf1, 0x32, 0xbc, 0x82, 0xc3, 0x43, 0x40,
0xe0, 0x60, 0x30, 0x60,
/* U+F03E "" */
0xff, 0xfe, 0x0, 0x18, 0x0, 0x66, 0x1, 0x98,
0x6, 0x2, 0x18, 0x1c, 0x66, 0xf9, 0xbf, 0xe6,
0xff, 0xdf, 0xff, 0xff, 0xff,
/* U+F044 "" */
0x0, 0x10, 0x1, 0xe7, 0xcc, 0xe0, 0xea, 0x85,
0x1a, 0x8, 0xc8, 0x46, 0x22, 0x30, 0x89, 0x92,
0x3c, 0x48, 0x81, 0x20, 0x4, 0x80, 0x11, 0xff,
0x80,
/* U+F048 "" */
0x87, 0x1e, 0xef, 0x1c, 0x38, 0x78, 0xdd, 0x8f,
0xc,
/* U+F04B "" */
0x0, 0x38, 0xb, 0x82, 0x30, 0x87, 0x20, 0x68,
0xe, 0x3, 0x83, 0xa3, 0x89, 0x83, 0xc0, 0xc0,
0x0,
/* U+F04C "" */
0xf7, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
0x95, 0xf7,
/* U+F04D "" */
0xff, 0xe0, 0x18, 0x6, 0x1, 0x80, 0x60, 0x18,
0x6, 0x1, 0x80, 0x7f, 0xf0,
/* U+F051 "" */
0xc3, 0xc6, 0xec, 0x78, 0x70, 0xe3, 0xdd, 0xe3,
0x84,
/* U+F060 "" */
0x4, 0x0, 0xc0, 0x18, 0x3, 0x0, 0x60, 0xf,
0xff, 0x60, 0x3, 0x0, 0x18, 0x0, 0xc0, 0x4,
0x0,
/* U+F061 "" */
0x0, 0x0, 0x30, 0x1, 0x80, 0xc, 0x0, 0x6f,
0xff, 0x0, 0x60, 0xc, 0x1, 0x80, 0x30, 0x0,
0x0,
/* U+F062 "" */
0xe, 0x1, 0xc0, 0x54, 0x12, 0xc6, 0x4c, 0x88,
0x81, 0x0, 0x20, 0x4, 0x0, 0x80, 0x10, 0x2,
0x0,
/* U+F063 "" */
0x4, 0x0, 0x80, 0x10, 0x2, 0x0, 0x40, 0x8,
0x31, 0x1b, 0x26, 0x35, 0x83, 0xe0, 0x38, 0x2,
0x0,
/* U+F071 "" */
0x3, 0x0, 0x1e, 0x1, 0x48, 0x2, 0x90, 0x1a,
0x61, 0xc8, 0x82, 0x21, 0x18, 0x6, 0x42, 0xb,
0x8, 0x38, 0x0, 0x7f, 0xff,
/* U+F075 "" */
0xf, 0xc0, 0xc0, 0xc4, 0x0, 0xb0, 0x3, 0x80,
0x6, 0x0, 0x18, 0x0, 0x70, 0x3, 0x40, 0x9,
0x0, 0xcf, 0xfc, 0x30, 0x0,
/* U+F0AC "" */
0xf, 0xc0, 0xf3, 0xc6, 0xc9, 0x92, 0x12, 0xff,
0xfe, 0x21, 0x18, 0x84, 0x62, 0x11, 0xff, 0xfd,
0x21, 0x26, 0x49, 0x8f, 0x3c, 0x1f, 0xe0,
/* U+F0F3 "" */
0x6, 0x0, 0xf0, 0x10, 0x82, 0x4, 0x20, 0x42,
0x4, 0x20, 0x42, 0x4, 0x60, 0x64, 0x2, 0xff,
0xf0, 0x0, 0xf, 0x0, 0x60,
/* U+F118 "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x10, 0x2, 0xc8,
0xce, 0x23, 0x18, 0x0, 0x60, 0x1, 0x80, 0x7,
0x21, 0x34, 0x78, 0x88, 0x4, 0x18, 0x60, 0x3f,
0x0,
/* U+F124 "" */
0x0, 0x20, 0xf, 0x7, 0xa1, 0xe6, 0x78, 0x4f,
0xec, 0x2, 0xc0, 0x38, 0x3, 0x80, 0x30, 0x3,
0x0, 0x20,
/* U+F1EB "" */
0x7, 0xf8, 0xe, 0x3, 0x86, 0x0, 0x3b, 0x0,
0x2, 0x0, 0x0, 0x0, 0xfc, 0x0, 0xc1, 0xc0,
0x40, 0x10, 0x0, 0x0, 0x0, 0x70, 0x0, 0x1c,
0x0, 0x2, 0x0,
/* U+F1F8 "" */
0xf, 0x81, 0x88, 0xff, 0xf4, 0x2, 0x40, 0x24,
0x2, 0x60, 0x26, 0x2, 0x60, 0x26, 0x2, 0x60,
0x62, 0x6, 0x20, 0x63, 0xfc,
/* U+F240 "" */
0x7f, 0xfc, 0x80, 0x2, 0xbf, 0xf3, 0xbf, 0xf3,
0xbf, 0xf3, 0x80, 0x2, 0x80, 0x2, 0x7f, 0xfc,
/* U+F241 "" */
0x7f, 0xfc, 0x80, 0x2, 0xbf, 0x83, 0xbf, 0x83,
0xbf, 0x83, 0x80, 0x2, 0x80, 0x2, 0x7f, 0xfc,
/* U+F242 "" */
0x7f, 0xfc, 0x80, 0x2, 0x9f, 0x3, 0x9f, 0x3,
0x9f, 0x3, 0x80, 0x2, 0x80, 0x2, 0x7f, 0xfc,
/* U+F243 "" */
0x7f, 0xfc, 0x80, 0x2, 0xb8, 0x3, 0xb8, 0x3,
0xb8, 0x3, 0x80, 0x2, 0x80, 0x2, 0x7f, 0xfc,
/* U+F244 "" */
0x7f, 0xfc, 0x80, 0x2, 0x80, 0x3, 0x80, 0x3,
0x80, 0x3, 0x80, 0x2, 0x80, 0x2, 0x7f, 0xfc,
/* U+F293 "" */
0xc, 0x7, 0x2, 0xd9, 0x36, 0xb1, 0xf0, 0x70,
0x38, 0x7f, 0x64, 0xc2, 0xc1, 0xc0, 0xc0, 0x40,
/* U+F376 "" */
0x7e, 0xdc, 0x81, 0x82, 0x83, 0x83, 0x87, 0xc3,
0x87, 0xc3, 0x83, 0x82, 0x83, 0x2, 0x76, 0xfc,
/* U+F377 "" */
0x80, 0x0, 0x30, 0x0, 0x3, 0x0, 0x0, 0x7f,
0xf8, 0x4e, 0x1, 0x10, 0xc0, 0x64, 0x18, 0x19,
0x3, 0x86, 0x40, 0x31, 0x10, 0x7, 0x43, 0xfe,
0x60, 0x0, 0xc, 0x0, 0x1, 0xc0, 0x0, 0x10,
/* U+F3C5 "" */
0x1e, 0x18, 0x64, 0xa, 0x31, 0x92, 0x64, 0x98,
0xc5, 0x2, 0x40, 0x88, 0x43, 0x30, 0x48, 0x1c,
0x3, 0x0,
/* U+F4DA "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x10, 0x2, 0xc8,
0xce, 0x22, 0x98, 0x0, 0x60, 0x1, 0x80, 0x7,
0x21, 0x34, 0x78, 0x88, 0x4, 0x18, 0x60, 0x3f,
0x0,
/* U+F556 "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x10, 0x2, 0xc0,
0xe, 0x73, 0x98, 0x84, 0x60, 0x1, 0x80, 0x7,
0xc, 0x34, 0x78, 0x88, 0x4, 0x18, 0x60, 0x3f,
0x0,
/* U+F579 "" */
0xf, 0xc0, 0x61, 0xc2, 0x1, 0x90, 0x2, 0xdc,
0xee, 0x94, 0x5b, 0xb5, 0x67, 0x39, 0x80, 0x7,
0x3e, 0x34, 0x0, 0x88, 0x6, 0x18, 0x70, 0x3f,
0x0,
/* U+F584 "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x10, 0x2, 0xc4,
0x8e, 0x73, 0x98, 0xcc, 0x60, 0x1, 0x80, 0x7,
0x3f, 0x34, 0x78, 0x88, 0x4, 0x18, 0x60, 0x3f,
0x0,
/* U+F588 "" */
0x3, 0xf0, 0x1, 0x86, 0x1, 0x80, 0x60, 0x40,
0x8, 0x33, 0x33, 0x9, 0xce, 0x40, 0x0, 0x3,
0x80, 0x7, 0xe0, 0x1, 0xf4, 0xfc, 0xb1, 0x1e,
0x20, 0x60, 0x18, 0xe, 0x1c, 0x0, 0xfc, 0x0,
/* U+F598 "" */
0xf, 0xc0, 0x60, 0x82, 0x1, 0x90, 0x2, 0x88,
0x6, 0x23, 0x98, 0x0, 0x60, 0xc1, 0x83, 0x36,
0xc, 0xf4, 0x33, 0xc8, 0xf, 0x10, 0x0, 0x3f,
0x0,
/* U+F59B "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x10, 0xa, 0xd8,
0x6e, 0x33, 0x19, 0x2, 0x60, 0x1, 0x8f, 0xe7,
0x3f, 0x34, 0x78, 0x88, 0x4, 0x18, 0x60, 0x3f,
0x0,
/* U+F5A4 "" */
0xf, 0xc0, 0x61, 0x82, 0x1, 0x10, 0x2, 0xc8,
0xce, 0x23, 0x18, 0x0, 0x60, 0x1, 0x80, 0x7,
0x0, 0x34, 0x0, 0x88, 0x4, 0x18, 0x60, 0x3f,
0x0,
/* U+F5B3 "" */
0xf, 0xc0, 0x40, 0x82, 0x1, 0x10, 0x2, 0x80,
0x6, 0x73, 0x98, 0x0, 0x65, 0xc9, 0xd7, 0x2f,
0x5c, 0xb7, 0x3, 0x8c, 0xc, 0x18, 0x60, 0x3f,
0x0,
/* U+F68C "" */
0xf0,
/* U+F68D "" */
0x8, 0x63, 0x18, 0xc4,
/* U+F68E "" */
0x0, 0x80, 0x40, 0x21, 0x10, 0x8c, 0x46, 0x23,
0x11, 0x88, 0x80,
/* U+F68F "" */
0x0, 0x10, 0x1, 0x0, 0x10, 0x11, 0x1, 0x10,
0x11, 0x9, 0x10, 0x91, 0x89, 0x18, 0x91, 0x89,
0x18, 0x91,
/* U+F695 "" */
0xc0, 0x1, 0x30, 0x0, 0x8c, 0x2, 0x43, 0x1,
0x20, 0x60, 0x90, 0x1c, 0x48, 0x6, 0x24, 0x1,
0xd2, 0x4, 0x39, 0x2, 0x4c, 0x91, 0x23, 0xc8,
0x90, 0xe4, 0x48, 0x9a, 0x24, 0x44,
/* U+F6A8 "" */
0x3, 0x0, 0xe, 0x0, 0x34, 0x33, 0xc8, 0x3f,
0x13, 0x30, 0x22, 0x60, 0x44, 0xf8, 0x99, 0x79,
0x4, 0x1a, 0x18, 0x1c, 0x0, 0x18, 0x0,
/* U+F6A9 "" */
0x3, 0x0, 0xe, 0x0, 0x34, 0x3, 0xc9, 0x1f,
0x13, 0x70, 0x23, 0xa0, 0x47, 0x78, 0x9b, 0x79,
0x22, 0x1a, 0x0, 0x1c, 0x0, 0x18, 0x0,
/* U+F6AA "" */
0xfd, 0x0,
/* U+F6AB "" */
0x1f, 0x86, 0xe, 0x80, 0x20, 0x0, 0x6, 0x0,
0xe0, 0x6, 0x0,
/* U+F6AC "" */
0x80, 0x0, 0x37, 0xfc, 0xe, 0x3, 0x93, 0x0,
0x78, 0xe0, 0x8, 0x1c, 0x0, 0x27, 0xc0, 0x39,
0xb8, 0x10, 0x34, 0x0, 0xc, 0x0, 0x73, 0x0,
0x38, 0xe0, 0x8, 0x18, 0x0, 0x4,
/* U+F7C2 "" */
0x1f, 0xc8, 0x14, 0x96, 0x35, 0x80, 0x60, 0x18,
0x6, 0x1, 0x80, 0x60, 0x18, 0x6, 0x1, 0x80,
0x7f, 0xf0
};
/*---------------------
* GLYPH DESCRIPTION
*--------------------*/
static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */,
{.bitmap_index = 0, .adv_w = 196, .box_w = 12, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 21, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 46, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 71, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 96, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 121, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 146, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 171, .adv_w = 224, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 198, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 223, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 248, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 273, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 298, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 323, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 348, .adv_w = 196, .box_w = 12, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 369, .adv_w = 196, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1},
{.bitmap_index = 383, .adv_w = 168, .box_w = 9, .box_h = 9, .ofs_x = 1, .ofs_y = 1},
{.bitmap_index = 394, .adv_w = 224, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 416, .adv_w = 280, .box_w = 15, .box_h = 14, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 443, .adv_w = 224, .box_w = 13, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 466, .adv_w = 252, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 496, .adv_w = 224, .box_w = 13, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 519, .adv_w = 196, .box_w = 12, .box_h = 12, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 537, .adv_w = 280, .box_w = 17, .box_h = 13, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 565, .adv_w = 224, .box_w = 14, .box_h = 12, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 586, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 611, .adv_w = 140, .box_w = 7, .box_h = 10, .ofs_x = 1, .ofs_y = 0},
{.bitmap_index = 620, .adv_w = 168, .box_w = 10, .box_h = 13, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 637, .adv_w = 140, .box_w = 8, .box_h = 10, .ofs_x = 0, .ofs_y = 0},
{.bitmap_index = 647, .adv_w = 168, .box_w = 10, .box_h = 10, .ofs_x = 0, .ofs_y = 0},
{.bitmap_index = 660, .adv_w = 140, .box_w = 7, .box_h = 10, .ofs_x = 1, .ofs_y = 0},
{.bitmap_index = 669, .adv_w = 196, .box_w = 12, .box_h = 11, .ofs_x = 0, .ofs_y = 0},
{.bitmap_index = 686, .adv_w = 196, .box_w = 12, .box_h = 11, .ofs_x = 0, .ofs_y = 0},
{.bitmap_index = 703, .adv_w = 168, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 720, .adv_w = 168, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 737, .adv_w = 224, .box_w = 14, .box_h = 12, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 758, .adv_w = 224, .box_w = 14, .box_h = 12, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 779, .adv_w = 224, .box_w = 14, .box_h = 13, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 802, .adv_w = 196, .box_w = 12, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 823, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 848, .adv_w = 196, .box_w = 12, .box_h = 12, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 866, .adv_w = 280, .box_w = 18, .box_h = 12, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 893, .adv_w = 196, .box_w = 12, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 914, .adv_w = 252, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 1},
{.bitmap_index = 930, .adv_w = 252, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 1},
{.bitmap_index = 946, .adv_w = 252, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 1},
{.bitmap_index = 962, .adv_w = 252, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 1},
{.bitmap_index = 978, .adv_w = 252, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 1},
{.bitmap_index = 994, .adv_w = 168, .box_w = 9, .box_h = 14, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1010, .adv_w = 252, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 1},
{.bitmap_index = 1026, .adv_w = 280, .box_w = 18, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1058, .adv_w = 168, .box_w = 10, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1076, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1101, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1126, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1151, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1176, .adv_w = 280, .box_w = 18, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1208, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1233, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1258, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1283, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1308, .adv_w = 280, .box_w = 1, .box_h = 4, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1309, .adv_w = 280, .box_w = 5, .box_h = 6, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1313, .adv_w = 280, .box_w = 9, .box_h = 9, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1324, .adv_w = 280, .box_w = 12, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1342, .adv_w = 280, .box_w = 17, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1372, .adv_w = 252, .box_w = 15, .box_h = 12, .ofs_x = 1, .ofs_y = -1},
{.bitmap_index = 1395, .adv_w = 252, .box_w = 15, .box_h = 12, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 1418, .adv_w = 280, .box_w = 3, .box_h = 3, .ofs_x = 7, .ofs_y = -1},
{.bitmap_index = 1420, .adv_w = 280, .box_w = 12, .box_h = 7, .ofs_x = 3, .ofs_y = -1},
{.bitmap_index = 1431, .adv_w = 280, .box_w = 17, .box_h = 14, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1461, .adv_w = 168, .box_w = 10, .box_h = 14, .ofs_x = 0, .ofs_y = -2}
};
/*---------------------
* CHARACTER MAPPING
*--------------------*/
static const uint16_t unicode_list_0[] = {
0x0, 0x1a1, 0x320, 0x322, 0x327, 0x32a, 0x339, 0x342,
0x347, 0x34d, 0x350, 0x359, 0x3be, 0xfb6, 0xfbc, 0xfc1,
0xfc2, 0xfc6, 0xfc7, 0xfc8, 0xfca, 0xfce, 0xfdc, 0xfdd,
0xff3, 0xff9, 0xffd, 0x1000, 0x1001, 0x1002, 0x1006, 0x1015,
0x1016, 0x1017, 0x1018, 0x1026, 0x102a, 0x1061, 0x10a8, 0x10cd,
0x10d9, 0x11a0, 0x11ad, 0x11f5, 0x11f6, 0x11f7, 0x11f8, 0x11f9,
0x1248, 0x132b, 0x132c, 0x137a, 0x148f, 0x150b, 0x152e, 0x1539,
0x153d, 0x154d, 0x1550, 0x1559, 0x1568, 0x1641, 0x1642, 0x1643,
0x1644, 0x164a, 0x165d, 0x165e, 0x165f, 0x1660, 0x1661, 0x1777
};
/*Collect the unicode lists and glyph_id offsets*/
static const lv_font_fmt_txt_cmap_t cmaps[] =
{
{
.range_start = 57419, .range_length = 6008, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 72, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
}
};
/*--------------------
* ALL CUSTOM DATA
*--------------------*/
#if LVGL_VERSION_MAJOR == 8
/*Store all the custom data of the font*/
static lv_font_fmt_txt_glyph_cache_t cache;
#endif
#if LVGL_VERSION_MAJOR >= 8
static const lv_font_fmt_txt_dsc_t font_dsc = {
#else
static lv_font_fmt_txt_dsc_t font_dsc = {
#endif
.glyph_bitmap = glyph_bitmap,
.glyph_dsc = glyph_dsc,
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
.cmap_num = 1,
.bpp = 1,
.kern_classes = 0,
.bitmap_format = 0,
#if LVGL_VERSION_MAJOR == 8
.cache = &cache
#endif
};
/*-----------------
* PUBLIC FONT
*----------------*/
/*Initialize a public general font descriptor*/
#if LVGL_VERSION_MAJOR >= 8
const lv_font_t font_awesome_14_1 = {
#else
lv_font_t font_awesome_14_1 = {
#endif
.get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/
.get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/
.line_height = 15, /*The maximum line height required by the font*/
.base_line = 2, /*Baseline measured from the bottom of the line*/
#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0)
.subpx = LV_FONT_SUBPX_NONE,
#endif
#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8
.underline_position = -1,
.underline_thickness = 1,
#endif
.dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
.fallback = NULL,
#endif
.user_data = NULL,
};
#endif /*#if FONT_AWESOME_14_1*/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
#ifndef FONT_AWESOME_SYMBOLS_H
#define FONT_AWESOME_SYMBOLS_H
#define FONT_AWESOME_EMOJI_NEUTRAL "\xef\x96\xa4"
#define FONT_AWESOME_EMOJI_HAPPY "\xef\x84\x98"
#define FONT_AWESOME_EMOJI_LAUGHING "\xef\x96\x9b"
#define FONT_AWESOME_EMOJI_FUNNY "\xef\x96\x88"
#define FONT_AWESOME_EMOJI_SAD "\xee\x8e\x84"
#define FONT_AWESOME_EMOJI_ANGRY "\xef\x95\x96"
#define FONT_AWESOME_EMOJI_CRYING "\xef\x96\xb3"
#define FONT_AWESOME_EMOJI_LOVING "\xef\x96\x84"
#define FONT_AWESOME_EMOJI_EMBARRASSED "\xef\x95\xb9"
#define FONT_AWESOME_EMOJI_SURPRISED "\xee\x8d\xab"
#define FONT_AWESOME_EMOJI_SHOCKED "\xee\x8d\xb5"
#define FONT_AWESOME_EMOJI_THINKING "\xee\x8e\x9b"
#define FONT_AWESOME_EMOJI_WINKING "\xef\x93\x9a"
#define FONT_AWESOME_EMOJI_COOL "\xee\x8e\x98"
#define FONT_AWESOME_EMOJI_RELAXED "\xee\x8e\x92"
#define FONT_AWESOME_EMOJI_DELICIOUS "\xee\x8d\xb2"
#define FONT_AWESOME_EMOJI_KISSY "\xef\x96\x98"
#define FONT_AWESOME_EMOJI_CONFIDENT "\xee\x90\x89"
#define FONT_AWESOME_EMOJI_SLEEPY "\xee\x8e\x8d"
#define FONT_AWESOME_EMOJI_SILLY "\xee\x8e\xa4"
#define FONT_AWESOME_EMOJI_CONFUSED "\xee\x8d\xad"
#define FONT_AWESOME_BATTERY_FULL "\xef\x89\x80"
#define FONT_AWESOME_BATTERY_3 "\xef\x89\x81"
#define FONT_AWESOME_BATTERY_2 "\xef\x89\x82"
#define FONT_AWESOME_BATTERY_1 "\xef\x89\x83"
#define FONT_AWESOME_BATTERY_EMPTY "\xef\x89\x84"
#define FONT_AWESOME_BATTERY_SLASH "\xef\x8d\xb7"
#define FONT_AWESOME_BATTERY_CHARGING "\xef\x8d\xb6"
#define FONT_AWESOME_WIFI "\xef\x87\xab"
#define FONT_AWESOME_WIFI_FAIR "\xef\x9a\xab"
#define FONT_AWESOME_WIFI_WEAK "\xef\x9a\xaa"
#define FONT_AWESOME_WIFI_OFF "\xef\x9a\xac"
#define FONT_AWESOME_SIGNAL_FULL "\xef\x80\x92"
#define FONT_AWESOME_SIGNAL_4 "\xef\x9a\x8f"
#define FONT_AWESOME_SIGNAL_3 "\xef\x9a\x8e"
#define FONT_AWESOME_SIGNAL_2 "\xef\x9a\x8d"
#define FONT_AWESOME_SIGNAL_1 "\xef\x9a\x8c"
#define FONT_AWESOME_SIGNAL_OFF "\xef\x9a\x95"
#define FONT_AWESOME_VOLUME_HIGH "\xef\x80\xa8"
#define FONT_AWESOME_VOLUME_MEDIUM "\xef\x9a\xa8"
#define FONT_AWESOME_VOLUME_LOW "\xef\x80\xa7"
#define FONT_AWESOME_VOLUME_MUTE "\xef\x9a\xa9"
#define FONT_AWESOME_MUSIC "\xef\x80\x81"
#define FONT_AWESOME_CHECK "\xef\x80\x8c"
#define FONT_AWESOME_XMARK "\xef\x80\x8d"
#define FONT_AWESOME_POWER "\xef\x80\x91"
#define FONT_AWESOME_GEAR "\xef\x80\x93"
#define FONT_AWESOME_TRASH "\xef\x87\xb8"
#define FONT_AWESOME_HOME "\xef\x80\x95"
#define FONT_AWESOME_IMAGE "\xef\x80\xbe"
#define FONT_AWESOME_EDIT "\xef\x81\x84"
#define FONT_AWESOME_PREV "\xef\x81\x88"
#define FONT_AWESOME_NEXT "\xef\x81\x91"
#define FONT_AWESOME_PLAY "\xef\x81\x8b"
#define FONT_AWESOME_PAUSE "\xef\x81\x8c"
#define FONT_AWESOME_STOP "\xef\x81\x8d"
#define FONT_AWESOME_MICARROW_LEFT "\xef\x81\xa0"
#define FONT_AWESOME_ARROW_RIGHT "\xef\x81\xa1"
#define FONT_AWESOME_ARROW_UP "\xef\x81\xa2"
#define FONT_AWESOME_ARROW_DOWN "\xef\x81\xa3"
#define FONT_AWESOME_WARNING "\xef\x81\xb1"
#define FONT_AWESOME_BELL "\xef\x83\xb3"
#define FONT_AWESOME_LOCATION "\xef\x8f\x85"
#define FONT_AWESOME_GLOBE "\xef\x82\xac"
#define FONT_AWESOME_LOCATION_ARROW "\xef\x84\xa4"
#define FONT_AWESOME_SD_CARD "\xef\x9f\x82"
#define FONT_AWESOME_BLUETOOTH "\xef\x8a\x93"
#define FONT_AWESOME_COMMENT "\xef\x81\xb5"
#define FONT_AWESOME_AI_CHIP "\xee\x87\xac"
#define FONT_AWESOME_USER "\xef\x80\x87"
#define FONT_AWESOME_USER_ROBOT "\xee\x81\x8b"
#define FONT_AWESOME_DOWNLOAD "\xef\x80\x99"
#endif

48889
main/fonts/font_puhui_14_1.c Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,8 @@
## IDF Component Manager Manifest File
dependencies:
78/esp-wifi-connect: "~1.3.0"
78/esp-wifi-connect: "~1.4.1"
78/esp-opus-encoder: "~1.1.0"
78/esp-ml307: "~1.6.0"
78/esp-ml307: "~1.6.3"
espressif/led_strip: "^2.4.1"
espressif/esp_codec_dev: "^1.3.1"
espressif/esp-sr: "^1.9.0"

View File

@@ -36,8 +36,8 @@ void Ota::SetPostData(const std::string& post_data) {
}
bool Ota::CheckVersion() {
std::string current_version = esp_app_get_description()->version;
ESP_LOGI(TAG, "Current version: %s", current_version.c_str());
current_version_ = esp_app_get_description()->version;
ESP_LOGI(TAG, "Current version: %s", current_version_.c_str());
if (check_version_url_.length() < 10) {
ESP_LOGE(TAG, "Check version URL is not properly set");
@@ -109,7 +109,7 @@ bool Ota::CheckVersion() {
cJSON_Delete(root);
// Check if the version is newer, for example, 0.1.0 is newer than 0.0.1
has_new_version_ = IsNewVersionAvailable(current_version, firmware_version_);
has_new_version_ = IsNewVersionAvailable(current_version_, firmware_version_);
if (has_new_version_) {
ESP_LOGI(TAG, "New version available: %s", firmware_version_.c_str());
} else {
@@ -169,7 +169,6 @@ void Ota::Upgrade(const std::string& firmware_url) {
size_t total_read = 0, recent_read = 0;
auto last_calc_time = esp_timer_get_time();
while (true) {
taskYIELD(); // Avoid watchdog timeout
int ret = http->Read(buffer.data(), buffer.size());
if (ret < 0) {
ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret));

View File

@@ -19,10 +19,14 @@ public:
void StartUpgrade(std::function<void(int progress, size_t speed)> callback);
void MarkCurrentVersionValid();
const std::string& GetFirmwareVersion() const { return firmware_version_; }
const std::string& GetCurrentVersion() const { return current_version_; }
private:
std::string check_version_url_;
bool has_new_version_ = false;
bool has_mqtt_config_ = false;
std::string current_version_;
std::string firmware_version_;
std::string firmware_url_;
std::string post_data_;

View File

View File

@@ -1,27 +0,0 @@
#ifndef PROTOCOL_H
#define PROTOCOL_H
#include <cJSON.h>
#include <string>
#include <functional>
class Protocol {
public:
virtual ~Protocol() = default;
virtual void OnIncomingAudio(std::function<void(const std::string& data)> callback) = 0;
virtual void OnIncomingJson(std::function<void(const cJSON* root)> callback) = 0;
virtual void SendAudio(const std::string& data) = 0;
virtual void SendText(const std::string& text) = 0;
virtual void SendState(const std::string& state) = 0;
virtual void SendAbort() = 0;
virtual bool OpenAudioChannel() = 0;
virtual void CloseAudioChannel() = 0;
virtual void OnAudioChannelOpened(std::function<void()> callback) = 0;
virtual void OnAudioChannelClosed(std::function<void()> callback) = 0;
virtual bool IsAudioChannelOpened() const = 0;
virtual int GetServerSampleRate() const = 0;
};
#endif // PROTOCOL_H

View File

@@ -72,9 +72,9 @@ bool MqttProtocol::StartMqttClient() {
} else if (strcmp(type->valuestring, "goodbye") == 0) {
auto session_id = cJSON_GetObjectItem(root, "session_id");
if (session_id == nullptr || session_id_ == session_id->valuestring) {
if (on_audio_channel_closed_ != nullptr) {
on_audio_channel_closed_();
}
Application::GetInstance().Schedule([this]() {
CloseAudioChannel();
});
}
} else if (on_incoming_json_ != nullptr) {
on_incoming_json_(root);
@@ -85,6 +85,9 @@ bool MqttProtocol::StartMqttClient() {
ESP_LOGI(TAG, "Connecting to endpoint %s", endpoint_.c_str());
if (!mqtt_->Connect(endpoint_, 8883, client_id_, username_, password_)) {
ESP_LOGE(TAG, "Failed to connect to endpoint");
if (on_network_error_ != nullptr) {
on_network_error_("无法连接服务");
}
return false;
}
@@ -126,23 +129,6 @@ void MqttProtocol::SendAudio(const std::string& data) {
udp_->Send(encrypted);
}
void MqttProtocol::SendState(const std::string& state) {
std::string message = "{";
message += "\"session_id\":\"" + session_id_ + "\",";
message += "\"type\":\"state\",";
message += "\"state\":\"" + state + "\"";
message += "}";
SendText(message);
}
void MqttProtocol::SendAbort() {
std::string message = "{";
message += "\"session_id\":\"" + session_id_ + "\",";
message += "\"type\":\"abort\"";
message += "}";
SendText(message);
}
void MqttProtocol::CloseAudioChannel() {
{
std::lock_guard<std::mutex> lock(channel_mutex_);
@@ -167,7 +153,6 @@ bool MqttProtocol::OpenAudioChannel() {
if (mqtt_ == nullptr || !mqtt_->IsConnected()) {
ESP_LOGI(TAG, "MQTT is not connected, try to connect now");
if (!StartMqttClient()) {
ESP_LOGE(TAG, "Failed to connect to MQTT");
return false;
}
}
@@ -188,6 +173,9 @@ bool MqttProtocol::OpenAudioChannel() {
EventBits_t bits = xEventGroupWaitBits(event_group_handle_, MQTT_PROTOCOL_SERVER_HELLO_EVENT, pdTRUE, pdFALSE, pdMS_TO_TICKS(10000));
if (!(bits & MQTT_PROTOCOL_SERVER_HELLO_EVENT)) {
ESP_LOGE(TAG, "Failed to receive server hello");
if (on_network_error_ != nullptr) {
on_network_error_("等待响应超时");
}
return false;
}
@@ -240,22 +228,6 @@ bool MqttProtocol::OpenAudioChannel() {
return true;
}
void MqttProtocol::OnIncomingJson(std::function<void(const cJSON* root)> callback) {
on_incoming_json_ = callback;
}
void MqttProtocol::OnIncomingAudio(std::function<void(const std::string& data)> callback) {
on_incoming_audio_ = callback;
}
void MqttProtocol::OnAudioChannelOpened(std::function<void()> callback) {
on_audio_channel_opened_ = callback;
}
void MqttProtocol::OnAudioChannelClosed(std::function<void()> callback) {
on_audio_channel_closed_ = callback;
}
void MqttProtocol::ParseServerHello(const cJSON* root) {
auto transport = cJSON_GetObjectItem(root, "transport");
if (transport == nullptr || strcmp(transport->valuestring, "udp") != 0) {
@@ -297,11 +269,6 @@ void MqttProtocol::ParseServerHello(const cJSON* root) {
xEventGroupSetBits(event_group_handle_, MQTT_PROTOCOL_SERVER_HELLO_EVENT);
}
int MqttProtocol::GetServerSampleRate() const {
return server_sample_rate_;
}
static const char hex_chars[] = "0123456789ABCDEF";
// 辅助函数,将单个十六进制字符转换为对应的数值
static inline uint8_t CharToHex(char c) {

View File

@@ -14,6 +14,7 @@
#include <string>
#include <map>
#include <mutex>
#define MQTT_PING_INTERVAL_SECONDS 90
#define MQTT_RECONNECT_INTERVAL_MS 10000
@@ -24,27 +25,14 @@ public:
MqttProtocol();
~MqttProtocol();
void OnIncomingAudio(std::function<void(const std::string& data)> callback);
void OnIncomingJson(std::function<void(const cJSON* root)> callback);
void SendAudio(const std::string& data);
void SendText(const std::string& text);
void SendState(const std::string& state);
void SendAbort();
bool OpenAudioChannel();
void CloseAudioChannel();
void OnAudioChannelOpened(std::function<void()> callback);
void OnAudioChannelClosed(std::function<void()> callback);
bool IsAudioChannelOpened() const;
int GetServerSampleRate() const;
void SendAudio(const std::string& data) override;
bool OpenAudioChannel() override;
void CloseAudioChannel() override;
bool IsAudioChannelOpened() const override;
private:
EventGroupHandle_t event_group_handle_;
std::function<void(const cJSON* root)> on_incoming_json_;
std::function<void(const std::string& data)> on_incoming_audio_;
std::function<void()> on_audio_channel_opened_;
std::function<void()> on_audio_channel_closed_;
std::string endpoint_;
std::string client_id_;
std::string username_;
@@ -61,12 +49,12 @@ private:
int udp_port_;
uint32_t local_sequence_;
uint32_t remote_sequence_;
std::string session_id_;
int server_sample_rate_ = 16000;
bool StartMqttClient();
void ParseServerHello(const cJSON* root);
std::string DecodeHexString(const std::string& hex_string);
void SendText(const std::string& text) override;
};

View File

@@ -0,0 +1,59 @@
#include "protocol.h"
#include <esp_log.h>
#define TAG "Protocol"
void Protocol::OnIncomingJson(std::function<void(const cJSON* root)> callback) {
on_incoming_json_ = callback;
}
void Protocol::OnIncomingAudio(std::function<void(const std::string& data)> callback) {
on_incoming_audio_ = callback;
}
void Protocol::OnAudioChannelOpened(std::function<void()> callback) {
on_audio_channel_opened_ = callback;
}
void Protocol::OnAudioChannelClosed(std::function<void()> callback) {
on_audio_channel_closed_ = callback;
}
void Protocol::OnNetworkError(std::function<void(const std::string& message)> callback) {
on_network_error_ = callback;
}
void Protocol::SendAbortSpeaking(AbortReason reason) {
std::string message = "{\"session_id\":\"" + session_id_ + "\",\"type\":\"abort\"";
if (reason == kAbortReasonWakeWordDetected) {
message += ",\"reason\":\"wake_word_detected\"";
}
message += "}";
SendText(message);
}
void Protocol::SendWakeWordDetected(const std::string& wake_word) {
std::string json = "{\"session_id\":\"" + session_id_ +
"\",\"type\":\"listen\",\"state\":\"detect\",\"text\":\"" + wake_word + "\"}";
SendText(json);
}
void Protocol::SendStartListening(ListeningMode mode) {
std::string message = "{\"session_id\":\"" + session_id_ + "\"";
message += ",\"type\":\"listen\",\"state\":\"start\"";
if (mode == kListeningModeAlwaysOn) {
message += ",\"mode\":\"realtime\"";
} else if (mode == kListeningModeAutoStop) {
message += ",\"mode\":\"auto\"";
} else {
message += ",\"mode\":\"manual\"";
}
message += "}";
SendText(message);
}
void Protocol::SendStopListening() {
std::string message = "{\"session_id\":\"" + session_id_ + "\",\"type\":\"listen\",\"state\":\"stop\"}";
SendText(message);
}

63
main/protocols/protocol.h Normal file
View File

@@ -0,0 +1,63 @@
#ifndef PROTOCOL_H
#define PROTOCOL_H
#include <cJSON.h>
#include <string>
#include <functional>
struct BinaryProtocol3 {
uint8_t type;
uint8_t reserved;
uint16_t payload_size;
uint8_t payload[];
} __attribute__((packed));
enum AbortReason {
kAbortReasonNone,
kAbortReasonWakeWordDetected
};
enum ListeningMode {
kListeningModeAutoStop,
kListeningModeManualStop,
kListeningModeAlwaysOn // 需要 AEC 支持
};
class Protocol {
public:
virtual ~Protocol() = default;
inline int server_sample_rate() const {
return server_sample_rate_;
}
void OnIncomingAudio(std::function<void(const std::string& data)> callback);
void OnIncomingJson(std::function<void(const cJSON* root)> callback);
void OnAudioChannelOpened(std::function<void()> callback);
void OnAudioChannelClosed(std::function<void()> callback);
void OnNetworkError(std::function<void(const std::string& message)> callback);
virtual bool OpenAudioChannel() = 0;
virtual void CloseAudioChannel() = 0;
virtual bool IsAudioChannelOpened() const = 0;
virtual void SendAudio(const std::string& data) = 0;
virtual void SendWakeWordDetected(const std::string& wake_word);
virtual void SendStartListening(ListeningMode mode);
virtual void SendStopListening();
virtual void SendAbortSpeaking(AbortReason reason);
protected:
std::function<void(const cJSON* root)> on_incoming_json_;
std::function<void(const std::string& data)> on_incoming_audio_;
std::function<void()> on_audio_channel_opened_;
std::function<void()> on_audio_channel_closed_;
std::function<void(const std::string& message)> on_network_error_;
int server_sample_rate_ = 16000;
std::string session_id_;
virtual void SendText(const std::string& text) = 0;
};
#endif // PROTOCOL_H

View File

@@ -0,0 +1,150 @@
#include "websocket_protocol.h"
#include "board.h"
#include "system_info.h"
#include "application.h"
#include <cstring>
#include <cJSON.h>
#include <esp_log.h>
#include <arpa/inet.h>
#define TAG "WS"
#ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET
WebsocketProtocol::WebsocketProtocol() {
event_group_handle_ = xEventGroupCreate();
}
WebsocketProtocol::~WebsocketProtocol() {
if (websocket_ != nullptr) {
delete websocket_;
}
vEventGroupDelete(event_group_handle_);
}
void WebsocketProtocol::SendAudio(const std::string& data) {
if (websocket_ == nullptr) {
return;
}
websocket_->Send(data.data(), data.size(), true);
}
void WebsocketProtocol::SendText(const std::string& text) {
if (websocket_ == nullptr) {
return;
}
websocket_->Send(text);
}
bool WebsocketProtocol::IsAudioChannelOpened() const {
return websocket_ != nullptr;
}
void WebsocketProtocol::CloseAudioChannel() {
if (websocket_ != nullptr) {
delete websocket_;
websocket_ = nullptr;
}
}
bool WebsocketProtocol::OpenAudioChannel() {
if (websocket_ != nullptr) {
delete websocket_;
}
std::string url = CONFIG_WEBSOCKET_URL;
std::string token = "Bearer " + std::string(CONFIG_WEBSOCKET_ACCESS_TOKEN);
websocket_ = Board::GetInstance().CreateWebSocket();
websocket_->SetHeader("Authorization", token.c_str());
websocket_->SetHeader("Protocol-Version", "1");
websocket_->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
websocket_->OnData([this](const char* data, size_t len, bool binary) {
if (binary) {
if (on_incoming_audio_ != nullptr) {
on_incoming_audio_(std::string(data, len));
}
} else {
// Parse JSON data
auto root = cJSON_Parse(data);
auto type = cJSON_GetObjectItem(root, "type");
if (type != NULL) {
if (strcmp(type->valuestring, "hello") == 0) {
ParseServerHello(root);
} else {
if (on_incoming_json_ != nullptr) {
on_incoming_json_(root);
}
}
} else {
ESP_LOGE(TAG, "Missing message type, data: %s", data);
}
cJSON_Delete(root);
}
});
websocket_->OnDisconnected([this]() {
ESP_LOGI(TAG, "Websocket disconnected");
if (on_audio_channel_closed_ != nullptr) {
on_audio_channel_closed_();
}
});
if (!websocket_->Connect(url.c_str())) {
ESP_LOGE(TAG, "Failed to connect to websocket server");
if (on_network_error_ != nullptr) {
on_network_error_("无法连接服务");
}
return false;
}
// Send hello message to describe the client
// keys: message type, version, audio_params (format, sample_rate, channels)
std::string message = "{";
message += "\"type\":\"hello\",";
message += "\"version\": 1,";
message += "\"transport\":\"websocket\",";
message += "\"audio_params\":{";
message += "\"format\":\"opus\", \"sample_rate\":16000, \"channels\":1, \"frame_duration\":" + std::to_string(OPUS_FRAME_DURATION_MS);
message += "}}";
websocket_->Send(message);
// Wait for server hello
EventBits_t bits = xEventGroupWaitBits(event_group_handle_, WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT, pdTRUE, pdFALSE, pdMS_TO_TICKS(10000));
if (!(bits & WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT)) {
ESP_LOGE(TAG, "Failed to receive server hello");
if (on_network_error_ != nullptr) {
on_network_error_("等待响应超时");
}
return false;
}
if (on_audio_channel_opened_ != nullptr) {
on_audio_channel_opened_();
}
return true;
}
void WebsocketProtocol::ParseServerHello(const cJSON* root) {
auto transport = cJSON_GetObjectItem(root, "transport");
if (transport == nullptr || strcmp(transport->valuestring, "websocket") != 0) {
ESP_LOGE(TAG, "Unsupported transport: %s", transport->valuestring);
return;
}
auto audio_params = cJSON_GetObjectItem(root, "audio_params");
if (audio_params != NULL) {
auto sample_rate = cJSON_GetObjectItem(audio_params, "sample_rate");
if (sample_rate != NULL) {
server_sample_rate_ = sample_rate->valueint;
}
}
xEventGroupSetBits(event_group_handle_, WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT);
}
#endif

View File

@@ -0,0 +1,31 @@
#ifndef _WEBSOCKET_PROTOCOL_H_
#define _WEBSOCKET_PROTOCOL_H_
#include "protocol.h"
#include <web_socket.h>
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#define WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT (1 << 0)
class WebsocketProtocol : public Protocol {
public:
WebsocketProtocol();
~WebsocketProtocol();
void SendAudio(const std::string& data) override;
bool OpenAudioChannel() override;
void CloseAudioChannel() override;
bool IsAudioChannelOpened() const override;
private:
EventGroupHandle_t event_group_handle_;
WebSocket* websocket_ = nullptr;
void ParseServerHello(const cJSON* root);
void SendText(const std::string& text) override;
};
#endif

View File

@@ -11,7 +11,7 @@ Settings::Settings(const std::string& ns, bool read_write) : ns_(ns), read_write
Settings::~Settings() {
if (nvs_handle_ != 0) {
if (read_write_) {
if (read_write_ && dirty_) {
ESP_ERROR_CHECK(nvs_commit(nvs_handle_));
}
nvs_close(nvs_handle_);
@@ -37,6 +37,7 @@ std::string Settings::GetString(const std::string& key, const std::string& defau
void Settings::SetString(const std::string& key, const std::string& value) {
if (read_write_) {
ESP_ERROR_CHECK(nvs_set_str(nvs_handle_, key.c_str(), value.c_str()));
dirty_ = true;
} else {
ESP_LOGW(TAG, "Namespace %s is not open for writing", ns_.c_str());
}
@@ -57,6 +58,23 @@ int32_t Settings::GetInt(const std::string& key, int32_t default_value) {
void Settings::SetInt(const std::string& key, int32_t value) {
if (read_write_) {
ESP_ERROR_CHECK(nvs_set_i32(nvs_handle_, key.c_str(), value));
dirty_ = true;
} else {
ESP_LOGW(TAG, "Namespace %s is not open for writing", ns_.c_str());
}
}
void Settings::EraseKey(const std::string& key) {
if (read_write_) {
ESP_ERROR_CHECK(nvs_erase_key(nvs_handle_, key.c_str()));
} else {
ESP_LOGW(TAG, "Namespace %s is not open for writing", ns_.c_str());
}
}
void Settings::EraseAll() {
if (read_write_) {
ESP_ERROR_CHECK(nvs_erase_all(nvs_handle_));
} else {
ESP_LOGW(TAG, "Namespace %s is not open for writing", ns_.c_str());
}

View File

@@ -13,11 +13,14 @@ public:
void SetString(const std::string& key, const std::string& value);
int32_t GetInt(const std::string& key, int32_t default_value = 0);
void SetInt(const std::string& key, int32_t value);
void EraseKey(const std::string& key);
void EraseAll();
private:
std::string ns_;
nvs_handle_t nvs_handle_ = 0;
bool read_write_ = false;
bool dirty_ = false;
};
#endif

View File

@@ -1,24 +0,0 @@
#ifndef _SYSTEM_RESET_H
#define _SYSTEM_RESET_H
class SystemReset {
public:
static SystemReset& GetInstance() {
static SystemReset instance;
return instance;
}
void CheckButtons();
private:
SystemReset(); // 构造函数私有化
SystemReset(const SystemReset&) = delete; // 禁用拷贝构造
SystemReset& operator=(const SystemReset&) = delete; // 禁用赋值操作
void ResetNvsFlash();
void ResetToFactory();
void RestartInSeconds(int seconds);
};
#endif

View File

@@ -4,9 +4,9 @@
#include <esp_log.h>
#include <model_path.h>
#include <arpa/inet.h>
#include <sstream>
#define DETECTION_RUNNING_EVENT 1
#define WAKE_WORD_ENCODED_EVENT 2
static const char* TAG = "WakeWordDetect";
@@ -24,10 +24,7 @@ WakeWordDetect::~WakeWordDetect() {
}
if (wake_word_encode_task_stack_ != nullptr) {
free(wake_word_encode_task_stack_);
}
if (audio_detection_task_stack_ != nullptr) {
heap_caps_free(audio_detection_task_stack_);
heap_caps_free(wake_word_encode_task_stack_);
}
vEventGroupDelete(event_group_);
@@ -43,6 +40,13 @@ void WakeWordDetect::Initialize(int channels, bool reference) {
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;
while (std::getline(ss, word, ';')) {
wake_words_.push_back(word);
}
}
}
@@ -80,16 +84,14 @@ void WakeWordDetect::Initialize(int channels, bool reference) {
afe_detection_data_ = esp_afe_sr_v1.create_from_config(&afe_config);
const size_t audio_detection_task_stack_size = 4096 * 2;
audio_detection_task_stack_ = (StackType_t*)heap_caps_malloc(audio_detection_task_stack_size, MALLOC_CAP_SPIRAM);
xTaskCreateStatic([](void* arg) {
xTaskCreate([](void* arg) {
auto this_ = (WakeWordDetect*)arg;
this_->AudioDetectionTask();
vTaskDelete(NULL);
}, "audio_detection", audio_detection_task_stack_size, this, 1, audio_detection_task_stack_, &audio_detection_task_buffer_);
}, "audio_detection", 4096 * 2, this, 1, nullptr);
}
void WakeWordDetect::OnWakeWordDetected(std::function<void()> callback) {
void WakeWordDetect::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) {
wake_word_detected_callback_ = callback;
}
@@ -149,11 +151,11 @@ void WakeWordDetect::AudioDetectionTask() {
}
if (res->wakeup_state == WAKENET_DETECTED) {
ESP_LOGI(TAG, "Wake word detected");
StopDetection();
last_detected_wake_word_ = wake_words_[res->wake_word_index - 1];
if (wake_word_detected_callback_) {
wake_word_detected_callback_();
wake_word_detected_callback_(last_detected_wake_word_);
}
}
}
@@ -170,10 +172,9 @@ void WakeWordDetect::StoreWakeWordData(uint16_t* data, size_t samples) {
}
void WakeWordDetect::EncodeWakeWordData() {
xEventGroupClearBits(event_group_, WAKE_WORD_ENCODED_EVENT);
wake_word_opus_.clear();
if (wake_word_encode_task_stack_ == nullptr) {
wake_word_encode_task_stack_ = (StackType_t*)malloc(4096 * 8);
wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(4096 * 8, MALLOC_CAP_SPIRAM);
}
wake_word_encode_task_ = xTaskCreateStatic([](void* arg) {
auto this_ = (WakeWordDetect*)arg;
@@ -187,15 +188,18 @@ void WakeWordDetect::EncodeWakeWordData() {
encoder->Encode(pcm, [this_](const uint8_t* opus, size_t opus_size) {
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.emplace_back(std::string(reinterpret_cast<const char*>(opus), opus_size));
this_->wake_word_cv_.notify_one();
this_->wake_word_cv_.notify_all();
});
}
this_->wake_word_pcm_.clear();
auto end_time = esp_timer_get_time();
ESP_LOGI(TAG, "Encode wake word opus %zu packets in %lld ms", this_->wake_word_opus_.size(), (end_time - start_time) / 1000);
xEventGroupSetBits(this_->event_group_, WAKE_WORD_ENCODED_EVENT);
this_->wake_word_cv_.notify_one();
{
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.push_back("");
this_->wake_word_cv_.notify_all();
}
delete encoder;
vTaskDelete(NULL);
}, "encode_detect_packets", 4096 * 8, this, 1, wake_word_encode_task_stack_, &wake_word_encode_task_buffer_);
@@ -204,12 +208,9 @@ void WakeWordDetect::EncodeWakeWordData() {
bool WakeWordDetect::GetWakeWordOpus(std::string& opus) {
std::unique_lock<std::mutex> lock(wake_word_mutex_);
wake_word_cv_.wait(lock, [this]() {
return !wake_word_opus_.empty() || (xEventGroupGetBits(event_group_) & WAKE_WORD_ENCODED_EVENT);
return !wake_word_opus_.empty();
});
if (wake_word_opus_.empty()) {
return false;
}
opus.swap(wake_word_opus_.front());
wake_word_opus_.pop_front();
return true;
return !opus.empty();
}

View File

@@ -23,28 +23,27 @@ public:
void Initialize(int channels, bool reference);
void Feed(std::vector<int16_t>& data);
void OnWakeWordDetected(std::function<void()> callback);
void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback);
void OnVadStateChange(std::function<void(bool speaking)> callback);
void StartDetection();
void StopDetection();
bool IsDetectionRunning();
void EncodeWakeWordData();
bool GetWakeWordOpus(std::string& opus);
const std::string& GetLastDetectedWakeWord() const { return last_detected_wake_word_; }
private:
esp_afe_sr_data_t* afe_detection_data_ = nullptr;
char* wakenet_model_ = NULL;
std::vector<std::string> wake_words_;
std::vector<int16_t> input_buffer_;
EventGroupHandle_t event_group_;
std::function<void()> wake_word_detected_callback_;
std::function<void(const std::string& wake_word)> wake_word_detected_callback_;
std::function<void(bool speaking)> vad_state_change_callback_;
bool is_speaking_ = false;
int channels_;
bool reference_;
TaskHandle_t audio_detection_task_ = nullptr;
StaticTask_t audio_detection_task_buffer_;
StackType_t* audio_detection_task_stack_ = nullptr;
std::string last_detected_wake_word_;
TaskHandle_t wake_word_encode_task_ = nullptr;
StaticTask_t wake_word_encode_task_buffer_;

View File

@@ -14,6 +14,7 @@ CONFIG_USE_WAKENET=y
CONFIG_SR_WN_WN9_NIHAOXIAOZHI_TTS=y
CONFIG_USE_MULTINET=n
ESP_TASK_WDT_TIMEOUT_S=10
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y

View File

@@ -64,7 +64,7 @@ def get_board_name(folder):
return "bread-compact-wifi"
elif "KevinBox1" in basename:
return "kevin-box-1"
if basename.startswith("v0.7"):
if basename.startswith("v0.7") or basename.startswith("v0.8") or basename.startswith("v0.9"):
return basename.split("_")[1]
raise Exception(f"Unknown board name: {basename}")