From ba5e47bc77dd305777bb64c61022f10cf9b40889 Mon Sep 17 00:00:00 2001 From: Misaki Date: Thu, 16 Oct 2025 11:36:45 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=99=E6=98=AF=E4=B8=80=E6=AC=A1=E9=95=BF?= =?UTF-8?q?=E4=B9=85=E7=9A=84=E6=8F=90=E4=BA=A4=EF=BC=9A=201.=20=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E7=95=8C=E9=9D=A2=E5=A2=9E=E5=8A=A0=E4=BA=86=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E4=B8=BB=E9=A1=B5=E7=9A=84=E6=8C=89=E9=92=AE=202.=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86gif=E6=B8=B2=E6=9F=93=E5=86=85?= =?UTF-8?q?=E5=AD=98=E6=B3=84=E6=BC=8F=E7=9A=84=E4=B8=A5=E9=87=8Dbug=203.?= =?UTF-8?q?=20=E5=B0=86PetDao=E5=BD=93=E4=B8=AD=E7=9A=84cJSON=20API?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E4=B8=BAcpp=5Fjson=EF=BC=8C=E5=AE=8C?= =?UTF-8?q?=E7=BE=8E=E9=80=9A=E8=BF=87=E6=B5=8B=E8=AF=95=204.=20=E6=95=B4?= =?UTF-8?q?=E5=90=88=E5=B7=B2=E7=BB=8F=E5=AE=9E=E7=8E=B0=E7=9A=84=E5=90=84?= =?UTF-8?q?=E7=A7=8D=E4=B8=8A=E5=B1=82=E5=BB=BA=E7=AD=91=EF=BC=8C=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E4=BA=86=E4=B8=80=E4=B8=AA=E5=AE=A0=E7=89=A9=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E5=9F=BA=E6=9C=AC=E4=B8=9A=E5=8A=A1=E5=BA=94=E7=94=A8?= =?UTF-8?q?=EF=BC=8C=E7=94=A8=E4=BA=8E=E6=A0=B7=E5=93=81=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E7=94=A8=205.=20=E9=87=8D=E6=9E=84=E4=BA=86?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E6=92=AD=E6=94=BE=E7=B1=BB=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E5=85=B6=E6=9B=B4modern=EF=BC=8C=E6=9B=B4=E5=8A=A0=E4=BE=BF?= =?UTF-8?q?=E4=BA=8E=E7=A7=BB=E6=A4=8D=E5=92=8C=E6=8B=93=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Bionic_Core/CommClass/CommClass.cpp | 2 +- Bionic_Core/CppHandle/CppHandle.cpp | 660 ++++++------------ Bionic_Core/PetBaseClass/PetDao.cpp | 247 +++---- Bionic_Core/PetBaseClass/PetDao.h | 33 +- Bionic_Core/PetBaseClass/PetObserver.h | 25 +- .../ToolsClass/AudioOutput/AudioOutput.cpp | 127 +++- .../ToolsClass/AudioOutput/AudioOutput.h | 62 +- Bionic_Core/ToolsClass/CppJson/cpp_json.h | 169 ++++- .../ToolsClass/LVGL_Render/LVGLRender.cpp | 93 +-- .../ToolsClass/LVGL_Render/LVGLRender.h | 27 +- .../ToolsClass/LVGL_Render/Lvpp/lvpp.h | 550 +++++++++++++-- .../LVGL_Render/LvppApp/BaseApp.cpp | 5 + .../ToolsClass/LVGL_Render/LvppApp/BaseApp.h | 415 +++++++++++ .../SDFileManager/SDFileManager.cpp | 28 +- .../ToolsClass/SDFileManager/SDFileManager.h | 8 +- .../SpeechRecognizer/SimpleI2SForwarder.cpp | 9 + .../SpeechRecognizer/SimpleI2SForwarder.h | 124 ++++ .../SpeechRecognizer/SpeechRecognizer.cpp | 144 ++-- .../SpeechRecognizer/SpeechRecognizer.h | 8 +- .../SpeechRecognizer/VadSlidingWindow.cpp | 9 + .../SpeechRecognizer/VadSlidingWindow.h | 377 ++++++++++ Lib/Audio_Driver/PCM5101.c | 14 +- Lib/Audio_Driver/PCM5101.h | 9 + Lib/Display/LCD_Driver/ST77916.c | 10 +- Lib/Display/LCD_Driver/ST77916.h | 6 +- Lib/Display/LVGL_Driver/LVGL_Driver.c | 15 +- Lib/Display/LVGL_Driver/LVGL_Driver.h | 2 +- Lib/SD_Card/SD_MMC.c | 4 +- Lib/SD_Card/SD_MMC.h | 2 +- Lib/Wireless/Wireless.c | 4 +- Lib/Wireless/Wireless.h | 6 +- Temp.md | 85 +++ main/Bionic_sphere.c | 15 +- main/CMakeLists.txt | 4 + sdkconfig | 585 +--------------- sdkconfig.old | 581 +-------------- test/driver_test/drivers_test.c | 2 +- 项目开发日志.md | 29 + 38 files changed, 2487 insertions(+), 2008 deletions(-) create mode 100644 Bionic_Core/ToolsClass/LVGL_Render/LvppApp/BaseApp.cpp create mode 100644 Bionic_Core/ToolsClass/LVGL_Render/LvppApp/BaseApp.h create mode 100644 Bionic_Core/ToolsClass/SpeechRecognizer/SimpleI2SForwarder.cpp create mode 100644 Bionic_Core/ToolsClass/SpeechRecognizer/SimpleI2SForwarder.h create mode 100644 Bionic_Core/ToolsClass/SpeechRecognizer/VadSlidingWindow.cpp create mode 100644 Bionic_Core/ToolsClass/SpeechRecognizer/VadSlidingWindow.h diff --git a/Bionic_Core/CommClass/CommClass.cpp b/Bionic_Core/CommClass/CommClass.cpp index c27dc83..3fe8ba4 100644 --- a/Bionic_Core/CommClass/CommClass.cpp +++ b/Bionic_Core/CommClass/CommClass.cpp @@ -303,7 +303,7 @@ void WebSocketManager::heartbeatThread() { if (connected && config.heartbeat_interval > 0) { // 如果处于连接状态且心跳间隔大于0 // 发送心跳消息 cppjson::Json hb = cppjson::Json::object(); - hb.set("type", cppjson::Json("heartbeat")) + hb.set("type", cppjson::Json("ping")) .set("timestamp", cppjson::Json(esp_log_timestamp())); sendJson(hb); // 已经重载好了,直接塞 diff --git a/Bionic_Core/CppHandle/CppHandle.cpp b/Bionic_Core/CppHandle/CppHandle.cpp index b8e043f..d608926 100644 --- a/Bionic_Core/CppHandle/CppHandle.cpp +++ b/Bionic_Core/CppHandle/CppHandle.cpp @@ -4,217 +4,20 @@ #include "CppHandle.h" #include "OTAClass.h" -#include "PetBaseClass.h" -#include "PetDao.h" #include #include - -void testPetSystem() { - std::cout << "Test point1" << std::endl; - // 创建阶段策略 - auto stageStrategy = std::make_unique(); - stageStrategy->addStage(PetStageType::PET_STAGE_YOUNG, "models/young.obj"); - stageStrategy->addStage(PetStageType::PET_STAGE_ADULT, "models/adult.obj"); - stageStrategy->addStage(PetStageType::PET_STAGE_OLD, "models/old.obj"); - stageStrategy->addStageAudio(PetStageType::PET_STAGE_YOUNG, "audio/young.mp3"); - stageStrategy->addStageAudio(PetStageType::PET_STAGE_ADULT, "audio/adult.mp3"); - stageStrategy->addStageAudio(PetStageType::PET_STAGE_OLD, "audio/old.mp3"); - - // 创建动作策略 - auto actionStrategy = std::make_unique(); - actionStrategy->addAction(PetActionType::PET_ACTION_EAT, "models/eat.obj"); - actionStrategy->addAction(PetActionType::PET_ACTION_HAPPY, "models/happy.obj"); - actionStrategy->addAction(PetActionType::PET_ACTION_SLEEP, "models/sleep.obj"); - actionStrategy->addAction(PetActionType::PET_ACTION_ANGRY, "models/angry.obj"); - actionStrategy->addAction(PetActionType::PET_ACTION_SAD, "models/sad.obj"); - actionStrategy->addAction(PetActionType::PET_ACTION_EVOLVE, "models/evolve.obj"); - actionStrategy->addAction(PetActionType::PET_ACTION_TOUCH, "models/touch.obj"); - - actionStrategy->addActionAudio(PetActionType::PET_ACTION_EAT, "audio/eat.mp3"); - actionStrategy->addActionAudio(PetActionType::PET_ACTION_HAPPY, "audio/happy.mp3"); - actionStrategy->addActionAudio(PetActionType::PET_ACTION_SLEEP, "audio/sleep.mp3"); - std::cout << "Test point2" << std::endl; - - // 创建宠物信息 - PetBaseInfo info; - info.pet_name = "芝士雪豹"; - info.pet_hp = 100; - info.pet_density = 50; - info.pet_identity = "我是顶真"; - - // 创建宠物 - auto pet = std::make_shared(info, std::move(stageStrategy), std::move(actionStrategy)); - std::cout << "Test point3" << std::endl; - - // 创建音频观察者 - auto audioStrategy = std::make_shared(); - audioStrategy->setAudioCallback([](const std::string& audioPath) { - std::cout << "Playing audio: " << audioPath << std::endl; - }); - audioStrategy->subscribe(pet); - std::cout << "Test point4" << std::endl; - - // 创建渲染器观察者 - auto rendererStrategy = std::make_shared( - pet->getStageStrategy(), - pet->getActionStrategy() - ); - rendererStrategy->setRenderCallback([](const std::string& modelPath) { - std::cout << "Rendering model: " << modelPath << std::endl; - }); - rendererStrategy->subscribe(pet); - std::cout << "Test point5" << std::endl; - - // 执行一些动作 - std::cout << "=== Testing basic actions ===" << std::endl; - pet->feed(); - pet->play(); - pet->touch(); - std::cout << "Test point6" << std::endl; - - // 检查当前状态 - std::cout << "Current HP: " << pet->getPetInfo().pet_hp << std::endl; - std::cout << "Current density: " << pet->getPetInfo().pet_density << std::endl; - std::cout << "Current stage: " << static_cast(pet->getCurrentStage()) << std::endl; - std::cout << "Current action: " << static_cast(pet->getCurrentAction()) << std::endl; - std::cout << "Test point7" << std::endl; - - // 测试进化 - std::cout << "\n=== Testing evolution ===" << std::endl; - // 直接修改亲密度来测试进化 - PetBaseInfo newInfo = pet->getPetInfo(); - newInfo.pet_density = 100; // 达到进化条件 - pet->setPetInfo(newInfo); - std::cout << "Test point8" << std::endl; - - if (pet->checkEvolution()) { - std::cout << "Evolution successful!" << std::endl; - } else { - std::cout << "Evolution failed!" << std::endl; - } - - // 进一步增加亲密度到150,尝试再次进化 - newInfo.pet_density = 150; - pet->setPetInfo(newInfo); - if (pet->checkEvolution()) { - std::cout << "Second evolution successful!" << std::endl; - } else { - std::cout << "Second evolution failed!" << std::endl; - } - - std::cout << "Final stage: " << static_cast(pet->getCurrentStage()) << std::endl; - - PetDAO petDAO(SDFileManager::getInstance()); - petDAO.savePet(pet, "my_pet.json"); - // 列出所有宠物文件 - auto petFiles = petDAO.listPetFiles(); - for (const auto& file : petFiles) { - std::cout << "Pet file: " << file << std::endl; - } - std::cout << SDFileManager::getInstance()->catCommand("/sdcard/pet_data/my_pet.json") << std::endl; -} - - -#include "SpeechRecognizer.h" -#include -#include -// 命令回调函数 -void commandCallback(int command_id, const std::string& phrase, float probability) { - ESP_LOGI("Example", "Received command: ID=%d, Phrase='%s', Probability=%.2f", - command_id, phrase.c_str(), probability); - - // 根据命令执行相应操作 - switch (command_id) { - case 0: - ESP_LOGI("Example", "执行命令0"); - // 执行命令0的操作 - break; - case 1: - ESP_LOGI("Example", "执行命令1"); - // 执行命令1的操作 - break; - case 2: - ESP_LOGI("Example", "执行命令2"); - // 执行命令2的操作 - break; - default: - ESP_LOGI("Example", "未知的命令ID: %d", command_id); - break; - } -} - -// 状态回调函数 -void stateCallback(const std::string& state) { - ESP_LOGI("Example", "状态改变到: %s", state.c_str()); -} -#include "SDFileManager.h" -void testMIC() { - // 初始化NVS - esp_err_t ret = nvs_flash_init(); - if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { - ESP_ERROR_CHECK(nvs_flash_erase()); - ret = nvs_flash_init(); - } - ESP_ERROR_CHECK(ret); - - // 初始化SD卡管理器 - SDFileManager::getInstance()->tryInitSDCard(); - - // 获取SpeechRecognizer实例 - SpeechRecognizer* recognizer = SpeechRecognizer::getInstance(); - - // 配置识别器 - SpeechRecognizerConfig config; - config.enable_vad = true; - config.vad_mode = VAD_MODE_3; // 更高的VAD灵敏度 - config.model_path = "/sdcard/srmodels"; - - // 初始化 - if (!recognizer->init(config)) { - ESP_LOGE("main", "Failed to initialize speech recognizer"); - return; - } - - // 添加自定义命令 - std::vector> commands = { - {0, "kai deng"}, // 开灯 - {1, "guan deng"}, // 关灯 - {2, "ti gao liang du"}, // 提高亮度 - {3, "jiang di liang du"}, // 降低亮度 - {4, "bo fang yin yue"}, // 播放音乐 - {5, "ting zhi bo fang"} // 停止播放 - }; - - if (!recognizer->addCommands(commands)) { - ESP_LOGE("main", "Failed to add some commands"); - } - - // 注册回调函数 - recognizer->registerCommandCallback(commandCallback); - recognizer->registerStateCallback(stateCallback); - - // 开始识别 - if (!recognizer->start()) { - ESP_LOGE("main", "Failed to start speech recognition"); - return; - } - - ESP_LOGI("main", "Speech recognition system started successfully"); - -} - +#include #include "ToolsClass.h" #include "WifiConnectors.h" #include "CommClass.h" #include "sys_conf_singleton.h" #include "HttpOtaUpdater.h" -using namespace std::chrono; -const auto sleep_time = seconds{ - 5 -}; +#include "AudioOutput.h" + // OTA相关 HttpOtaUpdater otaUpdater; + void setupOtaCallbacks() { // 设置进度回调 otaUpdater.setProgressCallback([](int progress, int total) { @@ -222,15 +25,15 @@ void setupOtaCallbacks() { }); // 设置状态回调 - otaUpdater.setStateCallback([](HttpOtaUpdater::OtaState state, const std::string& message) { - const char* stateNames[] = { + otaUpdater.setStateCallback([](HttpOtaUpdater::OtaState state, const std::string &message) { + const char *stateNames[] = { "IDLE", "CONNECTING", "DOWNLOADING", "VERIFYING", "SUCCESS", "FAILED" }; ESP_LOGI("OTA", "State: %s - %s", stateNames[static_cast(state)], message.c_str()); }); // 设置完成回调 - otaUpdater.setFinishCallback([](bool success, const std::string& message) { + otaUpdater.setFinishCallback([](bool success, const std::string &message) { if (success) { ESP_LOGI("OTA", "Completed successfully: %s", message.c_str()); } else { @@ -243,6 +46,55 @@ void setupOtaCallbacks() { // otaUpdater.skipCertCommonNameCheck(true); // 仅用于测试 } + +// 把 HTTP 文件完整下载到 PSRAM,返回首地址和长度 +uint8_t *download_to_psram(const char *url, size_t *out_len) +{ + uint8_t *buf = nullptr; + size_t total = 0; + + esp_http_client_config_t cfg = { + .url = url, + .timeout_ms = 10000, + .keep_alive_enable = true, + }; + esp_http_client_handle_t client = esp_http_client_init(&cfg); + + if (esp_http_client_open(client, 0) != ESP_OK) { + ESP_LOGE("HTTP", "Failed to open HTTP connection"); + } + int content_len = esp_http_client_fetch_headers(client); + if (content_len <= 0) { + ESP_LOGE("HTTP", "Content-Length invalid"); + goto err; + } + buf = (uint8_t *)heap_caps_malloc(content_len, MALLOC_CAP_SPIRAM); + if (!buf) { + ESP_LOGE("HTTP", "PSRAM malloc %d bytes failed", content_len); + goto err; + } + + int read; + do { + read = esp_http_client_read(client, (char *)buf + total, content_len - total); + if (read > 0) total += read; + } while (read > 0 && total < content_len); + + if (total != content_len) { + ESP_LOGE("HTTP", "Download incomplete %d/%d", total, content_len); + heap_caps_free(buf); + buf = nullptr; + goto err; + } + *out_len = total; + ESP_LOGI("HTTP", "Download done, %d bytes in PSRAM", total); + + err: + esp_http_client_close(client); + esp_http_client_cleanup(client); + return buf; +} + // JSON 数据回调函数 /** 交互所使用的json内容 { @@ -250,14 +102,14 @@ void setupOtaCallbacks() { "xxx":"xxx", // 其他数据 ...... } + 此回调函数为核心业务处理函数!!! */ -void onJsonData(const cppjson::Json& json) -{ +void onJsonData(const cppjson::Json &json) { // 打印收到的 JSON ESP_LOGI("JSON_CALLBACK", "收到JSON数据: %s", json.dump().c_str()); // 解析消息类型 - const cppjson::Json& type = json["type"]; + const cppjson::Json &type = json["type"]; if (!type.isString()) return; std::string typeStr = type.asString(); @@ -276,11 +128,11 @@ void onJsonData(const cppjson::Json& json) ESP_LOGI("JSON_CALLBACK", "收到OTA消息"); // 进一步处理OTA消息 // 获取OTA中的版本信息 - const cppjson::Json& version = json["version"]; + const cppjson::Json &version = json["version"]; if (!version.isString()) return; std::string versionStr = version.asString(); // 获取OTA中的HTTP URL - const cppjson::Json& url = json["url"]; + const cppjson::Json &url = json["url"]; if (!url.isString()) return; std::string urlStr = url.asString(); // 告诉服务端,升级开始 @@ -288,12 +140,25 @@ void onJsonData(const cppjson::Json& json) response.set("type", cppjson::Json("ota_start")); WebSocketManager::getInstance()->sendJson(response); otaUpdater.start(urlStr); + } else if (typeStr == "audio") { + ESP_LOGI("JSON_CALLBACK", "收到音频消息"); + // 进一步解析音频消息内容 + // 获取音频信息 + cppjson::Json audio_url = json["audio_url"]; // 音频信息 + cppjson::Json audio_size = json["audio_size"]; // 音频信息 + if (!audio_url.isObject()) return; + if (!audio_size.isObject()) return; + std::cout << "base64: " << audio_url.asString() << std::endl; + std::cout << "size: " << audio_size.asInt() << std::endl; + size_t size; + const uint8_t *pcm_buf = download_to_psram(audio_url.asString().c_str(), &size); + // 播放音频 + AudioOutput::getInstance()->playPcmStream(pcm_buf, size); } - } // WebSocket事件回调函数 -void onWebSocketEvent(WebSocketEvent event, const std::string& message) { +void onWebSocketEvent(WebSocketEvent event, const std::string &message) { switch (event) { case WebSocketEvent::CONNECTED: ESP_LOGI("EVENT_CALLBACK", "WebSocket已连接: %s", message.c_str()); @@ -311,16 +176,15 @@ void onWebSocketEvent(WebSocketEvent event, const std::string& message) { } // 发送状态信息函数 -void sendStatus() -{ +void sendStatus() { cppjson::Json status = cppjson::Json::object(); status.set("type", cppjson::Json("status")); cppjson::Json data = cppjson::Json::object(); data.set("free_heap", cppjson::Json(esp_get_free_heap_size())) - .set("uptime", cppjson::Json(xTaskGetTickCount() * portTICK_PERIOD_MS / 1000)); + .set("uptime", cppjson::Json(xTaskGetTickCount() * portTICK_PERIOD_MS / 1000)); - status.set("data", data); // 嵌套对象 + status.set("data", data); // 嵌套对象 if (WebSocketManager::getInstance()->sendJson(status)) { ESP_LOGI("SEND", "已发送状态信息"); @@ -330,8 +194,7 @@ void sendStatus() } // 发送问候消息函数 -void sendGreeting() -{ +void sendGreeting() { cppjson::Json greeting = cppjson::Json::object(); greeting.set("type", cppjson::Json("greeting")) .set("message", cppjson::Json("Hello from ESP32-S3")) @@ -343,6 +206,7 @@ void sendGreeting() ESP_LOGE("SEND", "发送问候消息失败"); } } + void websocket_task() { TickType_t lastStatusTime = 0; TickType_t lastHeartbeatTime = 0; @@ -392,7 +256,8 @@ void createWebSocket() { vTaskDelay(1000 / portTICK_PERIOD_MS); } // 保存SN - SysConfJson::getInstance()->saveSN(ToolsClass::GenerateSN(ToolsClass::getChipMAC(), ToolsClass::getChipSerialNumber())); + SysConfJson::getInstance()->saveSN( + ToolsClass::GenerateSN(ToolsClass::getChipMAC(), ToolsClass::getChipSerialNumber())); // 读取SN std::string sn = SysConfJson::getInstance()->loadSN(); ESP_LOGI("conf", "loaded sn = %s", sn.c_str()); @@ -400,15 +265,15 @@ void createWebSocket() { // 配置WebSocket WebSocketConfig config; config.uri = "ws://" + std::string("192.168.1.11") + ":" + std::to_string(8080) + "/ws"; - config.auto_reconnect = true; // 自动重连 - config.reconnect_interval = 5000; // 重连间隔(毫秒) - config.heartbeat_interval = 30000; // 心跳间隔(毫秒) + config.auto_reconnect = true; // 自动重连 + config.reconnect_interval = 5000; // 重连间隔(毫秒) + config.heartbeat_interval = 30000; // 心跳间隔(毫秒) config.max_reconnect_attempts = 10; // 最大重连尝试次数 // TODO: 此处通信类存在线程重复创建bug,似乎是来自esp-idf的bug,待查证 // 初始化WebSocket管理器 if (!WebSocketManager::getInstance()->initialize(config)) { ESP_LOGE("APP_TASK", "WebSocket管理器初始化失败"); - vTaskDelete(NULL); + vTaskDelete(nullptr); return; } @@ -432,248 +297,120 @@ void createWebSocket() { std::thread websocket_thread = ThreadManager::createThread(websocket_config, websocket_task); websocket_thread.detach(); } + #include "LVGLRender.h" -#include -#define TAG "BIN_TEST" - -/* 从 SD 卡读任意 .bin 到堆 → 直接显示 */ -void test_show_bin(const char* fileName) -{ - ESP_LOGI(TAG, "=== fast bin test: %s ===", fileName); - - /* 1. 读文件 */ - std::string path = "/sdcard/" + std::string(fileName); - std::string data = SDFileManager::getInstance()->readFileSync(path.c_str()); - if (data.empty()) { - ESP_LOGE(TAG, "read fail"); - return; - } - size_t sz = data.size(); - ESP_LOGI(TAG, "file size = %zu", sz); - ESP_LOG_BUFFER_HEX(TAG, data.data(), 16); // 头 16 字节 - - /* 2. 拷到堆(LVGL 长期持有)*/ - void* buf = heap_caps_malloc(sz, MALLOC_CAP_8BIT); - memcpy(buf, data.data(), sz); - - /* 3. 离线量好的尺寸(先填 320×240 测试,不对再改)*/ - uint32_t w = 720; - uint32_t h = 720; - if (sz != w * h * 2) { // RGB565 每像素 2 字节 - ESP_LOGW(TAG, "size mismatch, expect %ld, got %zu", w * h * 2, sz); - } - - /* 4. 一次性描述符 */ - static lv_img_dsc_t dsc; - dsc.data = static_cast(buf); - dsc.header.cf = LV_IMG_CF_TRUE_COLOR; - dsc.header.w = w; - dsc.header.h = h; - dsc.data_size = sz; - - /* 5. 直接显示(不经过 PNG 解码器)*/ - lv_obj_t* img = lv_img_create(lv_scr_act()); - lv_img_set_src(img, &dsc); - lv_obj_center(img); - - ESP_LOGI(TAG, "bin displayed, w=%ld h=%ld", w, h); -} +#include "SimpleI2SForwarder.h" #include "lvpp.h" +#include "BaseApp.h" LV_FONT_DECLARE(SiYuanHeiTiGoogleBan); // 使用全局智能指针管理主要对象 static std::shared_ptr g_screen; -using namespace lvgl_cpp; -struct AppCalc : BaseApp { - using BaseApp::BaseApp; - void onShow() override { - Label(*this).text("Calculator").center(); +static std::shared_ptr g_home; +static std::shared_ptr g_menu; + +int pet_Test() { + /* + // 存档 + PetDAO dao(SDFileManager::getInstance()); + const string filename = "cheese_snow_leopard.json"; + cout << "\n===== 存档 =====" << endl; + if (dao.savePet(pet, filename)) + cout << "✅ 已保存到 " << filename << endl; + else + cout << "❌ 保存失败" << endl; + + // 5. 读档 + cout << "\n===== 读档 =====" << endl; + auto loaded = dao.loadPet(filename); + if (!loaded) { + cout << "❌ 读档失败" << endl; + return 0; } -}; -struct AppMusic : BaseApp { - using BaseApp::BaseApp; - void onShow() override { - btn_.emplace(*this); - gif.emplace(*this); - btn_->size(180, 60) - .align(LV_ALIGN_CENTER, nullptr, 0, 80) - .on(LV_EVENT_CLICKED, [&](lv_event_t*) { - /* 居中 GIF,背景黑 */ - lv_obj_set_style_bg_color(this->raw(), lv_color_black(), 0); - lv_obj_set_style_bg_opa(this->raw(), LV_OPA_COVER, 0); - gif->src("small_-min.gif") - .center(); - }); + cout << "✅ 读档成功,继续互动:" << endl; + printPet(*loaded); - /* 按钮文字 */ - lv_obj_t* label = lv_label_create(btn_->raw()); - lv_obj_set_style_text_font(label, &SiYuanHeiTiGoogleBan, LV_PART_MAIN); - lv_label_set_text(label, "Gif测试"); - lv_obj_center(label); - } -private: - std::optional btn_; // c++17 轻量 RAII - std::optional gif; -}; + // 6. 对新对象继续互动 + loaded->neglect(); printPet(*loaded); + loaded->feed(); printPet(*loaded); + */ + return 1; +} -class ButtonApp : public BaseApp { -public: - using BaseApp::BaseApp; // 继承 exit 按钮机制 - void onShow() override { - /* 居中按钮:180×60 px,圆角,现代蓝 */ - btn_.emplace(*this); - btn_->size(180, 60) - .align(LV_ALIGN_CENTER, nullptr, 0, 80) - .on(LV_EVENT_CLICKED, [](lv_event_t*) { - lvgl_cpp::Toast::show("咕咕嘎嘎!", - lvgl_cpp::Toast::Type::INFO, - 1000); - }); - LV_FONT_DECLARE(SiYuanHeiTiGoogleBan); - /* 按钮文字 */ - lv_obj_t* label = lv_label_create(btn_->raw()); - lv_obj_set_style_text_font(label, &SiYuanHeiTiGoogleBan, LV_PART_MAIN); - lv_label_set_text(label, "按我"); - lv_obj_center(label); - } - -private: - std::optional btn_; // c++17 轻量 RAII -}; -static std::shared_ptr g_home; -static std::shared_ptr g_menu; void Cpp_Hand() { - // testMIC(); - // testPetSystem(); - SDFileManager::getInstance()->tryInitSDCard(); + // 打印设备信息 + ESP_LOGI("CppHandle::Cpp_Hand", "当前固件版本 %s:", ToolsClass::getDeviceVersion().c_str()); + ESP_LOGI("CppHandle::Cpp_Hand", "当前设备MAC地址 %s:", ToolsClass::getChipMAC().c_str()); + ESP_LOGI("CppHandle::Cpp_Hand", "当前设备固件序列号 %s:", ToolsClass::getChipSerialNumber().c_str()); + + SDFileManager::getInstance()->tryInitSDCard(); // 初始化SD卡 LVGLRender::getInstance()->tryToInitRenderGif(); // 初始化lvgl驱动(包括任务循环) - LVGLRender::setFps(60); // 设置lvgl刷新频率 + LVGLRender::setFps(60); // 设置lvgl刷新频率 - /* LVGL 已经初始化,屏幕驱动已注册 */ - // auto scr = lvgl_cpp::Screen{}; // RAII,离开作用域自动 del - // lv_scr_load(scr.raw()); // 让 LVGL 把它当活动屏幕 - - // lvgl_cpp::Image img(scr); - // img.bin("pic_no_alp_swap.bin", 720, 720) - // .center(); - // - // /* 1. 创建按钮 */ - // /* 1. 黑色文字样式 */ - // static lvgl_cpp::Style txt_style; - // txt_style.text_color(lv_color_black()); - // - // /* 2. 按钮 */ - // lvgl_cpp::Button btn{scr}; - // btn.size(150, 60).pos(40, 40); - // /* 3. 先创建 Label 对象并保存,再链式调 */ - // lvgl_cpp::Label lbl{btn}; // 一定要存实例 - // lbl.text("Click Me") - // .add_style(&txt_style.s, LV_PART_MAIN); // 样式作用到文字本身 - // /* 3. 注册点击事件 */ - // btn.on(LV_EVENT_CLICKED, [](lv_event_t*){ - // lvgl_cpp::Toast::show("Saved successfully !"); - // /* 2. 警告,3 s,顶部 */ - // lvgl_cpp::Toast::show("SD card missing", lvgl_cpp::Toast::Type::WARN, 3000, LV_ALIGN_TOP_MID, 20); - // - // /* 3. 错误,1.5 s,底部右侧 */ - // lvgl_cpp::Toast::show("Network error", lvgl_cpp::Toast::Type::ERROR, 1500, LV_ALIGN_BOTTOM_RIGHT, -30); - // ESP_LOGI("BTN", "pressed"); - // }); - // - // /* 1. 消息框 */ - // // const char* btns[] = {"Yes", "No", ""}; - // // auto mbox = lvgl_cpp::MsgBox::create("Hint", "Delete file ?", btns); - // - // /* 3. 进度条(双向 + 动画) */ - // lvgl_cpp::Bar bar(scr); - // bar.range(0, 100) - // .start_value(20) // 左端 - // .value(80, LV_ANIM_ON) // 右端带动画 - // .anim_time(500) - // .size(200, 15) - // .align(LV_ALIGN_CENTER, nullptr, 0, 40); - // - // /* 4. 折线:画一个 △ */ - // std::vector triangle = {{0,0}, {40,0}, {20,40}, {0,0}}; - // lvgl_cpp::Line line(scr); - // line.points(triangle) - // .y_invert(false) - // .align(LV_ALIGN_CENTER, nullptr, 0, 100); - // - // lvgl_cpp::Battery bat(scr); - // bat.size(60, 30) // 手机经典尺寸 - // .percent(true) // 显示 50% - // .onRead([]() -> uint8_t { // 替换成你的 ADC/INA219 回调 - // static uint8_t v = 100; - // if (v) --v; - // return v; - // }) - // .align(LV_ALIGN_TOP_RIGHT, -10, 10); // 九宫格对齐 - // - // /* 3. 日期时间 */ - // lvgl_cpp::DateTime dt(scr); - // dt.format("%m/%d %a %H:%M") - // .onRead([](char* buf, size_t len){ - // /* 这里用 SNTP / RTC 填充,示例直接给假时间 */ - // snprintf(buf, len, "06/25 Tue 14:30"); - // }) - // .align(LV_ALIGN_BOTTOM_MID, nullptr, 0, -10); - - - // lvgl_cpp::ColorPicker cp(scr); - // cp.size(120, 120) - // .align(LV_ALIGN_BOTTOM_MID, nullptr, 0, -20) - // .on(LV_EVENT_VALUE_CHANGED, [&](lv_event_t*){ - // lv_color_t c = cp.color(); - // ESP_LOGI("CP", "rgb=%d,%d,%d", c.ch.red, c.ch.green_h, c.ch.blue); - // }); - - // 1. 创建屏幕 + // 创建屏幕 g_screen = std::make_shared(); lv_scr_load(g_screen->raw()); - // 2. 延迟创建界面组件 + // 延迟创建界面组件 auto init_timer = std::make_unique([&]() { ESP_LOGI("MAIN", "Initializing UI components..."); - // 创建主页 - g_home = std::make_shared(*g_screen); + g_home = std::make_shared(*g_screen); g_home->bg("pic360.bin", 360, 360) - .onBattery([]() -> uint8_t { - static uint8_t v = 100; - if (v) --v; - return 88; - }) - .onDateTime([](char* buf, size_t len) { - snprintf(buf, len, "06/25 Tue 14:30"); - }); - - // 创建菜单 - g_menu = std::make_shared(*g_screen); - g_menu->addItem("Calcu", 100, 50) - .addItem("Gif测试", 100, 50) - .addItem("Button", 100, 50) - .onClick([](const char* name) { - ESP_LOGI("APP", "Launching: %s", name); - - /* 1. 让工厂创建子应用 */ - auto app = AppFactory::create(name, *g_screen); - if (!app) return; - - /* 2. 隐藏菜单,显示应用 */ - lv_obj_add_flag(g_menu->raw(), LV_OBJ_FLAG_HIDDEN); - app->onShow(); - - /* 3. 应用退出时回到菜单 */ - app->onExit([app_ptr = app.release()]() { // 转移所有权到 lambda - lv_obj_clear_flag(g_menu->raw(), LV_OBJ_FLAG_HIDDEN); - lv_obj_del(app_ptr->raw()); // 自毁 + .onBattery([]() -> uint8_t { + return static_cast(ToolsClass::getInstance()->getBatteryPer()); + }) + .onDateTime([](char *buf, const size_t len) { + snprintf(buf, len, "06/25 Tue 14:30"); }); - }); + // 创建菜单 + g_menu = std::make_shared(*g_screen); + g_menu->addItem("Calcu", 100, 50) + .addItem("Gif测试", 100, 50) + .addItem("Button", 100, 50) + .addItem("AI测试", 100, 50) + .addItem("长按录音", 100, 50) + .addItem("宠物样例", 100, 50) + .onClick([](const char *name) { + ESP_LOGI("APP", "Launching: %s", name); + // 让工厂创建子应用 + auto app = lvgl_cpp::AppFactory::create(name, *g_screen); + if (!app) return; + // 隐藏菜单,显示应用 + lv_obj_add_flag(g_menu->raw(), LV_OBJ_FLAG_HIDDEN); + app->onShow(); + + // 应用退出时回到菜单 + app->onExit([app_ptr = app.release()]() { + // 转移所有权到 lambda + lv_obj_clear_flag(g_menu->raw(), LV_OBJ_FLAG_HIDDEN); + lv_obj_del(app_ptr->raw()); // 自毁 + }); + }); + g_menu->onBack([]() { + ESP_LOGI("AppMenu", "返回主页回调执行"); + + // 添加调试信息 + if (g_menu && g_menu->raw()) { + ESP_LOGI("AppMenu", "隐藏菜单"); + lv_obj_add_flag(g_menu->raw(), LV_OBJ_FLAG_HIDDEN); + } else { + ESP_LOGE("AppMenu", "菜单对象无效"); + } + + if (g_home && g_home->raw()) { + ESP_LOGI("AppMenu", "显示主页"); + lv_obj_clear_flag(g_home->raw(), LV_OBJ_FLAG_HIDDEN); + } else { + ESP_LOGE("AppMenu", "主页对象无效"); + } + + // 强制刷新显示 + lv_refr_now(nullptr); + }); lv_obj_add_flag(g_menu->raw(), LV_OBJ_FLAG_HIDDEN); - // 安全的事件连接 g_home->onOpenMenu([]() { if (g_home && g_menu) { @@ -681,44 +418,43 @@ void Cpp_Hand() { lv_obj_clear_flag(g_menu->raw(), LV_OBJ_FLAG_HIDDEN); } }); - ESP_LOGI("MAIN", "UI initialization complete"); }, 1000, true); // 1秒后执行一次 // 注册应用 - AppFactory::registerApp("Calcu", [](Obj& p) { + lvgl_cpp::AppFactory::registerApp("Calcu", [](lvgl_cpp::Obj &p) { return std::make_unique(p); }); - AppFactory::registerApp("Gif测试", [](Obj& p) { + lvgl_cpp::AppFactory::registerApp("Gif测试", [](lvgl_cpp::Obj &p) { return std::make_unique(p); }); - AppFactory::registerApp("Button", [](lvgl_cpp::Obj& p) { + lvgl_cpp::AppFactory::registerApp("Button", [](lvgl_cpp::Obj &p) { return std::make_unique(p); }); - - // test_show_bin("pic_no_alp_swap.bin"); - - // LVGLRender::getInstance()->RenderGif("sequence02mmm.gif"); - - ESP_LOGI("CppHandle::Cpp_Hand", "当前固件版本 %s:", ToolsClass::getDeviceVersion().c_str()); - - ESP_LOGI("CppHandle::Cpp_Hand", "当前设备MAC地址 %s:", ToolsClass::getChipMAC().c_str()); - ESP_LOGI("CppHandle::Cpp_Hand", "当前设备固件序列号 %s:", ToolsClass::getChipSerialNumber().c_str()); + lvgl_cpp::AppFactory::registerApp("AI测试", [](lvgl_cpp::Obj &p) { + return std::make_unique(p); + }); + lvgl_cpp::AppFactory::registerApp("长按录音", [](lvgl_cpp::Obj &p) { + return std::make_unique(p); + }); + lvgl_cpp::AppFactory::registerApp("宠物样例", [](lvgl_cpp::Obj &p) { + return std::make_unique(p); + }); // 连接wifi - // WifiConnectors::getInstance()->connectWifi("Misaki-2.4G", "88888888", 5); + WifiConnectors::getInstance()->connectWifi("Misaki-2.4G", "88888888", 5); // 创建WebSocket - // createWebSocket(); + createWebSocket(); // 设置OTA回调 // setupOtaCallbacks(); - - while (true) { // 主线程线程循环 - // ThreadManager::print_sys_memory(); // 打印系统内存使用情况 + while (true) { + // 主线程线程循环 + ThreadManager::print_sys_memory(); // 打印系统内存使用情况 // ThreadManager::stats_task(); // 打印任务统计信息 - ESP_LOGI("APP_TASK", "Battery is:%ld", ToolsClass::getInstance()->getBatteryPer()); - ESP_LOGI("APP_TASK", "Battery is:%f", ToolsClass::getInstance()->getBatteryVolts()); - std::this_thread::sleep_for(sleep_time); // 休眠5秒 + // ESP_LOGI("APP_TASK", "Battery is:%ld", ToolsClass::getInstance()->getBatteryPer()); + // ESP_LOGI("APP_TASK", "Battery is:%f", ToolsClass::getInstance()->getBatteryVolts()); + std::this_thread::sleep_for(std::chrono::seconds(1)); // 休眠 1 秒 } } diff --git a/Bionic_Core/PetBaseClass/PetDao.cpp b/Bionic_Core/PetBaseClass/PetDao.cpp index 7c62341..720bd97 100644 --- a/Bionic_Core/PetBaseClass/PetDao.cpp +++ b/Bionic_Core/PetBaseClass/PetDao.cpp @@ -47,48 +47,43 @@ PetDAO::PetDAO(SDFileManager* fileManager) : fileManager(fileManager) { } bool PetDAO::savePet(const std::shared_ptr& pet, const std::string& filename) { - // 转换为JSON - cJSON* json = petToJson(pet); - if (!json) { + try { + // 转换为JSON + cppjson::Json json = petToJson(pet); + + // 序列化为字符串 + std::string jsonStr = json.dump(); + std::string fullPath = std::string(PET_DATA_DIR) + "/" + filename; + + // 保存到文件 + const bool success = fileManager->writeFileSync(fullPath.c_str(), jsonStr.c_str(), jsonStr.size(), "w"); + + return success; + } catch (const std::exception& e) { + std::cerr << "Save pet failed: " << e.what() << std::endl; return false; } - - // 转换为字符串 - char* jsonStr = cJSON_PrintUnformatted(json); - std::string fullPath = std::string(PET_DATA_DIR) + "/" + filename; - - // 保存到文件 - bool success = fileManager->writeFileSync(fullPath.c_str(), jsonStr); - - // 清理资源 - free(jsonStr); - cJSON_Delete(json); - - return success; } std::shared_ptr PetDAO::loadPet(const std::string& filename) { - std::string fullPath = std::string(PET_DATA_DIR) + "/" + filename; + try { + std::string fullPath = std::string(PET_DATA_DIR) + "/" + filename; - // 读取文件内容 - std::string content = fileManager->readFileSync(fullPath.c_str()); - if (content.empty()) { + // 读取文件内容 + std::string content = fileManager->readFileSync(fullPath.c_str()); + if (content.empty()) { + return nullptr; + } + + // 解析JSON + cppjson::Json json = cppjson::Json::parse(content); + + // 创建宠物对象 + return petFromJson(json); + } catch (const std::exception& e) { + std::cerr << "Load pet failed: " << e.what() << std::endl; return nullptr; } - - // 解析JSON - cJSON* json = cJSON_Parse(content.c_str()); - if (!json) { - return nullptr; - } - - // 创建宠物对象 - auto pet = petFromJson(json); - - // 清理资源 - cJSON_Delete(json); - - return pet; } std::vector PetDAO::listPetFiles() { @@ -100,204 +95,178 @@ bool PetDAO::deletePetFile(const std::string& filename) { return fileManager->rmCommand(fullPath.c_str(), false); } -cJSON* PetDAO::petToJson(const std::shared_ptr& pet) { - cJSON* json = cJSON_CreateObject(); - if (!json) return nullptr; +cppjson::Json PetDAO::petToJson(const std::shared_ptr& pet) { + auto json = cppjson::Json::object(); // 添加基本信息 - cJSON_AddStringToObject(json, "name", pet->pet_info.pet_name.c_str()); - cJSON_AddNumberToObject(json, "hp", pet->pet_info.pet_hp); - cJSON_AddNumberToObject(json, "density", pet->pet_info.pet_density); - cJSON_AddStringToObject(json, "identity", pet->pet_info.pet_identity.c_str()); + json.set("name", cppjson::Json(pet->pet_info.pet_name)) + .set("hp", cppjson::Json(pet->pet_info.pet_hp)) + .set("density", cppjson::Json(pet->pet_info.pet_density)) + .set("identity", cppjson::Json(pet->pet_info.pet_identity)); // 添加阶段策略 - cJSON* stageJson = stageStrategyToJson(pet->pet_stage_strategy); - if (stageJson) { - cJSON_AddItemToObject(json, "stage_strategy", stageJson); + auto stageJson = stageStrategyToJson(pet->pet_stage_strategy); + if (!stageJson.isNull()) { + json.set("stage_strategy", stageJson); } // 添加动作策略 - cJSON* actionJson = actionStrategyToJson(pet->pet_action_strategy); - if (actionJson) { - cJSON_AddItemToObject(json, "action_strategy", actionJson); + auto actionJson = actionStrategyToJson(pet->pet_action_strategy); + if (!actionJson.isNull()) { + json.set("action_strategy", actionJson); } return json; } -std::shared_ptr PetDAO::petFromJson(cJSON* json) { - if (!json) return nullptr; +std::shared_ptr PetDAO::petFromJson(const cppjson::Json& json) { + if (json.isNull()) return nullptr; // 创建宠物基本信息 PetBaseInfo info; - cJSON* nameItem = cJSON_GetObjectItemCaseSensitive(json, "name"); - if (cJSON_IsString(nameItem)) { - info.pet_name = nameItem->valuestring; + + if (json["name"].isString()) { + info.pet_name = json["name"].asString(); } - cJSON* hpItem = cJSON_GetObjectItemCaseSensitive(json, "hp"); - if (cJSON_IsNumber(hpItem)) { - info.pet_hp = hpItem->valueint; + if (json["hp"].isNumber()) { + info.pet_hp = json["hp"].asInt(); } - cJSON* densityItem = cJSON_GetObjectItemCaseSensitive(json, "density"); - if (cJSON_IsNumber(densityItem)) { - info.pet_density = densityItem->valueint; + if (json["density"].isNumber()) { + info.pet_density = json["density"].asInt(); } - cJSON* identityItem = cJSON_GetObjectItemCaseSensitive(json, "identity"); - if (cJSON_IsString(identityItem)) { - info.pet_identity = identityItem->valuestring; + if (json["identity"].isString()) { + info.pet_identity = json["identity"].asString(); } // 创建阶段策略 - cJSON* stageJson = cJSON_GetObjectItemCaseSensitive(json, "stage_strategy"); - auto stageStrategy = stageStrategyFromJson(stageJson); + auto stageStrategy = stageStrategyFromJson(json["stage_strategy"]); // 创建动作策略 - cJSON* actionJson = cJSON_GetObjectItemCaseSensitive(json, "action_strategy"); - auto actionStrategy = actionStrategyFromJson(actionJson); + auto actionStrategy = actionStrategyFromJson(json["action_strategy"]); // 创建宠物对象 return std::make_shared(info, stageStrategy, actionStrategy); } -cJSON* PetDAO::stageStrategyToJson(const std::shared_ptr& strategy) { - if (!strategy) return nullptr; - cJSON* json = cJSON_CreateObject(); - if (!json) return nullptr; +cppjson::Json PetDAO::stageStrategyToJson(const std::shared_ptr& strategy) { + if (!strategy) return {}; + + auto json = cppjson::Json::object(); // 添加当前阶段 - cJSON_AddStringToObject(json, "current_stage", - stageTypeToString(strategy->getCurrentStageType()).c_str()); + json.set("current_stage", cppjson::Json(stageTypeToString(strategy->getCurrentStageType()))); // 添加阶段模型映射 - cJSON* modelMap = cJSON_CreateObject(); + auto modelMap = cppjson::Json::object(); for (const auto& pair : strategy->getStageModelMap()) { - cJSON_AddStringToObject(modelMap, - stageTypeToString(pair.first).c_str(), - pair.second.c_str()); + modelMap.set(stageTypeToString(pair.first), cppjson::Json(pair.second)); } - cJSON_AddItemToObject(json, "stage_model_map", modelMap); + json.set("stage_model_map", modelMap); // 添加阶段音频映射 - cJSON* audioMap = cJSON_CreateObject(); + auto audioMap = cppjson::Json::object(); for (const auto& pair : strategy->getStageAudioMap()) { - cJSON_AddStringToObject(audioMap, - stageTypeToString(pair.first).c_str(), - pair.second.c_str()); + audioMap.set(stageTypeToString(pair.first), cppjson::Json(pair.second)); } - cJSON_AddItemToObject(json, "stage_audio_map", audioMap); + json.set("stage_audio_map", audioMap); return json; } -std::shared_ptr PetDAO::stageStrategyFromJson(cJSON* json) { - if (!json) return nullptr; +std::shared_ptr PetDAO::stageStrategyFromJson(const cppjson::Json& json) { + if (json.isNull()) return nullptr; auto strategy = std::make_shared(); // 获取当前阶段 - cJSON* currentStageItem = cJSON_GetObjectItemCaseSensitive(json, "current_stage"); - if (cJSON_IsString(currentStageItem)) { - strategy->current_stage = stringToStageType(currentStageItem->valuestring); + if (json["current_stage"].isString()) { + strategy->current_stage = stringToStageType(json["current_stage"].asString()); } // 获取阶段模型映射 - cJSON* modelMapItem = cJSON_GetObjectItemCaseSensitive(json, "stage_model_map"); - if (cJSON_IsObject(modelMapItem)) { - cJSON* child = modelMapItem->child; - while (child) { - if (cJSON_IsString(child)) { - PetStageType stage = stringToStageType(child->string); - strategy->stage_model_map[stage] = child->valuestring; + auto modelMapJson = json["stage_model_map"]; + if (modelMapJson.isObject()) { + for (const auto& item : modelMapJson.items()) { + if (item.second.isString()) { + PetStageType stage = stringToStageType(item.first); + strategy->stage_model_map[stage] = item.second.asString(); } - child = child->next; } } // 获取阶段音频映射 - cJSON* audioMapItem = cJSON_GetObjectItemCaseSensitive(json, "stage_audio_map"); - if (cJSON_IsObject(audioMapItem)) { - cJSON* child = audioMapItem->child; - while (child) { - if (cJSON_IsString(child)) { - PetStageType stage = stringToStageType(child->string); - strategy->stage_audio_map[stage] = child->valuestring; + auto audioMapJson = json["stage_audio_map"]; + if (audioMapJson.isObject()) { + for (const auto& item : audioMapJson.items()) { + if (item.second.isString()) { + PetStageType stage = stringToStageType(item.first); + strategy->stage_audio_map[stage] = item.second.asString(); } - child = child->next; } } return strategy; } -cJSON* PetDAO::actionStrategyToJson(const std::shared_ptr& strategy) { - if (!strategy) return nullptr; +cppjson::Json PetDAO::actionStrategyToJson(const std::shared_ptr& strategy) { + if (!strategy) return {}; - cJSON* json = cJSON_CreateObject(); - if (!json) return nullptr; + auto json = cppjson::Json::object(); // 添加当前动作 - cJSON_AddStringToObject(json, "current_action", - actionTypeToString(strategy->getCurrentActionType()).c_str()); + json.set("current_action", cppjson::Json(actionTypeToString(strategy->getCurrentActionType()))); // 添加动作模型映射 - cJSON* modelMap = cJSON_CreateObject(); + auto modelMap = cppjson::Json::object(); for (const auto& pair : strategy->getActionModelMap()) { - cJSON_AddStringToObject(modelMap, - actionTypeToString(pair.first).c_str(), - pair.second.c_str()); + modelMap.set(actionTypeToString(pair.first), cppjson::Json(pair.second)); } - cJSON_AddItemToObject(json, "action_model_map", modelMap); + json.set("action_model_map", modelMap); // 添加动作音频映射 - cJSON* audioMap = cJSON_CreateObject(); + auto audioMap = cppjson::Json::object(); for (const auto& pair : strategy->getActionAudioMap()) { - cJSON_AddStringToObject(audioMap, - actionTypeToString(pair.first).c_str(), - pair.second.c_str()); + audioMap.set(actionTypeToString(pair.first), cppjson::Json(pair.second)); } - cJSON_AddItemToObject(json, "action_audio_map", audioMap); + json.set("action_audio_map", audioMap); return json; } -std::shared_ptr PetDAO::actionStrategyFromJson(cJSON* json) { - if (!json) return nullptr; + +std::shared_ptr PetDAO::actionStrategyFromJson(const cppjson::Json& json) { + if (json.isNull()) return nullptr; auto strategy = std::make_shared(); // 获取当前动作 - cJSON* currentActionItem = cJSON_GetObjectItemCaseSensitive(json, "current_action"); - if (cJSON_IsString(currentActionItem)) { - strategy->current_action = stringToActionType(currentActionItem->valuestring); + if (json["current_action"].isString()) { + strategy->current_action = stringToActionType(json["current_action"].asString()); } // 获取动作模型映射 - cJSON* modelMapItem = cJSON_GetObjectItemCaseSensitive(json, "action_model_map"); - if (cJSON_IsObject(modelMapItem)) { - cJSON* child = modelMapItem->child; - while (child) { - if (cJSON_IsString(child)) { - PetActionType action = stringToActionType(child->string); - strategy->action_model_map[action] = child->valuestring; + auto modelMapJson = json["action_model_map"]; + if (modelMapJson.isObject()) { + for (const auto& item : modelMapJson.items()) { + if (item.second.isString()) { + PetActionType action = stringToActionType(item.first); + strategy->action_model_map[action] = item.second.asString(); } - child = child->next; } } // 获取动作音频映射 - cJSON* audioMapItem = cJSON_GetObjectItemCaseSensitive(json, "action_audio_map"); - if (cJSON_IsObject(audioMapItem)) { - cJSON* child = audioMapItem->child; - while (child) { - if (cJSON_IsString(child)) { - PetActionType action = stringToActionType(child->string); - strategy->action_audio_map[action] = child->valuestring; + auto audioMapJson = json["action_audio_map"]; + if (audioMapJson.isObject()) { + for (const auto& item : audioMapJson.items()) { + if (item.second.isString()) { + PetActionType action = stringToActionType(item.first); + strategy->action_audio_map[action] = item.second.asString(); } - child = child->next; } } diff --git a/Bionic_Core/PetBaseClass/PetDao.h b/Bionic_Core/PetBaseClass/PetDao.h index 80331d7..393b0a3 100644 --- a/Bionic_Core/PetBaseClass/PetDao.h +++ b/Bionic_Core/PetBaseClass/PetDao.h @@ -5,62 +5,61 @@ #include "PetBaseClass.h" #include "SDFileManager.h" -#include "cJSON.h" +#include "cpp_json.h" #include // 辅助函数:枚举类型与字符串的转换 namespace PetEnumConverter { // PetStageType 转换 std::string stageTypeToString(PetStageType stage); - PetStageType stringToStageType(const std::string& str); - + PetStageType stringToStageType(const std::string &str); // PetActionType 转换 std::string actionTypeToString(PetActionType action); - PetActionType stringToActionType(const std::string& str); + PetActionType stringToActionType(const std::string &str); } // PetDAO 类 - 负责宠物的数据持久化 class PetDAO { public: // 构造函数,需要SDFileManager实例 - explicit PetDAO(SDFileManager* fileManager); + explicit PetDAO(SDFileManager *fileManager); // 保存宠物数据到文件 - bool savePet(const std::shared_ptr& pet, const std::string& filename); + bool savePet(const std::shared_ptr &pet, const std::string &filename); // 从文件加载宠物数据 - std::shared_ptr loadPet(const std::string& filename); + std::shared_ptr loadPet(const std::string &filename); // 获取所有保存的宠物文件列表 std::vector listPetFiles(); // 删除宠物文件 - bool deletePetFile(const std::string& filename); + bool deletePetFile(const std::string &filename); private: // 将宠物数据转换为JSON对象 - cJSON* petToJson(const std::shared_ptr& pet); + cppjson::Json petToJson(const std::shared_ptr &pet); // 返回 cppjson::Json // 从JSON对象创建宠物 - std::shared_ptr petFromJson(cJSON* json); + std::shared_ptr petFromJson(const cppjson::Json &json); // 参数改为 cppjson::Json // 将阶段策略转换为JSON对象 - cJSON* stageStrategyToJson(const std::shared_ptr& strategy); + cppjson::Json stageStrategyToJson(const std::shared_ptr &strategy); // 从JSON对象创建阶段策略 - std::shared_ptr stageStrategyFromJson(cJSON* json); + std::shared_ptr stageStrategyFromJson(const cppjson::Json &json); // 将动作策略转换为JSON对象 - cJSON* actionStrategyToJson(const std::shared_ptr& strategy); + cppjson::Json actionStrategyToJson(const std::shared_ptr &strategy); // 从JSON对象创建动作策略 - std::shared_ptr actionStrategyFromJson(cJSON* json); + std::shared_ptr actionStrategyFromJson(const cppjson::Json &json); // 文件管理器实例 - SDFileManager* fileManager; + SDFileManager *fileManager; // 宠物数据存储目录 - static constexpr const char* PET_DATA_DIR = "/sdcard/pet_data"; + static constexpr const char *PET_DATA_DIR = "/sdcard/pet_data"; }; /** @@ -105,4 +104,4 @@ private: } } } - */ \ No newline at end of file + */ diff --git a/Bionic_Core/PetBaseClass/PetObserver.h b/Bionic_Core/PetBaseClass/PetObserver.h index 06557f3..a4b3312 100644 --- a/Bionic_Core/PetBaseClass/PetObserver.h +++ b/Bionic_Core/PetBaseClass/PetObserver.h @@ -3,10 +3,13 @@ // #pragma once #include "PetInterface.h" +// 用于回调函数传递回调类型,以区分回调的是宠物动作还是宠物阶段 +enum class PetType : uint8_t {Action, Stage}; + // 宠物音频播放观察者类,继承自宠物观察者 class PetAudioStrategy : public PetObserver, public std::enable_shared_from_this { public: - using AudioCallback = std::function; + using AudioCallback = std::function; /** * 构造时候就将“事件→音频”两张表填好 * @param actionAudios 动作→音频 @@ -66,7 +69,7 @@ public: if (!audio_callback) return; auto it = action_audio.find(action); if (it != action_audio.end()) { - audio_callback(it->second); // 播放对应音频 + audio_callback(PetType::Action, it->second); // 播放对应音频 } } /** @@ -79,7 +82,7 @@ public: if (!audio_callback) return; auto it = stage_audio.find(newStage); // 注意:播“新阶段”的音频 if (it != stage_audio.end()) { - audio_callback(it->second); + audio_callback(PetType::Stage, it->second); } } private: @@ -95,7 +98,7 @@ private: // 渲染器观察者类,继承自PetObserver class PetRendererStrategy : public PetObserver, public std::enable_shared_from_this { public: - using RenderCallback = std::function; + using RenderCallback = std::function; /** * 构造函数,可以传入动作到模型路径和阶段到模型路径的映射表 * @param actionModels 动作→模型路径映射 @@ -120,7 +123,7 @@ public: } ~PetRendererStrategy() override { // 自动取消注册 - if (auto subject = pet_subject.lock()) { + if (const auto subject = pet_subject.lock()) { subject->removeObserver(shared_from_this()); } } @@ -148,11 +151,11 @@ public: * 宠物动作时触发[Observer接口实现] * @param action 动作类型 */ - void onPetAction(PetActionType action) override { + void onPetAction(const PetActionType action) override { if (!render_callback) return; - auto it = action_models.find(action); + const auto it = action_models.find(action); if (it != action_models.end()) { - render_callback(it->second); // 渲染对应动作模型 + render_callback(PetType::Action, it->second); // 渲染对应动作模型 } } /** @@ -160,11 +163,11 @@ public: * @param oldStage 旧阶段 * @param newStage 新阶段 */ - void onPetStageChange(PetStageType oldStage, PetStageType newStage) override { + void onPetStageChange(PetStageType oldStage, const PetStageType newStage) override { if (!render_callback) return; - auto it = stage_models.find(newStage); // 注意:渲染"新阶段"的模型 + const auto it = stage_models.find(newStage); // 注意:渲染"新阶段"的模型,实际上并没有使用到旧阶段,但还是保留,留一手 if (it != stage_models.end()) { - render_callback(it->second); + render_callback(PetType::Stage, it->second); } } /** diff --git a/Bionic_Core/ToolsClass/AudioOutput/AudioOutput.cpp b/Bionic_Core/ToolsClass/AudioOutput/AudioOutput.cpp index 25884d3..4760247 100644 --- a/Bionic_Core/ToolsClass/AudioOutput/AudioOutput.cpp +++ b/Bionic_Core/ToolsClass/AudioOutput/AudioOutput.cpp @@ -1,7 +1,6 @@ // // Created by misaki on 2025/9/9. // - #include "AudioOutput.h" #include "esp_log.h" #include @@ -81,8 +80,8 @@ bool AudioOutput::playSync(const char* directory, const char* fileName) { return false; } -void AudioOutput::playAsync(const char* directory, const char* fileName, AudioCallback callback) { - ThreadConfig config = getThreadConfig("play_async"); +void AudioOutput::playAsync(const char* directory, const char* fileName, const AudioCallback& callback) { + const ThreadConfig config = getThreadConfig("play_async"); ThreadManager::createThread(config, [this, directory = std::string(directory), fileName = std::string(fileName), callback]() { @@ -90,9 +89,9 @@ void AudioOutput::playAsync(const char* directory, const char* fileName, AudioCa }).detach(); } -void AudioOutput::playInternal(const char* directory, const char* fileName, AudioCallback callback) { +void AudioOutput::playInternal(const char* directory, const char* fileName, const AudioCallback& callback) { bool success = false; - AudioState finalState = AudioState::ERROR; + auto finalState = AudioState::ERROR; { std::lock_guard lock(stateMutex); @@ -245,4 +244,122 @@ ThreadConfig AudioOutput::getThreadConfig(const char* operation) { void AudioOutput::setState(AudioState newState) { std::lock_guard lock(stateMutex); currentState = newState; +} + +bool AudioOutput::playPcmFile(const char* filePath, + uint32_t sampleRate, + i2s_data_bit_width_t bits, + i2s_slot_mode_t ch) +{ + // 简单文件尺寸获取 + FILE* f = fopen(filePath, "rb"); + if (!f) return false; + fseek(f, 0, SEEK_END); + const size_t bytes = ftell(f); + fclose(f); + + playPcmCommon(filePath, bytes, true, sampleRate, bits, ch, nullptr); + return getState() == AudioState::PLAYING; +} + +bool AudioOutput::playPcmStream(const uint8_t* pcmData, + size_t dataBytes, + uint32_t sampleRate, + i2s_data_bit_width_t bits, + i2s_slot_mode_t ch) +{ + playPcmCommon(pcmData, dataBytes, false, sampleRate, bits, ch, nullptr); + return getState() == AudioState::PLAYING; +} + +void AudioOutput::playPcmFileAsync(const char* filePath, + uint32_t sampleRate, + i2s_data_bit_width_t bits, + i2s_slot_mode_t ch, + const AudioCallback& cb) +{ + ThreadConfig cfg = getThreadConfig("pcm_file"); + std::thread([=](){ + playPcmCommon(filePath, 0, true, sampleRate, bits, ch, cb); + }).detach(); +} + +void AudioOutput::playPcmStreamAsync(const uint8_t* pcmData, + size_t dataBytes, + uint32_t sampleRate, + i2s_data_bit_width_t bits, + i2s_slot_mode_t ch, + const AudioCallback& cb) +{ + ThreadConfig cfg = getThreadConfig("pcm_stream"); + std::thread([=](){ + playPcmCommon(pcmData, dataBytes, false, sampleRate, bits, ch, cb); + }).detach(); +} + +void AudioOutput::playPcmCommon(const void* source, + size_t bytes, + bool isFile, + uint32_t sampleRate, + i2s_data_bit_width_t bits, + i2s_slot_mode_t ch, + const AudioCallback& cb) +{ + // 停止旧播放 + stop(); + // 重新配置 I2S 时钟/位宽/声道 + bsp_i2s_reconfig_clk(sampleRate, bits, ch); + // 打开“文件”或“内存”数据源 + FILE* f = nullptr; + const uint8_t* mem = nullptr; + size_t memLeft = 0; + if (isFile) { + f = fopen(static_cast(source), "rb"); + if (!f) { + if (cb) cb(AudioState::ERROR, static_cast(source)); + return; + } + } else { + mem = static_cast(source); + memLeft = bytes; + } + // 状态置为 PLAYING + { + std::lock_guard lk(stateMutex); + currentState = AudioState::PLAYING; + currentFilePath = isFile ? static_cast(source) : ""; + } + if (cb) cb(AudioState::PLAYING, currentFilePath.c_str()); + + // 循环送 PCM 数据到 I2S + constexpr size_t CHUNK = 512; // 任意 2 的幂 + int16_t buf[CHUNK]; + size_t bw; + while (true) { + size_t rd = 0; + if (isFile) { + rd = fread(buf, 1, sizeof(buf), f); + } else { + rd = memLeft > sizeof(buf) ? sizeof(buf) : memLeft; + memcpy(buf, mem, rd); + mem += rd; + memLeft -= rd; + } + if (rd == 0) break; + + // 音量实时缩放(复用已有逻辑) + const float vf = currentVolume / 100.0f; + for (size_t i = 0; i < rd / 2; ++i) buf[i] = static_cast(buf[i] * vf); + + // 写 I2S + i2s_channel_write(i2s_tx_chan, buf, rd, &bw, portMAX_DELAY); + } + + // 播放结束 + if (f) fclose(f); + { + std::lock_guard lk(stateMutex); + currentState = AudioState::STOPPED; + } + if (cb) cb(AudioState::STOPPED, currentFilePath.c_str()); } \ No newline at end of file diff --git a/Bionic_Core/ToolsClass/AudioOutput/AudioOutput.h b/Bionic_Core/ToolsClass/AudioOutput/AudioOutput.h index 3a4c24a..d2de2d6 100644 --- a/Bionic_Core/ToolsClass/AudioOutput/AudioOutput.h +++ b/Bionic_Core/ToolsClass/AudioOutput/AudioOutput.h @@ -1,14 +1,12 @@ // // Created by misaki on 2025/9/9. // - - - #pragma once - #include #include #include +#include + #include "ThreadManager.h" #include "SDFileManager.h" @@ -88,7 +86,7 @@ public: * @param fileName 文件名 * @param callback 回调函数 */ - void playAsync(const char* directory, const char* fileName, AudioCallback callback = nullptr); + void playAsync(const char* directory, const char* fileName, const AudioCallback& callback = nullptr); /** * 暂停播放 @@ -175,6 +173,58 @@ public: */ bool isFinished() const; + /** + * 播放 PCM 文件(阻塞) + * @param filePath PCM 文件路径 + * @param sampleRate 采样率 + * @param bits 数据位宽 + * @param ch 插槽模式 + * @return 是否成功 + */ + bool playPcmFile(const char* filePath, + uint32_t sampleRate = 16000, + i2s_data_bit_width_t bits = I2S_DATA_BIT_WIDTH_16BIT, + i2s_slot_mode_t ch = I2S_SLOT_MODE_MONO); + + // 异步播放 PCM 文件 + void playPcmFileAsync(const char* filePath, + uint32_t sampleRate = 16000, + i2s_data_bit_width_t bits = I2S_DATA_BIT_WIDTH_16BIT, + i2s_slot_mode_t ch = I2S_SLOT_MODE_MONO, + const AudioCallback& cb = nullptr); + /** + * 播放内存 PCM 流(阻塞) + * @param pcmData PCM 数据 + * @param dataBytes 数据字节数 + * @param sampleRate 采样率 + * @param bits 数据位宽 + * @param ch 插槽模式 + * @return 是否成功 + */ + bool playPcmStream(const uint8_t* pcmData, + size_t dataBytes, + uint32_t sampleRate = 16000, + i2s_data_bit_width_t bits = I2S_DATA_BIT_WIDTH_16BIT, + i2s_slot_mode_t ch = I2S_SLOT_MODE_MONO); + + // 异步播放 PCM 流 + void playPcmStreamAsync(const uint8_t* pcmData, + size_t dataBytes, + uint32_t sampleRate = 16000, + i2s_data_bit_width_t bits = I2S_DATA_BIT_WIDTH_16BIT, + i2s_slot_mode_t ch = I2S_SLOT_MODE_MONO, + const AudioCallback& cb = nullptr); + +private: + // 通用 PCM 播放实现 + void playPcmCommon(const void* source, + size_t bytes, + bool isFile, + uint32_t sampleRate, + i2s_data_bit_width_t bits, + i2s_slot_mode_t ch, + const AudioCallback& cb); + private: // 私有构造函数 AudioOutput(); @@ -184,7 +234,7 @@ private: ThreadConfig getThreadConfig(const char* operation); // 内部播放实现 - void playInternal(const char* directory, const char* fileName, AudioCallback callback); + void playInternal(const char* directory, const char* fileName, const AudioCallback& callback); // 状态转换辅助方法 void setState(AudioState newState); diff --git a/Bionic_Core/ToolsClass/CppJson/cpp_json.h b/Bionic_Core/ToolsClass/CppJson/cpp_json.h index 2b4666c..7b4794f 100644 --- a/Bionic_Core/ToolsClass/CppJson/cpp_json.h +++ b/Bionic_Core/ToolsClass/CppJson/cpp_json.h @@ -32,14 +32,15 @@ std::cout << "\n";\n */ -// cpp_json.hpp +// cpp_json.h #pragma once #include +#include #include #include #include -#include #include +#include namespace cppjson { @@ -152,42 +153,176 @@ public: cJSON_AddItemToArray(ptr_, cJSON_Duplicate(v.ptr_, 1)); return *this; } + // 便捷的 append 方法重载 + Json& append(int value) { + return append(Json(value)); + } + Json& append(double value) { + return append(Json(value)); + } + Json& append(bool value) { + return append(Json(value)); + } + Json& append(const char* value) { + return append(Json(value)); + } + Json& append(const std::string& value) { + return append(Json(value)); + } + Json& set(const std::string& key, const Json& v) { if (!isObject()) throw std::runtime_error("not object"); cJSON_AddItemToObject(ptr_, key.c_str(), cJSON_Duplicate(v.ptr_, 1)); return *this; } + // 便捷的 set 方法重载 + Json& set(const std::string& key, int value) { + return set(key, Json(value)); + } + Json& set(const std::string& key, double value) { + return set(key, Json(value)); + } + Json& set(const std::string& key, bool value) { + return set(key, Json(value)); + } + Json& set(const std::string& key, const char* value) { + return set(key, Json(value)); + } + Json& set(const std::string& key, const std::string& value) { + return set(key, Json(value)); + } - // 迭代器(只读) + // 获取对象的所有键 + [[nodiscard]] std::vector keys() const { + if (!isObject()) throw std::runtime_error("not object"); + std::vector result; + cJSON* child = ptr_->child; + while (child) { + if (child->string) { + result.emplace_back(child->string); + } + child = child->next; + } + return result; + } + + // 数组迭代器(只读) template - struct iterator_impl { + struct array_iterator_impl { using iterator_category = std::forward_iterator_tag; using value_type = Json; using difference_type = std::ptrdiff_t; using pointer = typename std::conditional::type*; using reference = typename std::conditional::type&; - iterator_impl(cJSON* h, cJSON* c) : head_(h), cur_(c) {} + array_iterator_impl(cJSON* head, cJSON* c) : head_(head), cur_(c) {} reference operator*() { tmp = std::make_unique(cur_, false); return *tmp; } pointer operator->() { return &(operator*()); } - iterator_impl& operator++() { cur_ = cur_ ? cur_->next : nullptr; return *this; } - friend bool operator==(const iterator_impl& a, const iterator_impl& b) { return a.cur_ == b.cur_; } - friend bool operator!=(const iterator_impl& a, const iterator_impl& b) { return !(a == b); } + array_iterator_impl& operator++() { cur_ = cur_ ? cur_->next : nullptr; return *this; } + friend bool operator==(const array_iterator_impl& a, const array_iterator_impl& b) { return a.cur_ == b.cur_; } + friend bool operator!=(const array_iterator_impl& a, const array_iterator_impl& b) { return !(a == b); } private: cJSON *head_, *cur_; - std::unique_ptr tmp; // 改为智能指针 + std::unique_ptr tmp; }; - using iterator = iterator_impl; - using const_iterator = iterator_impl; - iterator begin() { return iterator(ptr_, ptr_ ? ptr_->child : nullptr); } - iterator end() { return iterator(ptr_, nullptr); } - [[nodiscard]] const_iterator begin() const { return cbegin(); } - [[nodiscard]] const_iterator end() const { return cend(); } - [[nodiscard]] const_iterator cbegin() const { return const_iterator(ptr_, ptr_ ? ptr_->child : nullptr); } - [[nodiscard]] const_iterator cend() const { return const_iterator(ptr_, nullptr); } + using array_iterator = array_iterator_impl; + using const_array_iterator = array_iterator_impl; + + // 对象键值对迭代器 + template + struct object_iterator_impl { + using iterator_category = std::forward_iterator_tag; + using value_type = std::pair; + using difference_type = std::ptrdiff_t; + using pointer = typename std::conditional::type*; + using reference = typename std::conditional::type&; + object_iterator_impl(cJSON* c) : cur_(c) { + if (cur_ && cur_->string) { + updateValue(); + } + } + reference operator*() { + tmp = std::make_unique(cur_->string ? cur_->string : "", Json(cur_, false)); + return *tmp; + } + pointer operator->() { return &(operator*()); } + object_iterator_impl& operator++() { + cur_ = cur_ ? cur_->next : nullptr; + if (cur_) { + updateValue(); + } + return *this; + } + friend bool operator==(const object_iterator_impl& a, const object_iterator_impl& b) { return a.cur_ == b.cur_; } + friend bool operator!=(const object_iterator_impl& a, const object_iterator_impl& b) { return !(a == b); } + private: + void updateValue() { + if (cur_ && cur_->string) { + current_key = cur_->string; + } + } + cJSON* cur_ = nullptr; + std::string current_key; + std::unique_ptr tmp; + }; + using object_iterator = object_iterator_impl; + using const_object_iterator = object_iterator_impl; + // 统一的迭代器接口(用于数组) + array_iterator begin() { + if (!isArray()) throw std::runtime_error("not array"); + return {ptr_, ptr_ ? ptr_->child : nullptr}; + } + array_iterator end() { + if (!isArray()) throw std::runtime_error("not array"); + return {ptr_, nullptr}; + } + [[nodiscard]] const_array_iterator begin() const { return cbegin(); } + [[nodiscard]] const_array_iterator end() const { return cend(); } + [[nodiscard]] const_array_iterator cbegin() const { + if (!isArray()) throw std::runtime_error("not array"); + return {ptr_, ptr_ ? ptr_->child : nullptr}; + } + [[nodiscard]] const_array_iterator cend() const { + if (!isArray()) throw std::runtime_error("not array"); + return {ptr_, nullptr}; + } + // 对象项迭代器 + object_iterator begin_object() { + if (!isObject()) throw std::runtime_error("not object"); + return {ptr_ ? ptr_->child : nullptr}; + } + object_iterator end_object() { + if (!isObject()) throw std::runtime_error("not object"); + return {nullptr}; + } + [[nodiscard]] const_object_iterator begin_object() const { return cbegin_object(); } + [[nodiscard]] const_object_iterator end_object() const { return cend_object(); } + [[nodiscard]] const_object_iterator cbegin_object() const { + if (!isObject()) throw std::runtime_error("not object"); + return {ptr_ ? ptr_->child : nullptr}; + } + [[nodiscard]] const_object_iterator cend_object() const { + if (!isObject()) throw std::runtime_error("not object"); + return {nullptr}; + } + + // 便利的 items() 方法,用于结构化绑定 + class ItemsProxy { + public: + explicit ItemsProxy(Json* json) : json_(json) {} + object_iterator begin() { return json_->begin_object(); } + object_iterator end() { return json_->end_object(); } + [[nodiscard]] const_object_iterator begin() const { return json_->cbegin_object(); } + [[nodiscard]] const_object_iterator end() const { return json_->cend_object(); } + private: + Json* json_; + }; + + ItemsProxy items() { return ItemsProxy(this); } + [[nodiscard]] ItemsProxy items() const { return ItemsProxy(const_cast(this)); } /*-------- 工具 ------------------------------------------------*/ void swap(Json& rhs) noexcept { std::swap(ptr_, rhs.ptr_); std::swap(owner_, rhs.owner_); } diff --git a/Bionic_Core/ToolsClass/LVGL_Render/LVGLRender.cpp b/Bionic_Core/ToolsClass/LVGL_Render/LVGLRender.cpp index bc337f0..697c593 100644 --- a/Bionic_Core/ToolsClass/LVGL_Render/LVGLRender.cpp +++ b/Bionic_Core/ToolsClass/LVGL_Render/LVGLRender.cpp @@ -45,6 +45,8 @@ LVGLRender::LVGLRender() { I2C_Init(); LCD_Init(); LVGL_Init(); + + // std::this_thread::sleep_for(std::chrono::milliseconds(2000)); // 延时2秒 确保所有 LVGL 对象已创建完成 ESP_LOGI("LVGL_Render", "LVGL_Render构造函数...初始化媒体驱动成功..."); ESP_LOGI("LVGL_Render", "LVGL_Render构造函数...创建LVGL心跳..."); @@ -53,7 +55,7 @@ LVGLRender::LVGLRender() { trickConfig.core_id = 1; // 渲染分配给核0 trickConfig.name = "LVGL_Render"; trickConfig.priority = 5; // - trickConfig.stack_size = 4096; // 给LVGL一个较大的堆栈,避免栈溢出 + trickConfig.stack_size = 8192; // 给LVGL一个较大的堆栈,避免栈溢出 std::thread tick_thread = ThreadManager::createMemberThread(trickConfig, this, &LVGLRender::LVGL_Update); @@ -74,96 +76,7 @@ uint16_t LVGLRender::getFps() { return fps; } - - - void LVGLRender::tryToInitRenderGif() { ESP_LOGI("LVGL_Render", "尝试初始化 LVGL 底层驱动..."); } - -void LVGLRender::log() { - ESP_LOGI("LVGL_Render", "LVGL_Render log..."); -} - -#include "SDFileManager.h" -std::vector LVGLRender::readWholeFile(const std::string& path) -{ - ESP_LOGI("LVGLRender", "开始读取文件: %s", path.c_str()); - - // 直接用 SDFileManager 同步读整个文件 TODO: 考虑修改为异步读 - std::string content = SDFileManager::getInstance()->readFileSync(path.c_str()); - if (content.empty()) { - ESP_LOGE("LVGLRender", "readFileSync 失败或文件为空"); - return {}; - } - - // string -> vector,零拷贝 move - return {content.begin(), content.end()}; -} - -bool LVGLRender::getGifWH(const uint8_t* raw, uint32_t& w, uint32_t& h) -{ - if (!raw || memcmp(raw, "GIF", 3) != 0) { - ESP_LOGE("LVGLRender", "不是合法 GIF 文件头"); - return false; - } - // GIF87a/89a 宽高偏移 6~9 字节,小端 - w = raw[6] | (raw[7] << 8); - h = raw[8] | (raw[9] << 8); - ESP_LOGI("LVGLRender", "GIF 尺寸: %lu x %lu", w, h); - return true; -} -void LVGLRender::renderGifInternal(const std::vector& data, - uint32_t w, uint32_t h) -{ - // 删除旧对象 - if (current_gif_obj != nullptr) { - lv_obj_del(current_gif_obj); - current_gif_obj = nullptr; - } - - // 保存数据,防止被释放 - current_gif_data = data; - - // 构造新的描述符(不要 static) - lv_img_dsc_t gif_desc = {}; - gif_desc.header.cf = LV_IMG_CF_RAW_CHROMA_KEYED; - gif_desc.header.always_zero = 0; - gif_desc.header.reserved = 0; - gif_desc.header.w = static_cast(w); - gif_desc.header.h = static_cast(h); - gif_desc.data_size = current_gif_data.size(); - gif_desc.data = current_gif_data.data(); - - // 创建新的 GIF 对象 - current_gif_obj = lv_gif_create(lv_scr_act()); // copy到当前gif对象 - lv_gif_set_src(current_gif_obj, &gif_desc); // 设置源 - lv_obj_center(current_gif_obj); // 居中 - - ESP_LOGI("LVGLRender", "GIF 已渲染并循环播放"); -} - -void LVGLRender::RenderGif(const std::string &filename) { - if (filename == last_gif_filename) { - ESP_LOGW("LVGLRender", "重复加载同一 GIF,忽略"); - return; - } - last_gif_filename = filename; - - lv_obj_set_style_bg_color(lv_scr_act(), lv_color_black(), 0); // 背景黑色 - lv_obj_set_style_bg_opa(lv_scr_act(), LV_OPA_COVER, 0); // 透明度 - - std::string fullPath = ToolsClass::makeFullPath(filename); - std::vector gifBin = readWholeFile(fullPath); - if (gifBin.empty()) return; - - uint32_t w = 0, h = 0; - if (!getGifWH(gifBin.data(), w, h)) return; - - renderGifInternal(gifBin, w, h); -} - - - - diff --git a/Bionic_Core/ToolsClass/LVGL_Render/LVGLRender.h b/Bionic_Core/ToolsClass/LVGL_Render/LVGLRender.h index ceb5682..74f7fbb 100644 --- a/Bionic_Core/ToolsClass/LVGL_Render/LVGLRender.h +++ b/Bionic_Core/ToolsClass/LVGL_Render/LVGLRender.h @@ -4,7 +4,7 @@ /** * 本类为单例类,用于实现LVGL的渲染 * 封装了一整套的LVGL渲染流 - * 同时兼顾了底层的显示驱动 + * 同时兼顾了底层的显示驱动初始化 */ #pragma once @@ -23,33 +23,12 @@ public: public: static LVGLRender* getInstance(); - /** - * 全屏渲染gif动画 - * @brief 渲染GIF文件 - * @param filename GIF文件路径 - */ - void RenderGif(const std::string &filename); - static void setFps(uint16_t fps_); static uint16_t getFps(); void tryToInitRenderGif(); - void log(); - - // gif渲染 -private: - /* 同步读整个文件到 vector */ - std::vector readWholeFile(const std::string& path); - - /* 从原始数据解析 GIF 宽高 */ - bool getGifWH(const uint8_t* raw, uint32_t& w, uint32_t& h); - - /* 真正的渲染实现(空壳,先打印日志) */ - void renderGifInternal(const std::vector& data, - uint32_t w, uint32_t h); - private: explicit LVGLRender(); // 构造函数私有化 ~LVGLRender(); @@ -60,10 +39,6 @@ private: static std::mutex instance_mutex; /// 单例锁 static uint16_t fps; /// 帧率 - lv_obj_t* current_gif_obj = nullptr; /// 当前GIF对象 - std::vector current_gif_data; /// 当前GIF数据 - std::string last_gif_filename; /// 最后一次渲染的GIF文件名 - std::mutex mtx; }; diff --git a/Bionic_Core/ToolsClass/LVGL_Render/Lvpp/lvpp.h b/Bionic_Core/ToolsClass/LVGL_Render/Lvpp/lvpp.h index 0de582f..9a52b0b 100644 --- a/Bionic_Core/ToolsClass/LVGL_Render/Lvpp/lvpp.h +++ b/Bionic_Core/ToolsClass/LVGL_Render/Lvpp/lvpp.h @@ -2,18 +2,17 @@ // Created by misaki on 2025/9/26. // #pragma once +#include #include #include #include #include #include -#include #include #include "SDFileManager.h" #include "ToolsClass.h" LV_FONT_DECLARE(SiYuanHeiTiGoogleBan); // 思源黑体 Google版 namespace lvgl_cpp { - // 前向声明 class Obj; class Screen; @@ -28,13 +27,18 @@ class List; class TextArea; class Image; class IconButton; - // 基础封装 class Obj { public: Obj() = default; explicit Obj(lv_obj_t* raw) : ptr_(raw) {} - virtual ~Obj() { if (ptr_) lv_obj_del(ptr_); } + virtual ~Obj() { + if (ptr_ && lv_obj_is_valid(ptr_)) { + // 先移除所有事件回调 + lv_obj_remove_event_cb_with_user_data(ptr_, event_trampoline, nullptr); + lv_obj_del(ptr_); + } + } Obj(Obj&& o) noexcept : ptr_(std::exchange(o.ptr_, nullptr)) {} Obj& operator=(Obj&& o) noexcept { @@ -48,9 +52,9 @@ public: void reset() { if (ptr_) { lv_obj_del(ptr_); ptr_ = nullptr; } } // 通用链式 - Obj& size(const lv_coord_t w, const lv_coord_t h) { lv_obj_set_size(ptr_, w, h); return *this; } - Obj& pos(const lv_coord_t x, const lv_coord_t y) { lv_obj_set_pos(ptr_, x, y); return *this; } - Obj& center() { lv_obj_center(ptr_); return *this; } + virtual Obj& size(const lv_coord_t w, const lv_coord_t h) { lv_obj_set_size(ptr_, w, h); return *this; } + virtual Obj& pos(const lv_coord_t x, const lv_coord_t y) { lv_obj_set_pos(ptr_, x, y); return *this; } + virtual Obj& center() { lv_obj_center(ptr_); return *this; } Obj& align(const lv_align_t align, const lv_obj_t* base = nullptr, const lv_coord_t x = 0, const lv_coord_t y = 0) { if (base) { // 如果有 base lv_obj_align_to(ptr_, base, align, x, y); @@ -70,6 +74,10 @@ public: Obj& on(const lv_event_code_t code, EventCb cb) { auto* ctx = new EventCb(std::move(cb)); lv_obj_add_event_cb(ptr_, event_trampoline, code, ctx); + + // 保存回调上下文以便后续清理 + std::lock_guard lock(event_mutex_); + event_contexts_.push_back(ctx); return *this; } protected: // 改为 protected 以允许派生类访问 @@ -77,8 +85,16 @@ protected: // 改为 protected 以允许派生类访问 private: static void event_trampoline(lv_event_t* e) { const auto* ctx = static_cast(lv_event_get_user_data(e)); + lv_obj_t* obj = lv_event_get_target(e); // 获取事件对象 + // 安全检查:确保对象仍然有效 + if (!obj || !lv_obj_is_valid(obj) || !ctx) { + return; + } (*ctx)(e); } + + std::vector event_contexts_; + std::mutex event_mutex_; }; // 样式构造器 @@ -333,7 +349,7 @@ private: } }; -// Toast 消息泡泡 +// Toast 消息泡泡 TODO: 有严重空指针异常问题,需要fix class Toast { public: enum class Type : uint8_t { INFO, WARN, ERROR }; @@ -539,8 +555,8 @@ private: grad.stops[0].frac = 0; grad.stops[1].color = mainColor_; grad.stops[1].frac = 128; - grad.stops[2].color = lv_color_darken(mainColor_, LV_OPA_30); - grad.stops[2].frac = 255; + // grad.stops[2].color = lv_color_darken(mainColor_, LV_OPA_30); + // grad.stops[2].frac = 255; lv_obj_set_style_bg_grad(fill_, &grad, 0); } @@ -587,11 +603,154 @@ private: char buf_[64] = {0}; }; +class LongPressButton : public Obj { +public: + explicit LongPressButton(const Obj& parent) + : Obj(lv_btn_create(parent.raw())) + { + // 基础事件 + on(LV_EVENT_PRESSED, [this](lv_event_t*){ onPress(); }); + on(LV_EVENT_RELEASED, [this](lv_event_t*){ onRelease(); }); + on(LV_EVENT_PRESS_LOST,[this](lv_event_t*){ onRelease(); }); + } + + // 链式外观 + LongPressButton& size(const lv_coord_t w, const lv_coord_t h) + { lv_obj_set_size(raw(), w, h); return *this; } + // 短按回调(主线程) + LongPressButton& onShort(std::function cb) + { short_cb_ = std::move(cb); return *this; } + // 长按回调(工作线程循环,参数为停止标志) + LongPressButton& onLong(std::function&)> cb) + { long_cb_ = std::move(cb); return *this; } + // 设置判定长按的超时时间,默认 400 ms + LongPressButton& longPressTimeout(const uint32_t ms) + { long_timeout_ms_ = ms; return *this; } + + ~LongPressButton() override + { + stopThread(); + } +private: + // 按下 + void onPress() + { + pressed_ = true; + stop_flag_ = false; + // 启动“长按判定定时器” + long_timer_ = lv_timer_create([](lv_timer_t* t){ + auto* self = static_cast(t->user_data); + self->onLongTimeout(); + }, long_timeout_ms_, this); + lv_timer_set_repeat_count(long_timer_, 1); // 一次性 + } + + // 释放 + void onRelease() + { + if (!pressed_) return; + pressed_ = false; + if (long_timer_) { // 还在跑 → 短按 + lv_timer_del(long_timer_); + long_timer_ = nullptr; + if (short_cb_) short_cb_(); + } + else { // 已进入长按模式 + stopThread(); // 请求停止 + join + } + } + // 长按超时 → 启动工作线程 + void onLongTimeout() + { + long_timer_ = nullptr; // 定时器自毁 + if (!long_cb_) return; + // 启动工作线程 + stop_flag_ = false; + thread_ = ThreadManager::createThread( + ThreadConfig{ + .name = "lpbtn_worker", + .core_id = 1, // 可改 + .stack_size = 4096, + .priority = 5 + }, + &LongPressButton::workerEntry, this); + } + // 工作线程入口 + static void workerEntry(LongPressButton* self) + { + self->long_cb_(self->stop_flag_); + } + // 请求停止并等待线程结束 + void stopThread() + { + if (thread_.joinable()) { + stop_flag_ = true; + thread_.join(); + } + } + std::function short_cb_; + std::function&)> long_cb_; + uint32_t long_timeout_ms_ = 300; /// 默认 300 ms + + lv_timer_t* long_timer_ = nullptr; + std::atomic pressed_{false}; + std::atomic stop_flag_{false}; + std::thread thread_; +}; + // GIF 显示控件 class Gif final : public Obj { public: explicit Gif(const Obj& parent) : Obj(lv_gif_create(parent.raw())) {} - + ~Gif() override { + // drop(); + if (raw() && lv_obj_is_valid(raw())) + lv_gif_set_src(raw(), nullptr); + } + // 仅把 lv_obj 从树上摘下来,但不 delete 任何数据 + void detach() { + if (raw() && lv_obj_is_valid(raw())) { + lv_obj_remove_style_all(raw()); // 清掉所有样式,防止刷新 + lv_obj_clear_flag(raw(), LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_PRESS_LOCK); + lv_obj_move_foreground(raw()); // 移到最前,避免树遍历卡住 + lv_obj_del_async(raw()); // 让 LVGL 在下一轮把 obj 删掉 + ptr_ = nullptr; // 把基类保存的指针清零,防止再访问 + } + } + void stopTimer() { + if (raw() && lv_obj_is_valid(raw())) { + lv_gif_set_src(raw(), nullptr); // 再清解码器 + } + } + // 异步释放 GIF 资源 + void drop_async() { + if (!dsc_) return; + // 先让 LVGL 关闭解码器(需要 dsc_ 有效) + if (raw() && lv_obj_is_valid(raw())) + lv_gif_set_src(raw(), nullptr); + // 把 dsc_ 移出来(裸指针) + auto* keep = dsc_.release(); + // 脱钩 obj + detach(); + // 异步释放“纯数据” + lv_async_call([](void* p) { + auto* raw_dsc = static_cast(p); + if (raw_dsc) { + if (raw_dsc->data) heap_caps_free(const_cast(raw_dsc->data)); + delete raw_dsc; + } + }, keep); + } + // 主动释放解码缓冲区(必须在对象还活着时调用) + void drop() { + if (raw() && lv_obj_is_valid(raw())) { + lv_gif_set_src(raw(), nullptr); // 停定时器 gd_close_gif() + } + // 把内部指针清零,防止重复关闭 + if (dsc_) { + dsc_.reset(); // 释放 dsc_ + PSRAM + } + } bool getGifWH(const uint8_t* raw, uint32_t& w, uint32_t& h) { if (!raw || memcmp(raw, "GIF", 3) != 0) { @@ -607,8 +766,12 @@ public: // 从 SD 卡加载 GIF 文件(完整路径) Gif& src(const char* full_path) { + if (raw() && lv_obj_is_valid(raw())) { + lv_gif_set_src(raw(), nullptr); // 停止旧解码器 + } + // 读整个文件到 PSRAM - std::string content = SDFileManager::getInstance()->readFileSync(ToolsClass::makeFullPath(full_path)); + const std::string content = SDFileManager::getInstance()->readFileSync(ToolsClass::makeFullPath(full_path)); if (content.empty()) { ESP_LOGE("Gif", "read %s fail", full_path); return *this; @@ -631,7 +794,7 @@ public: // 构造一次性描述符(跟随 Gif 对象生命周期) dsc_ = std::unique_ptr(new lv_img_dsc_t{}); - dsc_->header.cf = LV_IMG_CF_RAW_CHROMA_KEYED; + dsc_->header.cf = LV_IMG_CF_RAW_CHROMA_KEYED/*LV_IMG_CF_TRUE_COLOR_ALPHA*/; dsc_->header.always_zero = 0; dsc_->header.reserved = 0; dsc_->header.w = static_cast(w); @@ -641,6 +804,9 @@ public: // 交给 LVGL lv_gif_set_src(raw(), dsc_.get()); + lv_obj_clear_flag(raw(), LV_OBJ_FLAG_SCROLLABLE); // 禁止滚动 + lv_obj_set_scroll_dir(raw(), LV_DIR_NONE); + return *this; } @@ -659,47 +825,275 @@ private: std::unique_ptr dsc_; }; +// GIF 播放控件 +class GifPlayer : public Obj { +public: + explicit GifPlayer(const Obj& parent) : Obj(lv_obj_create(parent.raw())) { + lv_obj_set_size(raw(), LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_clear_flag(raw(), LV_OBJ_FLAG_SCROLLABLE); + lv_obj_center(raw()); + } + ~GifPlayer() override { stop(); } + + // 设置/切换轮播图 TODO: 切换轮播图的时候存在定时器空指针重复释放问题 + GifPlayer& loop(const char* path, const uint32_t duration = 5000) { + loop_path_ = path ? path : ""; + loop_duration_ = duration; + if (!in_burst_) show_loop(); + return *this; + } + + // 插播一张图;播完自动回到轮播 + GifPlayer& burst(const char* path, const uint32_t duration = 5000) { + if (!path || !*path) return *this; + in_burst_ = true; + show_burst(path); // 创建并显示突发图 + burst_timer_ = lv_timer_create( + [](lv_timer_t* t){ + auto* self = static_cast(t->user_data); + self->in_burst_ = false; + if (self->burst_gif_) self->burst_gif_->drop_async(); + self->burst_gif_->drop_async(); // 内部已把 obj 和数据都处理完 + self->burst_gif_.reset(); // 只把 unique_ptr 置空,不再访问 obj + if (!self->loop_path_.empty()) self->show_loop(); + lv_timer_del(t); + self->burst_timer_ = nullptr; + }, duration, this); + return *this; + } + + // 停止并释放所有资源 + void stop() { + if (burst_timer_) { + lv_timer_del(burst_timer_); + burst_timer_ = nullptr; + } + burst_gif_.reset(); + loop_gif_.reset(); + in_burst_ = false; + } + +private: + void show_loop() { + if (burst_gif_) lv_obj_add_flag(burst_gif_->raw(), LV_OBJ_FLAG_HIDDEN); + if (!loop_gif_) { + loop_gif_ = std::make_unique(*this); + (void)loop_gif_->raw(); + } + lv_obj_clear_flag(loop_gif_->raw(), LV_OBJ_FLAG_HIDDEN); + loop_gif_->src(loop_path_.c_str()); + } + + void show_burst(const char* path) { + if (!burst_gif_) { + burst_gif_ = std::make_unique(*this); + (void)burst_gif_->raw(); + } + lv_obj_clear_flag(burst_gif_->raw(), LV_OBJ_FLAG_HIDDEN); + if (loop_gif_) lv_obj_add_flag(loop_gif_->raw(), LV_OBJ_FLAG_HIDDEN); + burst_gif_->src(path); + } + + bool in_burst_ = false; + std::string loop_path_; + uint32_t loop_duration_ = 5000; + lv_timer_t* burst_timer_ = nullptr; + std::unique_ptr loop_gif_; + std::unique_ptr burst_gif_; +}; + +// 纯文字透明浮层 +class TextOverlay : public Obj { +public: + explicit TextOverlay(const Obj& parent) + : Obj(lv_label_create(parent.raw())) + { + // 初始化为透明背景 + lv_obj_set_style_bg_opa(raw(), LV_OPA_TRANSP, 0); + lv_obj_set_style_pad_all(raw(), 0, 0); // 去掉内边距,真正的“裸文字” + lv_obj_clear_flag(raw(), LV_OBJ_FLAG_SCROLLABLE); // 不需要滚动 + lv_label_set_long_mode(raw(), LV_LABEL_LONG_WRAP); // 自动换行 + } + + // 文字内容 + TextOverlay& text(const char* txt) { + lv_label_set_text(raw(), txt); + return *this; + } + TextOverlay& text_fmt(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + // 先用栈缓冲格式化,再交给 LVGL + char buf[128]; + vsnprintf(buf, sizeof(buf), fmt, args); + lv_label_set_text(raw(), buf); + va_end(args); + return *this; + } + + // 文字颜色,默认白色 + TextOverlay& textColor(const lv_color_t c) { + lv_obj_set_style_text_color(raw(), c, 0); + return *this; + } + + // 字体,默认 &SiYuanHeiTiGoogleBan + TextOverlay& font(const lv_font_t* fnt) { + lv_obj_set_style_text_font(raw(), fnt, 0); + return *this; + } + + // 对齐方式:LEFT / CENTER / RIGHT + TextOverlay& alignText(const lv_text_align_t align) { + lv_obj_set_style_text_align(raw(), align, 0); + return *this; + } + + // 快速定位(沿用 Obj 的 align 链式) + TextOverlay& place(const lv_align_t align, + const lv_obj_t* base = nullptr, + const lv_coord_t x = 0, + const lv_coord_t y = 0) + { + this->align(align, base, x, y); + return *this; + } + + // 是否让文字区域可点击(默认不可点击) + TextOverlay& clickable(const bool en = true) { + if (en) lv_obj_add_flag(raw(), LV_OBJ_FLAG_CLICKABLE); + else lv_obj_clear_flag(raw(), LV_OBJ_FLAG_CLICKABLE); + return *this; + } +}; // 子应用抽象 class BaseApp : public Obj { public: - explicit BaseApp(const Obj& parent) : Obj(lv_obj_create(parent.raw())), btn_exit_(parent) { + explicit BaseApp(const Obj& parent) : Obj(lv_obj_create(parent.raw())) { lv_obj_set_size(raw(), 360, 360); - // 退出按钮 - btn_exit_.size(40, 40) - .align(LV_ALIGN_CENTER, nullptr, 0, -130) // 居中 - .on(LV_EVENT_CLICKED, [this](lv_event_t *) { this->exit(); }); - label_.align(LV_ALIGN_CENTER, nullptr, 0, 0); - label_.text(LV_SYMBOL_CLOSE); + + // 创建退出按钮 + btn_exit_ = std::make_unique