Update to 2.0.1

This commit is contained in:
2025-09-15 22:04:01 +08:00
parent 5c43129024
commit 7d7f5eae3d
74 changed files with 5253 additions and 439 deletions

View File

@@ -57,9 +57,11 @@ set(SOURCES "audio/audio_codec.cc"
"display/lvgl_display/lvgl_image.cc"
"display/lvgl_display/gif/lvgl_gif.cc"
"display/lvgl_display/gif/gifdec.c"
"display/esplog_display.cc"
"protocols/protocol.cc"
"protocols/mqtt_protocol.cc"
"protocols/websocket_protocol.cc"
"protocols/sleep_music_protocol.cc"
"mcp_server.cc"
"system_info.cc"
"application.cc"
@@ -67,6 +69,8 @@ set(SOURCES "audio/audio_codec.cc"
"settings.cc"
"device_state_event.cc"
"assets.cc"
"schedule_manager.cc"
"timer_manager.cc"
"main.cc"
)
@@ -516,6 +520,9 @@ elseif(CONFIG_BOARD_TYPE_SURFER_C3_1_14TFT)
set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_32})
elseif(CONFIG_BOARD_TYPE_ESP32S3_SMART_SPEAKER)
set(BOARD_TYPE "esp32s3-smart-speaker")
set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_WAKENET_ONLY})
endif()
file(GLOB BOARD_SOURCES
@@ -823,4 +830,4 @@ elseif(CONFIG_FLASH_CUSTOM_ASSETS)
message(STATUS "Custom assets flash configured: ${ASSETS_LOCAL_FILE} -> assets partition")
elseif(CONFIG_FLASH_NONE_ASSETS)
message(STATUS "Assets flashing disabled (FLASH_NONE_ASSETS)")
endif()
endif()

View File

@@ -94,6 +94,9 @@ choice BOARD_TYPE
config BOARD_TYPE_BREAD_COMPACT_WIFI_CAM
bool "面包板新版接线WiFi+ LCD + Camera"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP32S3_SMART_SPEAKER
bool "ESP32-S3 智能音箱PCM5102+INMP441"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_BREAD_COMPACT_ML307
bool "面包板新版接线ML307 AT"
depends on IDF_TARGET_ESP32S3
@@ -370,6 +373,9 @@ choice BOARD_TYPE
config BOARD_TYPE_SURFER_C3_1_14TFT
bool "Surfer-C3-1-14TFT"
depends on IDF_TARGET_ESP32C3
config BOARD_TYPE_YUNLIAO_S3
bool "小智云聊-S3"
depends on IDF_TARGET_ESP32S3
endchoice
choice ESP_S3_LCD_EV_Board_Version_TYPE
@@ -529,7 +535,7 @@ config USE_AUDIO_PROCESSOR
config USE_DEVICE_AEC
bool "Enable Device-Side AEC"
default n
depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE || BOARD_TYPE_LICHUANG_DEV || BOARD_TYPE_ESP32S3_KORVO2_V3 || BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75 || BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06 || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC || BOARD_TYPE_ESP_S3_LCD_EV_Board_2)
depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE || BOARD_TYPE_LICHUANG_DEV || BOARD_TYPE_ESP32S3_KORVO2_V3 || BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75 || BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06 || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC || BOARD_TYPE_ESP_S3_LCD_EV_Board_2 || BOARD_TYPE_YUNLIAO_S3)
help
因为性能不够,不建议和微信聊天界面风格同时开启
@@ -565,16 +571,25 @@ config RECEIVE_CUSTOM_MESSAGE
help
启用接收自定义消息功能,允许设备接收来自服务器的自定义消息(最好通过 MQTT 协议)
choice I2S_TYPE_TAIJIPI_S3
menu TAIJIPAI_S3_CONFIG
depends on BOARD_TYPE_ESP32S3_Taiji_Pi
prompt "taiji-pi-S3 I2S Type"
default TAIJIPAI_I2S_TYPE_STD
help
I2S 类型选择
config TAIJIPAI_I2S_TYPE_STD
bool "I2S Type STD"
config TAIJIPAI_I2S_TYPE_PDM
bool "I2S Type PDM"
endchoice
choice I2S_TYPE_TAIJIPI_S3
depends on BOARD_TYPE_ESP32S3_Taiji_Pi
prompt "taiji-pi-S3 I2S Type"
default TAIJIPAI_I2S_TYPE_STD
help
I2S 类型选择
config TAIJIPAI_I2S_TYPE_STD
bool "I2S Type STD"
config TAIJIPAI_I2S_TYPE_PDM
bool "I2S Type PDM"
endchoice
config I2S_USE_2SLOT
bool "Enable Use 2 Slot"
default n
help
启动双声道
endmenu
endmenu

View File

@@ -540,6 +540,12 @@ void Application::Start() {
// Play the success sound to indicate the device is ready
audio_service_.PlaySound(Lang::Sounds::OGG_SUCCESS);
}
// Start the main event loop task with priority 3
xTaskCreate([](void* arg) {
((Application*)arg)->MainEventLoop();
vTaskDelete(NULL);
}, "main_event_loop", 2048 * 4, this, 3, &main_event_loop_task_handle_);
}
// Add a async task to MainLoop
@@ -555,9 +561,6 @@ void Application::Schedule(std::function<void()> callback) {
// If other tasks need to access the websocket or chat state,
// they should use Schedule to call this function
void Application::MainEventLoop() {
// Raise the priority of the main event loop to avoid being interrupted by background tasks (which has priority 2)
vTaskPrioritySet(NULL, 3);
while (true) {
auto bits = xEventGroupWaitBits(event_group_, MAIN_EVENT_SCHEDULE |
MAIN_EVENT_SEND_AUDIO |
@@ -682,17 +685,6 @@ void Application::SetDeviceState(DeviceState state) {
auto display = board.GetDisplay();
auto led = board.GetLed();
led->OnStateChanged();
// 当从idle状态变成其他任何状态时停止音乐播放
if (previous_state == kDeviceStateIdle && state != kDeviceStateIdle) {
auto music = board.GetMusic();
if (music) {
ESP_LOGI(TAG, "Stopping music streaming due to state change: %s -> %s",
STATE_STRINGS[previous_state], STATE_STRINGS[state]);
music->StopStreaming();
}
}
switch (state) {
case kDeviceStateUnknown:
case kDeviceStateIdle:
@@ -843,11 +835,20 @@ bool Application::CanEnterSleepMode() {
}
void Application::SendMcpMessage(const std::string& payload) {
Schedule([this, payload]() {
if (protocol_) {
if (protocol_ == nullptr) {
return;
}
// Make sure you are using main thread to send MCP message
if (xTaskGetCurrentTaskHandle() == main_event_loop_task_handle_) {
ESP_LOGI(TAG, "Send MCP message in main thread");
protocol_->SendMcpMessage(payload);
} else {
ESP_LOGI(TAG, "Send MCP message in sub thread");
Schedule([this, payload = std::move(payload)]() {
protocol_->SendMcpMessage(payload);
}
});
});
}
}
void Application::SetAecMode(AecMode mode) {
@@ -889,6 +890,9 @@ void Application::AddAudioData(AudioStreamPacket&& packet) {
// 检查采样率是否匹配,如果不匹配则进行简单重采样
if (packet.sample_rate != codec->output_sample_rate()) {
// ESP_LOGI(TAG, "Resampling music audio from %d to %d Hz",
// packet.sample_rate, codec->output_sample_rate());
// 验证采样率参数
if (packet.sample_rate <= 0 || codec->output_sample_rate() <= 0) {
ESP_LOGE(TAG, "Invalid sample rates: %d -> %d",
@@ -896,12 +900,19 @@ void Application::AddAudioData(AudioStreamPacket&& packet) {
return;
}
// 尝试动态切换采样率
if (codec->SetOutputSampleRate(packet.sample_rate)) {
ESP_LOGI(TAG, "Successfully switched to music playback sampling rate: %d Hz", packet.sample_rate);
std::vector<int16_t> resampled;
if (packet.sample_rate > codec->output_sample_rate()) {
ESP_LOGI(TAG, "Music Player: Adjust the sampling rate from %d Hz to %d Hz",
codec->output_sample_rate(), packet.sample_rate);
// 尝试动态切换采样率
if (codec->SetOutputSampleRate(packet.sample_rate)) {
ESP_LOGI(TAG, "Successfully switched to music playback sampling rate: %d Hz", packet.sample_rate);
} else {
ESP_LOGW(TAG, "Unable to switch sampling rate, continue using current sampling rate: %d Hz", codec->output_sample_rate());
}
} else {
ESP_LOGW(TAG, "Unable to switch sampling rate, continue using current sampling rate: %d Hz", codec->output_sample_rate());
// 如果无法切换采样率,继续使用当前的采样率进行处理
if (packet.sample_rate > codec->output_sample_rate()) {
// 下采样:简单丢弃部分样本
float downsample_ratio = static_cast<float>(packet.sample_rate) / codec->output_sample_rate();
@@ -922,12 +933,13 @@ void Application::AddAudioData(AudioStreamPacket&& packet) {
// 上采样:线性插值
float upsample_ratio = codec->output_sample_rate() / static_cast<float>(packet.sample_rate);
size_t expected_size = static_cast<size_t>(pcm_data.size() * upsample_ratio + 0.5f);
std::vector<int16_t> resampled(expected_size);
resampled.reserve(expected_size);
for (size_t i = 0; i < pcm_data.size(); ++i) {
// 添加原始样本
resampled[i * static_cast<size_t>(upsample_ratio)] = pcm_data[i];
resampled.push_back(pcm_data[i]);
// 计算需要插值的样本数
int interpolation_count = static_cast<int>(upsample_ratio) - 1;
if (interpolation_count > 0 && i + 1 < pcm_data.size()) {
@@ -936,21 +948,22 @@ void Application::AddAudioData(AudioStreamPacket&& packet) {
for (int j = 1; j <= interpolation_count; ++j) {
float t = static_cast<float>(j) / (interpolation_count + 1);
int16_t interpolated = static_cast<int16_t>(current + (next - current) * t);
resampled[i * static_cast<size_t>(upsample_ratio) + j] = interpolated;
resampled.push_back(interpolated);
}
} else if (interpolation_count > 0) {
// 最后一个样本,直接重复
for (int j = 1; j <= interpolation_count; ++j) {
resampled[i * static_cast<size_t>(upsample_ratio) + j] = pcm_data[i];
resampled.push_back(pcm_data[i]);
}
}
}
pcm_data = std::move(resampled);
ESP_LOGI(TAG, "Upsampled %d -> %d samples (ratio: %.2f)",
pcm_data.size() / static_cast<size_t>(upsample_ratio), pcm_data.size(), upsample_ratio);
pcm_data.size(), resampled.size(), upsample_ratio);
}
}
pcm_data = std::move(resampled);
}
// 确保音频输出已启用

View File

@@ -61,11 +61,9 @@ public:
void SendMcpMessage(const std::string& payload);
void SetAecMode(AecMode mode);
AecMode GetAecMode() const { return aec_mode_; }
// 新增:接收外部音频数据(如音乐播放)
void AddAudioData(AudioStreamPacket&& packet);
void PlaySound(const std::string_view& sound);
AudioService& GetAudioService() { return audio_service_; }
void AddAudioData(AudioStreamPacket&& packet);
private:
Application();
@@ -86,6 +84,7 @@ private:
bool aborted_ = false;
int clock_ticks_ = 0;
TaskHandle_t check_new_version_task_handle_ = nullptr;
TaskHandle_t main_event_loop_task_handle_ = nullptr;
void OnWakeWordDetected();
void CheckNewVersion(Ota& ota);
@@ -94,4 +93,19 @@ private:
void SetListeningMode(ListeningMode mode);
};
class TaskPriorityReset {
public:
TaskPriorityReset(BaseType_t priority) {
original_priority_ = uxTaskPriorityGet(NULL);
vTaskPrioritySet(NULL, priority);
}
~TaskPriorityReset() {
vTaskPrioritySet(NULL, original_priority_);
}
private:
BaseType_t original_priority_;
};
#endif // _APPLICATION_H_

View File

@@ -35,17 +35,28 @@ void AudioCodec::Start() {
}
// 保存原始输出采样率
if (original_output_sample_rate_ == 0){
if (original_output_sample_rate_ == 0) {
original_output_sample_rate_ = output_sample_rate_;
ESP_LOGI(TAG, "Saved original output sample rate: %d Hz", original_output_sample_rate_);
}
if (tx_handle_ != nullptr) {
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
esp_err_t err = i2s_channel_enable(tx_handle_);
if (err == ESP_ERR_INVALID_STATE) {
// 已经启用,忽略
ESP_LOGW(TAG, "TX channel already enabled");
} else {
ESP_ERROR_CHECK(err);
}
}
if (rx_handle_ != nullptr) {
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_));
esp_err_t err = i2s_channel_enable(rx_handle_);
if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGW(TAG, "RX channel already enabled");
} else {
ESP_ERROR_CHECK(err);
}
}
EnableInput(true);

View File

@@ -59,4 +59,4 @@ protected:
virtual int Write(const int16_t* data, int samples) = 0;
};
#endif // _AUDIO_CODEC_H
#endif // _AUDIO_CODEC_H

View File

@@ -672,6 +672,7 @@ void AudioService::SetModelsList(srmodel_list_t* models_list) {
models_list_ = models_list;
}
void AudioService::UpdateOutputTimestamp() {
last_output_time_ = std::chrono::steady_clock::now();
}

View File

@@ -108,9 +108,7 @@ public:
bool ReadAudioData(std::vector<int16_t>& data, int sample_rate, int samples);
void ResetDecoder();
void SetModelsList(srmodel_list_t* models_list);
void UpdateOutputTimestamp();
private:
AudioCodec* codec_ = nullptr;
AudioServiceCallbacks callbacks_;

View File

@@ -6,13 +6,14 @@
Es8388AudioCodec::Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
gpio_num_t pa_pin, uint8_t es8388_addr) {
gpio_num_t pa_pin, uint8_t es8388_addr, bool input_reference) {
duplex_ = true; // 是否双工
input_reference_ = false; // 是否使用参考输入,实现回声消除
input_channels_ = 1; // 输入通道数
input_reference_ = input_reference; // 是否使用参考输入,实现回声消除
input_channels_ = input_reference_ ? 2 : 1; // 输入通道数
input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate;
pa_pin_ = pa_pin; CreateDuplexChannels(mclk, bclk, ws, dout, din);
pa_pin_ = pa_pin;
CreateDuplexChannels(mclk, bclk, ws, dout, din);
// Do initialize of related interface: data_if, ctrl_if and gpio_if
audio_codec_i2s_cfg_t i2s_cfg = {
@@ -144,13 +145,21 @@ void Es8388AudioCodec::EnableInput(bool enable) {
if (enable) {
esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16,
.channel = 1,
.channel_mask = 0,
.channel = (uint8_t) input_channels_,
.channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
.sample_rate = (uint32_t)input_sample_rate_,
.mclk_multiple = 0,
};
if (input_reference_) {
fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1);
}
ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 24.0));
if (input_reference_) {
uint8_t gain = (11 << 4) + 0;
ctrl_if_->write_reg(ctrl_if_, 0x09, 1, &gain, 1);
}else{
ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 24.0));
}
} else {
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
}
@@ -175,6 +184,9 @@ void Es8388AudioCodec::EnableOutput(bool enable) {
// Set analog output volume to 0dB, default is -45dB
uint8_t reg_val = 30; // 0dB
if(input_reference_){
reg_val = 27;
}
uint8_t regs[] = { 46, 47, 48, 49 }; // HP_LVOL, HP_RVOL, SPK_LVOL, SPK_RVOL
for (uint8_t reg : regs) {
ctrl_if_->write_reg(ctrl_if_, reg, 1, &reg_val, 1);
@@ -200,7 +212,7 @@ int Es8388AudioCodec::Read(int16_t* dest, int samples) {
}
int Es8388AudioCodec::Write(const int16_t* data, int samples) {
if (output_enabled_) {
if (output_enabled_ && output_dev_ && data != nullptr) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
}
return samples;

View File

@@ -29,7 +29,7 @@ private:
public:
Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
gpio_num_t pa_pin, uint8_t es8388_addr);
gpio_num_t pa_pin, uint8_t es8388_addr, bool input_reference = false);
virtual ~Es8388AudioCodec();
virtual void SetOutputVolume(int volume) override;

View File

@@ -105,8 +105,8 @@ NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sampl
.slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_MONO,
.slot_mask = I2S_STD_SLOT_LEFT,
.slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = I2S_STD_SLOT_BOTH,
.ws_width = I2S_DATA_BIT_WIDTH_32BIT,
.ws_pol = false,
.bit_shift = true,
@@ -136,6 +136,9 @@ NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sampl
chan_cfg.id = (i2s_port_t)1;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_));
std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_;
// RX 使用单声道 LEFT
std_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_MONO;
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT;
std_cfg.gpio_cfg.bclk = mic_sck;
std_cfg.gpio_cfg.ws = mic_ws;
std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED;
@@ -214,6 +217,84 @@ NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sampl
ESP_LOGI(TAG, "Simplex channels created");
}
NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask,gpio_num_t mic_sck, gpio_num_t mic_din) {
duplex_ = false;
input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate;
// Create a new channel for speaker
i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)1, I2S_ROLE_MASTER);
tx_chan_cfg.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM;
tx_chan_cfg.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM;
tx_chan_cfg.auto_clear_after_cb = true;
tx_chan_cfg.auto_clear_before_cb = false;
tx_chan_cfg.intr_priority = 0;
ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_handle_, NULL));
i2s_std_config_t tx_std_cfg = {
.clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT,
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
#ifdef I2S_HW_VERSION_2
.ext_clk_freq_hz = 0,
#endif
},
.slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_MONO,
.slot_mask = spk_slot_mask,
.ws_width = I2S_DATA_BIT_WIDTH_32BIT,
.ws_pol = false,
.bit_shift = true,
#ifdef I2S_HW_VERSION_2
.left_align = true,
.big_endian = false,
.bit_order_lsb = false
#endif
},
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = spk_bclk,
.ws = spk_ws,
.dout = spk_dout,
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &tx_std_cfg));
#if SOC_I2S_SUPPORTS_PDM_RX
// Create a new channel for MIC in PDM mode
i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)0, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&rx_chan_cfg, NULL, &rx_handle_));
i2s_pdm_rx_config_t pdm_rx_cfg = {
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG((uint32_t)input_sample_rate_),
/* The data bit-width of PDM mode is fixed to 16 */
.slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.clk = mic_sck,
.din = mic_din,
.invert_flags = {
.clk_inv = false,
},
},
};
ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle_, &pdm_rx_cfg));
#else
ESP_LOGE(TAG, "PDM is not supported");
#endif
ESP_LOGI(TAG, "Simplex channels created");
}
NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din) {
duplex_ = false;
input_sample_rate_ = input_sample_rate;
@@ -280,25 +361,30 @@ NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output
int NoAudioCodec::Write(const int16_t* data, int samples) {
std::lock_guard<std::mutex> lock(data_if_mutex_);
std::vector<int32_t> buffer(samples);
// 立体声交织输出L,R,L,R ... 每声道32位
std::vector<int32_t> buffer(samples * 2);
// output_volume_: 0-100
// volume_factor_: 0-65536
int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536;
for (int i = 0; i < samples; i++) {
int64_t temp = int64_t(data[i]) * volume_factor; // 使用 int64_t 进行乘法运算
int32_t s32;
if (temp > INT32_MAX) {
buffer[i] = INT32_MAX;
s32 = INT32_MAX;
} else if (temp < INT32_MIN) {
buffer[i] = INT32_MIN;
s32 = INT32_MIN;
} else {
buffer[i] = static_cast<int32_t>(temp);
s32 = static_cast<int32_t>(temp);
}
// 交织到左右声道
buffer[2 * i] = s32; // Left
buffer[2 * i + 1] = s32; // Right复制
}
size_t bytes_written;
ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), samples * sizeof(int32_t), &bytes_written, portMAX_DELAY));
return bytes_written / sizeof(int32_t);
ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), (samples * 2) * sizeof(int32_t), &bytes_written, portMAX_DELAY));
return bytes_written / sizeof(int32_t) / 2; // 返回每声道样本数
}
int NoAudioCodec::Read(int16_t* dest, int samples) {
@@ -329,4 +415,4 @@ int NoAudioCodecSimplexPdm::Read(int16_t* dest, int samples) {
// 计算实际读取的样本数
return bytes_read / sizeof(int16_t);
}
}

View File

@@ -32,7 +32,8 @@ public:
class NoAudioCodecSimplexPdm : public NoAudioCodec {
public:
NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din);
NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_din);
int Read(int16_t* dest, int samples);
};
#endif // _NO_AUDIO_CODEC_H
#endif // _NO_AUDIO_CODEC_H

View File

@@ -2,19 +2,21 @@
#include "system_info.h"
#include "settings.h"
#include "display/display.h"
#include "display/oled_display.h"
#include "assets/lang_config.h"
#include "esp32_music.h"
#include <esp_log.h>
#include <esp_ota_ops.h>
#include <esp_chip_info.h>
#include <esp_random.h>
#include "esp32_music.h"
#define TAG "Board"
Board::Board() {
music_ = nullptr; // 先初始化为空指针
Settings settings("board", true);
uuid_ = settings.GetString("uuid");
if (uuid_.empty()) {
@@ -22,10 +24,6 @@ Board::Board() {
settings.SetString("uuid", uuid_);
}
ESP_LOGI(TAG, "UUID=%s SKU=%s", uuid_.c_str(), BOARD_NAME);
// 初始化音乐播放器
music_ = new Esp32Music();
ESP_LOGI(TAG, "Music player initialized for all boards");
}
Board::~Board() {
@@ -173,6 +171,21 @@ std::string Board::GetSystemInfoJson() {
json += R"("label":")" + std::string(ota_partition->label) + R"(")";
json += R"(},)";
// Append display info
auto display = GetDisplay();
if (display) {
json += R"("display":{)";
if (dynamic_cast<OledDisplay*>(display)) {
json += R"("monochrome":)" + std::string("true") + R"(,)";
} else {
json += R"("monochrome":)" + std::string("false") + R"(,)";
}
json += R"("width":)" + std::to_string(display->width()) + R"(,)";
json += R"("height":)" + std::to_string(display->height()) + R"(,)";
json.pop_back(); // Remove the last comma
}
json += R"(},)";
json += R"("board":)" + GetBoardJson();
// Close the JSON object

View File

@@ -11,9 +11,10 @@
#include "led/led.h"
#include "backlight.h"
#include "camera.h"
#include "music.h"
#include "assets.h"
#include "music.h"
void* create_board();
class AudioCodec;
@@ -32,6 +33,7 @@ protected:
// 音乐播放器实例
Music* music_;
public:
static Board& GetInstance() {
static Board* instance = static_cast<Board*>(create_board());
@@ -64,4 +66,4 @@ void* create_board() { \
return new BOARD_CLASS_NAME(); \
}
#endif // BOARD_H
#endif // BOARD_H

View File

@@ -63,33 +63,26 @@ bool Esp32Camera::Capture() {
// 显示预览图片
auto display = dynamic_cast<LvglDisplay*>(Board::GetInstance().GetDisplay());
if (display != nullptr) {
// Create a new preview image
auto img_dsc = (lv_img_dsc_t*)heap_caps_calloc(1, sizeof(lv_img_dsc_t), MALLOC_CAP_8BIT);
img_dsc->header.magic = LV_IMAGE_HEADER_MAGIC;
img_dsc->header.cf = LV_COLOR_FORMAT_RGB565;
img_dsc->header.flags = 0;
img_dsc->header.w = fb_->width;
img_dsc->header.h = fb_->height;
img_dsc->header.stride = fb_->width * 2;
img_dsc->data_size = fb_->width * fb_->height * 2;
img_dsc->data = (uint8_t*)heap_caps_malloc(img_dsc->data_size, MALLOC_CAP_SPIRAM);
if (img_dsc->data == nullptr) {
auto data = (uint8_t*)heap_caps_malloc(fb_->len, MALLOC_CAP_SPIRAM);
if (data == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory for preview image");
heap_caps_free(img_dsc);
return false;
}
auto src = (uint16_t*)fb_->buf;
auto dst = (uint16_t*)img_dsc->data;
auto dst = (uint16_t*)data;
size_t pixel_count = fb_->len / 2;
for (size_t i = 0; i < pixel_count; i++) {
// 交换每个16位字内的字节
dst[i] = __builtin_bswap16(src[i]);
}
display->SetPreviewImage(img_dsc);
auto image = std::make_unique<LvglAllocatedImage>(data, fb_->len, fb_->width, fb_->height, fb_->width * 2, LV_COLOR_FORMAT_RGB565);
display->SetPreviewImage(std::move(image));
}
return true;
}
bool Esp32Camera::SetHMirror(bool enabled) {
sensor_t *s = esp_camera_sensor_get();
if (s == nullptr) {

View File

@@ -166,11 +166,12 @@ Esp32Music::Esp32Music() : last_downloaded_data_(), current_music_url_(), curren
song_name_displayed_(false), current_lyric_url_(), lyrics_(),
current_lyric_index_(-1), lyric_thread_(), is_lyric_running_(false),
display_mode_(DISPLAY_MODE_LYRICS), is_playing_(false), is_downloading_(false),
play_thread_(), download_thread_(), audio_buffer_(), buffer_mutex_(),
is_paused_(false), play_thread_(), download_thread_(), audio_buffer_(), buffer_mutex_(),
buffer_cv_(), buffer_size_(0), mp3_decoder_(nullptr), mp3_frame_info_(),
mp3_decoder_initialized_(false) {
ESP_LOGI(TAG, "Music player initialized with default spectrum display mode");
InitializeMp3Decoder();
// 延迟MP3解码器初始化避免在构造函数中初始化导致的问题
// InitializeMp3Decoder();
}
Esp32Music::~Esp32Music() {
@@ -282,9 +283,9 @@ Esp32Music::~Esp32Music() {
}
bool Esp32Music::Download(const std::string& song_name, const std::string& artist_name) {
ESP_LOGI(TAG, "Starting to get music details for: %s", song_name.c_str());
ESP_LOGI(TAG, "云端由MeowEmbeddedMusicServer喵波音律嵌入式提供");
ESP_LOGI(TAG, "喵波音律QQ交流群:865754861");
ESP_LOGI(TAG, "Starting to get music details for: %s", song_name.c_str());
// 清空之前的下载数据
last_downloaded_data_.clear();
@@ -357,21 +358,17 @@ bool Esp32Music::Download(const std::string& song_name, const std::string& artis
if (cJSON_IsString(audio_url) && audio_url->valuestring && strlen(audio_url->valuestring) > 0) {
ESP_LOGI(TAG, "Audio URL path: %s", audio_url->valuestring);
// 第二步:直接使用audio_url播放音乐
std::string audio_path = audio_url->valuestring;
current_music_url_ = audio_path;
// 第二步:直接使用音频URL开始流式播放
std::string current_music_url_ = audio_url->valuestring;
ESP_LOGI(TAG, "云端由MeowEmbeddedMusicServer喵波音律嵌入式提供");
ESP_LOGI(TAG, "喵波音律QQ交流群:865754861");
ESP_LOGI(TAG, "Starting streaming playback for: %s", song_name.c_str());
song_name_displayed_ = false; // 重置歌名显示标志
StartStreaming(current_music_url_);
// 处理歌词URL - 只有在歌词显示模式下才启动歌词
if (cJSON_IsString(lyric_url) && lyric_url->valuestring && strlen(lyric_url->valuestring) > 0) {
// 直接使用歌词URL
std::string lyric_path = lyric_url->valuestring;
current_lyric_url_ = lyric_path;
// 使用歌词URL获取歌词
std::string current_lyric_url_ = lyric_url->valuestring;
// 根据显示模式决定是否启动歌词
if (display_mode_ == DISPLAY_MODE_LYRICS) {
@@ -431,6 +428,14 @@ bool Esp32Music::StartStreaming(const std::string& music_url) {
ESP_LOGD(TAG, "Starting streaming for URL: %s", music_url.c_str());
// 确保MP3解码器已初始化
if (!mp3_decoder_initialized_) {
if (!InitializeMp3Decoder()) {
ESP_LOGE(TAG, "Failed to initialize MP3 decoder");
return false;
}
}
// 停止之前的播放和下载
is_downloading_ = false;
is_playing_ = false;
@@ -491,6 +496,7 @@ bool Esp32Music::StopStreaming() {
// 停止下载和播放标志
is_downloading_ = false;
is_playing_ = false;
is_paused_ = false; // 重置暂停状态
// 清空歌名显示
auto& board = Board::GetInstance();
@@ -723,8 +729,6 @@ void Esp32Music::PlayAudioStream() {
});
}
ESP_LOGI(TAG, "云端由MeowEmbeddedMusicServer喵波音律嵌入式提供");
ESP_LOGI(TAG, "喵波音律QQ交流群:865754861");
ESP_LOGI(TAG, "Starting playback with buffer size: %d", buffer_size_);
size_t total_played = 0;
@@ -744,18 +748,20 @@ void Esp32Music::PlayAudioStream() {
bool id3_processed = false;
while (is_playing_) {
// 检查是否被暂停
if (is_paused_) {
ESP_LOGD(TAG, "Music playback paused, waiting...");
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
// 检查设备状态,只有在空闲状态才播放音乐
auto& app = Application::GetInstance();
DeviceState current_state = app.GetDeviceState();
// 状态转换:说话中-》聆听中-》待机状态-》播放音乐
if (current_state == kDeviceStateListening || current_state == kDeviceStateSpeaking) {
if (current_state == kDeviceStateSpeaking) {
ESP_LOGI(TAG, "Device is in speaking state, switching to listening state for music playback");
}
if (current_state == kDeviceStateListening) {
ESP_LOGI(TAG, "Device is in listening state, switching to idle state for music playback");
}
// 等小智把话说完了,变成聆听状态之后,马上转成待机状态,进入音乐播放
if (current_state == kDeviceStateListening) {
ESP_LOGI(TAG, "Device is in listening state, switching to idle state for music playback");
// 切换状态
app.ToggleChatState(); // 变成待机状态
vTaskDelay(pdMS_TO_TICKS(300));
@@ -1133,8 +1139,6 @@ bool Esp32Music::DownloadLyrics(const std::string& lyric_url) {
add_auth_headers(http.get());
// 打开GET连接
ESP_LOGI(TAG, "云端由MeowEmbeddedMusicServer喵波音律嵌入式提供");
ESP_LOGI(TAG, "喵波音律QQ交流群:865754861");
if (!http->Open("GET", current_url)) {
ESP_LOGE(TAG, "Failed to open HTTP connection for lyrics");
// 移除delete http; 因为unique_ptr会自动管理内存
@@ -1426,4 +1430,99 @@ void Esp32Music::SetDisplayMode(DisplayMode mode) {
ESP_LOGI(TAG, "Display mode changed from %s to %s",
(old_mode == DISPLAY_MODE_SPECTRUM) ? "SPECTRUM" : "LYRICS",
(mode == DISPLAY_MODE_SPECTRUM) ? "SPECTRUM" : "LYRICS");
}
// MCP工具需要的方法实现
bool Esp32Music::SetVolume(int volume) {
ESP_LOGI(TAG, "SetVolume called with volume: %d", volume);
// 验证音量范围
if (volume < 0 || volume > 100) {
ESP_LOGW(TAG, "Invalid volume level: %d, must be between 0-100", volume);
return false;
}
// 通过Board获取AudioCodec并设置音量
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
if (codec) {
codec->SetOutputVolume(volume);
ESP_LOGI(TAG, "Volume set to %d%%", volume);
return true;
} else {
ESP_LOGE(TAG, "No audio codec available");
return false;
}
}
bool Esp32Music::PlaySong() {
ESP_LOGI(TAG, "PlaySong called");
return false;
}
bool Esp32Music::StopSong() {
ESP_LOGI(TAG, "StopSong called");
return StopStreaming();
}
bool Esp32Music::PauseSong() {
ESP_LOGI(TAG, "PauseSong called");
// 检查是否正在播放
if (!is_playing_) {
ESP_LOGW(TAG, "No music is currently playing");
return false;
}
// 检查是否已经暂停
if (is_paused_) {
ESP_LOGW(TAG, "Music is already paused");
return true;
}
// 设置暂停标志
is_paused_ = true;
ESP_LOGI(TAG, "Music playback paused");
// 更新显示状态
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
if (display && !current_song_name_.empty()) {
std::string formatted_song_name = "" + current_song_name_ + "》已暂停";
display->SetMusicInfo(formatted_song_name.c_str());
ESP_LOGI(TAG, "Updated display: %s", formatted_song_name.c_str());
}
return true;
}
bool Esp32Music::ResumeSong() {
ESP_LOGI(TAG, "ResumeSong called");
// 检查是否正在播放
if (!is_playing_) {
ESP_LOGW(TAG, "No music is currently playing");
return false;
}
// 检查是否已经恢复
if (!is_paused_) {
ESP_LOGW(TAG, "Music is not paused");
return true;
}
// 清除暂停标志
is_paused_ = false;
ESP_LOGI(TAG, "Music playback resumed");
// 更新显示状态
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
if (display && !current_song_name_.empty()) {
std::string formatted_song_name = "" + current_song_name_ + "》播放中...";
display->SetMusicInfo(formatted_song_name.c_str());
ESP_LOGI(TAG, "Updated display: %s", formatted_song_name.c_str());
}
return true;
}

View File

@@ -38,7 +38,6 @@ private:
std::string current_music_url_;
std::string current_song_name_;
bool song_name_displayed_;
std::atomic<bool> stop_flag_{false}; // 停止播放标志位
// 歌词相关
std::string current_lyric_url_;
@@ -51,6 +50,7 @@ private:
std::atomic<DisplayMode> display_mode_;
std::atomic<bool> is_playing_;
std::atomic<bool> is_downloading_;
std::atomic<bool> is_paused_;
std::thread play_thread_;
std::thread download_thread_;
int64_t current_play_time_ms_; // 当前播放时间(毫秒)
@@ -102,11 +102,20 @@ public:
virtual bool StopStreaming() override; // 停止流式播放
virtual size_t GetBufferSize() const override { return buffer_size_; }
virtual bool IsDownloading() const override { return is_downloading_; }
virtual bool IsPlaying() const override { return is_playing_; }
virtual bool IsPaused() const override { return is_paused_; }
virtual int16_t* GetAudioData() override { return final_pcm_data_fft; }
// 显示模式控制方法
void SetDisplayMode(DisplayMode mode);
DisplayMode GetDisplayMode() const { return display_mode_.load(); }
// MCP工具需要的方法
virtual bool PlaySong() override;
virtual bool SetVolume(int volume) override;
virtual bool StopSong() override;
virtual bool PauseSong() override;
virtual bool ResumeSong() override;
};
#endif // ESP32_MUSIC_H

View File

@@ -15,7 +15,16 @@ public:
virtual bool StopStreaming() = 0; // 停止流式播放
virtual size_t GetBufferSize() const = 0;
virtual bool IsDownloading() const = 0;
virtual bool IsPlaying() const = 0;
virtual bool IsPaused() const = 0;
virtual int16_t* GetAudioData() = 0;
// MCP工具需要的方法
virtual bool PlaySong() = 0;
virtual bool SetVolume(int volume) = 0;
virtual bool StopSong() = 0;
virtual bool PauseSong() = 0;
virtual bool ResumeSong() = 0;
};
#endif // MUSIC_H

View File

@@ -0,0 +1,211 @@
# MPU6050传感器集成说明
## 概述
本项目为ESP32-S3智能音箱开发板集成了MPU6050六轴传感器支持提供了现代化的C++封装接口。
## 文件结构
- `mpu6050_sensor.h` - MPU6050传感器封装类的头文件
- `mpu6050_sensor.cc` - MPU6050传感器封装类的实现文件
- `mpu6050_test.cc` - MPU6050传感器测试程序可选
- `esp32s3_smart_speaker.cc` - 已集成MPU6050支持的板子实现
## 功能特性
### 1. 传感器支持
- **加速度计**: 支持±2g, ±4g, ±8g, ±16g量程
- **陀螺仪**: 支持±250°/s, ±500°/s, ±1000°/s, ±2000°/s量程
- **温度传感器**: 内置温度传感器
- **姿态角计算**: 使用互补滤波算法计算俯仰角、横滚角和偏航角
### 2. 技术特点
- 使用ESP-IDF的现代I2C Master API
- 支持多量程配置
- 内置数字低通滤波器
- 可配置采样率
- 互补滤波姿态解算
- 完整的错误处理机制
## 硬件连接
根据`config.h`中的定义:
```c
// IMU传感器 (I2C接口)
#define IMU_I2C_SDA_PIN GPIO_NUM_21
#define IMU_I2C_SCL_PIN GPIO_NUM_20
#define IMU_INT_PIN GPIO_NUM_19
```
### 连接方式
- **VCC**: 3.3V
- **GND**: 地
- **SDA**: GPIO21
- **SCL**: GPIO20
- **INT**: GPIO19中断引脚可选
## 使用方法
### 1. 基本使用
```cpp
#include "mpu6050_sensor.h"
// 创建传感器实例
auto sensor = std::make_unique<Mpu6050Sensor>(i2c_bus_handle);
// 初始化传感器
if (sensor->Initialize(ACCE_FS_4G, GYRO_FS_500DPS)) {
// 唤醒传感器
if (sensor->WakeUp()) {
// 验证设备ID
uint8_t device_id;
if (sensor->GetDeviceId(&device_id)) {
ESP_LOGI(TAG, "MPU6050 initialized, ID: 0x%02X", device_id);
}
}
}
```
### 2. 读取传感器数据
```cpp
mpu6050_acce_value_t acce;
mpu6050_gyro_value_t gyro;
mpu6050_temp_value_t temp;
complimentary_angle_t angle;
// 读取加速度计数据
if (sensor->GetAccelerometer(&acce)) {
ESP_LOGI(TAG, "Accelerometer - X:%.2f, Y:%.2f, Z:%.2f",
acce.acce_x, acce.acce_y, acce.acce_z);
}
// 读取陀螺仪数据
if (sensor->GetGyroscope(&gyro)) {
ESP_LOGI(TAG, "Gyroscope - X:%.2f, Y:%.2f, Z:%.2f",
gyro.gyro_x, gyro.gyro_y, gyro.gyro_z);
}
// 读取温度数据
if (sensor->GetTemperature(&temp)) {
ESP_LOGI(TAG, "Temperature: %.2f°C", temp.temp);
}
// 计算姿态角
if (sensor->ComplimentaryFilter(&acce, &gyro, &angle)) {
ESP_LOGI(TAG, "Attitude - Pitch:%.2f°, Roll:%.2f°, Yaw:%.2f°",
angle.pitch, angle.roll, angle.yaw);
}
```
### 3. 获取传感器状态
```cpp
// 检查是否已初始化
if (sensor->IsInitialized()) {
// 获取状态信息
std::string status = sensor->GetStatusJson();
ESP_LOGI(TAG, "Sensor status: %s", status.c_str());
}
```
## 配置参数
### 加速度计量程
- `ACCE_FS_2G`: ±2g (16384 LSB/g)
- `ACCE_FS_4G`: ±4g (8192 LSB/g)
- `ACCE_FS_8G`: ±8g (4096 LSB/g)
- `ACCE_FS_16G`: ±16g (2048 LSB/g)
### 陀螺仪量程
- `GYRO_FS_250DPS`: ±250°/s (131 LSB/°/s)
- `GYRO_FS_500DPS`: ±500°/s (65.5 LSB/°/s)
- `GYRO_FS_1000DPS`: ±1000°/s (32.8 LSB/°/s)
- `GYRO_FS_2000DPS`: ±2000°/s (16.4 LSB/°/s)
### 互补滤波参数
- **alpha**: 0.98 (默认值,表示更信任陀螺仪)
- **采样率**: 125Hz
- **数字低通滤波器**: 5Hz
## 集成到板子
MPU6050已经集成到`Esp32s3SmartSpeaker`类中:
1. **自动初始化**: 在板子构造函数中自动初始化MPU6050
2. **后台任务**: 自动创建后台任务持续读取传感器数据
3. **状态报告**: 在`GetBoardJson()`中报告传感器状态
## 日志输出
传感器会输出以下日志信息:
```
I (1234) SmartSpeaker: MPU6050 sensor initialized successfully (ID: 0x68)
I (1235) SmartSpeaker: IMU data task created successfully
I (1236) SmartSpeaker: IMU data task started
I (1237) SmartSpeaker: Accelerometer - X:0.12, Y:-0.05, Z:0.98
I (1238) SmartSpeaker: Gyroscope - X:0.15, Y:-0.02, Z:0.08
I (1239) SmartSpeaker: Temperature: 25.3°C
I (1240) SmartSpeaker: Attitude - Pitch:2.1°, Roll:-1.5°, Yaw:0.3°
```
## 故障排除
### 常见问题
1. **设备ID不匹配**
- 检查I2C连接
- 确认设备地址是否正确
- 检查电源供应
2. **初始化失败**
- 检查I2C总线配置
- 确认GPIO引脚配置正确
- 检查上拉电阻
3. **数据读取失败**
- 检查I2C通信
- 确认传感器已唤醒
- 检查采样率配置
### 调试建议
1. 启用I2C调试日志
2. 检查硬件连接
3. 使用示波器检查I2C信号
4. 验证电源电压稳定性
## 技术细节
### I2C配置
- **时钟频率**: 100kHz
- **地址**: 0x68 (7位地址)
- **上拉电阻**: 内部使能
### 寄存器配置
- **加速度计量程**: 寄存器0x1C
- **陀螺仪量程**: 寄存器0x1B
- **数字低通滤波器**: 寄存器0x1A
- **采样率**: 寄存器0x19
- **电源管理**: 寄存器0x6B
### 数据格式
- **加速度计**: 16位有符号整数转换为g值
- **陀螺仪**: 16位有符号整数转换为度/秒
- **温度**: 16位有符号整数转换为摄氏度
## 扩展功能
### 可扩展的功能
1. **中断支持**: 可以配置数据就绪中断
2. **运动检测**: 可以配置运动检测中断
3. **自由落体检测**: 可以配置自由落体检测
4. **FIFO支持**: 可以使用FIFO缓冲区
5. **DMP支持**: 可以使用数字运动处理器
### 性能优化
1. **降低采样率**: 减少功耗
2. **使用中断**: 避免轮询
3. **FIFO缓冲**: 批量读取数据
4. **休眠模式**: 不使用时进入低功耗模式
## 许可证
本代码遵循项目的许可证要求。

View File

@@ -0,0 +1,376 @@
#include "adc_manager.h"
#include "application.h"
#include <board.h>
#include <cmath>
#include <cstring>
#include <esp_adc/adc_cali.h>
#include <esp_adc/adc_oneshot.h>
#include <esp_log.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#define TAG "AdcManager"
// 检测时间定义(毫秒)
#define PRESSURE_DETECTION_TIME_MS 2000 // 压力检测持续时间2秒
#define LOW_VALUE_DETECTION_TIME_MS 2000 // 低值检测持续时间2秒
AdcManager &AdcManager::GetInstance() {
static AdcManager instance;
return instance;
}
bool AdcManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "AdcManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing AdcManager...");
// 初始化ADC数组
memset(pressure_adc_values_, 0, sizeof(pressure_adc_values_));
InitializeAdc();
// InitializeDigitalOutput(); // 暂时注释掉DO初始化
// 先设置初始化状态,再启动任务
initialized_ = true;
// 初始化后立刻读取一次,便于快速确认链路
int init_read_raw = -1;
esp_err_t init_read_ret = adc_oneshot_read(
adc1_handle_, PRESSURE_SENSOR_ADC_CHANNEL, &init_read_raw);
if (init_read_ret != ESP_OK) {
ESP_LOGE(TAG, "Initial ADC read failed: %s",
esp_err_to_name(init_read_ret));
} else {
ESP_LOGI(TAG, "Initial ADC read ok: Raw=%d", init_read_raw);
}
// 启动ADC任务
StartAdcTask();
ESP_LOGI(TAG, "AdcManager initialized successfully");
return true;
}
void AdcManager::InitializeAdc() {
ESP_LOGI(TAG, "Initializing ADC for pressure sensor on GPIO4 (ADC1_CH3)...");
// 初始化ADC驱动
adc_oneshot_unit_init_cfg_t init_config1 = {
.unit_id = ADC_UNIT_1,
};
esp_err_t ret = adc_oneshot_new_unit(&init_config1, &adc1_handle_);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize ADC unit: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "ADC unit initialized successfully");
// 配置ADC通道
adc_oneshot_chan_cfg_t chan_config = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_12,
};
ret = adc_oneshot_config_channel(adc1_handle_, PRESSURE_SENSOR_ADC_CHANNEL,
&chan_config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure ADC channel %d: %s",
PRESSURE_SENSOR_ADC_CHANNEL, esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "ADC channel %d configured successfully",
PRESSURE_SENSOR_ADC_CHANNEL);
// 初始化ADC校准
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = ADC_UNIT_1,
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_12,
};
ret = adc_cali_create_scheme_curve_fitting(&cali_config, &adc1_cali_handle_);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "ADC calibration not available: %s", esp_err_to_name(ret));
adc1_cali_handle_ = NULL;
} else {
ESP_LOGI(TAG, "ADC calibration initialized successfully");
}
ESP_LOGI(TAG, "ADC initialized for pressure sensor monitoring on GPIO4");
}
void AdcManager::ReadPressureSensorData() {
if (!initialized_) {
return;
}
int adc_value;
esp_err_t ret =
adc_oneshot_read(adc1_handle_, PRESSURE_SENSOR_ADC_CHANNEL, &adc_value);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to read pressure sensor ADC: %s",
esp_err_to_name(ret));
return;
}
// 直接使用原始ADC值不进行电压转换
current_pressure_value_ = adc_value;
// 压力检测触发音乐播放
static bool last_pressure_state = false;
static int64_t pressure_start_time = 0;
static bool pressure_triggered = false;
if (last_pressure_state) {
// 长时间不动检测逻辑
CheckLongTimeNoMovement(current_pressure_value_);
}
// 每隔5次打印一次详细日志便于定位问题
static int adc_log_counter = 0;
adc_log_counter++;
if (adc_log_counter >= 10) {
ESP_LOGI(TAG, "ADC read: Raw=%d", adc_value);
adc_log_counter = 0;
}
bool current_pressure_state = IsPressureDetected();
if (current_pressure_state && !last_pressure_state) {
// 压力开始检测,记录开始时间
pressure_start_time = esp_timer_get_time();
pressure_triggered = false;
ESP_LOGI(TAG, "Pressure detection started");
} else if (current_pressure_state && last_pressure_state) {
// 压力持续检测中检查是否超过2秒
int64_t current_time = esp_timer_get_time();
int64_t duration_ms =
(current_time - pressure_start_time) / 1000; // 转换为毫秒
if (duration_ms >= PRESSURE_DETECTION_TIME_MS && !pressure_triggered) {
ESP_LOGI(TAG,
"Pressure detected for %ld ms! Triggering music playback...",
(long)duration_ms);
// 触发音乐播放
TriggerMusicPlayback();
pressure_triggered = true; // 防止重复触发
}
} else if (!current_pressure_state && last_pressure_state) {
// 压力结束检测
ESP_LOGI(TAG, "Pressure detection ended");
pressure_triggered = false;
}
last_pressure_state = current_pressure_state;
// ADC值小于100时的暂停检测也需要持续2秒
static int64_t low_value_start_time = 0;
static bool low_value_triggered = false;
if (adc_value < 100) {
if (low_value_start_time == 0) {
// 第一次检测到小于100的值记录开始时间
low_value_start_time = esp_timer_get_time();
low_value_triggered = false;
ESP_LOGI(TAG, "ADC low value detection started (value: %d)", adc_value);
} else {
// 持续检测小于100的值检查是否超过2秒
int64_t current_time = esp_timer_get_time();
int64_t duration_ms =
(current_time - low_value_start_time) / 1000; // 转换为毫秒
if (duration_ms >= LOW_VALUE_DETECTION_TIME_MS && !low_value_triggered) {
ESP_LOGI(TAG,
"ADC low value detected for %ld ms! (value: %d) Triggering music pause...",
(long)duration_ms, adc_value);
TriggerMusicPauseback();
low_value_triggered = true; // 防止重复触发
}
}
} else {
// ADC值大于等于100重置低值检测
if (low_value_start_time != 0) {
ESP_LOGI(TAG, "ADC low value detection ended (value: %d)", adc_value);
low_value_start_time = 0;
low_value_triggered = false;
}
}
}
void AdcManager::StartAdcTask() {
if (!initialized_) {
ESP_LOGE(TAG, "AdcManager not initialized");
return;
}
BaseType_t ret =
xTaskCreate(AdcTask, "adc_task", 4096, this, 2, &adc_task_handle_);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create ADC task");
} else {
ESP_LOGI(TAG, "ADC task created successfully");
}
}
void AdcManager::StopAdcTask() {
if (adc_task_handle_) {
vTaskDelete(adc_task_handle_);
adc_task_handle_ = nullptr;
}
}
void AdcManager::AdcTask(void *pvParameters) {
AdcManager *manager = static_cast<AdcManager *>(pvParameters);
ESP_LOGI(TAG, "ADC task started");
while (true) {
if (manager->initialized_) {
manager->ReadPressureSensorData();
}
vTaskDelay(pdMS_TO_TICKS(100)); // 100ms间隔
}
}
int AdcManager::GetCurrentPressureValue() const {
return current_pressure_value_;
}
const int *AdcManager::GetPressureAdcValues() const {
return pressure_adc_values_;
}
size_t AdcManager::GetPressureSampleCount() const {
return (pressure_data_index_ == 0) ? kPressureAdcDataCount
: pressure_data_index_;
}
bool AdcManager::IsPressureDetected() const {
if (!initialized_) {
return false;
}
return current_pressure_value_ > 1000; // 压力阈值100
}
bool AdcManager::IsLightPressure() const {
if (!initialized_) {
return false;
}
return current_pressure_value_ > 500; // 轻压阈值500
}
void AdcManager::CheckLongTimeNoMovement(int adc_value) {
uint32_t current_time = esp_timer_get_time() / 1000000; // 转换为秒
// 计算ADC值变化
int adc_change = abs(adc_value - last_stable_value_);
if (adc_change > kMovementThreshold) {
// 有显著变化,重置不动检测
last_stable_value_ = adc_value;
no_movement_start_time_ = current_time;
is_no_movement_detected_ = false;
} else {
// 变化很小,检查是否长时间不动
if (no_movement_start_time_ == 0) {
no_movement_start_time_ = current_time;
}
uint32_t no_movement_duration = current_time - no_movement_start_time_;
if (no_movement_duration >= kLongTimeThreshold &&
!is_no_movement_detected_) {
is_no_movement_detected_ = true;
ESP_LOGW(TAG,
"Long time no movement detected! Duration: %lu seconds, ADC: %d",
no_movement_duration, adc_value);
// 停止音乐播放和语音交互
TriggerMusicPauseback();
}
}
}
bool AdcManager::IsLongTimeNoMovement() const {
if (!initialized_) {
return false;
}
return is_no_movement_detected_;
}
uint32_t AdcManager::GetNoMovementDuration() const {
if (!initialized_ || no_movement_start_time_ == 0) {
return 0;
}
uint32_t current_time = esp_timer_get_time() / 1000000;
return current_time - no_movement_start_time_;
}
void AdcManager::TriggerMusicPauseback() {
ESP_LOGI(TAG, "Triggering music pauseback");
auto music = Board::GetInstance().GetMusic();
if (!music) {
ESP_LOGI(TAG, "No music player found");
return;
}
// 检查音乐状态,避免重复操作
if (!music->IsPlaying() && !music->IsPaused()) {
ESP_LOGI(TAG, "Music is not playing or paused, skipping pause operation");
return;
}
music->PauseSong();
// 停止语音交互
auto &app = Application::GetInstance();
app.GetAudioService().EnableWakeWordDetection(false);
ESP_LOGI(TAG, "Stopped wake word detection due to long time no movement");
}
void AdcManager::TriggerMusicPlayback() {
ESP_LOGI(TAG, "Triggering music playback");
// 确保音频输出已启用
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
if (!codec) {
ESP_LOGE(TAG, "Audio codec not available");
return;
}
codec->EnableOutput(true);
ESP_LOGI(TAG, "Audio output enabled");
// 通过Board接口获取音乐播放器并触发播放
auto music = board.GetMusic();
if (!music) {
ESP_LOGI(TAG, "No music player found");
return;
}
if (music->IsPlaying()) {
ESP_LOGI(TAG, "Music is already playing");
return;
}
if (music->IsDownloading()) {
ESP_LOGI(TAG, "Music is already downloading");
return;
}
if (music->IsPaused()) {
ESP_LOGI(TAG, "Music is already paused");
music->ResumeSong();
return;
}
auto song_name = "稻香";
auto artist_name = "";
if (!music->Download(song_name, artist_name)) {
ESP_LOGI(TAG, "获取音乐资源失败");
return;
}
auto download_result = music->GetDownloadResult();
ESP_LOGI(TAG, "Music details result: %s", download_result.c_str());
}

View File

@@ -0,0 +1,80 @@
#ifndef ADC_MANAGER_H
#define ADC_MANAGER_H
#include "config.h"
#include <esp_adc/adc_oneshot.h>
#include <esp_adc/adc_cali.h>
#include <esp_adc/adc_cali_scheme.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
class AdcManager {
public:
static AdcManager& GetInstance();
// 初始化ADC系统
bool Initialize();
// 读取压感传感器数据
void ReadPressureSensorData();
// 获取当前压感值
int GetCurrentPressureValue() const;
// 获取ADC原始值数组
const int* GetPressureAdcValues() const;
// 获取有效样本数量
size_t GetPressureSampleCount() const;
// 启动/停止ADC任务
void StartAdcTask();
void StopAdcTask();
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
// 基于ADC值判断压力状态
bool IsPressureDetected() const;
bool IsLightPressure() const;
// 长时间不动检测
bool IsLongTimeNoMovement() const;
uint32_t GetNoMovementDuration() const; // 返回不动持续时间(秒)
// 压力检测触发音乐播放
void TriggerMusicPlayback();
void TriggerMusicPauseback();
private:
AdcManager() = default;
~AdcManager() = default;
AdcManager(const AdcManager&) = delete;
AdcManager& operator=(const AdcManager&) = delete;
void InitializeAdc();
void CheckLongTimeNoMovement(int adc_value);
static void AdcTask(void *pvParameters);
bool initialized_ = false;
adc_oneshot_unit_handle_t adc1_handle_;
adc_cali_handle_t adc1_cali_handle_;
// 压感传感器数据
static constexpr size_t kPressureAdcDataCount = 10;
int pressure_adc_values_[kPressureAdcDataCount];
size_t pressure_data_index_ = 0;
int current_pressure_value_ = 0;
// 长时间不动检测相关变量
int last_stable_value_ = 0;
uint32_t no_movement_start_time_ = 0;
bool is_no_movement_detected_ = false;
static constexpr int kMovementThreshold = 50; // ADC变化阈值
static constexpr uint32_t kLongTimeThreshold = 30; // 长时间阈值(秒)
// 任务句柄
TaskHandle_t adc_task_handle_ = nullptr;
};
#endif // ADC_MANAGER_H

View File

@@ -0,0 +1,131 @@
#include "button_manager.h"
#include "application.h"
#include <esp_log.h>
#define TAG "ButtonManager"
ButtonManager& ButtonManager::GetInstance() {
static ButtonManager instance;
return instance;
}
ButtonManager::ButtonManager()
: boot_button_(BOOT_BUTTON_GPIO),
volume_up_button_(VOLUME_UP_BUTTON_GPIO),
volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) {
}
bool ButtonManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "ButtonManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing ButtonManager...");
// 设置按钮回调
SetupButtonCallbacks();
initialized_ = true;
ESP_LOGI(TAG, "ButtonManager initialized successfully");
return true;
}
void ButtonManager::SetupButtonCallbacks() {
ESP_LOGI(TAG, "Setting up button callbacks...");
// BOOT按钮回调
boot_button_.OnClick([]() {
ESP_LOGI(TAG, "Boot button clicked");
});
boot_button_.OnLongPress([]() {
ESP_LOGI(TAG, "BOOT long pressed: play boot tone");
// 确保音频输出已启用
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
if (!codec) {
ESP_LOGE(TAG, "Audio codec not available");
return;
}
codec->EnableOutput(true);
codec->SetOutputVolume(10);
auto music = Board::GetInstance().GetMusic();
if (!music) {
ESP_LOGE(TAG, "Music player not available");
return;
}
auto song_name = "稻香";
auto artist_name = "";
if (!music->Download(song_name, artist_name)) {
ESP_LOGI(TAG, "获取音乐资源失败");
return;
}
auto download_result = music->GetDownloadResult();
ESP_LOGI(TAG, "Music details result: %s", download_result.c_str());
});
// 音量上按钮回调
volume_up_button_.OnClick([]() {
ESP_LOGI(TAG, "Volume up button clicked");
// 通过AudioService间接控制音量
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
codec->SetOutputVolume(codec->output_volume() + 10);
ESP_LOGI(TAG, "Volume up requested");
});
volume_up_button_.OnLongPress([]() {
ESP_LOGI(TAG, "Volume up long pressed: switching to voice interaction mode");
// 播放进入语音交互模式的提示音
auto& app = Application::GetInstance();
app.PlaySound("success"); // 播放成功提示音
// 暂停音乐播放
auto music = Board::GetInstance().GetMusic();
if (music && music->IsPlaying()) {
music->PauseSong();
ESP_LOGI(TAG, "Music paused for voice interaction");
}
// 切换到语音交互模式
app.GetAudioService().EnableWakeWordDetection(true);
app.GetAudioService().EnableVoiceProcessing(true);
ESP_LOGI(TAG, "Switched to voice interaction mode - waiting for user voice input");
});
// 音量下按钮回调
volume_down_button_.OnClick([]() {
ESP_LOGI(TAG, "Volume down button clicked");
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
codec->SetOutputVolume(codec->output_volume() - 10);
ESP_LOGI(TAG, "Volume down requested");
});
volume_down_button_.OnLongPress([]() {
ESP_LOGI(TAG, "Volume down long pressed: stopping audio playback and voice interaction");
// 播放停止提示音
auto& app = Application::GetInstance();
app.PlaySound("exclamation"); // 播放感叹号提示音
// 停止音乐播放
auto music = Board::GetInstance().GetMusic();
if (music && music->IsPlaying()) {
music->PauseSong();
ESP_LOGI(TAG, "Music playback stopped");
}
// 停止语音交互
app.GetAudioService().EnableWakeWordDetection(false);
app.GetAudioService().EnableVoiceProcessing(false);
ESP_LOGI(TAG, "Voice interaction stopped");
});
}

View File

@@ -0,0 +1,31 @@
#ifndef BUTTON_MANAGER_H
#define BUTTON_MANAGER_H
#include "button.h"
#include "config.h"
class ButtonManager {
public:
static ButtonManager& GetInstance();
// 初始化按钮系统
bool Initialize();
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
private:
ButtonManager();
~ButtonManager() = default;
ButtonManager(const ButtonManager&) = delete;
ButtonManager& operator=(const ButtonManager&) = delete;
void SetupButtonCallbacks();
bool initialized_ = false;
Button boot_button_;
Button volume_up_button_;
Button volume_down_button_;
};
#endif // BUTTON_MANAGER_H

View File

@@ -0,0 +1,77 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
// ESP32-S3 智能音箱开发板配置
// 音频配置
#define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 16000
// 扬声器I2S配置
#define AUDIO_I2S_GPIO_WS GPIO_NUM_47 // 扬声器Word Select
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_17 // 扬声器Bit Clock
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_15 // 扬声器数据输出
// INMP441 I2S麦克风配置 (BCLK/WS/DIN)
// 说明: INMP441 是 I2S 数字麦,需要标准 I2S 三线
// 建议映射: BCLK=GPIO14, WS=GPIO38, DIN=GPIO16可按需要调整
#define AUDIO_MIC_I2S_BCLK GPIO_NUM_14
#define AUDIO_MIC_I2S_WS GPIO_NUM_38
#define AUDIO_MIC_I2S_DIN GPIO_NUM_16
// 用户交互
#define BUILTIN_LED_GPIO GPIO_NUM_48
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40
#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_41
// IMU传感器 (I2C接口)
#define IMU_I2C_SDA_PIN GPIO_NUM_21
#define IMU_I2C_SCL_PIN GPIO_NUM_20
#define IMU_INT_PIN GPIO_NUM_13
// 压感传感器 (ADC接口)
#define PRESSURE_SENSOR_ADC_CHANNEL ADC_CHANNEL_3 // GPIO4 (ADC1_CHANNEL_3)
// 功能IO定义
// LED控制
#define LED_RING_GPIO GPIO_NUM_6 // LED灯环控制
#define STATUS_LED_GPIO GPIO_NUM_10 // 状态指示灯
// 无显示配置
#define DISPLAY_SDA_PIN GPIO_NUM_NC
#define DISPLAY_SCL_PIN GPIO_NUM_NC
#define DISPLAY_WIDTH 0
#define DISPLAY_HEIGHT 0
#define DISPLAY_MIRROR_X false
#define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY false
#define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
// 无摄像头配置
#define CAMERA_PIN_PWDN GPIO_NUM_NC
#define CAMERA_PIN_RESET GPIO_NUM_NC
#define CAMERA_PIN_XCLK GPIO_NUM_NC
#define CAMERA_PIN_SIOD GPIO_NUM_NC
#define CAMERA_PIN_SIOC GPIO_NUM_NC
#define CAMERA_PIN_D0 GPIO_NUM_NC
#define CAMERA_PIN_D1 GPIO_NUM_NC
#define CAMERA_PIN_D2 GPIO_NUM_NC
#define CAMERA_PIN_D3 GPIO_NUM_NC
#define CAMERA_PIN_D4 GPIO_NUM_NC
#define CAMERA_PIN_D5 GPIO_NUM_NC
#define CAMERA_PIN_D6 GPIO_NUM_NC
#define CAMERA_PIN_D7 GPIO_NUM_NC
#define CAMERA_PIN_VSYNC GPIO_NUM_NC
#define CAMERA_PIN_HREF GPIO_NUM_NC
#define CAMERA_PIN_PCLK GPIO_NUM_NC
// 开发板版本
#define SMART_SPEAKER_VERSION "1.0.0"
#endif // _BOARD_CONFIG_H_

View File

@@ -0,0 +1,15 @@
{
"target": "esp32s3",
"builds": [
{
"name": "esp32s3-smart-speaker",
"sdkconfig_append": [
"CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=n",
"CONFIG_LWIP_IPV6=n",
"CONFIG_USE_ESP_WAKE_WORD=y",
"CONFIG_USE_AUDIO_DEBUGGER=y",
"CONFIG_AUDIO_DEBUG_UDP_SERVER=\"192.168.122.143:8000\""
]
}
]
}

View File

@@ -0,0 +1,151 @@
#include "assets.h"
#include "adc_manager.h"
#include "button_manager.h"
#include "codecs/no_audio_codec.h"
#include "config.h"
#include "esplog_display.h"
#include "esp32_music.h"
#include "gpio_manager.h"
#include "imu_manager.h"
#include "led/single_led.h"
#include "tools_manager.h"
#include "wifi_board.h"
#include "wifi_manager.h"
#include <driver/i2c_master.h>
#include <esp_log.h>
#include <string>
#define TAG "SmartSpeaker"
class Esp32s3SmartSpeaker : public WifiBoard {
private:
// I2C总线句柄
i2c_master_bus_handle_t codec_i2c_bus_;
void InitializeManagers() {
ESP_LOGI(TAG, "Initializing managers...");
// 初始化各个管理器Initialize内部会自动启动任务
AdcManager::GetInstance().Initialize();
ImuManager::GetInstance().Initialize();
ButtonManager::GetInstance().Initialize();
GpioManager::GetInstance().Initialize();
ToolsManager::GetInstance().Initialize();
WifiManager::GetInstance().Initialize();
ESP_LOGI(TAG, "All managers initialized successfully");
}
void InitializeCodecI2c() {
return;
}
public:
Esp32s3SmartSpeaker() {
ESP_LOGI(TAG, "Initializing ESP32-S3 Smart Speaker");
// 初始化音乐播放器
music_ = new Esp32Music();
ESP_LOGI(TAG, "Music player initialized");
// 初始化I2C总线
InitializeCodecI2c();
// 初始化各个管理器
InitializeManagers();
ESP_LOGI(TAG, "ESP32-S3 Smart Speaker initialized successfully");
}
virtual ~Esp32s3SmartSpeaker() = default;
virtual std::string GetBoardType() override {
return std::string("esp32s3-smart-speaker");
}
virtual AudioCodec *GetAudioCodec() override {
static NoAudioCodecSimplex audio_codec(
AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE,
// 扬声器(标准 I2S 输出)
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
// 麦克风(标准 I2S 输入,单声道)
AUDIO_MIC_I2S_BCLK,
AUDIO_MIC_I2S_WS,
AUDIO_MIC_I2S_DIN);
return &audio_codec;
}
virtual Display *GetDisplay() override {
static EspLogDisplay display;
return &display;
}
virtual Led *GetLed() override {
static SingleLed led(BUILTIN_LED_GPIO);
return &led;
}
virtual std::string GetBoardJson() override {
char json_buffer[2048];
// 安全地获取管理器状态,避免在初始化过程中访问
bool imu_initialized = false;
bool adc_initialized = false;
int pressure_value = 0;
size_t pressure_sample_count = 0;
bool imu_sensor_initialized = false;
try {
auto& imu_manager = ImuManager::GetInstance();
imu_initialized = imu_manager.IsInitialized();
auto& adc_manager = AdcManager::GetInstance();
adc_initialized = adc_manager.IsInitialized();
pressure_value = adc_manager.GetCurrentPressureValue();
pressure_sample_count = adc_manager.GetPressureSampleCount();
auto imu_sensor = imu_manager.GetImuSensor();
imu_sensor_initialized = imu_sensor && imu_sensor->IsInitialized();
} catch (...) {
ESP_LOGW(TAG, "Error accessing managers in GetBoardJson, using default values");
}
snprintf(json_buffer, sizeof(json_buffer),
"{"
"\"board_type\":\"esp32s3-smart-speaker\","
"\"version\":\"%s\","
"\"features\":[\"audio\",\"imu\",\"pressure\",\"led_ring\",\"fan\",\"relay\",\"status_led\"],"
"\"audio_codec\":\"NoAudioCodecSimplex\","
"\"audio_method\":\"i2s_standard\","
"\"microphone\":\"INMP441_I2S\","
"\"speaker\":\"NoAudioCodec\","
"\"imu_initialized\":%s,"
"\"pressure_sensor_initialized\":%s,"
"\"pressure_sensor\":{\"current_value\":%d,\"adc_channel\":%d,\"sample_count\":%u},"
"\"imu_sensor\":{\"type\":\"MPU6050\",\"initialized\":%s,\"status\":\"unknown\"}"
"}",
SMART_SPEAKER_VERSION,
imu_initialized ? "true" : "false",
adc_initialized ? "true" : "false",
pressure_value,
PRESSURE_SENSOR_ADC_CHANNEL,
(unsigned int)pressure_sample_count,
imu_sensor_initialized ? "true" : "false"
);
ESP_LOGI(TAG, "GetBoardJson completed successfully");
return std::string(json_buffer);
}
virtual Assets *GetAssets() override {
static Assets assets(std::string(ASSETS_XIAOZHI_WAKENET_SMALL));
return &assets;
}
};
DECLARE_BOARD(Esp32s3SmartSpeaker);

View File

@@ -0,0 +1,53 @@
#include "gpio_manager.h"
#include <esp_log.h>
#define TAG "GpioManager"
GpioManager& GpioManager::GetInstance() {
static GpioManager instance;
return instance;
}
bool GpioManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "GpioManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing GpioManager...");
// 初始化GPIO输出
gpio_config_t io_conf = {};
// LED灯环控制
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = (1ULL << LED_RING_GPIO);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
gpio_config(&io_conf);
// 状态指示灯
io_conf.pin_bit_mask = (1ULL << STATUS_LED_GPIO);
gpio_config(&io_conf);
initialized_ = true;
ESP_LOGI(TAG, "GpioManager initialized successfully");
return true;
}
void GpioManager::SetLedRing(bool state) {
if (!initialized_) {
ESP_LOGE(TAG, "GpioManager not initialized");
return;
}
gpio_set_level(LED_RING_GPIO, state ? 1 : 0);
}
void GpioManager::SetStatusLed(bool state) {
if (!initialized_) {
ESP_LOGE(TAG, "GpioManager not initialized");
return;
}
gpio_set_level(STATUS_LED_GPIO, state ? 1 : 0);
}

View File

@@ -0,0 +1,30 @@
#ifndef GPIO_MANAGER_H
#define GPIO_MANAGER_H
#include "config.h"
#include <driver/gpio.h>
class GpioManager {
public:
static GpioManager& GetInstance();
// 初始化GPIO系统
bool Initialize();
// GPIO控制方法
void SetLedRing(bool state);
void SetStatusLed(bool state);
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
private:
GpioManager() = default;
~GpioManager() = default;
GpioManager(const GpioManager&) = delete;
GpioManager& operator=(const GpioManager&) = delete;
bool initialized_ = false;
};
#endif // GPIO_MANAGER_H

View File

@@ -0,0 +1,139 @@
#include "imu_manager.h"
#include <esp_log.h>
#define TAG "ImuManager"
ImuManager& ImuManager::GetInstance() {
static ImuManager instance;
return instance;
}
bool ImuManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "ImuManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing ImuManager...");
InitializeImu();
// 启动IMU任务
StartImuTask();
initialized_ = true;
ESP_LOGI(TAG, "ImuManager initialized successfully");
return true;
}
void ImuManager::InitializeImu() {
ESP_LOGI(TAG, "Initializing MPU6050 IMU sensor...");
// IMU传感器I2C总线
i2c_master_bus_config_t imu_i2c_cfg = {
.i2c_port = I2C_NUM_1,
.sda_io_num = IMU_I2C_SDA_PIN,
.scl_io_num = IMU_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,
.allow_pd = false,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&imu_i2c_cfg, &imu_i2c_bus_));
// 初始化MPU6050传感器
mpu6050_sensor_ = std::make_unique<Mpu6050Sensor>(imu_i2c_bus_);
if (mpu6050_sensor_) {
uint8_t device_id;
if (mpu6050_sensor_->GetDeviceId(&device_id)) {
ESP_LOGI(TAG, "MPU6050 device ID: 0x%02X", device_id);
if (device_id == MPU6050_WHO_AM_I_VAL) {
if (mpu6050_sensor_->Initialize(ACCE_FS_4G, GYRO_FS_500DPS)) {
if (mpu6050_sensor_->WakeUp()) {
initialized_ = true;
ESP_LOGI(TAG, "MPU6050 sensor initialized successfully");
} else {
ESP_LOGE(TAG, "Failed to wake up MPU6050");
}
} else {
ESP_LOGE(TAG, "Failed to initialize MPU6050");
}
} else {
ESP_LOGE(TAG, "MPU6050 device ID mismatch: expected 0x%02X, got 0x%02X",
MPU6050_WHO_AM_I_VAL, device_id);
}
} else {
ESP_LOGE(TAG, "Failed to read MPU6050 device ID");
}
} else {
ESP_LOGE(TAG, "Failed to create MPU6050 sensor instance");
}
if (!initialized_) {
ESP_LOGW(TAG, "IMU sensor initialization failed - continuing without IMU");
}
}
void ImuManager::StartImuTask() {
if (!initialized_) {
ESP_LOGW(TAG, "ImuManager not initialized, skipping IMU task creation");
return;
}
BaseType_t ret = xTaskCreate(ImuDataTask, "imu_data_task", 4096, this, 5, &imu_task_handle_);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create IMU data task");
} else {
ESP_LOGI(TAG, "IMU data task created successfully");
}
}
void ImuManager::StopImuTask() {
if (imu_task_handle_) {
vTaskDelete(imu_task_handle_);
imu_task_handle_ = nullptr;
}
}
void ImuManager::ImuDataTask(void *pvParameters) {
ImuManager *manager = static_cast<ImuManager *>(pvParameters);
ESP_LOGI(TAG, "IMU data task started");
mpu6050_acce_value_t acce;
mpu6050_gyro_value_t gyro;
mpu6050_temp_value_t temp;
complimentary_angle_t angle;
while (true) {
if (manager->mpu6050_sensor_ && manager->initialized_) {
// 读取加速度计数据
if (manager->mpu6050_sensor_->GetAccelerometer(&acce)) {
ESP_LOGI(TAG, "Accelerometer - X:%.2f, Y:%.2f, Z:%.2f", acce.acce_x,
acce.acce_y, acce.acce_z);
}
// 读取陀螺仪数据
if (manager->mpu6050_sensor_->GetGyroscope(&gyro)) {
ESP_LOGI(TAG, "Gyroscope - X:%.2f, Y:%.2f, Z:%.2f", gyro.gyro_x,
gyro.gyro_y, gyro.gyro_z);
}
// 读取温度数据
if (manager->mpu6050_sensor_->GetTemperature(&temp)) {
ESP_LOGI(TAG, "Temperature: %.2f°C", temp.temp);
}
// 计算姿态角
if (manager->mpu6050_sensor_->ComplimentaryFilter(&acce, &gyro, &angle)) {
ESP_LOGI(TAG, "Attitude - Pitch:%.2f°, Roll:%.2f°, Yaw:%.2f°",
angle.pitch, angle.roll, angle.yaw);
}
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}

View File

@@ -0,0 +1,43 @@
#ifndef IMU_MANAGER_H
#define IMU_MANAGER_H
#include "config.h"
#include "mpu6050_sensor.h"
#include <driver/i2c_master.h>
#include <memory>
class ImuManager {
public:
static ImuManager& GetInstance();
// 初始化IMU系统
bool Initialize();
// 启动/停止IMU任务
void StartImuTask();
void StopImuTask();
// 获取IMU传感器实例
Mpu6050Sensor* GetImuSensor() const { return mpu6050_sensor_.get(); }
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
private:
ImuManager() = default;
~ImuManager() = default;
ImuManager(const ImuManager&) = delete;
ImuManager& operator=(const ImuManager&) = delete;
void InitializeImu();
static void ImuDataTask(void *pvParameters);
bool initialized_ = false;
i2c_master_bus_handle_t imu_i2c_bus_;
std::unique_ptr<Mpu6050Sensor> mpu6050_sensor_;
// 任务句柄
TaskHandle_t imu_task_handle_ = nullptr;
};
#endif // IMU_MANAGER_H

View File

@@ -0,0 +1,280 @@
#include "mpu6050_sensor.h"
#include <esp_timer.h>
#include <cmath>
const char* Mpu6050Sensor::TAG = "MPU6050";
Mpu6050Sensor::Mpu6050Sensor(i2c_master_bus_handle_t i2c_bus, uint8_t device_addr)
: i2c_bus_(i2c_bus), device_addr_(device_addr), initialized_(false),
acce_fs_(ACCE_FS_4G), gyro_fs_(GYRO_FS_500DPS), dt_(0.0f), alpha_(0.98f) {
// 初始化设备句柄
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = device_addr_,
.scl_speed_hz = 100000,
};
esp_err_t ret = i2c_master_bus_add_device(i2c_bus_, &dev_cfg, &device_handle_);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to add MPU6050 device to I2C bus: %s", esp_err_to_name(ret));
device_handle_ = nullptr;
}
// 初始化互补滤波
InitializeComplimentaryFilter();
}
Mpu6050Sensor::~Mpu6050Sensor() {
if (device_handle_) {
i2c_master_bus_rm_device(device_handle_);
}
}
bool Mpu6050Sensor::Initialize(mpu6050_acce_fs_t acce_fs, mpu6050_gyro_fs_t gyro_fs) {
if (!device_handle_) {
ESP_LOGE(TAG, "Device handle is null");
return false;
}
acce_fs_ = acce_fs;
gyro_fs_ = gyro_fs;
// 配置加速度计量程
uint8_t acce_config = (acce_fs << 3) & 0x18;
if (!WriteRegister(0x1C, acce_config)) {
ESP_LOGE(TAG, "Failed to configure accelerometer");
return false;
}
// 配置陀螺仪量程
uint8_t gyro_config = (gyro_fs << 3) & 0x18;
if (!WriteRegister(0x1B, gyro_config)) {
ESP_LOGE(TAG, "Failed to configure gyroscope");
return false;
}
// 配置数字低通滤波器
if (!WriteRegister(0x1A, 0x06)) { // DLPF_CFG = 6 (5Hz)
ESP_LOGE(TAG, "Failed to configure DLPF");
return false;
}
// 配置采样率 (1kHz / (1 + 7) = 125Hz)
if (!WriteRegister(0x19, 0x07)) {
ESP_LOGE(TAG, "Failed to configure sample rate");
return false;
}
initialized_ = true;
ESP_LOGI(TAG, "MPU6050 initialized successfully");
ESP_LOGI(TAG, "Accelerometer range: %d, Gyroscope range: %d", acce_fs, gyro_fs);
return true;
}
bool Mpu6050Sensor::WakeUp() {
if (!device_handle_) {
ESP_LOGE(TAG, "Device handle is null");
return false;
}
// 清除睡眠模式位
if (!WriteRegister(0x6B, 0x00)) {
ESP_LOGE(TAG, "Failed to wake up MPU6050");
return false;
}
// 等待传感器稳定
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "MPU6050 woken up");
return true;
}
bool Mpu6050Sensor::GetDeviceId(uint8_t* device_id) {
if (!device_id) {
ESP_LOGE(TAG, "Device ID pointer is null");
return false;
}
return ReadRegister(MPU6050_WHO_AM_I_REG, device_id, 1);
}
bool Mpu6050Sensor::GetAccelerometer(mpu6050_acce_value_t* acce) {
if (!acce) {
ESP_LOGE(TAG, "Accelerometer data pointer is null");
return false;
}
uint8_t data[6];
if (!ReadRegister(0x3B, data, 6)) {
ESP_LOGE(TAG, "Failed to read accelerometer data");
return false;
}
// 组合16位数据
int16_t raw_x = (data[0] << 8) | data[1];
int16_t raw_y = (data[2] << 8) | data[3];
int16_t raw_z = (data[4] << 8) | data[5];
// 根据量程转换为g值
float scale_factor;
switch (acce_fs_) {
case ACCE_FS_2G: scale_factor = 16384.0f; break;
case ACCE_FS_4G: scale_factor = 8192.0f; break;
case ACCE_FS_8G: scale_factor = 4096.0f; break;
case ACCE_FS_16G: scale_factor = 2048.0f; break;
default: scale_factor = 8192.0f; break;
}
acce->acce_x = raw_x / scale_factor;
acce->acce_y = raw_y / scale_factor;
acce->acce_z = raw_z / scale_factor;
return true;
}
bool Mpu6050Sensor::GetGyroscope(mpu6050_gyro_value_t* gyro) {
if (!gyro) {
ESP_LOGE(TAG, "Gyroscope data pointer is null");
return false;
}
uint8_t data[6];
if (!ReadRegister(0x43, data, 6)) {
ESP_LOGE(TAG, "Failed to read gyroscope data");
return false;
}
// 组合16位数据
int16_t raw_x = (data[0] << 8) | data[1];
int16_t raw_y = (data[2] << 8) | data[3];
int16_t raw_z = (data[4] << 8) | data[5];
// 根据量程转换为度/秒
float scale_factor;
switch (gyro_fs_) {
case GYRO_FS_250DPS: scale_factor = 131.0f; break;
case GYRO_FS_500DPS: scale_factor = 65.5f; break;
case GYRO_FS_1000DPS: scale_factor = 32.8f; break;
case GYRO_FS_2000DPS: scale_factor = 16.4f; break;
default: scale_factor = 65.5f; break;
}
gyro->gyro_x = raw_x / scale_factor;
gyro->gyro_y = raw_y / scale_factor;
gyro->gyro_z = raw_z / scale_factor;
return true;
}
bool Mpu6050Sensor::GetTemperature(mpu6050_temp_value_t* temp) {
if (!temp) {
ESP_LOGE(TAG, "Temperature data pointer is null");
return false;
}
uint8_t data[2];
if (!ReadRegister(0x41, data, 2)) {
ESP_LOGE(TAG, "Failed to read temperature data");
return false;
}
// 组合16位数据
int16_t raw_temp = (data[0] << 8) | data[1];
// 转换为摄氏度: T = (TEMP_OUT / 340) + 36.53
temp->temp = raw_temp / 340.0f + 36.53f;
return true;
}
bool Mpu6050Sensor::ComplimentaryFilter(const mpu6050_acce_value_t* acce,
const mpu6050_gyro_value_t* gyro,
complimentary_angle_t* angle) {
if (!acce || !gyro || !angle) {
ESP_LOGE(TAG, "Input pointers are null");
return false;
}
uint64_t current_time = GetCurrentTimeUs();
// 计算时间间隔
if (last_time_ > 0) {
dt_ = (current_time - last_time_) / 1000000.0f; // 转换为秒
} else {
dt_ = 0.01f; // 默认10ms
}
// 从加速度计计算俯仰角和横滚角
float accel_pitch = atan2f(acce->acce_y, sqrtf(acce->acce_x * acce->acce_x + acce->acce_z * acce->acce_z)) * 180.0f / M_PI;
float accel_roll = atan2f(-acce->acce_x, acce->acce_z) * 180.0f / M_PI;
// 互补滤波
angle->pitch = alpha_ * (last_angle_.pitch + gyro->gyro_x * dt_) + (1.0f - alpha_) * accel_pitch;
angle->roll = alpha_ * (last_angle_.roll + gyro->gyro_y * dt_) + (1.0f - alpha_) * accel_roll;
angle->yaw = last_angle_.yaw + gyro->gyro_z * dt_; // 偏航角只能通过陀螺仪积分
// 更新上次的角度和时间
last_angle_ = *angle;
last_time_ = current_time;
return true;
}
std::string Mpu6050Sensor::GetStatusJson() const {
std::string json = "{";
json += "\"initialized\":" + std::string(initialized_ ? "true" : "false") + ",";
json += "\"device_address\":" + std::to_string(device_addr_) + ",";
json += "\"accelerometer_range\":" + std::to_string(static_cast<int>(acce_fs_)) + ",";
json += "\"gyroscope_range\":" + std::to_string(static_cast<int>(gyro_fs_)) + ",";
json += "\"filter_alpha\":" + std::to_string(alpha_) + ",";
json += "\"sample_rate\":125";
json += "}";
return json;
}
bool Mpu6050Sensor::WriteRegister(uint8_t reg_addr, uint8_t data) {
if (!device_handle_) {
return false;
}
uint8_t write_buf[2] = {reg_addr, data};
esp_err_t ret = i2c_master_transmit(device_handle_, write_buf, 2, 1000);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to write register 0x%02X: %s", reg_addr, esp_err_to_name(ret));
return false;
}
return true;
}
bool Mpu6050Sensor::ReadRegister(uint8_t reg_addr, uint8_t* data, size_t len) {
if (!device_handle_ || !data) {
return false;
}
esp_err_t ret = i2c_master_transmit_receive(device_handle_, &reg_addr, 1, data, len, 1000);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to read register 0x%02X: %s", reg_addr, esp_err_to_name(ret));
return false;
}
return true;
}
uint64_t Mpu6050Sensor::GetCurrentTimeUs() {
return esp_timer_get_time();
}
void Mpu6050Sensor::InitializeComplimentaryFilter() {
last_angle_.pitch = 0.0f;
last_angle_.roll = 0.0f;
last_angle_.yaw = 0.0f;
last_time_ = 0;
dt_ = 0.01f;
alpha_ = 0.98f; // 互补滤波系数0.98表示更信任陀螺仪
}

View File

@@ -0,0 +1,184 @@
#ifndef MPU6050_SENSOR_H
#define MPU6050_SENSOR_H
#include <driver/i2c_master.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <string>
// MPU6050相关定义
#define MPU6050_I2C_ADDRESS 0x68
#define MPU6050_WHO_AM_I_REG 0x75
#define MPU6050_WHO_AM_I_VAL 0x68
// 加速度计量程
typedef enum {
ACCE_FS_2G = 0, // ±2g
ACCE_FS_4G = 1, // ±4g
ACCE_FS_8G = 2, // ±8g
ACCE_FS_16G = 3 // ±16g
} mpu6050_acce_fs_t;
// 陀螺仪量程
typedef enum {
GYRO_FS_250DPS = 0, // ±250°/s
GYRO_FS_500DPS = 1, // ±500°/s
GYRO_FS_1000DPS = 2, // ±1000°/s
GYRO_FS_2000DPS = 3 // ±2000°/s
} mpu6050_gyro_fs_t;
// 传感器数据结构
typedef struct {
float acce_x;
float acce_y;
float acce_z;
} mpu6050_acce_value_t;
typedef struct {
float gyro_x;
float gyro_y;
float gyro_z;
} mpu6050_gyro_value_t;
typedef struct {
float temp;
} mpu6050_temp_value_t;
typedef struct {
float pitch;
float roll;
float yaw;
} complimentary_angle_t;
/**
* @brief MPU6050传感器封装类
*
* 提供现代化的C++接口来操作MPU6050六轴传感器
* 支持加速度计、陀螺仪、温度传感器和互补滤波
*/
class Mpu6050Sensor {
public:
/**
* @brief 构造函数
* @param i2c_bus I2C总线句柄
* @param device_addr 设备地址默认为0x68
*/
explicit Mpu6050Sensor(i2c_master_bus_handle_t i2c_bus, uint8_t device_addr = MPU6050_I2C_ADDRESS);
/**
* @brief 析构函数
*/
~Mpu6050Sensor();
/**
* @brief 初始化传感器
* @param acce_fs 加速度计量程
* @param gyro_fs 陀螺仪量程
* @return true表示初始化成功false表示失败
*/
bool Initialize(mpu6050_acce_fs_t acce_fs = ACCE_FS_4G, mpu6050_gyro_fs_t gyro_fs = GYRO_FS_500DPS);
/**
* @brief 唤醒传感器
* @return true表示成功false表示失败
*/
bool WakeUp();
/**
* @brief 获取设备ID
* @param device_id 输出设备ID
* @return true表示成功false表示失败
*/
bool GetDeviceId(uint8_t* device_id);
/**
* @brief 获取加速度计数据
* @param acce 输出加速度计数据
* @return true表示成功false表示失败
*/
bool GetAccelerometer(mpu6050_acce_value_t* acce);
/**
* @brief 获取陀螺仪数据
* @param gyro 输出陀螺仪数据
* @return true表示成功false表示失败
*/
bool GetGyroscope(mpu6050_gyro_value_t* gyro);
/**
* @brief 获取温度数据
* @param temp 输出温度数据
* @return true表示成功false表示失败
*/
bool GetTemperature(mpu6050_temp_value_t* temp);
/**
* @brief 互补滤波计算姿态角
* @param acce 加速度计数据
* @param gyro 陀螺仪数据
* @param angle 输出姿态角
* @return true表示成功false表示失败
*/
bool ComplimentaryFilter(const mpu6050_acce_value_t* acce,
const mpu6050_gyro_value_t* gyro,
complimentary_angle_t* angle);
/**
* @brief 检查传感器是否已初始化
* @return true表示已初始化false表示未初始化
*/
bool IsInitialized() const { return initialized_; }
/**
* @brief 获取传感器状态信息
* @return JSON格式的状态信息
*/
std::string GetStatusJson() const;
private:
i2c_master_bus_handle_t i2c_bus_;
i2c_master_dev_handle_t device_handle_;
uint8_t device_addr_;
bool initialized_;
mpu6050_acce_fs_t acce_fs_;
mpu6050_gyro_fs_t gyro_fs_;
// 互补滤波相关
float dt_;
float alpha_;
complimentary_angle_t last_angle_;
uint64_t last_time_;
static const char* TAG;
/**
* @brief 写入寄存器
* @param reg_addr 寄存器地址
* @param data 数据
* @return true表示成功false表示失败
*/
bool WriteRegister(uint8_t reg_addr, uint8_t data);
/**
* @brief 读取寄存器
* @param reg_addr 寄存器地址
* @param data 输出数据
* @param len 数据长度
* @return true表示成功false表示失败
*/
bool ReadRegister(uint8_t reg_addr, uint8_t* data, size_t len);
/**
* @brief 获取当前时间戳(微秒)
* @return 时间戳
*/
uint64_t GetCurrentTimeUs();
/**
* @brief 初始化互补滤波
*/
void InitializeComplimentaryFilter();
};
#endif // MPU6050_SENSOR_H

View File

@@ -0,0 +1,199 @@
#include "tools_manager.h"
#include "application.h"
#include "board.h"
#include <esp_log.h>
#define TAG "ToolsManager"
ToolsManager& ToolsManager::GetInstance() {
static ToolsManager instance;
return instance;
}
bool ToolsManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "ToolsManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing ToolsManager...");
// 注册各种工具
RegisterMcpTools();
RegisterSystemTools();
RegisterAudioTools();
RegisterSensorTools();
initialized_ = true;
ESP_LOGI(TAG, "ToolsManager initialized successfully");
return true;
}
void ToolsManager::RegisterMcpTools() {
ESP_LOGI(TAG, "Registering MCP tools...");
auto& mcp_server = McpServer::GetInstance();
// 系统信息查询工具
mcp_server.AddTool(
"self.smart_speaker.get_system_info",
"获取智能音箱系统信息,包括板卡类型、版本、功能特性等",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& board = Board::GetInstance();
return board.GetBoardJson();
}
);
// 设备状态查询工具
mcp_server.AddTool(
"self.smart_speaker.get_device_state",
"获取设备当前状态,包括启动状态、连接状态等",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
DeviceState state = app.GetDeviceState();
const char* state_str = "unknown";
switch (state) {
case kDeviceStateStarting: state_str = "starting"; break;
case kDeviceStateWifiConfiguring: state_str = "configuring"; break;
case kDeviceStateIdle: state_str = "idle"; break;
case kDeviceStateConnecting: state_str = "connecting"; break;
case kDeviceStateListening: state_str = "listening"; break;
case kDeviceStateSpeaking: state_str = "speaking"; break;
case kDeviceStateUpgrading: state_str = "upgrading"; break;
case kDeviceStateActivating: state_str = "activating"; break;
case kDeviceStateAudioTesting: state_str = "audio_testing"; break;
case kDeviceStateFatalError: state_str = "fatal_error"; break;
default: state_str = "unknown"; break;
}
return std::string("{\"state\":\"") + state_str + "\"}";
}
);
ESP_LOGI(TAG, "MCP tools registered successfully");
}
void ToolsManager::RegisterSystemTools() {
ESP_LOGI(TAG, "Registering system tools...");
auto& mcp_server = McpServer::GetInstance();
// 系统重启工具
mcp_server.AddTool(
"self.smart_speaker.reboot",
"重启智能音箱系统",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
app.Reboot();
return "{\"message\":\"System reboot initiated\"}";
}
);
// 设备控制工具
mcp_server.AddTool(
"self.smart_speaker.start_listening",
"开始语音监听",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
app.StartListening();
return "{\"message\":\"Started listening\"}";
}
);
mcp_server.AddTool(
"self.smart_speaker.stop_listening",
"停止语音监听",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
app.StopListening();
return "{\"message\":\"Stopped listening\"}";
}
);
ESP_LOGI(TAG, "System tools registered successfully");
}
void ToolsManager::RegisterAudioTools() {
ESP_LOGI(TAG, "Registering audio tools...");
auto& mcp_server = McpServer::GetInstance();
// 音频播放工具
mcp_server.AddTool(
"self.smart_speaker.play_sound",
"播放指定音效。sound: 音效名称(activation, welcome, upgrade, wificonfig等)",
PropertyList({Property("sound", kPropertyTypeString, "activation")}),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
std::string sound = properties["sound"].value<std::string>();
app.PlaySound(sound);
return "{\"message\":\"Playing sound: " + sound + "\"}";
}
);
// 语音检测状态工具
mcp_server.AddTool(
"self.smart_speaker.is_voice_detected",
"检查是否检测到语音",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
bool voice_detected = app.IsVoiceDetected();
return "{\"voice_detected\":" + std::string(voice_detected ? "true" : "false") + "}";
}
);
ESP_LOGI(TAG, "Audio tools registered successfully");
}
void ToolsManager::RegisterSensorTools() {
ESP_LOGI(TAG, "Registering sensor tools...");
auto& mcp_server = McpServer::GetInstance();
// 压感传感器读取工具
mcp_server.AddTool(
"self.smart_speaker.get_pressure_sensor",
"获取压感传感器数据包括当前值、ADC通道、样本数量等",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& board = Board::GetInstance();
std::string board_json = board.GetBoardJson();
// 从board JSON中提取压感传感器信息
// 这里简化处理直接返回board信息中包含的传感器数据
return board_json;
}
);
// IMU传感器状态工具
mcp_server.AddTool(
"self.smart_speaker.get_imu_status",
"获取IMU传感器状态信息",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& board = Board::GetInstance();
std::string board_json = board.GetBoardJson();
// 从board JSON中提取IMU信息
return board_json;
}
);
// 传感器数据重置工具
mcp_server.AddTool(
"self.smart_speaker.reset_sensor_data",
"重置传感器数据缓冲区",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
// TODO: 实现传感器数据重置
return "{\"message\":\"Sensor data reset requested\"}";
}
);
ESP_LOGI(TAG, "Sensor tools registered successfully");
}

View File

@@ -0,0 +1,32 @@
#ifndef TOOLS_MANAGER_H
#define TOOLS_MANAGER_H
#include <string>
#include "mcp_server.h"
class ToolsManager {
public:
static ToolsManager& GetInstance();
// 初始化工具系统
bool Initialize();
// 工具注册方法
void RegisterMcpTools();
void RegisterSystemTools();
void RegisterAudioTools();
void RegisterSensorTools();
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
private:
ToolsManager() = default;
~ToolsManager() = default;
ToolsManager(const ToolsManager&) = delete;
ToolsManager& operator=(const ToolsManager&) = delete;
bool initialized_ = false;
};
#endif // TOOLS_MANAGER_H

View File

@@ -0,0 +1,55 @@
#include "wifi_manager.h"
#include "settings.h"
#include "wifi_station.h"
#include <esp_log.h>
#define TAG "WifiManager"
WifiManager& WifiManager::GetInstance() {
static WifiManager instance;
return instance;
}
bool WifiManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "WifiManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing WifiManager...");
// 配置WiFi设置
ConfigureWifiSettings();
// 设置默认凭据
SetDefaultCredentials();
initialized_ = true;
ESP_LOGI(TAG, "WifiManager initialized successfully");
return true;
}
void WifiManager::ConfigureWifiSettings() {
ESP_LOGI(TAG, "Configuring WiFi settings...");
// 配置WiFi参数到NVS
Settings wifi_settings("wifi", true);
// 设置不记住BSSID (不区分MAC地址)
wifi_settings.SetInt("remember_bssid", 0);
// 设置最大发射功率
wifi_settings.SetInt("max_tx_power", 0);
ESP_LOGI(TAG, "WiFi settings configured");
}
void WifiManager::SetDefaultCredentials() {
ESP_LOGI(TAG, "Setting default WiFi credentials...");
// 添加默认WiFi配置
auto &wifi_station = WifiStation::GetInstance();
wifi_station.AddAuth("xoxo", "12340000");
ESP_LOGI(TAG, "Default WiFi credentials added: SSID=xoxo, Password=12340000");
}

View File

@@ -0,0 +1,30 @@
#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H
#include <string>
class WifiManager {
public:
static WifiManager& GetInstance();
// 初始化WiFi系统
bool Initialize();
// WiFi配置方法
void SetDefaultCredentials();
void ConfigureWifiSettings();
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
private:
WifiManager() = default;
~WifiManager() = default;
WifiManager(const WifiManager&) = delete;
WifiManager& operator=(const WifiManager&) = delete;
bool initialized_ = false;
};
#endif // WIFI_MANAGER_H

View File

@@ -1,6 +1,7 @@
#include "sscma_camera.h"
#include "mcp_server.h"
#include "lvgl_display.h"
#include "lvgl_image.h"
#include "board.h"
#include "system_info.h"
#include "config.h"
@@ -245,7 +246,8 @@ bool SscmaCamera::Capture() {
// 显示预览图片
auto display = dynamic_cast<LvglDisplay*>(Board::GetInstance().GetDisplay());
if (display != nullptr) {
display->SetPreviewImage(&preview_image_);
auto image = std::make_unique<LvglSourceImage>(&preview_image_);
display->SetPreviewImage(std::move(image));
}
return true;
}

View File

@@ -1,5 +1,5 @@
# 由于原来的麦克风型号停产2025年7月之后的太极派JC3636W518更换了麦克风并且更换了屏幕玻璃所以在产品标签上批次号大于2528的用户请选择I2S Type PDM,
# 新增双声道配置
# 编译配置命令
**配置编译目标为 ESP32S3**
@@ -19,7 +19,12 @@ idf.py menuconfig
```
Xiaozhi Assistant -> Board Type -> 太极小派esp32s3
Xiaozhi Assistant -> taiji-pi-S3 I2S Type -> I2S Type PDM
Xiaozhi Assistant -> TAIJIPAI_S3_CONFIG -> taiji-pi-S3 I2S Type -> I2S Type PDM
```
**如果需要选择双声道:**
```
Xiaozhi Assistant -> TAIJIPAI_S3_CONFIG -> Enabel use 2 slot
```
**修改PSRAM配置**

View File

@@ -618,9 +618,17 @@ public:
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
#ifdef CONFIG_I2S_USE_2SLOT
I2S_STD_SLOT_BOTH,
#endif
AUDIO_MIC_SCK_PIN,
AUDIO_MIC_WS_PIN,
#ifdef CONFIG_I2S_USE_2SLOT
AUDIO_MIC_SD_PIN,
I2S_STD_SLOT_LEFT
#else
AUDIO_MIC_SD_PIN
#endif
);
#else
static NoAudioCodecSimplexPdm audio_codec(
@@ -629,6 +637,9 @@ public:
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
#ifdef CONFIG_I2S_USE_2SLOT
I2S_STD_SLOT_BOTH,
#endif
AUDIO_MIC_WS_PIN,
AUDIO_MIC_SD_PIN
);

View File

@@ -0,0 +1,88 @@
# 小智云聊S3
## 简介
小智云聊S3是小智AI的魔改项目是首个2.8寸护眼大屏+大字体+2000mah大电池的量产成品做了大量创新和优化。
## 合并版
合并版代码在小智AI主项目中维护跟随主项目的一起版本更新便于用户自行扩展和第三方固件扩展。支持语音唤醒、语音打断、OTA、4G自由切换等功能。
>### 按键操作
>- **开机**: 关机状态长按1秒后释放按键自动开机
>- **关机**: 开机状态长按1秒后释放按键标题栏会显示'请稍候'再等2秒自动关机
>- **唤醒/打断**: 正常通话环境下,单击按键
>- **切换4G/Wifi**: 启动过程或者配网界面1秒钟内双击按键需安装4G模块
>- **重新配网**: 开机状态1秒钟内三击按键会自动重启并进入配网界面
## 魔改版
魔改版由于底层改动太大,代码单独维护,定期合并主项目代码。
>### 为什么是魔改
>- 首个实现微信二维码配网。
>- 首个支持单手机配网。
>- 首个支持扫二维码访问控制台。
>- 首发支持繁体、日文、英文版界面
>- 首个全语音操控模式
>- 独家提供一键刷机脚本等多种刷机方式
## 版本区别
>| 特性 | 合并版 | 魔改版 |
>| --- | --- | --- |
>| 语音打断 | ✓ | ✓ |
>| 4G功能 | ✓ | ✓ |
>| 自动更新固件 | ✓ | X |
>| 第三方固件支持 | ✓ | X |
>| 天气待机界面 | X | ✓ |
>| 闹钟提醒 | X | ✓ |
>| 网络音乐播放 | X | ✓ |
>| 微信扫码配网 | X | ✓ |
>| 单手机配网 | X | ✓ |
>| 扫码访问控制台 | X | ✓ |
>| 繁日英文界面 | X | ✓ |
>| 多语言支持 | X | ✓ |
>| 外接蓝牙音箱 | X | ✓ |
# 编译配置命令
**克隆工程**
```bash
git clone https://github.com/78/xiaozhi-esp32.git
```
**进入工程**
```bash
cd xiaozhi-esp32
```
**配置编译目标为 ESP32S3**
```bash
idf.py set-target esp32s3
```
**打开 menuconfig**
```bash
idf.py menuconfig
```
**选择板子**
```bash
- `Xiaozhi Assistant``Board Type` → 选择 `小智云聊-S3` → 选择 `Enable Device-Side AEC`
```
**编译**
```ba
idf.py build
```
**下载并打开串口终端**
```bash
idf.py build flash monitor
```

View File

@@ -0,0 +1,59 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
#define AUDIO_INPUT_REFERENCE true
#define AUDIO_INPUT_SAMPLE_RATE 24000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
#define AUDIO_DEFAULT_OUTPUT_VOLUME 70
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_14
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_13
#define AUDIO_I2S_GPIO_WS GPIO_NUM_11
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_12
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_10
#define AUDIO_CODEC_PA_PIN GPIO_NUM_17
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_21
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_18
#define AUDIO_CODEC_ES8388_ADDR ES8388_CODEC_DEFAULT_ADDR
#define BOOT_BUTTON_PIN GPIO_NUM_2
#define BOOT_5V_PIN GPIO_NUM_3 //5V升压输出
#define BOOT_4G_PIN GPIO_NUM_5 //4G模块使能
#define MON_BATT_PIN GPIO_NUM_43 //检测PMU电池指示
#define MON_BATT_CNT 70 //检测PMU电池秒数
#define MON_USB_PIN GPIO_NUM_47 //检测USB插入
#define ML307_RX_PIN GPIO_NUM_16
#define ML307_TX_PIN GPIO_NUM_15
#define DISPLAY_SPI_LCD_HOST SPI2_HOST
#define DISPLAY_SPI_CLOCK_HZ (40 * 1000 * 1000)
#define DISPLAY_SPI_PIN_SCLK 42
#define DISPLAY_SPI_PIN_MOSI 40
#define DISPLAY_SPI_PIN_MISO -1
#define DISPLAY_SPI_PIN_LCD_DC 41
#define DISPLAY_SPI_PIN_LCD_RST 45
#define DISPLAY_SPI_PIN_LCD_CS -1
#define DISPLAY_PIN_TOUCH_CS -1
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_46
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
#define DISPLAY_SWAP_XY true
#define DISPLAY_MIRROR_X false
#define DISPLAY_MIRROR_Y true
#define DISPLAY_INVERT_COLOR false
#define DISPLAY_RGB_ORDER_COLOR LCD_RGB_ELEMENT_ORDER_RGB
#define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0
#define KEY_EXPIRE_MS 800
#endif // _BOARD_CONFIG_H_

View File

@@ -0,0 +1,11 @@
{
"target": "esp32s3",
"builds": [
{
"name": "yunliao-s3",
"sdkconfig_append": [
"CONFIG_USE_DEVICE_AEC=y"
]
}
]
}

View File

@@ -0,0 +1,203 @@
#include "power_manager.h"
#include "esp_sleep.h"
#include "driver/rtc_io.h"
#include "esp_log.h"
#include "config.h"
#include <esp_sleep.h>
#include "esp_log.h"
#include "settings.h"
#define TAG "PowerManager"
static QueueHandle_t gpio_evt_queue = NULL;
uint16_t battCnt;//闪灯次数
int battLife = -1; //电量
// 中断服务程序
static void IRAM_ATTR batt_mon_isr_handler(void* arg) {
uint32_t gpio_num = (uint32_t) arg;
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
// 添加任务处理函数
static void batt_mon_task(void* arg) {
uint32_t io_num;
while(1) {
if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
battCnt++;
}
}
}
static void calBattLife() {
// 计算电量
battLife = battCnt;
if (battLife > 100){
battLife = 100;
}
// ESP_LOGI(TAG, "Battery life:%d", (int)battLife);
// 重置计数器
battCnt = 0;
}
PowerManager::PowerManager(){
}
void PowerManager::Initialize(){
// 初始化5V控制引脚
gpio_config_t io_conf_5v = {
.pin_bit_mask = 1<<BOOT_5V_PIN,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf_5v));
// 初始化4G控制引脚
gpio_config_t io_conf_4g = {
.pin_bit_mask = 1<<BOOT_4G_PIN,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_ENABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf_4g));
// 电池电量监测引脚配置
gpio_config_t io_conf_batt_mon = {
.pin_bit_mask = 1ull<<MON_BATT_PIN,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_POSEDGE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf_batt_mon));
// 创建电量GPIO事件队列
gpio_evt_queue = xQueueCreate(2, sizeof(uint32_t));
// 安装电量GPIO ISR服务
ESP_ERROR_CHECK(gpio_install_isr_service(0));
// 添加中断处理
ESP_ERROR_CHECK(gpio_isr_handler_add(MON_BATT_PIN, batt_mon_isr_handler, (void*)MON_BATT_PIN));
// 创建监控任务
xTaskCreate(&batt_mon_task, "batt_mon_task", 1024, NULL, 10, NULL);
// 初始化监测引脚
gpio_config_t mon_conf = {};
mon_conf.pin_bit_mask = 1ULL << MON_USB_PIN;
mon_conf.mode = GPIO_MODE_INPUT;
mon_conf.pull_up_en = GPIO_PULLUP_DISABLE;
mon_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_config(&mon_conf);
// 创建电池电量检查定时器
esp_timer_create_args_t timer_args = {
.callback = [](void* arg) {
PowerManager* self = static_cast<PowerManager*>(arg);
self->CheckBatteryStatus();
},
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "battery_check_timer",
.skip_unhandled_events = true,
};
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_));
ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000));
}
void PowerManager::CheckBatteryStatus(){
call_count_++;
if(call_count_ >= MON_BATT_CNT) {
calBattLife();
call_count_ = 0;
}
bool new_charging_status = IsCharging();
if (new_charging_status != is_charging_) {
is_charging_ = new_charging_status;
if (charging_callback_) {
charging_callback_(is_charging_);
}
}
bool new_discharging_status = IsDischarging();
if (new_discharging_status != is_discharging_) {
is_discharging_ = new_discharging_status;
if (discharging_callback_) {
discharging_callback_(is_discharging_);
}
}
}
bool PowerManager::IsCharging() {
return gpio_get_level(MON_USB_PIN) == 1 && !IsChargingDone();
}
bool PowerManager::IsDischarging() {
return gpio_get_level(MON_USB_PIN) == 0;
}
bool PowerManager::IsChargingDone() {
return battLife >= 95;
}
int PowerManager::GetBatteryLevel() {
return battLife;
}
void PowerManager::OnChargingStatusChanged(std::function<void(bool)> callback) {
charging_callback_ = callback;
}
void PowerManager::OnChargingStatusDisChanged(std::function<void(bool)> callback) {
discharging_callback_ = callback;
}
void PowerManager::CheckStartup() {
Settings settings1("board", true);
if(settings1.GetInt("sleep_flag", 0) > 0){
vTaskDelay(pdMS_TO_TICKS(1000));
if( gpio_get_level(BOOT_BUTTON_PIN) == 1) {
Sleep(); //进入休眠模式
}else{
settings1.SetInt("sleep_flag", 0);
}
}
}
void PowerManager::Start5V() {
gpio_set_level(BOOT_5V_PIN, 1);
}
void PowerManager::Shutdown5V() {
gpio_set_level(BOOT_5V_PIN, 0);
}
void PowerManager::Start4G() {
gpio_set_level(BOOT_4G_PIN, 1);
}
void PowerManager::Shutdown4G() {
gpio_set_level(BOOT_4G_PIN, 0);
gpio_set_level(ML307_RX_PIN,1);
gpio_set_level(ML307_TX_PIN,1);
}
void PowerManager::Sleep() {
ESP_LOGI(TAG, "Entering deep sleep");
Settings settings("board", true);
settings.SetInt("sleep_flag", 1);
Shutdown4G();
Shutdown5V();
if(gpio_evt_queue) {
vQueueDelete(gpio_evt_queue);
gpio_evt_queue = NULL;
}
ESP_ERROR_CHECK(gpio_isr_handler_remove(BOOT_BUTTON_PIN));
ESP_ERROR_CHECK(esp_sleep_enable_ext0_wakeup(BOOT_BUTTON_PIN, 0));
ESP_ERROR_CHECK(rtc_gpio_pulldown_dis(BOOT_BUTTON_PIN));
ESP_ERROR_CHECK(rtc_gpio_pullup_en(BOOT_BUTTON_PIN));
esp_deep_sleep_start();
}

View File

@@ -0,0 +1,37 @@
#ifndef __POWERMANAGER_H__
#define __POWERMANAGER_H__
#include <functional>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/timers.h"
class PowerManager{
public:
PowerManager();
void Initialize();
bool IsCharging();
bool IsDischarging();
bool IsChargingDone();
int GetBatteryLevel();
void CheckStartup();
void Start5V();
void Shutdown5V();
void Start4G();
void Shutdown4G();
void Sleep();
void CheckBatteryStatus();
void OnChargingStatusChanged(std::function<void(bool)> callback);
void OnChargingStatusDisChanged(std::function<void(bool)> callback);
private:
esp_timer_handle_t timer_handle_;
std::function<void(bool)> charging_callback_;
std::function<void(bool)> discharging_callback_;
int is_charging_ = -1;
int is_discharging_ = -1;
int call_count_ = 0;
};
#endif

View File

@@ -0,0 +1,207 @@
#include "lvgl_theme.h"
#include "dual_network_board.h"
#include "codecs/es8388_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "power_save_timer.h"
#include "power_manager.h"
#include "assets/lang_config.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <wifi_station.h>
#define TAG "YunliaoS3"
class YunliaoS3 : public DualNetworkBoard {
private:
i2c_master_bus_handle_t codec_i2c_bus_;
Button boot_button_;
SpiLcdDisplay* display_;
PowerSaveTimer* power_save_timer_;
PowerManager* power_manager_;
void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 600);
power_save_timer_->OnEnterSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(10);
});
power_save_timer_->OnExitSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness();
});
power_save_timer_->OnShutdownRequest([this]() {
ESP_LOGI(TAG, "Shutting down");
power_manager_->Sleep();
});
power_save_timer_->SetEnabled(true);
}
void InitializeI2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = I2C_NUM_0,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.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 InitializeSpi() {
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = DISPLAY_SPI_PIN_MOSI;
buscfg.miso_io_num = DISPLAY_SPI_PIN_MISO;
buscfg.sclk_io_num = DISPLAY_SPI_PIN_SCLK;
buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(DISPLAY_SPI_LCD_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
power_save_timer_->WakeUp();
auto& app = Application::GetInstance();
app.ToggleChatState();
});
boot_button_.OnDoubleClick([this]() {
ESP_LOGI(TAG, "Button OnDoubleClick");
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
SwitchNetworkType();
}
});
boot_button_.OnMultipleClick([this]() {
ESP_LOGI(TAG, "Button OnThreeClick");
if (GetNetworkType() == NetworkType::WIFI) {
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
wifi_board.ResetWifiConfiguration();
}
},3);
boot_button_.OnLongPress([this]() {
ESP_LOGI(TAG, "Button LongPress to Sleep");
display_->SetStatus(Lang::Strings::PLEASE_WAIT);
vTaskDelay(pdMS_TO_TICKS(2000));
power_manager_->Sleep();
});
}
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 = DISPLAY_SPI_PIN_LCD_CS;
io_config.dc_gpio_num = DISPLAY_SPI_PIN_LCD_DC;
io_config.spi_mode = 3;
io_config.pclk_hz = DISPLAY_SPI_CLOCK_HZ;
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(DISPLAY_SPI_LCD_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 = DISPLAY_SPI_PIN_LCD_RST;
panel_config.rgb_ele_order = DISPLAY_RGB_ORDER_COLOR;
panel_config.bits_per_pixel = 16;
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, DISPLAY_INVERT_COLOR);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
display_ = new SpiLcdDisplay(panel_io, panel, DISPLAY_WIDTH,
DISPLAY_HEIGHT, DISPLAY_OFFSET_X,
DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X,
DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
auto& theme_manager = LvglThemeManager::GetInstance();
auto theme = theme_manager.GetTheme("dark");
if (theme != nullptr) {
display_->SetTheme(theme);
}
}
public:
YunliaoS3() :
DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, GPIO_NUM_NC, 0),
boot_button_(BOOT_BUTTON_PIN),
power_manager_(new PowerManager()){
power_manager_->Start5V();
power_manager_->Initialize();
InitializeI2c();
power_manager_->CheckStartup();
InitializePowerSaveTimer();
InitializeSpi();
InitializeButtons();
InitializeSt7789Display();
power_manager_->OnChargingStatusDisChanged([this](bool is_discharging) {
if(power_save_timer_){
if (is_discharging) {
power_save_timer_->SetEnabled(true);
} else {
power_save_timer_->SetEnabled(false);
}
}
});
if(GetNetworkType() == NetworkType::WIFI){
power_manager_->Shutdown4G();
}
GetBacklight()->RestoreBrightness();
}
virtual AudioCodec* GetAudioCodec() override {
static Es8388AudioCodec audio_codec(
codec_i2c_bus_,
I2C_NUM_0,
AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN,
AUDIO_CODEC_PA_PIN,
AUDIO_CODEC_ES8388_ADDR,
AUDIO_INPUT_REFERENCE
);
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight;
}
virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override {
level = power_manager_->GetBatteryLevel();
charging = power_manager_->IsCharging();
discharging = power_manager_->IsDischarging();
return true;
}
virtual void SetPowerSaveMode(bool enabled) override {
if (!enabled) {
power_save_timer_->WakeUp();
}
DualNetworkBoard::SetPowerSaveMode(enabled);
}
};
DECLARE_BOARD(YunliaoS3);

View File

@@ -47,24 +47,9 @@ void Display::SetChatMessage(const char* role, const char* content) {
void Display::SetTheme(Theme* theme) {
current_theme_ = theme;
Settings settings("display", true);
settings.SetString("theme", theme->name());
}
void Display::SetMusicInfo(const char* song_name) {
// 默认实现:对于非微信模式,将歌名显示在聊天消息标签中
DisplayLockGuard lock(this);
if (chat_message_label_ == nullptr) {
return;
}
if (song_name != nullptr && strlen(song_name) > 0) {
std::string music_text = "";
music_text += song_name;
lv_label_set_text(chat_message_label_, music_text.c_str());
SetEmotion(FONT_AWESOME_MUSIC);
} else {
lv_label_set_text(chat_message_label_, "");
SetEmotion(FONT_AWESOME_NEUTRAL);
if (theme != nullptr) {
Settings settings("display", true);
settings.SetString("theme", theme->name());
}
}

View File

@@ -38,11 +38,12 @@ public:
virtual void SetTheme(Theme* theme);
virtual Theme* GetTheme() { return current_theme_; }
virtual void UpdateStatusBar(bool update_all = false);
virtual void SetMusicInfo(const char* song_name);
virtual void SetPowerSaveMode(bool on);
// 音乐播放相关方法
virtual void SetMusicInfo(const char* info) {}
virtual void start() {}
virtual void clearScreen() {} // 清除FFT显示默认为空实现
virtual void stopFft() {} // 停止FFT显示默认为空实现
virtual void stopFft() {}
inline int width() const { return width_; }
inline int height() const { return height_; }
@@ -56,8 +57,6 @@ protected:
friend class DisplayLockGuard;
virtual bool Lock(int timeout_ms = 0) = 0;
virtual void Unlock() = 0;
lv_obj_t* chat_message_label_ = nullptr;
lv_obj_t *emotion_label_ = nullptr;
};

View File

@@ -0,0 +1,57 @@
#include "esplog_display.h"
#include "esp_log.h"
#define TAG "Display2Log"
EspLogDisplay::EspLogDisplay()
{}
EspLogDisplay::~EspLogDisplay()
{}
void EspLogDisplay::SetStatus(const char* status)
{
ESP_LOGW(TAG, "SetStatus: %s", status);
}
void EspLogDisplay::ShowNotification(const char* notification, int duration_ms)
{
ESP_LOGW(TAG, "ShowNotification: %s", notification);
}
void EspLogDisplay::ShowNotification(const std::string &notification, int duration_ms)
{
ShowNotification(notification.c_str(), duration_ms);
}
void EspLogDisplay::SetEmotion(const char* emotion)
{
ESP_LOGW(TAG, "SetEmotion: %s", emotion);
}
void EspLogDisplay::SetChatMessage(const char* role, const char* content)
{
ESP_LOGW(TAG, "Role:%s", role);
ESP_LOGW(TAG, " %s", content);
}
// 音乐播放相关用日志模拟UI行为
// 显示当前播放的歌曲信息
void EspLogDisplay::SetMusicInfo(const char* info)
{
ESP_LOGW(TAG, "MusicInfo: %s", info ? info : "");
}
// 启动频谱显示(此处仅打印日志)
void EspLogDisplay::start()
{
ESP_LOGW(TAG, "Spectrum start");
}
// 停止频谱显示(此处仅打印日志)
void EspLogDisplay::stopFft()
{
ESP_LOGW(TAG, "Spectrum stop");
}

View File

@@ -0,0 +1,33 @@
#ifndef ESPLOG_DISPLAY_H_
#define ESPLOG_DISPLAY_H_
#include "display.h"
#include <string>
class EspLogDisplay : public Display {
public:
EspLogDisplay();
~EspLogDisplay();
virtual void SetStatus(const char* status);
virtual void ShowNotification(const char* notification, int duration_ms = 3000);
virtual void ShowNotification(const std::string &notification, int duration_ms = 3000);
virtual void SetEmotion(const char* emotion) override;
virtual void SetChatMessage(const char* role, const char* content) override;
// 音乐播放相关(无屏版本用日志模拟)
virtual void SetMusicInfo(const char* info) override;
virtual void start() override;
virtual void stopFft() override;
virtual inline void SetPreviewImage(const lv_img_dsc_t* image) {}
virtual inline void SetTheme(const std::string& theme_name) {}
virtual inline void UpdateStatusBar(bool update_all = false) override {}
protected:
virtual inline bool Lock(int timeout_ms = 0) override { return true; }
virtual inline void Unlock() override {}
};
#endif

View File

@@ -28,30 +28,30 @@ void LcdDisplay::InitializeLcdThemes() {
// light theme
auto light_theme = new LvglTheme("light");
light_theme->set_background_color(lv_color_white());
light_theme->set_text_color(lv_color_black());
light_theme->set_chat_background_color(lv_color_hex(0xE0E0E0));
light_theme->set_user_bubble_color(lv_color_hex(0x95EC69));
light_theme->set_assistant_bubble_color(lv_color_white());
light_theme->set_system_bubble_color(lv_color_hex(0xE0E0E0));
light_theme->set_system_text_color(lv_color_hex(0x666666));
light_theme->set_border_color(lv_color_hex(0xE0E0E0));
light_theme->set_low_battery_color(lv_color_black());
light_theme->set_background_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255)
light_theme->set_text_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
light_theme->set_chat_background_color(lv_color_hex(0xE0E0E0)); //rgb(224, 224, 224)
light_theme->set_user_bubble_color(lv_color_hex(0x00FF00)); //rgb(0, 128, 0)
light_theme->set_assistant_bubble_color(lv_color_hex(0xDDDDDD)); //rgb(221, 221, 221)
light_theme->set_system_bubble_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255)
light_theme->set_system_text_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
light_theme->set_border_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
light_theme->set_low_battery_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
light_theme->set_text_font(text_font);
light_theme->set_icon_font(icon_font);
light_theme->set_large_icon_font(large_icon_font);
// dark theme
auto dark_theme = new LvglTheme("dark");
dark_theme->set_background_color(lv_color_hex(0x121212));
dark_theme->set_text_color(lv_color_white());
dark_theme->set_chat_background_color(lv_color_hex(0x1E1E1E));
dark_theme->set_user_bubble_color(lv_color_hex(0x1A6C37));
dark_theme->set_assistant_bubble_color(lv_color_hex(0x333333));
dark_theme->set_system_bubble_color(lv_color_hex(0x2A2A2A));
dark_theme->set_system_text_color(lv_color_hex(0xAAAAAA));
dark_theme->set_border_color(lv_color_hex(0x333333));
dark_theme->set_low_battery_color(lv_color_hex(0xFF0000));
dark_theme->set_background_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
dark_theme->set_text_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255)
dark_theme->set_chat_background_color(lv_color_hex(0x1F1F1F)); //rgb(31, 31, 31)
dark_theme->set_user_bubble_color(lv_color_hex(0x00FF00)); //rgb(0, 128, 0)
dark_theme->set_assistant_bubble_color(lv_color_hex(0x222222)); //rgb(34, 34, 34)
dark_theme->set_system_bubble_color(lv_color_hex(0x000000)); //rgb(0, 0, 0)
dark_theme->set_system_text_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255)
dark_theme->set_border_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255)
dark_theme->set_low_battery_color(lv_color_hex(0xFF0000)); //rgb(255, 0, 0)
dark_theme->set_text_font(text_font);
dark_theme->set_icon_font(icon_font);
dark_theme->set_large_icon_font(large_icon_font);
@@ -120,6 +120,9 @@ SpiLcdDisplay::SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_h
ESP_LOGI(TAG, "Initialize LVGL port");
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
port_cfg.task_priority = 1;
#if CONFIG_SOC_CPU_CORES_NUM > 1
port_cfg.task_affinity = 1;
#endif
lvgl_port_init(&port_cfg);
ESP_LOGI(TAG, "Adding LCD display");
@@ -451,7 +454,7 @@ void LcdDisplay::SetupUI() {
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
emoji_image_ = lv_img_create(screen);
lv_obj_align(emoji_image_, LV_ALIGN_TOP_MID, 0, text_font->line_height + lvgl_theme->spacing(2));
lv_obj_align(emoji_image_, LV_ALIGN_TOP_MID, 0, text_font->line_height + lvgl_theme->spacing(8));
// Display AI logo while booting
emoji_label_ = lv_label_create(screen);
@@ -521,8 +524,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
lv_obj_t* msg_bubble = lv_obj_create(content_);
lv_obj_set_style_radius(msg_bubble, 8, 0);
lv_obj_set_scrollbar_mode(msg_bubble, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_border_width(msg_bubble, 1, 0);
lv_obj_set_style_border_color(msg_bubble, lvgl_theme->border_color(), 0);
lv_obj_set_style_border_width(msg_bubble, 0, 0);
lv_obj_set_style_pad_all(msg_bubble, lvgl_theme->spacing(4), 0);
// Create the message text
@@ -561,6 +563,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
if (strcmp(role, "user") == 0) {
// User messages are right-aligned with green background
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->user_bubble_color(), 0);
lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0);
// Set text color for contrast
lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0);
@@ -576,6 +579,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
} else if (strcmp(role, "assistant") == 0) {
// Assistant messages are left-aligned with white background
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->assistant_bubble_color(), 0);
lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0);
// Set text color for contrast
lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0);
@@ -591,6 +595,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
} else if (strcmp(role, "system") == 0) {
// System messages are center-aligned with light gray background
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->system_bubble_color(), 0);
lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0);
// Set text color for contrast
lv_obj_set_style_text_color(msg_text, lvgl_theme->system_text_color(), 0);
@@ -657,84 +662,88 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
chat_message_label_ = msg_text;
}
void LcdDisplay::SetPreviewImage(const lv_img_dsc_t* img_dsc) {
void LcdDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
DisplayLockGuard lock(this);
if (content_ == nullptr) {
return;
}
if (image == nullptr) {
return;
}
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
if (img_dsc != nullptr) {
// Create a message bubble for image preview
lv_obj_t* img_bubble = lv_obj_create(content_);
lv_obj_set_style_radius(img_bubble, 8, 0);
lv_obj_set_scrollbar_mode(img_bubble, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_border_width(img_bubble, 1, 0);
lv_obj_set_style_border_color(img_bubble, lvgl_theme->border_color(), 0);
lv_obj_set_style_pad_all(img_bubble, lvgl_theme->spacing(4), 0);
// Set image bubble background color (similar to system message)
lv_obj_set_style_bg_color(img_bubble, lvgl_theme->assistant_bubble_color(), 0);
// 设置自定义属性标记气泡类型
lv_obj_set_user_data(img_bubble, (void*)"image");
// Create a message bubble for image preview
lv_obj_t* img_bubble = lv_obj_create(content_);
lv_obj_set_style_radius(img_bubble, 8, 0);
lv_obj_set_scrollbar_mode(img_bubble, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_border_width(img_bubble, 0, 0);
lv_obj_set_style_pad_all(img_bubble, lvgl_theme->spacing(4), 0);
// Set image bubble background color (similar to system message)
lv_obj_set_style_bg_color(img_bubble, lvgl_theme->assistant_bubble_color(), 0);
lv_obj_set_style_bg_opa(img_bubble, LV_OPA_70, 0);
// 设置自定义属性标记气泡类型
lv_obj_set_user_data(img_bubble, (void*)"image");
// Create the image object inside the bubble
lv_obj_t* preview_image = lv_image_create(img_bubble);
// Calculate appropriate size for the image
lv_coord_t max_width = LV_HOR_RES * 70 / 100; // 70% of screen width
lv_coord_t max_height = LV_VER_RES * 50 / 100; // 50% of screen height
// Calculate zoom factor to fit within maximum dimensions
lv_coord_t img_width = img_dsc->header.w;
lv_coord_t img_height = img_dsc->header.h;
if (img_width == 0 || img_height == 0) {
img_width = max_width;
img_height = max_height;
ESP_LOGW(TAG, "Invalid image dimensions: %ld x %ld, using default dimensions: %ld x %ld", img_width, img_height, max_width, max_height);
}
lv_coord_t zoom_w = (max_width * 256) / img_width;
lv_coord_t zoom_h = (max_height * 256) / img_height;
lv_coord_t zoom = (zoom_w < zoom_h) ? zoom_w : zoom_h;
// Ensure zoom doesn't exceed 256 (100%)
if (zoom > 256) zoom = 256;
// Set image properties
lv_image_set_src(preview_image, img_dsc);
lv_image_set_scale(preview_image, zoom);
// Add event handler to clean up copied data when image is deleted
lv_obj_add_event_cb(preview_image, [](lv_event_t* e) {
lv_img_dsc_t* img_dsc = (lv_img_dsc_t*)lv_event_get_user_data(e);
if (img_dsc != nullptr) {
heap_caps_free((void*)img_dsc->data);
heap_caps_free(img_dsc);
}
}, LV_EVENT_DELETE, (void*)img_dsc);
// Calculate actual scaled image dimensions
lv_coord_t scaled_width = (img_width * zoom) / 256;
lv_coord_t scaled_height = (img_height * zoom) / 256;
// Set bubble size to be 16 pixels larger than the image (8 pixels on each side)
lv_obj_set_width(img_bubble, scaled_width + 16);
lv_obj_set_height(img_bubble, scaled_height + 16);
// Don't grow in flex layout
lv_obj_set_style_flex_grow(img_bubble, 0, 0);
// Center the image within the bubble
lv_obj_center(preview_image);
// Left align the image bubble like assistant messages
lv_obj_align(img_bubble, LV_ALIGN_LEFT_MID, 0, 0);
// Auto-scroll to the image bubble
lv_obj_scroll_to_view_recursive(img_bubble, LV_ANIM_ON);
// Create the image object inside the bubble
lv_obj_t* preview_image = lv_image_create(img_bubble);
// Calculate appropriate size for the image
lv_coord_t max_width = LV_HOR_RES * 70 / 100; // 70% of screen width
lv_coord_t max_height = LV_VER_RES * 50 / 100; // 50% of screen height
// Calculate zoom factor to fit within maximum dimensions
auto img_dsc = image->image_dsc();
lv_coord_t img_width = img_dsc->header.w;
lv_coord_t img_height = img_dsc->header.h;
if (img_width == 0 || img_height == 0) {
img_width = max_width;
img_height = max_height;
ESP_LOGW(TAG, "Invalid image dimensions: %ld x %ld, using default dimensions: %ld x %ld", img_width, img_height, max_width, max_height);
}
lv_coord_t zoom_w = (max_width * 256) / img_width;
lv_coord_t zoom_h = (max_height * 256) / img_height;
lv_coord_t zoom = (zoom_w < zoom_h) ? zoom_w : zoom_h;
// Ensure zoom doesn't exceed 256 (100%)
if (zoom > 256) zoom = 256;
// Set image properties
lv_image_set_src(preview_image, img_dsc);
lv_image_set_scale(preview_image, zoom);
// Add event handler to clean up LvglImage when image is deleted
// We need to transfer ownership of the unique_ptr to the event callback
LvglImage* raw_image = image.release(); // 释放智能指针的所有权
lv_obj_add_event_cb(preview_image, [](lv_event_t* e) {
LvglImage* img = (LvglImage*)lv_event_get_user_data(e);
if (img != nullptr) {
delete img; // 通过删除 LvglImage 对象来正确释放内存
}
}, LV_EVENT_DELETE, (void*)raw_image);
// Calculate actual scaled image dimensions
lv_coord_t scaled_width = (img_width * zoom) / 256;
lv_coord_t scaled_height = (img_height * zoom) / 256;
// Set bubble size to be 16 pixels larger than the image (8 pixels on each side)
lv_obj_set_width(img_bubble, scaled_width + 16);
lv_obj_set_height(img_bubble, scaled_height + 16);
// Don't grow in flex layout
lv_obj_set_style_flex_grow(img_bubble, 0, 0);
// Center the image within the bubble
lv_obj_center(preview_image);
// Left align the image bubble like assistant messages
lv_obj_align(img_bubble, LV_ALIGN_LEFT_MID, 0, 0);
// Auto-scroll to the image bubble
lv_obj_scroll_to_view_recursive(img_bubble, LV_ANIM_ON);
}
#else
void LcdDisplay::SetupUI() {
@@ -858,37 +867,35 @@ void LcdDisplay::SetupUI() {
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
}
void LcdDisplay::SetPreviewImage(const lv_img_dsc_t* img_dsc) {
void LcdDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
DisplayLockGuard lock(this);
if (preview_image_ == nullptr) {
ESP_LOGE(TAG, "Preview image is not initialized");
return;
}
auto old_src = (const lv_img_dsc_t*)lv_image_get_src(preview_image_);
if (old_src != nullptr) {
lv_image_set_src(preview_image_, nullptr);
heap_caps_free((void*)old_src->data);
heap_caps_free((void*)old_src);
}
if (img_dsc != nullptr) {
// 设置图片源并显示预览图片
lv_image_set_src(preview_image_, img_dsc);
if (img_dsc->header.w > 0 && img_dsc->header.h > 0) {
// zoom factor 0.5
lv_image_set_scale(preview_image_, 128 * width_ / img_dsc->header.w);
}
// Hide emoji_box_
lv_obj_add_flag(emoji_box_, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
esp_timer_stop(preview_timer_);
ESP_ERROR_CHECK(esp_timer_start_once(preview_timer_, PREVIEW_IMAGE_DURATION_MS * 1000));
} else {
if (image == nullptr) {
esp_timer_stop(preview_timer_);
lv_obj_remove_flag(emoji_box_, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
preview_image_cached_.reset();
return;
}
preview_image_cached_ = std::move(image);
auto img_dsc = preview_image_cached_->image_dsc();
// 设置图片源并显示预览图片
lv_image_set_src(preview_image_, img_dsc);
if (img_dsc->header.w > 0 && img_dsc->header.h > 0) {
// zoom factor 0.5
lv_image_set_scale(preview_image_, 128 * width_ / img_dsc->header.w);
}
// Hide emoji_box_
lv_obj_add_flag(emoji_box_, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
esp_timer_stop(preview_timer_);
ESP_ERROR_CHECK(esp_timer_start_once(preview_timer_, PREVIEW_IMAGE_DURATION_MS * 1000));
}
void LcdDisplay::SetChatMessage(const char* role, const char* content) {
@@ -955,7 +962,8 @@ void LcdDisplay::SetEmotion(const char* emotion) {
#if CONFIG_USE_WECHAT_MESSAGE_STYLE
// Wechat message style中如果emotion是neutral则不显示
if (strcmp(emotion, "neutral") == 0) {
uint32_t child_count = lv_obj_get_child_cnt(content_);
if (strcmp(emotion, "neutral") == 0 && child_count > 0) {
// Stop GIF animation if running
if (gif_controller_) {
gif_controller_->Stop();
@@ -1101,33 +1109,3 @@ void LcdDisplay::SetTheme(Theme* theme) {
// No errors occurred. Save theme to settings
Display::SetTheme(lvgl_theme);
}
void LcdDisplay::SetMusicInfo(const char* song_name) {
#if CONFIG_USE_WECHAT_MESSAGE_STYLE
// 微信模式下不显示歌名,保持原有聊天功能
return;
#else
// 非微信模式:在表情下方显示歌名
DisplayLockGuard lock(this);
if (chat_message_label_ == nullptr) {
return;
}
if (song_name != nullptr && strlen(song_name) > 0) {
std::string music_text = "";
music_text += song_name;
lv_label_set_text(chat_message_label_, music_text.c_str());
// 确保显示 emotion_label_ 和 chat_message_label_隐藏 preview_image_
if (emotion_label_ != nullptr) {
lv_obj_clear_flag(emotion_label_, LV_OBJ_FLAG_HIDDEN);
}
if (preview_image_ != nullptr) {
lv_obj_add_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
}
} else {
// 清空歌名显示
lv_label_set_text(chat_message_label_, "");
}
#endif
}

View File

@@ -31,6 +31,7 @@ protected:
lv_obj_t* emoji_box_ = nullptr;
lv_obj_t* chat_message_label_ = nullptr;
esp_timer_handle_t preview_timer_ = nullptr;
std::unique_ptr<LvglImage> preview_image_cached_ = nullptr;
void InitializeLcdThemes();
void SetupUI();
@@ -44,14 +45,11 @@ protected:
public:
~LcdDisplay();
virtual void SetEmotion(const char* emotion) override;
virtual void SetPreviewImage(const lv_img_dsc_t* img_dsc) override;
virtual void SetChatMessage(const char* role, const char* content) override;
virtual void SetPreviewImage(std::unique_ptr<LvglImage> image) override;
// Add theme switching function
virtual void SetTheme(Theme* theme) override;
// Add set music info function
virtual void SetMusicInfo(const char* song_name) override;
};
// SPI LCD显示器

View File

@@ -3,6 +3,9 @@
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <esp_log.h>
#define TAG "GIF"
#define MIN(A, B) ((A) < (B) ? (A) : (B))
#define MAX(A, B) ((A) > (B) ? (A) : (B))
@@ -80,13 +83,13 @@ static gd_GIF * gif_open(gd_GIF * gif_base)
/* Header */
f_gif_read(gif_base, sigver, 3);
if(memcmp(sigver, "GIF", 3) != 0) {
LV_LOG_WARN("invalid signature");
ESP_LOGW(TAG, "invalid signature");
goto fail;
}
/* Version */
f_gif_read(gif_base, sigver, 3);
if(memcmp(sigver, "89a", 3) != 0) {
LV_LOG_WARN("invalid version");
if(memcmp(sigver, "89a", 3) != 0 && memcmp(sigver, "87a", 3) != 0) {
ESP_LOGW(TAG, "invalid version");
goto fail;
}
/* Width x Height */
@@ -96,7 +99,7 @@ static gd_GIF * gif_open(gd_GIF * gif_base)
f_gif_read(gif_base, &fdsz, 1);
/* Presence of GCT */
if(!(fdsz & 0x80)) {
LV_LOG_WARN("no global color table");
ESP_LOGW(TAG, "no global color table");
goto fail;
}
/* Color Space's Depth */
@@ -110,18 +113,18 @@ static gd_GIF * gif_open(gd_GIF * gif_base)
f_gif_read(gif_base, &aspect, 1);
/* Create gd_GIF Structure. */
if(0 == width || 0 == height){
LV_LOG_WARN("Zero size image");
ESP_LOGW(TAG, "Zero size image");
goto fail;
}
#if LV_GIF_CACHE_DECODE_DATA
if(0 == (INT_MAX - sizeof(gd_GIF) - LZW_CACHE_SIZE) / width / height / 5){
LV_LOG_WARN("Image dimensions are too large");
ESP_LOGW(TAG, "Image dimensions are too large");
goto fail;
}
gif = lv_malloc(sizeof(gd_GIF) + 5 * width * height + LZW_CACHE_SIZE);
#else
if(0 == (INT_MAX - sizeof(gd_GIF)) / width / height / 5){
LV_LOG_WARN("Image dimensions are too large");
ESP_LOGW(TAG, "Image dimensions are too large");
goto fail;
}
gif = lv_malloc(sizeof(gd_GIF) + 5 * width * height);
@@ -292,7 +295,7 @@ read_ext(gd_GIF * gif)
read_application_ext(gif);
break;
default:
LV_LOG_WARN("unknown extension: %02X\n", label);
ESP_LOGW(TAG, "unknown extension: %02X\n", label);
}
}
@@ -386,7 +389,7 @@ read_image_data(gd_GIF *gif, int interlace)
/* copy data to frame buffer */
while (sp > p_stack) {
if(frm_off >= frm_size){
LV_LOG_WARN("LZW table token overflows the frame buffer");
ESP_LOGW(TAG, "LZW table token overflows the frame buffer");
return -1;
}
*ptr++ = *(--sp);
@@ -593,7 +596,7 @@ read_image_data(gd_GIF * gif, int interlace)
entry = table->entries[key];
str_len = entry.length;
if(frm_off + str_len > frm_size){
LV_LOG_WARN("LZW table token overflows the frame buffer");
ESP_LOGW(TAG, "LZW table token overflows the frame buffer");
lv_free(table);
return -1;
}
@@ -635,7 +638,7 @@ read_image(gd_GIF * gif)
gif->fw = read_num(gif);
gif->fh = read_num(gif);
if(gif->fx + (uint32_t)gif->fw > gif->width || gif->fy + (uint32_t)gif->fh > gif->height){
LV_LOG_WARN("Frame coordinates out of image bounds");
ESP_LOGW(TAG, "Frame coordinates out of image bounds");
return -1;
}
f_gif_read(gif, &fisrz, 1);

View File

@@ -14,6 +14,7 @@ LvglGif::LvglGif(const lv_img_dsc_t* img_dsc)
gif_ = gd_open_gif_data(img_dsc->data);
if (!gif_) {
ESP_LOGE(TAG, "Failed to open GIF from image descriptor");
return;
}
// Setup LVGL image descriptor
@@ -33,7 +34,7 @@ LvglGif::LvglGif(const lv_img_dsc_t* img_dsc)
}
loaded_ = true;
ESP_LOGI(TAG, "GIF loaded from image descriptor: %dx%d", gif_->width, gif_->height);
ESP_LOGD(TAG, "GIF loaded from image descriptor: %dx%d", gif_->width, gif_->height);
}
// Destructor
@@ -72,7 +73,7 @@ void LvglGif::Start() {
// Render first frame
NextFrame();
ESP_LOGI(TAG, "GIF animation started");
ESP_LOGD(TAG, "GIF animation started");
}
}
@@ -80,7 +81,7 @@ void LvglGif::Pause() {
if (timer_) {
playing_ = false;
lv_timer_pause(timer_);
ESP_LOGI(TAG, "GIF animation paused");
ESP_LOGD(TAG, "GIF animation paused");
}
}
@@ -93,7 +94,7 @@ void LvglGif::Resume() {
if (timer_) {
playing_ = true;
lv_timer_resume(timer_);
ESP_LOGI(TAG, "GIF animation resumed");
ESP_LOGD(TAG, "GIF animation resumed");
}
}
@@ -106,7 +107,7 @@ void LvglGif::Stop() {
if (gif_) {
gd_rewind(gif_);
NextFrame();
ESP_LOGI(TAG, "GIF animation stopped and rewound");
ESP_LOGD(TAG, "GIF animation stopped and rewound");
}
}
@@ -172,7 +173,7 @@ void LvglGif::NextFrame() {
if (timer_) {
lv_timer_pause(timer_);
}
ESP_LOGI(TAG, "GIF animation completed");
ESP_LOGD(TAG, "GIF animation completed");
}
// Render current frame

View File

@@ -4,6 +4,7 @@
#include <cstdlib>
#include <cstring>
#include <font_awesome.h>
#include <img_converters.h>
#include "lvgl_display.h"
#include "board.h"
@@ -201,12 +202,7 @@ void LvglDisplay::UpdateStatusBar(bool update_all) {
esp_pm_lock_release(pm_lock_);
}
void LvglDisplay::SetPreviewImage(const lv_img_dsc_t* image) {
// Do nothing but free the image
if (image != nullptr) {
heap_caps_free((void*)image->data);
heap_caps_free((void*)image);
}
void LvglDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
}
void LvglDisplay::SetPowerSaveMode(bool on) {
@@ -218,3 +214,29 @@ void LvglDisplay::SetPowerSaveMode(bool on) {
SetEmotion("neutral");
}
}
bool LvglDisplay::SnapshotToJpeg(uint8_t*& jpeg_output_data, size_t& jpeg_output_data_size, int quality) {
DisplayLockGuard lock(this);
lv_obj_t* screen = lv_screen_active();
lv_draw_buf_t* draw_buffer = lv_snapshot_take(screen, LV_COLOR_FORMAT_RGB565);
if (draw_buffer == nullptr) {
return false;
}
// swap bytes
uint16_t* data = (uint16_t*)draw_buffer->data;
size_t pixel_count = draw_buffer->data_size / 2;
for (size_t i = 0; i < pixel_count; i++) {
data[i] = __builtin_bswap16(data[i]);
}
if (!fmt2jpg(draw_buffer->data, draw_buffer->data_size, draw_buffer->header.w, draw_buffer->header.h,
PIXFORMAT_RGB565, quality, &jpeg_output_data, &jpeg_output_data_size)) {
lv_draw_buf_destroy(draw_buffer);
return false;
}
lv_draw_buf_destroy(draw_buffer);
return true;
}

View File

@@ -2,6 +2,7 @@
#define LVGL_DISPLAY_H
#include "display.h"
#include "lvgl_image.h"
#include <lvgl.h>
#include <esp_timer.h>
@@ -19,9 +20,10 @@ public:
virtual void SetStatus(const char* status);
virtual void ShowNotification(const char* notification, int duration_ms = 3000);
virtual void ShowNotification(const std::string &notification, int duration_ms = 3000);
virtual void SetPreviewImage(const lv_img_dsc_t* image);
virtual void SetPreviewImage(std::unique_ptr<LvglImage> image);
virtual void UpdateStatusBar(bool update_all = false);
virtual void SetPowerSaveMode(bool on);
virtual bool SnapshotToJpeg(uint8_t*& jpeg_output_data, size_t& jpeg_output_size, int quality = 80);
protected:
esp_pm_lock_handle_t pm_lock_ = nullptr;

View File

@@ -2,19 +2,21 @@
#include <cbin_font.h>
#include <esp_log.h>
#include <stdexcept>
#include <cstring>
#include <esp_heap_caps.h>
#define TAG "LvglImage"
LvglRawImage::LvglRawImage(void* data, size_t size) {
bzero(&image_dsc_, sizeof(image_dsc_));
image_dsc_.data_size = size;
image_dsc_.data = static_cast<uint8_t*>(data);
image_dsc_.header.magic = LV_IMAGE_HEADER_MAGIC;
image_dsc_.header.cf = LV_COLOR_FORMAT_RAW_ALPHA;
image_dsc_.header.w = 0;
image_dsc_.header.h = 0;
image_dsc_.data_size = size;
image_dsc_.data = static_cast<uint8_t*>(data);
}
bool LvglRawImage::IsGif() const {
@@ -31,3 +33,32 @@ LvglCBinImage::~LvglCBinImage() {
cbin_img_dsc_delete(image_dsc_);
}
}
LvglAllocatedImage::LvglAllocatedImage(void* data, size_t size) {
bzero(&image_dsc_, sizeof(image_dsc_));
image_dsc_.data_size = size;
image_dsc_.data = static_cast<uint8_t*>(data);
if (lv_image_decoder_get_info(&image_dsc_, &image_dsc_.header) != LV_RESULT_OK) {
ESP_LOGE(TAG, "Failed to get image info, data: %p size: %u", data, size);
throw std::runtime_error("Failed to get image info");
}
}
LvglAllocatedImage::LvglAllocatedImage(void* data, size_t size, int width, int height, int stride, int color_format) {
bzero(&image_dsc_, sizeof(image_dsc_));
image_dsc_.data_size = size;
image_dsc_.data = static_cast<uint8_t*>(data);
image_dsc_.header.magic = LV_IMAGE_HEADER_MAGIC;
image_dsc_.header.cf = color_format;
image_dsc_.header.w = width;
image_dsc_.header.h = height;
image_dsc_.header.stride = stride;
}
LvglAllocatedImage::~LvglAllocatedImage() {
if (image_dsc_.data) {
heap_caps_free((void*)image_dsc_.data);
image_dsc_.data = nullptr;
}
}

View File

@@ -39,4 +39,15 @@ public:
private:
const lv_img_dsc_t* image_dsc_;
};
class LvglAllocatedImage : public LvglImage {
public:
LvglAllocatedImage(void* data, size_t size);
LvglAllocatedImage(void* data, size_t size, int width, int height, int stride, int color_format);
virtual ~LvglAllocatedImage();
virtual const lv_img_dsc_t* image_dsc() const override { return &image_dsc_; }
private:
lv_img_dsc_t image_dsc_;
};

View File

@@ -40,6 +40,9 @@ OledDisplay::OledDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handl
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
port_cfg.task_priority = 1;
port_cfg.task_stack = 6144;
#if CONFIG_SOC_CPU_CORES_NUM > 1
port_cfg.task_affinity = 1;
#endif
lvgl_port_init(&port_cfg);
ESP_LOGI(TAG, "Adding OLED display");

View File

@@ -15,7 +15,7 @@ dependencies:
78/esp_lcd_nv3023: ~1.0.0
78/esp-wifi-connect: ~2.5.2
78/esp-opus-encoder: ~2.4.1
78/esp-ml307: ~3.3.3
78/esp-ml307: ~3.3.5
78/xiaozhi-fonts: ~1.5.2
espressif/led_strip: ~3.0.1
espressif/esp_codec_dev: ~1.4.0

View File

@@ -4,6 +4,8 @@
#include <nvs_flash.h>
#include <driver/gpio.h>
#include <esp_event.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "application.h"
#include "system_info.h"
@@ -27,5 +29,4 @@ extern "C" void app_main(void)
// Launch the application
auto& app = Application::GetInstance();
app.Start();
app.MainEventLoop();
}

View File

@@ -19,9 +19,10 @@
#include "lvgl_theme.h"
#include "lvgl_display.h"
#define TAG "MCP"
#include "schedule_manager.h"
#include "timer_manager.h"
#define DEFAULT_TOOLCALL_STACK_SIZE 6144
#define TAG "MCP"
McpServer::McpServer() {
}
@@ -54,17 +55,6 @@ void McpServer::AddCommonTools() {
[&board](const PropertyList& properties) -> ReturnValue {
return board.GetDeviceStatusJson();
});
AddTool("self.search_music",
"Transfer to music playback tool.\n"
"Use this tool for: \n"
"1. When a user sends a music playback request.\n"
"2. When music playback related parameters are not configured in the character introduction.\n",
PropertyList(),
[&board](const PropertyList &properties) -> ReturnValue {
ESP_LOGW(TAG, "Use self.music.play_song tool to play music.");
return "Please use MPC Tool self.music.play_song tool to play music.";
});
AddTool("self.audio_speaker.set_volume",
"Set the volume of the audio speaker. If the current volume is unknown, you must call `self.get_device_status` tool first and then call this tool.",
@@ -123,6 +113,9 @@ void McpServer::AddCommonTools() {
Property("question", kPropertyTypeString)
}),
[camera](const PropertyList& properties) -> ReturnValue {
// Lower the priority to do the camera capture
TaskPriorityReset priority_reset(1);
if (!camera->Capture()) {
throw std::runtime_error("Failed to capture photo");
}
@@ -131,73 +124,104 @@ void McpServer::AddCommonTools() {
});
}
#endif
auto music = board.GetMusic();
if (music)
{
if (music) {
AddTool("self.music.play_song",
"Play the specified song. When users request to play music, this tool will automatically retrieve song details and start streaming.\n"
"parameter:\n"
" `song_name`: The name of the song to be played.\n"
"return:\n"
" Play status information without confirmation, immediately play the song.",
PropertyList({Property("song_name", kPropertyTypeString)}),
[music](const PropertyList &properties) -> ReturnValue
{
auto song_name = properties["song_name"].value<std::string>();
if (!music->Download(song_name))
{
return "{\"success\": false, \"message\": \"Failed to obtain music resources\"}";
}
auto download_result = music->GetDownloadResult();
ESP_LOGD(TAG, "Music details result: %s", download_result.c_str());
return true;
});
"Play the specified song. When users request to play music, this tool will automatically retrieve song details and start streaming.\n"
"parameter:\n"
" `song_name`: The name of the song to be played.\n"
"return:\n"
" Play status information without confirmation, immediately play the song.",
PropertyList({Property("song_name", kPropertyTypeString)}),
[music](const PropertyList &properties) -> ReturnValue {
auto song_name = properties["song_name"].value<std::string>();
if (!music->Download(song_name)) {
return "{\"success\": false, \"message\": \"Failed to obtain music resources\"}";
}
auto download_result = music->GetDownloadResult();
ESP_LOGD(TAG, "Music details result: %s", download_result.c_str());
return true;
});
AddTool("self.music.set_volume",
"Set music volume (0-100).",
PropertyList({Property("volume", kPropertyTypeInteger, 0, 100)}),
[music](const PropertyList &properties) -> ReturnValue {
int vol = properties["volume"].value<int>();
bool ok = music->SetVolume(vol);
return ok;
});
AddTool("self.music.play",
"Play current music.",
PropertyList(),
[music](const PropertyList &properties) -> ReturnValue {
bool ok = music->PlaySong();
return ok;
});
// 兼容更明确的命名stop_song / pause_song / resume_song
AddTool("self.music.stop_song",
"Stop current song.",
PropertyList(),
[music](const PropertyList &properties) -> ReturnValue {
bool ok = music->StopSong();
return ok;
});
AddTool("self.music.pause_song",
"Pause current song.",
PropertyList(),
[music](const PropertyList &properties) -> ReturnValue {
bool ok = music->PauseSong();
return ok;
});
AddTool("self.music.resume_song",
"Resume current song.",
PropertyList(),
[music](const PropertyList &properties) -> ReturnValue {
bool ok = music->ResumeSong();
return ok;
});
AddTool("self.music.set_display_mode",
"Set the display mode for music playback. You can choose to display the spectrum or lyrics, for example, if the user says' open spectrum 'or' display spectrum ', the corresponding display mode will be set for' open lyrics' or 'display lyrics'.\n"
"parameter:\n"
" `mode`: Display mode, with optional values of 'spectrum' or 'lyrics'.\n"
"return:\n"
" Set result information.",
PropertyList({
Property("mode", kPropertyTypeString) // Display mode: "spectrum" or "lyrics"
}),
[music](const PropertyList &properties) -> ReturnValue
{
auto mode_str = properties["mode"].value<std::string>();
"Set the display mode for music playback. You can choose to display the spectrum or lyrics, for example, if the user says' open spectrum 'or' display spectrum ', the corresponding display mode will be set for' open lyrics' or 'display lyrics'.\n"
"parameter:\n"
" `mode`: Display mode, with optional values of 'spectrum' or 'lyrics'.\n"
"return:\n"
" Set result information.",
PropertyList({
Property("mode", kPropertyTypeString) // Display mode: "spectrum" or "lyrics"
}),
[music](const PropertyList &properties) -> ReturnValue {
auto mode_str = properties["mode"].value<std::string>();
// Convert to lowercase for comparison
std::transform(mode_str.begin(), mode_str.end(), mode_str.begin(), ::tolower);
// Convert to lowercase for comparison
std::transform(mode_str.begin(), mode_str.end(), mode_str.begin(), ::tolower);
if (mode_str == "spectrum" || mode_str == "频谱")
{
// Set to spectrum display mode
auto esp32_music = static_cast<Esp32Music *>(music);
if (mode_str == "spectrum" || mode_str == "频谱") {
// Set to spectrum display mode
auto esp32_music = static_cast<Esp32Music *>(music);
esp32_music->SetDisplayMode(Esp32Music::DISPLAY_MODE_SPECTRUM);
return "{\"success\": true, \"message\": \"Switched to spectrum display mode\"}";
}
else if (mode_str == "lyrics" || mode_str == "歌词")
{
// Set to lyrics display mode
auto esp32_music = static_cast<Esp32Music *>(music);
esp32_music->SetDisplayMode(Esp32Music::DISPLAY_MODE_LYRICS);
return "{\"success\": true, \"message\": \"Switched to lyrics display mode\"}";
}
else
{
return "{\"success\": false, \"message\": \"Invalid display mode, please use 'spectrum' or 'lyrics'\"}";
}
return "{\"success\": true, \"message\": \"Switched to spectrum display mode\"}";
} else if (mode_str == "lyrics" || mode_str == "歌词") {
// Set to lyrics display mode
auto esp32_music = static_cast<Esp32Music *>(music);
esp32_music->SetDisplayMode(Esp32Music::DISPLAY_MODE_LYRICS);
return "{\"success\": true, \"message\": \"Switched to lyrics display mode\"}";
} else {
return "{\"success\": false, \"message\": \"Invalid display mode, please use 'spectrum' or 'lyrics'\"}";
}
return "{\"success\": false, \"message\": \"Failed to set display mode\"}";
});
return "{\"success\": false, \"message\": \"Failed to set display mode\"}";
});
}
// Restore the original tools list to the end of the tools list
tools_.insert(tools_.end(), original_tools.begin(), original_tools.end());
}
void McpServer::AddUserOnlyTools() {
// System tools
AddUserOnlyTool("self.get_system_info",
@@ -212,10 +236,10 @@ void McpServer::AddUserOnlyTools() {
PropertyList(),
[this](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
app.Schedule([]() {
app.Schedule([&app]() {
ESP_LOGW(TAG, "User requested reboot");
vTaskDelay(pdMS_TO_TICKS(1000));
auto& app = Application::GetInstance();
app.Reboot();
});
return true;
@@ -231,8 +255,7 @@ void McpServer::AddUserOnlyTools() {
ESP_LOGI(TAG, "User requested firmware upgrade from URL: %s", url.c_str());
auto& app = Application::GetInstance();
app.Schedule([url]() {
auto& app = Application::GetInstance();
app.Schedule([url, &app]() {
auto ota = std::make_unique<Ota>();
bool success = app.UpgradeFirmware(*ota, url);
@@ -246,7 +269,7 @@ void McpServer::AddUserOnlyTools() {
// Display control
#ifdef HAVE_LVGL
auto display = static_cast<LvglDisplay*>(Board::GetInstance().GetDisplay());
auto display = dynamic_cast<LvglDisplay*>(Board::GetInstance().GetDisplay());
if (display) {
AddUserOnlyTool("self.screen.get_info", "Information about the screen, including width, height, etc.",
PropertyList(),
@@ -254,13 +277,70 @@ void McpServer::AddUserOnlyTools() {
cJSON *json = cJSON_CreateObject();
cJSON_AddNumberToObject(json, "width", display->width());
cJSON_AddNumberToObject(json, "height", display->height());
if (static_cast<OledDisplay*>(display)) {
if (dynamic_cast<OledDisplay*>(display)) {
cJSON_AddBoolToObject(json, "monochrome", true);
} else {
cJSON_AddBoolToObject(json, "monochrome", false);
}
return json;
});
AddUserOnlyTool("self.screen.snapshot", "Snapshot the screen and upload it to a specific URL",
PropertyList({
Property("url", kPropertyTypeString),
Property("quality", kPropertyTypeInteger, 80, 1, 100)
}),
[display](const PropertyList& properties) -> ReturnValue {
auto url = properties["url"].value<std::string>();
auto quality = properties["quality"].value<int>();
uint8_t* jpeg_output_data = nullptr;
size_t jpeg_output_size = 0;
if (!display->SnapshotToJpeg(jpeg_output_data, jpeg_output_size, quality)) {
throw std::runtime_error("Failed to snapshot screen");
}
ESP_LOGI(TAG, "Upload snapshot %u bytes to %s", jpeg_output_size, url.c_str());
// 构造multipart/form-data请求体
std::string boundary = "----ESP32_SCREEN_SNAPSHOT_BOUNDARY";
auto http = Board::GetInstance().GetNetwork()->CreateHttp(3);
http->SetHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
if (!http->Open("POST", url)) {
free(jpeg_output_data);
throw std::runtime_error("Failed to open URL: " + url);
}
{
// 文件字段头部
std::string file_header;
file_header += "--" + boundary + "\r\n";
file_header += "Content-Disposition: form-data; name=\"file\"; filename=\"screenshot.jpg\"\r\n";
file_header += "Content-Type: image/jpeg\r\n";
file_header += "\r\n";
http->Write(file_header.c_str(), file_header.size());
}
// JPEG数据
http->Write((const char*)jpeg_output_data, jpeg_output_size);
free(jpeg_output_data);
{
// multipart尾部
std::string multipart_footer;
multipart_footer += "\r\n--" + boundary + "--\r\n";
http->Write(multipart_footer.c_str(), multipart_footer.size());
}
http->Write("", 0);
if (http->GetStatusCode() != 200) {
throw std::runtime_error("Unexpected status code: " + std::to_string(http->GetStatusCode()));
}
std::string result = http->ReadAll();
http->Close();
ESP_LOGI(TAG, "Snapshot screen result: %s", result.c_str());
return true;
});
AddUserOnlyTool("self.screen.preview_image", "Preview an image on the screen",
PropertyList({
@@ -273,12 +353,16 @@ void McpServer::AddUserOnlyTools() {
if (!http->Open("GET", url)) {
throw std::runtime_error("Failed to open URL: " + url);
}
if (http->GetStatusCode() != 200) {
throw std::runtime_error("Unexpected status code: " + std::to_string(http->GetStatusCode()));
int status_code = http->GetStatusCode();
if (status_code != 200) {
throw std::runtime_error("Unexpected status code: " + std::to_string(status_code));
}
size_t content_length = http->GetBodyLength();
char* data = (char*)heap_caps_malloc(content_length, MALLOC_CAP_8BIT);
if (data == nullptr) {
throw std::runtime_error("Failed to allocate memory for image: " + url);
}
size_t total_read = 0;
while (total_read < content_length) {
int ret = http->Read(data + total_read, content_length - total_read);
@@ -286,24 +370,15 @@ void McpServer::AddUserOnlyTools() {
heap_caps_free(data);
throw std::runtime_error("Failed to download image: " + url);
}
if (ret == 0) {
break;
}
total_read += ret;
}
http->Close();
auto img_dsc = (lv_img_dsc_t*)heap_caps_calloc(1, sizeof(lv_img_dsc_t), MALLOC_CAP_8BIT);
img_dsc->data_size = content_length;
img_dsc->data = (uint8_t*)data;
if (lv_image_decoder_get_info(img_dsc, &img_dsc->header) != LV_RESULT_OK) {
heap_caps_free(data);
heap_caps_free(img_dsc);
throw std::runtime_error("Failed to get image info");
}
ESP_LOGI(TAG, "Preview image: %s size: %d resolution: %d x %d", url.c_str(), content_length, img_dsc->header.w, img_dsc->header.h);
auto& app = Application::GetInstance();
app.Schedule([display, img_dsc]() {
display->SetPreviewImage(img_dsc);
});
auto image = std::make_unique<LvglAllocatedImage>(data, content_length);
display->SetPreviewImage(std::move(image));
return true;
});
}
@@ -325,6 +400,206 @@ void McpServer::AddUserOnlyTools() {
});
}
}
// 日程管理工具
auto &schedule_manager = ScheduleManager::GetInstance();
AddTool("self.schedule.create_event",
"Create a new schedule event. Support intelligent classification and reminder functions.\n"
"parameter:\n"
" `title`: Event title (required)\n"
" `description`: Event description (optional)\n"
" `start_time`: Start timestamp (required)\n"
" `end_time`: End timestamp (optional, 0 means no end time)\n"
" `category`: Event category (optional, if not provided, it will be automatically classified)\n"
" `is_all_day`: Whether it is an all-day event (optional, default false)\n"
" `reminder_minutes`: Reminder time (optional, default 15 minutes)\n"
"Return:\n"
" Event ID string, used for subsequent operations",
PropertyList({Property("title", kPropertyTypeString),
Property("description", kPropertyTypeString, ""),
Property("start_time", kPropertyTypeInteger),
Property("end_time", kPropertyTypeInteger, 0),
Property("category", kPropertyTypeString, ""),
Property("is_all_day", kPropertyTypeBoolean, false),
Property("reminder_minutes", kPropertyTypeInteger, 15, 0, 1440)}),
[&schedule_manager](const PropertyList &properties) -> ReturnValue {
auto title = properties["title"].value<std::string>();
auto description = properties["description"].value<std::string>();
time_t start_time = properties["start_time"].value<int>();
time_t end_time = properties["end_time"].value<int>();
auto category = properties["category"].value<std::string>();
bool is_all_day = properties["is_all_day"].value<bool>();
int reminder_minutes = properties["reminder_minutes"].value<int>();
std::string event_id = schedule_manager.CreateEvent(title, description, start_time, end_time,category, is_all_day, reminder_minutes);
if (event_id.empty())
{
return "{\"success\": false, \"message\": \"Event creation failed\"}";
}
return "{\"success\": true, \"event_id\": \"" + event_id + "\", \"message\": \"Event created successfully\"}";
});
AddTool("self.schedule.get_events",
"Get all schedule events.\n"
"Return:\n"
" JSON array of event objects",
PropertyList(),
[&schedule_manager](const PropertyList &properties) -> ReturnValue {
std::string json_str = schedule_manager.ExportToJson();
return json_str;
});
AddTool("self.schedule.delete_event",
"Delete a schedule event.\n"
"Parameter:\n"
" `event_id`: ID of the event to delete (required)\n"
"Return:\n"
" Operation result",
PropertyList({Property("event_id", kPropertyTypeString)}),
[&schedule_manager](const PropertyList &properties) -> ReturnValue {
auto event_id = properties["event_id"].value<std::string>();
bool success = schedule_manager.DeleteEvent(event_id);
if (success) {
return "{\"success\": true, \"message\": \"Event deleted successfully\"}";
} else {
return "{\"success\": false, \"message\": \"Event deletion failed\"}";
}
});
AddTool("self.schedule.get_statistics",
"Obtain schedule statistics information.\n"
"Return:\n"
" JSON object of statistics information",
PropertyList(),
[&schedule_manager](const PropertyList &properties) -> ReturnValue {
int total_events = schedule_manager.GetEventCount();
cJSON *json = cJSON_CreateObject();
cJSON_AddNumberToObject(json, "total_events", total_events);
cJSON_AddBoolToObject(json, "success", true);
return json;
});
// 定时任务工具
auto &timer_manager = TimerManager::GetInstance();
AddTool("self.timer.create_countdown",
"Create a countdown timer.\n"
"Return:\n"
" `name`: Timer name (required)\n"
" `duration_ms`: Duration in milliseconds (required)\n"
" `description`: Description (optional)\n"
"Return:\n"
" Timer ID",
PropertyList({Property("name", kPropertyTypeString),
Property("duration_ms", kPropertyTypeInteger, 1000, 100, 3600000),
Property("description", kPropertyTypeString, "")}),
[&timer_manager](const PropertyList &properties) -> ReturnValue {
auto name = properties["name"].value<std::string>();
uint32_t duration_ms = properties["duration_ms"].value<int>();
auto description = properties["description"].value<std::string>();
std::string timer_id = timer_manager.CreateCountdownTimer(name, duration_ms, description);
return "{\"success\": true, \"timer_id\": \"" + timer_id + "\", \"message\": \"Countdown timer successfully created\"}";
});
AddTool("self.timer.create_delayed_task",
"Create a task for delaying the execution of MCP tools.\n"
"Parameter:\n"
" `name`: Task name (required)\n"
" `delay_ms`: Delay time in milliseconds (required)\n"
" `mcp_tool_name`: MCP tool name (required)\n"
" `mcp_tool_args`: MCP tool arguments (optional)\n"
" `description`: Description (optional)\n"
"Return:\n"
" Task ID",
PropertyList({Property("name", kPropertyTypeString),
Property("delay_ms", kPropertyTypeInteger, 1000, 100, 3600000),
Property("mcp_tool_name", kPropertyTypeString),
Property("mcp_tool_args", kPropertyTypeString, ""),
Property("description", kPropertyTypeString, "")}),
[&timer_manager](const PropertyList &properties) -> ReturnValue
{
auto name = properties["name"].value<std::string>();
uint32_t delay_ms = properties["delay_ms"].value<int>();
auto mcp_tool_name = properties["mcp_tool_name"].value<std::string>();
auto mcp_tool_args = properties["mcp_tool_args"].value<std::string>();
auto description = properties["description"].value<std::string>();
std::string task_id = timer_manager.CreateDelayedMcpTask(
name, delay_ms, mcp_tool_name, mcp_tool_args, description);
return "{\"success\": true, \"task_id\": \"" + task_id + "\", \"message\": \"Delay task created successfully\"}";
});
AddTool("self.timer.start_task",
"Start scheduled tasks.\n"
"Parameter:\n"
" `task_id`: Task ID (required)\n"
"Return:\n"
" Operation result",
PropertyList({Property("task_id", kPropertyTypeString)}),
[&timer_manager](const PropertyList &properties) -> ReturnValue {
auto task_id = properties["task_id"].value<std::string>();
bool success = timer_manager.StartTask(task_id);
if (success) {
return "{\"success\": true, \"message\": \"Task started successfully\"}";
} else {
return "{\"success\": false, \"message\": \"Task start failed\"}";
}
});
AddTool("self.timer.stop_task",
"Stop scheduled tasks.\n"
"Parameter:\n"
" `task_id`: Task ID (Required)\n"
"Return:\n"
" Operation result",
PropertyList({Property("task_id", kPropertyTypeString)}),
[&timer_manager](const PropertyList &properties) -> ReturnValue {
auto task_id = properties["task_id"].value<std::string>();
bool success = timer_manager.StopTask(task_id);
if (success) {
return "{\"success\": true, \"message\": \"Task stopped successfully\"}";
} else {
return "{\"success\": false, \"message\": \"Task stop failed\"}";
}
});
AddTool("self.timer.get_tasks",
"Get all scheduled tasks.\n"
"Return:\n"
" JSON array of task objects",
PropertyList(),
[&timer_manager](const PropertyList &properties) -> ReturnValue {
std::string json_str = timer_manager.ExportToJson();
return json_str;
});
AddTool("self.timer.get_statistics",
"Obtain timer statistics information.\n"
"Return:\n"
" JSON object of statistics information",
PropertyList(),
[&timer_manager](const PropertyList &properties) -> ReturnValue {
int total_tasks = timer_manager.GetTaskCount();
cJSON *json = cJSON_CreateObject();
cJSON_AddNumberToObject(json, "total_tasks", total_tasks);
cJSON_AddBoolToObject(json, "success", true);
return json;
});
}
void McpServer::AddTool(McpTool* tool) {
@@ -455,13 +730,7 @@ void McpServer::ParseMessage(const cJSON* json) {
ReplyError(id_int, "Invalid arguments");
return;
}
auto stack_size = cJSON_GetObjectItem(params, "stackSize");
if (stack_size != nullptr && !cJSON_IsNumber(stack_size)) {
ESP_LOGE(TAG, "tools/call: Invalid stackSize");
ReplyError(id_int, "Invalid stackSize");
return;
}
DoToolCall(id_int, std::string(tool_name->valuestring), tool_arguments, stack_size ? stack_size->valueint : DEFAULT_TOOLCALL_STACK_SIZE);
DoToolCall(id_int, std::string(tool_name->valuestring), tool_arguments);
} else {
ESP_LOGE(TAG, "Method not implemented: %s", method_str.c_str());
ReplyError(id_int, "Method not implemented: " + method_str);
@@ -541,7 +810,7 @@ void McpServer::GetToolsList(int id, const std::string& cursor, bool list_user_o
ReplyResult(id, json);
}
void McpServer::DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments, int stack_size) {
void McpServer::DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments) {
auto tool_iter = std::find_if(tools_.begin(), tools_.end(),
[&tool_name](const McpTool* tool) {
return tool->name() == tool_name;
@@ -583,15 +852,9 @@ void McpServer::DoToolCall(int id, const std::string& tool_name, const cJSON* to
return;
}
// Start a task to receive data with stack size
esp_pthread_cfg_t cfg = esp_pthread_get_default_config();
cfg.thread_name = "tool_call";
cfg.stack_size = stack_size;
cfg.prio = 1;
esp_pthread_set_cfg(&cfg);
// Use a thread to call the tool to avoid blocking the main thread
tool_call_thread_ = std::thread([this, id, tool_iter, arguments = std::move(arguments)]() {
// Use main thread to call the tool
auto& app = Application::GetInstance();
app.Schedule([this, id, tool_iter, arguments = std::move(arguments)]() {
try {
ReplyResult(id, (*tool_iter)->Call(arguments));
} catch (const std::exception& e) {
@@ -599,5 +862,4 @@ void McpServer::DoToolCall(int id, const std::string& tool_name, const cJSON* to
ReplyError(id, e.what());
}
});
tool_call_thread_.detach();
}

View File

@@ -336,10 +336,9 @@ private:
void ReplyError(int id, const std::string& message);
void GetToolsList(int id, const std::string& cursor, bool list_user_only_tools);
void DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments, int stack_size);
void DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments);
std::vector<McpTool*> tools_;
std::thread tool_call_thread_;
};
#endif // MCP_SERVER_H

View File

@@ -0,0 +1,105 @@
#include "sleep_music_protocol.h"
#include "board.h"
#include "application.h"
#include "protocol.h"
#include <cstring>
#include <esp_log.h>
#define TAG "SleepMusic"
SleepMusicProtocol& SleepMusicProtocol::GetInstance() {
static SleepMusicProtocol instance;
return instance;
}
SleepMusicProtocol::SleepMusicProtocol() {
event_group_handle_ = xEventGroupCreate();
}
SleepMusicProtocol::~SleepMusicProtocol() {
vEventGroupDelete(event_group_handle_);
}
bool SleepMusicProtocol::IsAudioChannelOpened() const {
return is_connected_ && websocket_ != nullptr && websocket_->IsConnected();
}
void SleepMusicProtocol::CloseAudioChannel() {
if (websocket_) {
ESP_LOGI(TAG, "Closing sleep music audio channel");
// 清理状态
is_connected_ = false;
// 关闭WebSocket连接
websocket_.reset();
ESP_LOGI(TAG, "Sleep music audio channel closed");
}
}
bool SleepMusicProtocol::OpenAudioChannel() {
std::string url = "ws://180.76.190.230:8765";
ESP_LOGI(TAG, "Connecting to sleep music server: %s", url.c_str());
auto network = Board::GetInstance().GetNetwork();
websocket_ = network->CreateWebSocket(2); // 使用不同的WebSocket实例ID
if (websocket_ == nullptr) {
ESP_LOGE(TAG, "Failed to create websocket for sleep music");
return false;
}
// 设置WebSocket数据接收回调
websocket_->OnData([this](const char* data, size_t len, bool binary) {
if (binary) {
// 接收到的二进制数据是OPUS编码的音频帧
OnAudioDataReceived(data, len);
} else {
ESP_LOGW(TAG, "Received non-binary data from sleep music server, ignoring");
}
});
websocket_->OnDisconnected([this]() {
ESP_LOGI(TAG, "Sleep music websocket disconnected");
});
// 连接到睡眠音乐服务器
if (!websocket_->Connect(url.c_str())) {
ESP_LOGE(TAG, "Failed to connect to sleep music server");
return false;
}
// 设置连接成功事件
xEventGroupSetBits(event_group_handle_, SLEEP_MUSIC_PROTOCOL_CONNECTED_EVENT);
ESP_LOGI(TAG, "Successfully connected to sleep music server");
is_connected_ = true;
return true;
}
void SleepMusicProtocol::OnAudioDataReceived(const char* data, size_t len) {
if (len == 0) {
ESP_LOGW(TAG, "Received empty audio data");
return;
}
ESP_LOGD(TAG, "Received audio frame: %zu bytes", len);
// 创建AudioStreamPacket
auto packet = std::make_unique<AudioStreamPacket>();
packet->sample_rate = SAMPLE_RATE;
packet->frame_duration = FRAME_DURATION_MS;
packet->timestamp = 0; // 睡眠音乐不需要时间戳同步
packet->payload.resize(len);
std::memcpy(packet->payload.data(), data, len);
// 将音频包推入解码队列
auto& app = Application::GetInstance();
auto& audio_service = app.GetAudioService();
if (!audio_service.PushPacketToDecodeQueue(std::move(packet), false)) {
ESP_LOGW(TAG, "Audio decode queue is full, dropping packet");
}
}

View File

@@ -0,0 +1,35 @@
#ifndef _SLEEP_MUSIC_PROTOCOL_H_
#define _SLEEP_MUSIC_PROTOCOL_H_
#include <web_socket.h>
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <memory>
#define SLEEP_MUSIC_PROTOCOL_CONNECTED_EVENT (1 << 0)
class SleepMusicProtocol {
public:
static SleepMusicProtocol& GetInstance();
bool OpenAudioChannel();
void CloseAudioChannel();
bool IsAudioChannelOpened() const;
private:
SleepMusicProtocol();
~SleepMusicProtocol();
EventGroupHandle_t event_group_handle_;
std::unique_ptr<WebSocket> websocket_;
bool is_connected_ = false;
// 睡眠音乐服务器配置
static constexpr int SAMPLE_RATE = 24000; // 24kHz
static constexpr int CHANNELS = 2; // 立体声
static constexpr int FRAME_DURATION_MS = 60; // 60ms帧时长
void OnAudioDataReceived(const char* data, size_t len);
};
#endif

409
main/schedule_manager.cc Normal file
View File

@@ -0,0 +1,409 @@
#include "schedule_manager.h"
#include <esp_log.h>
#include <esp_timer.h>
#include <cstring>
#include <algorithm>
#include <sstream>
const char* ScheduleManager::TAG = "ScheduleManager";
ScheduleManager::ScheduleManager() {
ESP_LOGI(TAG, "ScheduleManager initialized");
}
ScheduleManager::~ScheduleManager() {
ESP_LOGI(TAG, "ScheduleManager destroyed");
}
std::string ScheduleManager::CreateEvent(const std::string& title,
const std::string& description,
time_t start_time,
time_t end_time,
const std::string& category,
bool is_all_day,
int reminder_minutes) {
std::lock_guard<std::mutex> lock(events_mutex_);
if (title.empty()) {
ESP_LOGE(TAG, "Event title cannot be empty");
return "";
}
if (!IsEventTimeValid(start_time, end_time)) {
ESP_LOGE(TAG, "Invalid event time");
return "";
}
ScheduleEvent event;
event.id = GenerateEventId();
event.title = title;
event.description = description;
event.start_time = start_time;
event.end_time = end_time;
event.is_all_day = is_all_day;
event.reminder_minutes = reminder_minutes;
event.created_time = time(nullptr);
event.updated_time = event.created_time;
// 智能分类
if (category.empty()) {
event.category = CategorizeEvent(title, description);
} else {
event.category = category;
}
events_[event.id] = event;
ESP_LOGI(TAG, "Created event: %s (ID: %s)", title.c_str(), event.id.c_str());
return event.id;
}
bool ScheduleManager::UpdateEvent(const std::string& event_id,
const std::string& title,
const std::string& description,
time_t start_time,
time_t end_time,
const std::string& category,
bool is_all_day,
int reminder_minutes) {
std::lock_guard<std::mutex> lock(events_mutex_);
auto it = events_.find(event_id);
if (it == events_.end()) {
ESP_LOGE(TAG, "Event not found: %s", event_id.c_str());
return false;
}
ScheduleEvent& event = it->second;
if (!title.empty()) {
event.title = title;
}
if (!description.empty()) {
event.description = description;
}
if (start_time > 0) {
event.start_time = start_time;
}
if (end_time > 0) {
event.end_time = end_time;
}
if (!category.empty()) {
event.category = category;
}
if (reminder_minutes >= 0) {
event.reminder_minutes = reminder_minutes;
}
event.is_all_day = is_all_day;
event.updated_time = time(nullptr);
ESP_LOGI(TAG, "Updated event: %s", event_id.c_str());
return true;
}
bool ScheduleManager::DeleteEvent(const std::string& event_id) {
std::lock_guard<std::mutex> lock(events_mutex_);
auto it = events_.find(event_id);
if (it == events_.end()) {
ESP_LOGE(TAG, "Event not found: %s", event_id.c_str());
return false;
}
events_.erase(it);
ESP_LOGI(TAG, "Deleted event: %s", event_id.c_str());
return true;
}
ScheduleEvent* ScheduleManager::GetEvent(const std::string& event_id) {
std::lock_guard<std::mutex> lock(events_mutex_);
auto it = events_.find(event_id);
if (it == events_.end()) {
return nullptr;
}
return &it->second;
}
std::vector<ScheduleEvent> ScheduleManager::GetEventsByDate(time_t date) {
std::lock_guard<std::mutex> lock(events_mutex_);
std::vector<ScheduleEvent> result;
struct tm date_tm = *localtime(&date);
for (const auto& pair : events_) {
const ScheduleEvent& event = pair.second;
struct tm event_tm = *localtime(&event.start_time);
if (event_tm.tm_year == date_tm.tm_year &&
event_tm.tm_mon == date_tm.tm_mon &&
event_tm.tm_mday == date_tm.tm_mday) {
result.push_back(event);
}
}
// 按开始时间排序
std::sort(result.begin(), result.end(),
[](const ScheduleEvent& a, const ScheduleEvent& b) {
return a.start_time < b.start_time;
});
return result;
}
std::vector<ScheduleEvent> ScheduleManager::GetEventsByCategory(const std::string& category) {
std::lock_guard<std::mutex> lock(events_mutex_);
std::vector<ScheduleEvent> result;
for (const auto& pair : events_) {
const ScheduleEvent& event = pair.second;
if (event.category == category) {
result.push_back(event);
}
}
return result;
}
std::vector<ScheduleEvent> ScheduleManager::GetUpcomingEvents(int days) {
std::lock_guard<std::mutex> lock(events_mutex_);
std::vector<ScheduleEvent> result;
time_t now = time(nullptr);
time_t future_time = now + (days * 24 * 60 * 60);
for (const auto& pair : events_) {
const ScheduleEvent& event = pair.second;
if (event.start_time >= now && event.start_time <= future_time && !event.is_completed) {
result.push_back(event);
}
}
// 按开始时间排序
std::sort(result.begin(), result.end(),
[](const ScheduleEvent& a, const ScheduleEvent& b) {
return a.start_time < b.start_time;
});
return result;
}
std::vector<ScheduleEvent> ScheduleManager::GetEventsByKeyword(const std::string& keyword) {
std::lock_guard<std::mutex> lock(events_mutex_);
std::vector<ScheduleEvent> result;
std::string lower_keyword = keyword;
std::transform(lower_keyword.begin(), lower_keyword.end(), lower_keyword.begin(), ::tolower);
for (const auto& pair : events_) {
const ScheduleEvent& event = pair.second;
std::string lower_title = event.title;
std::string lower_description = event.description;
std::transform(lower_title.begin(), lower_title.end(), lower_title.begin(), ::tolower);
std::transform(lower_description.begin(), lower_description.end(), lower_description.begin(), ::tolower);
if (lower_title.find(lower_keyword) != std::string::npos ||
lower_description.find(lower_keyword) != std::string::npos) {
result.push_back(event);
}
}
return result;
}
std::string ScheduleManager::CategorizeEvent(const std::string& title, const std::string& description) {
std::string text = title + " " + description;
std::transform(text.begin(), text.end(), text.begin(), ::tolower);
// 工作相关关键词
if (text.find("会议") != std::string::npos || text.find("工作") != std::string::npos ||
text.find("项目") != std::string::npos || text.find("报告") != std::string::npos ||
text.find("deadline") != std::string::npos || text.find("meeting") != std::string::npos) {
return CategoryToString(EventCategory::WORK);
}
// 学习相关关键词
if (text.find("学习") != std::string::npos || text.find("课程") != std::string::npos ||
text.find("考试") != std::string::npos || text.find("作业") != std::string::npos ||
text.find("study") != std::string::npos || text.find("exam") != std::string::npos) {
return CategoryToString(EventCategory::STUDY);
}
// 健康相关关键词
if (text.find("运动") != std::string::npos || text.find("健身") != std::string::npos ||
text.find("医院") != std::string::npos || text.find("体检") != std::string::npos ||
text.find("exercise") != std::string::npos || text.find("doctor") != std::string::npos) {
return CategoryToString(EventCategory::HEALTH);
}
// 娱乐相关关键词
if (text.find("电影") != std::string::npos || text.find("游戏") != std::string::npos ||
text.find("聚会") != std::string::npos || text.find("娱乐") != std::string::npos ||
text.find("movie") != std::string::npos || text.find("party") != std::string::npos) {
return CategoryToString(EventCategory::ENTERTAINMENT);
}
// 旅行相关关键词
if (text.find("旅行") != std::string::npos || text.find("旅游") != std::string::npos ||
text.find("出差") != std::string::npos || text.find("travel") != std::string::npos ||
text.find("trip") != std::string::npos) {
return CategoryToString(EventCategory::TRAVEL);
}
// 家庭相关关键词
if (text.find("家庭") != std::string::npos || text.find("家人") != std::string::npos ||
text.find("孩子") != std::string::npos || text.find("family") != std::string::npos ||
text.find("child") != std::string::npos) {
return CategoryToString(EventCategory::FAMILY);
}
return CategoryToString(EventCategory::OTHER);
}
void ScheduleManager::SetReminderCallback(ReminderCallback callback) {
reminder_callback_ = callback;
}
void ScheduleManager::CheckReminders() {
std::lock_guard<std::mutex> lock(events_mutex_);
time_t now = time(nullptr);
for (const auto& pair : events_) {
const ScheduleEvent& event = pair.second;
if (event.is_completed || event.reminder_minutes <= 0) {
continue;
}
time_t reminder_time = event.start_time - (event.reminder_minutes * 60);
if (now >= reminder_time && now < event.start_time) {
ESP_LOGI(TAG, "Reminder triggered for event: %s", event.title.c_str());
if (reminder_callback_) {
reminder_callback_(event);
}
}
}
}
int ScheduleManager::GetEventCount() {
std::lock_guard<std::mutex> lock(events_mutex_);
return events_.size();
}
int ScheduleManager::GetEventCountByCategory(const std::string& category) {
std::lock_guard<std::mutex> lock(events_mutex_);
int count = 0;
for (const auto& pair : events_) {
if (pair.second.category == category) {
count++;
}
}
return count;
}
std::map<std::string, int> ScheduleManager::GetCategoryStatistics() {
std::lock_guard<std::mutex> lock(events_mutex_);
std::map<std::string, int> stats;
for (const auto& pair : events_) {
stats[pair.second.category]++;
}
return stats;
}
bool ScheduleManager::SaveToStorage() {
// TODO: 实现数据持久化到NVS或SPIFFS
ESP_LOGW(TAG, "SaveToStorage not implemented yet");
return true;
}
bool ScheduleManager::LoadFromStorage() {
// TODO: 实现从NVS或SPIFFS加载数据
ESP_LOGW(TAG, "LoadFromStorage not implemented yet");
return true;
}
std::string ScheduleManager::ExportToJson() {
std::lock_guard<std::mutex> lock(events_mutex_);
std::stringstream json;
json << "{\"events\":[";
bool first = true;
for (const auto& pair : events_) {
if (!first) json << ",";
first = false;
const ScheduleEvent& event = pair.second;
json << "{"
<< "\"id\":\"" << event.id << "\","
<< "\"title\":\"" << event.title << "\","
<< "\"description\":\"" << event.description << "\","
<< "\"category\":\"" << event.category << "\","
<< "\"start_time\":" << event.start_time << ","
<< "\"end_time\":" << event.end_time << ","
<< "\"is_all_day\":" << (event.is_all_day ? "true" : "false") << ","
<< "\"reminder_minutes\":" << event.reminder_minutes << ","
<< "\"is_completed\":" << (event.is_completed ? "true" : "false") << ","
<< "\"created_time\":" << event.created_time << ","
<< "\"updated_time\":" << event.updated_time
<< "}";
}
json << "]}";
return json.str();
}
bool ScheduleManager::ImportFromJson(const std::string& json_data) {
// TODO: 实现JSON导入功能
ESP_LOGW(TAG, "ImportFromJson not implemented yet");
return false;
}
std::string ScheduleManager::GenerateEventId() {
static int counter = 0;
return "event_" + std::to_string(++counter) + "_" + std::to_string(time(nullptr));
}
std::string ScheduleManager::CategoryToString(EventCategory category) {
switch (category) {
case EventCategory::WORK: return "工作";
case EventCategory::LIFE: return "生活";
case EventCategory::STUDY: return "学习";
case EventCategory::HEALTH: return "健康";
case EventCategory::ENTERTAINMENT: return "娱乐";
case EventCategory::TRAVEL: return "旅行";
case EventCategory::FAMILY: return "家庭";
case EventCategory::OTHER: return "其他";
default: return "其他";
}
}
EventCategory ScheduleManager::StringToCategory(const std::string& category_str) {
if (category_str == "工作") return EventCategory::WORK;
if (category_str == "生活") return EventCategory::LIFE;
if (category_str == "学习") return EventCategory::STUDY;
if (category_str == "健康") return EventCategory::HEALTH;
if (category_str == "娱乐") return EventCategory::ENTERTAINMENT;
if (category_str == "旅行") return EventCategory::TRAVEL;
if (category_str == "家庭") return EventCategory::FAMILY;
return EventCategory::OTHER;
}
bool ScheduleManager::IsEventTimeValid(time_t start_time, time_t end_time) {
if (start_time <= 0) return false;
if (end_time > 0 && end_time <= start_time) return false;
return true;
}
void ScheduleManager::UpdateEventTimestamp(ScheduleEvent& event) {
event.updated_time = time(nullptr);
}

120
main/schedule_manager.h Normal file
View File

@@ -0,0 +1,120 @@
#ifndef SCHEDULE_MANAGER_H
#define SCHEDULE_MANAGER_H
#include <string>
#include <vector>
#include <map>
#include <functional>
#include <ctime>
#include <mutex>
#include <esp_log.h>
// 日程事件结构
struct ScheduleEvent {
std::string id; // 唯一标识符
std::string title; // 事件标题
std::string description; // 事件描述
std::string category; // 事件分类(工作、生活、学习等)
time_t start_time; // 开始时间
time_t end_time; // 结束时间
bool is_all_day; // 是否全天事件
bool is_recurring; // 是否重复事件
std::string recurrence; // 重复规则daily, weekly, monthly
int reminder_minutes; // 提醒时间(分钟)
bool is_completed; // 是否已完成
time_t created_time; // 创建时间
time_t updated_time; // 更新时间
ScheduleEvent() : start_time(0), end_time(0), is_all_day(false),
is_recurring(false), reminder_minutes(0),
is_completed(false), created_time(0), updated_time(0) {}
};
// 智能分类枚举
enum class EventCategory {
WORK, // 工作
LIFE, // 生活
STUDY, // 学习
HEALTH, // 健康
ENTERTAINMENT, // 娱乐
TRAVEL, // 旅行
FAMILY, // 家庭
OTHER // 其他
};
// 提醒回调函数类型
using ReminderCallback = std::function<void(const ScheduleEvent&)>;
class ScheduleManager {
public:
static ScheduleManager& GetInstance() {
static ScheduleManager instance;
return instance;
}
// 事件管理
std::string CreateEvent(const std::string& title,
const std::string& description,
time_t start_time,
time_t end_time,
const std::string& category = "",
bool is_all_day = false,
int reminder_minutes = 15);
bool UpdateEvent(const std::string& event_id,
const std::string& title = "",
const std::string& description = "",
time_t start_time = 0,
time_t end_time = 0,
const std::string& category = "",
bool is_all_day = false,
int reminder_minutes = -1);
bool DeleteEvent(const std::string& event_id);
ScheduleEvent* GetEvent(const std::string& event_id);
// 查询功能
std::vector<ScheduleEvent> GetEventsByDate(time_t date);
std::vector<ScheduleEvent> GetEventsByCategory(const std::string& category);
std::vector<ScheduleEvent> GetUpcomingEvents(int days = 7);
std::vector<ScheduleEvent> GetEventsByKeyword(const std::string& keyword);
// 智能分类
std::string CategorizeEvent(const std::string& title, const std::string& description);
// 提醒功能
void SetReminderCallback(ReminderCallback callback);
void CheckReminders();
// 统计功能
int GetEventCount();
int GetEventCountByCategory(const std::string& category);
std::map<std::string, int> GetCategoryStatistics();
// 数据持久化
bool SaveToStorage();
bool LoadFromStorage();
// 导出功能
std::string ExportToJson();
bool ImportFromJson(const std::string& json_data);
private:
ScheduleManager();
~ScheduleManager();
std::string GenerateEventId();
std::string CategoryToString(EventCategory category);
EventCategory StringToCategory(const std::string& category_str);
bool IsEventTimeValid(time_t start_time, time_t end_time);
void UpdateEventTimestamp(ScheduleEvent& event);
std::map<std::string, ScheduleEvent> events_;
std::mutex events_mutex_;
ReminderCallback reminder_callback_;
static const char* TAG;
};
#endif // SCHEDULE_MANAGER_H

559
main/timer_manager.cc Normal file
View File

@@ -0,0 +1,559 @@
#include "timer_manager.h"
#include <esp_log.h>
#include <esp_timer.h>
#include <cstring>
#include <sstream>
const char* TimerManager::TAG = "TimerManager";
TimerManager::TimerManager() : is_running_(false) {
ESP_LOGI(TAG, "TimerManager initialized");
}
TimerManager::~TimerManager() {
StopManager();
ESP_LOGI(TAG, "TimerManager destroyed");
}
std::string TimerManager::CreateCountdownTimer(const std::string& name,
uint32_t duration_ms,
const std::string& description) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
TimerTask task;
task.id = GenerateTaskId();
task.name = name;
task.type = TimerType::COUNTDOWN;
task.status = TimerStatus::PENDING;
task.duration_ms = duration_ms;
task.description = description;
task.created_time = time(nullptr);
tasks_[task.id] = task;
ESP_LOGI(TAG, "Created countdown timer: %s (ID: %s, Duration: %u ms)",
name.c_str(), task.id.c_str(), duration_ms);
return task.id;
}
std::string TimerManager::CreateDelayedMcpTask(const std::string& name,
uint32_t delay_ms,
const std::string& mcp_tool_name,
const std::string& mcp_tool_args,
const std::string& description) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
TimerTask task;
task.id = GenerateTaskId();
task.name = name;
task.type = TimerType::DELAYED_EXEC;
task.status = TimerStatus::PENDING;
task.duration_ms = delay_ms;
task.mcp_tool_name = mcp_tool_name;
task.mcp_tool_args = mcp_tool_args;
task.description = description;
task.created_time = time(nullptr);
tasks_[task.id] = task;
ESP_LOGI(TAG, "Created delayed MCP task: %s (ID: %s, Delay: %u ms, Tool: %s)",
name.c_str(), task.id.c_str(), delay_ms, mcp_tool_name.c_str());
return task.id;
}
std::string TimerManager::CreatePeriodicTask(const std::string& name,
uint32_t interval_ms,
int repeat_count,
const std::string& mcp_tool_name,
const std::string& mcp_tool_args,
const std::string& description) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
TimerTask task;
task.id = GenerateTaskId();
task.name = name;
task.type = TimerType::PERIODIC;
task.status = TimerStatus::PENDING;
task.interval_ms = interval_ms;
task.repeat_count = repeat_count;
task.current_repeat = 0;
task.mcp_tool_name = mcp_tool_name;
task.mcp_tool_args = mcp_tool_args;
task.description = description;
task.created_time = time(nullptr);
tasks_[task.id] = task;
ESP_LOGI(TAG, "Created periodic task: %s (ID: %s, Interval: %u ms, Repeat: %d)",
name.c_str(), task.id.c_str(), interval_ms, repeat_count);
return task.id;
}
std::string TimerManager::CreateScheduledTask(const std::string& name,
time_t scheduled_time,
const std::string& mcp_tool_name,
const std::string& mcp_tool_args,
const std::string& description) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
TimerTask task;
task.id = GenerateTaskId();
task.name = name;
task.type = TimerType::SCHEDULED;
task.status = TimerStatus::PENDING;
task.scheduled_time = scheduled_time;
task.mcp_tool_name = mcp_tool_name;
task.mcp_tool_args = mcp_tool_args;
task.description = description;
task.created_time = time(nullptr);
tasks_[task.id] = task;
ESP_LOGI(TAG, "Created scheduled task: %s (ID: %s, Time: %ld)",
name.c_str(), task.id.c_str(), scheduled_time);
return task.id;
}
bool TimerManager::StartTask(const std::string& task_id) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
auto it = tasks_.find(task_id);
if (it == tasks_.end()) {
ESP_LOGE(TAG, "Task not found: %s", task_id.c_str());
return false;
}
TimerTask& task = it->second;
if (task.status != TimerStatus::PENDING) {
ESP_LOGW(TAG, "Task %s is not in pending status", task_id.c_str());
return false;
}
// 创建FreeRTOS定时器
TimerHandle_t timer_handle = xTimerCreate(
task.name.c_str(),
pdMS_TO_TICKS(task.duration_ms),
(task.type == TimerType::PERIODIC) ? pdTRUE : pdFALSE,
(void*)task_id.c_str(),
TimerCallback
);
if (timer_handle == nullptr) {
ESP_LOGE(TAG, "Failed to create timer for task: %s", task_id.c_str());
return false;
}
timers_[task_id] = timer_handle;
task.status = TimerStatus::RUNNING;
task.start_time = time(nullptr);
if (xTimerStart(timer_handle, 0) != pdPASS) {
ESP_LOGE(TAG, "Failed to start timer for task: %s", task_id.c_str());
xTimerDelete(timer_handle, 0);
timers_.erase(task_id);
task.status = TimerStatus::FAILED;
return false;
}
ESP_LOGI(TAG, "Started task: %s", task_id.c_str());
return true;
}
bool TimerManager::StopTask(const std::string& task_id) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
auto it = tasks_.find(task_id);
if (it == tasks_.end()) {
ESP_LOGE(TAG, "Task not found: %s", task_id.c_str());
return false;
}
TimerTask& task = it->second;
if (task.status != TimerStatus::RUNNING) {
ESP_LOGW(TAG, "Task %s is not running", task_id.c_str());
return false;
}
auto timer_it = timers_.find(task_id);
if (timer_it != timers_.end()) {
xTimerStop(timer_it->second, 0);
xTimerDelete(timer_it->second, 0);
timers_.erase(timer_it);
}
task.status = TimerStatus::CANCELLED;
task.end_time = time(nullptr);
ESP_LOGI(TAG, "Stopped task: %s", task_id.c_str());
return true;
}
bool TimerManager::CancelTask(const std::string& task_id) {
return StopTask(task_id);
}
bool TimerManager::DeleteTask(const std::string& task_id) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
// 先停止任务
StopTask(task_id);
auto it = tasks_.find(task_id);
if (it == tasks_.end()) {
ESP_LOGE(TAG, "Task not found: %s", task_id.c_str());
return false;
}
tasks_.erase(it);
ESP_LOGI(TAG, "Deleted task: %s", task_id.c_str());
return true;
}
TimerTask* TimerManager::GetTask(const std::string& task_id) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
auto it = tasks_.find(task_id);
if (it == tasks_.end()) {
return nullptr;
}
return &it->second;
}
std::vector<TimerTask> TimerManager::GetAllTasks() {
std::lock_guard<std::mutex> lock(tasks_mutex_);
std::vector<TimerTask> result;
for (const auto& pair : tasks_) {
result.push_back(pair.second);
}
return result;
}
std::vector<TimerTask> TimerManager::GetTasksByStatus(TimerStatus status) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
std::vector<TimerTask> result;
for (const auto& pair : tasks_) {
if (pair.second.status == status) {
result.push_back(pair.second);
}
}
return result;
}
std::vector<TimerTask> TimerManager::GetRunningTasks() {
return GetTasksByStatus(TimerStatus::RUNNING);
}
std::vector<TimerTask> TimerManager::GetUpcomingTasks(int minutes) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
std::vector<TimerTask> result;
time_t now = time(nullptr);
time_t future_time = now + (minutes * 60);
for (const auto& pair : tasks_) {
const TimerTask& task = pair.second;
if (task.status == TimerStatus::PENDING &&
task.scheduled_time >= now &&
task.scheduled_time <= future_time) {
result.push_back(task);
}
}
return result;
}
int TimerManager::GetTaskCount() {
std::lock_guard<std::mutex> lock(tasks_mutex_);
return tasks_.size();
}
int TimerManager::GetTaskCountByStatus(TimerStatus status) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
int count = 0;
for (const auto& pair : tasks_) {
if (pair.second.status == status) {
count++;
}
}
return count;
}
int TimerManager::GetTaskCountByType(TimerType type) {
std::lock_guard<std::mutex> lock(tasks_mutex_);
int count = 0;
for (const auto& pair : tasks_) {
if (pair.second.type == type) {
count++;
}
}
return count;
}
void TimerManager::StartManager() {
if (is_running_) {
ESP_LOGW(TAG, "TimerManager is already running");
return;
}
is_running_ = true;
worker_thread_ = std::thread(&TimerManager::TaskWorker, this);
ESP_LOGI(TAG, "TimerManager started");
}
void TimerManager::StopManager() {
if (!is_running_) {
return;
}
is_running_ = false;
if (worker_thread_.joinable()) {
worker_thread_.join();
}
// 停止所有定时器
std::lock_guard<std::mutex> lock(tasks_mutex_);
for (auto& pair : timers_) {
xTimerStop(pair.second, 0);
xTimerDelete(pair.second, 0);
}
timers_.clear();
ESP_LOGI(TAG, "TimerManager stopped");
}
bool TimerManager::IsRunning() {
return is_running_;
}
void TimerManager::SetTaskCompletedCallback(std::function<void(const TimerTask&)> callback) {
task_completed_callback_ = callback;
}
void TimerManager::SetTaskFailedCallback(std::function<void(const TimerTask&, const std::string&)> callback) {
task_failed_callback_ = callback;
}
bool TimerManager::SaveToStorage() {
// TODO: 实现数据持久化到NVS或SPIFFS
ESP_LOGW(TAG, "SaveToStorage not implemented yet");
return true;
}
bool TimerManager::LoadFromStorage() {
// TODO: 实现从NVS或SPIFFS加载数据
ESP_LOGW(TAG, "LoadFromStorage not implemented yet");
return true;
}
std::string TimerManager::ExportToJson() {
std::lock_guard<std::mutex> lock(tasks_mutex_);
std::stringstream json;
json << "{\"tasks\":[";
bool first = true;
for (const auto& pair : tasks_) {
if (!first) json << ",";
first = false;
const TimerTask& task = pair.second;
json << "{"
<< "\"id\":\"" << task.id << "\","
<< "\"name\":\"" << task.name << "\","
<< "\"description\":\"" << task.description << "\","
<< "\"duration_ms\":" << task.duration_ms << ","
<< "\"interval_ms\":" << task.interval_ms << ","
<< "\"repeat_count\":" << task.repeat_count << ","
<< "\"current_repeat\":" << task.current_repeat << ","
<< "\"created_time\":" << task.created_time << ","
<< "\"start_time\":" << task.start_time << ","
<< "\"end_time\":" << task.end_time << ","
<< "\"scheduled_time\":" << task.scheduled_time << ","
<< "\"mcp_tool_name\":\"" << task.mcp_tool_name << "\","
<< "\"mcp_tool_args\":\"" << task.mcp_tool_args << "\","
<< "\"user_data\":\"" << task.user_data << "\",";
std::string status_str;
switch (task.status) {
case TimerStatus::PENDING: status_str = "pending"; break;
case TimerStatus::RUNNING: status_str = "running"; break;
case TimerStatus::COMPLETED: status_str = "completed"; break;
case TimerStatus::CANCELLED: status_str = "cancelled"; break;
case TimerStatus::FAILED: status_str = "failed"; break;
}
json << "\"status\":\"" << status_str << "\",";
std::string type_str;
switch (task.type) {
case TimerType::COUNTDOWN: type_str = "countdown"; break;
case TimerType::DELAYED_EXEC: type_str = "delayed_exec"; break;
case TimerType::PERIODIC: type_str = "periodic"; break;
case TimerType::SCHEDULED: type_str = "scheduled"; break;
}
json << "\"type\":\"" << type_str << "\"";
json << "}";
}
json << "]}";
return json.str();
}
std::string TimerManager::GenerateTaskId() {
static int counter = 0;
return "task_" + std::to_string(++counter) + "_" + std::to_string(time(nullptr));
}
void TimerManager::TaskWorker() {
ESP_LOGI(TAG, "Task worker thread started");
while (is_running_) {
vTaskDelay(pdMS_TO_TICKS(1000));
// 检查定时任务
std::lock_guard<std::mutex> lock(tasks_mutex_);
time_t now = time(nullptr);
for (auto& pair : tasks_) {
TimerTask& task = pair.second;
if (task.status == TimerStatus::PENDING &&
task.type == TimerType::SCHEDULED &&
now >= task.scheduled_time) {
ESP_LOGI(TAG, "Executing scheduled task: %s", task.id.c_str());
ExecuteTask(task);
}
}
}
ESP_LOGI(TAG, "Task worker thread stopped");
}
void TimerManager::ExecuteTask(TimerTask& task) {
task.status = TimerStatus::RUNNING;
task.start_time = time(nullptr);
bool success = true;
std::string error_msg;
try {
if (!task.mcp_tool_name.empty()) {
success = ExecuteMcpTool(task.mcp_tool_name, task.mcp_tool_args);
if (!success) {
error_msg = "MCP tool execution failed";
}
}
} catch (const std::exception& e) {
success = false;
error_msg = e.what();
} catch (...) {
success = false;
error_msg = "Unknown error occurred";
}
task.end_time = time(nullptr);
if (success) {
if (task.type == TimerType::PERIODIC) {
task.current_repeat++;
if (task.repeat_count == -1 || task.current_repeat < task.repeat_count) {
// 继续下一次重复
task.status = TimerStatus::PENDING;
ESP_LOGI(TAG, "Periodic task %s completed repeat %d/%d",
task.id.c_str(), task.current_repeat, task.repeat_count);
} else {
// 所有重复完成
task.status = TimerStatus::COMPLETED;
ESP_LOGI(TAG, "Periodic task %s completed all repeats", task.id.c_str());
NotifyTaskCompleted(task);
}
} else {
task.status = TimerStatus::COMPLETED;
ESP_LOGI(TAG, "Task %s completed successfully", task.id.c_str());
NotifyTaskCompleted(task);
}
} else {
task.status = TimerStatus::FAILED;
ESP_LOGE(TAG, "Task %s failed: %s", task.id.c_str(), error_msg.c_str());
NotifyTaskFailed(task, error_msg);
}
}
bool TimerManager::ExecuteMcpTool(const std::string& tool_name, const std::string& args) {
// TODO: 实现MCP工具执行
ESP_LOGW(TAG, "ExecuteMcpTool not implemented yet: %s with args: %s",
tool_name.c_str(), args.c_str());
return true;
}
void TimerManager::UpdateTaskStatus(TimerTask& task, TimerStatus status) {
task.status = status;
if (status == TimerStatus::RUNNING) {
task.start_time = time(nullptr);
} else if (status == TimerStatus::COMPLETED || status == TimerStatus::FAILED) {
task.end_time = time(nullptr);
}
}
void TimerManager::NotifyTaskCompleted(const TimerTask& task) {
if (task_completed_callback_) {
task_completed_callback_(task);
}
}
void TimerManager::NotifyTaskFailed(const TimerTask& task, const std::string& error) {
if (task_failed_callback_) {
task_failed_callback_(task, error);
}
}
void TimerManager::TimerCallback(TimerHandle_t timer_handle) {
const char* task_id = (const char*)pvTimerGetTimerID(timer_handle);
ESP_LOGI(TAG, "Timer callback triggered for task: %s", task_id);
// 获取任务管理器实例
TimerManager& manager = TimerManager::GetInstance();
std::lock_guard<std::mutex> lock(manager.tasks_mutex_);
auto it = manager.tasks_.find(task_id);
if (it != manager.tasks_.end()) {
TimerTask& task = it->second;
if (task.type == TimerType::COUNTDOWN) {
// 倒计时完成
task.status = TimerStatus::COMPLETED;
task.end_time = time(nullptr);
ESP_LOGI(TAG, "Countdown timer %s completed", task_id);
manager.NotifyTaskCompleted(task);
} else if (task.type == TimerType::DELAYED_EXEC) {
// 延时执行MCP工具
manager.ExecuteTask(task);
}
}
// 删除一次性定时器
if (xTimerIsTimerActive(timer_handle) == pdFALSE) {
xTimerDelete(timer_handle, 0);
manager.timers_.erase(task_id);
}
}

165
main/timer_manager.h Normal file
View File

@@ -0,0 +1,165 @@
#ifndef TIMER_MANAGER_H
#define TIMER_MANAGER_H
#include <string>
#include <vector>
#include <map>
#include <functional>
#include <thread>
#include <atomic>
#include <mutex>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/timers.h>
// 定时任务类型
enum class TimerType {
COUNTDOWN, // 倒计时
DELAYED_EXEC, // 延时执行
PERIODIC, // 周期性任务
SCHEDULED // 定时执行
};
// 定时任务状态
enum class TimerStatus {
PENDING, // 等待中
RUNNING, // 运行中
COMPLETED, // 已完成
CANCELLED, // 已取消
FAILED // 失败
};
// MCP工具回调函数类型
using McpToolCallback = std::function<bool(const std::string& tool_name, const std::string& arguments)>;
// 定时任务结构
struct TimerTask {
std::string id; // 唯一标识符
std::string name; // 任务名称
TimerType type; // 任务类型
TimerStatus status; // 任务状态
uint32_t duration_ms; // 持续时间(毫秒)
time_t scheduled_time; // 预定执行时间
time_t created_time; // 创建时间
time_t start_time; // 开始时间
time_t end_time; // 结束时间
// MCP工具相关
std::string mcp_tool_name; // MCP工具名称
std::string mcp_tool_args; // MCP工具参数
McpToolCallback callback; // 回调函数
// 周期性任务相关
uint32_t interval_ms; // 间隔时间(毫秒)
int repeat_count; // 重复次数(-1表示无限
int current_repeat; // 当前重复次数
// 用户数据
std::string user_data; // 用户自定义数据
std::string description; // 任务描述
TimerTask() : type(TimerType::COUNTDOWN), status(TimerStatus::PENDING),
duration_ms(0), scheduled_time(0), created_time(0),
start_time(0), end_time(0), interval_ms(0),
repeat_count(0), current_repeat(0) {}
};
class TimerManager {
public:
static TimerManager& GetInstance() {
static TimerManager instance;
return instance;
}
// 倒计时器功能
std::string CreateCountdownTimer(const std::string& name,
uint32_t duration_ms,
const std::string& description = "");
// 延时执行MCP工具
std::string CreateDelayedMcpTask(const std::string& name,
uint32_t delay_ms,
const std::string& mcp_tool_name,
const std::string& mcp_tool_args = "",
const std::string& description = "");
// 周期性任务
std::string CreatePeriodicTask(const std::string& name,
uint32_t interval_ms,
int repeat_count = -1, // -1表示无限重复
const std::string& mcp_tool_name = "",
const std::string& mcp_tool_args = "",
const std::string& description = "");
// 定时执行任务
std::string CreateScheduledTask(const std::string& name,
time_t scheduled_time,
const std::string& mcp_tool_name,
const std::string& mcp_tool_args = "",
const std::string& description = "");
// 任务管理
bool StartTask(const std::string& task_id);
bool StopTask(const std::string& task_id);
bool CancelTask(const std::string& task_id);
bool DeleteTask(const std::string& task_id);
// 查询功能
TimerTask* GetTask(const std::string& task_id);
std::vector<TimerTask> GetAllTasks();
std::vector<TimerTask> GetTasksByStatus(TimerStatus status);
std::vector<TimerTask> GetRunningTasks();
std::vector<TimerTask> GetUpcomingTasks(int minutes = 60);
// 统计功能
int GetTaskCount();
int GetTaskCountByStatus(TimerStatus status);
int GetTaskCountByType(TimerType type);
// 系统控制
void StartManager();
void StopManager();
bool IsRunning();
// 回调设置
void SetTaskCompletedCallback(std::function<void(const TimerTask&)> callback);
void SetTaskFailedCallback(std::function<void(const TimerTask&, const std::string&)> callback);
// 数据持久化
bool SaveToStorage();
bool LoadFromStorage();
// 导出功能
std::string ExportToJson();
private:
TimerManager();
~TimerManager();
// 内部方法
std::string GenerateTaskId();
void TaskWorker();
void ExecuteTask(TimerTask& task);
bool ExecuteMcpTool(const std::string& tool_name, const std::string& args);
void UpdateTaskStatus(TimerTask& task, TimerStatus status);
void NotifyTaskCompleted(const TimerTask& task);
void NotifyTaskFailed(const TimerTask& task, const std::string& error);
// FreeRTOS定时器回调
static void TimerCallback(TimerHandle_t timer_handle);
std::map<std::string, TimerTask> tasks_;
std::map<std::string, TimerHandle_t> timers_;
std::mutex tasks_mutex_;
std::atomic<bool> is_running_;
std::thread worker_thread_;
// 回调函数
std::function<void(const TimerTask&)> task_completed_callback_;
std::function<void(const TimerTask&, const std::string&)> task_failed_callback_;
static const char* TAG;
};
#endif // TIMER_MANAGER_H