ba5e47bc77
1. 应用界面增加了返回主页的按钮 2. 修复了gif渲染内存泄漏的严重bug 3. 将PetDao当中的cJSON API替换为cpp_json,完美通过测试 4. 整合已经实现的各种上层建筑,实现了一个宠物对话基本业务应用,用于样品测试展示用 5. 重构了音频播放类,使其更modern,更加便于移植和拓展
461 lines
16 KiB
C++
461 lines
16 KiB
C++
//
|
|
// Created by misaki on 2025/9/2.
|
|
//
|
|
#include "CppHandle.h"
|
|
|
|
#include "OTAClass.h"
|
|
#include <iostream>
|
|
#include <optional>
|
|
#include <mbedtls/base64.h>
|
|
|
|
#include "ToolsClass.h"
|
|
#include "WifiConnectors.h"
|
|
#include "CommClass.h"
|
|
#include "sys_conf_singleton.h"
|
|
#include "HttpOtaUpdater.h"
|
|
#include "AudioOutput.h"
|
|
|
|
// OTA相关
|
|
HttpOtaUpdater otaUpdater;
|
|
|
|
void setupOtaCallbacks() {
|
|
// 设置进度回调
|
|
otaUpdater.setProgressCallback([](int progress, int total) {
|
|
ESP_LOGI("OTA", "Progress: %d%%", progress);
|
|
});
|
|
|
|
// 设置状态回调
|
|
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<int>(state)], message.c_str());
|
|
});
|
|
|
|
// 设置完成回调
|
|
otaUpdater.setFinishCallback([](bool success, const std::string &message) {
|
|
if (success) {
|
|
ESP_LOGI("OTA", "Completed successfully: %s", message.c_str());
|
|
} else {
|
|
ESP_LOGE("OTA", "Failed: %s", message.c_str());
|
|
}
|
|
});
|
|
|
|
// 如果需要HTTPS,可以在这里设置证书(保留供后期使用)
|
|
// otaUpdater.setCACert(my_ca_cert_pem);
|
|
// 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内容
|
|
{
|
|
"type": "xxx", // 消息类型
|
|
"xxx":"xxx", // 其他数据
|
|
......
|
|
}
|
|
此回调函数为核心业务处理函数!!!
|
|
*/
|
|
void onJsonData(const cppjson::Json &json) {
|
|
// 打印收到的 JSON
|
|
ESP_LOGI("JSON_CALLBACK", "收到JSON数据: %s", json.dump().c_str());
|
|
|
|
// 解析消息类型
|
|
const cppjson::Json &type = json["type"];
|
|
if (!type.isString()) return;
|
|
|
|
std::string typeStr = type.asString();
|
|
|
|
if (typeStr == "greeting") {
|
|
ESP_LOGI("JSON_CALLBACK", "收到服务器问候消息");
|
|
} else if (typeStr == "heartbeat") {
|
|
ESP_LOGI("JSON_CALLBACK", "收到心跳响应");
|
|
} else if (typeStr == "response") {
|
|
ESP_LOGI("JSON_CALLBACK", "收到服务器响应");
|
|
} else if (typeStr == "echo") {
|
|
ESP_LOGI("JSON_CALLBACK", "收到回显消息");
|
|
} else if (typeStr == "broadcast") {
|
|
ESP_LOGI("JSON_CALLBACK", "收到广播消息");
|
|
} else if (typeStr == "ota") {
|
|
ESP_LOGI("JSON_CALLBACK", "收到OTA消息");
|
|
// 进一步处理OTA消息
|
|
// 获取OTA中的版本信息
|
|
const cppjson::Json &version = json["version"];
|
|
if (!version.isString()) return;
|
|
std::string versionStr = version.asString();
|
|
// 获取OTA中的HTTP URL
|
|
const cppjson::Json &url = json["url"];
|
|
if (!url.isString()) return;
|
|
std::string urlStr = url.asString();
|
|
// 告诉服务端,升级开始
|
|
cppjson::Json response = cppjson::Json::object();
|
|
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) {
|
|
switch (event) {
|
|
case WebSocketEvent::CONNECTED:
|
|
ESP_LOGI("EVENT_CALLBACK", "WebSocket已连接: %s", message.c_str());
|
|
break;
|
|
case WebSocketEvent::DISCONNECTED:
|
|
ESP_LOGI("EVENT_CALLBACK", "WebSocket已断开: %s", message.c_str());
|
|
break;
|
|
case WebSocketEvent::DATA_RECEIVED:
|
|
ESP_LOGI("EVENT_CALLBACK", "收到原始数据: %s", message.c_str());
|
|
break;
|
|
case WebSocketEvent::ERROR:
|
|
ESP_LOGE("EVENT_CALLBACK", "WebSocket错误: %s", message.c_str());
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 发送状态信息函数
|
|
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));
|
|
|
|
status.set("data", data); // 嵌套对象
|
|
|
|
if (WebSocketManager::getInstance()->sendJson(status)) {
|
|
ESP_LOGI("SEND", "已发送状态信息");
|
|
} else {
|
|
ESP_LOGE("SEND", "发送状态信息失败");
|
|
}
|
|
}
|
|
|
|
// 发送问候消息函数
|
|
void sendGreeting() {
|
|
cppjson::Json greeting = cppjson::Json::object();
|
|
greeting.set("type", cppjson::Json("greeting"))
|
|
.set("message", cppjson::Json("Hello from ESP32-S3"))
|
|
.set("timestamp", cppjson::Json(xTaskGetTickCount() * portTICK_PERIOD_MS / 1000));
|
|
|
|
if (WebSocketManager::getInstance()->sendJson(greeting)) {
|
|
ESP_LOGI("SEND", "已发送问候消息");
|
|
} else {
|
|
ESP_LOGE("SEND", "发送问候消息失败");
|
|
}
|
|
}
|
|
|
|
void websocket_task() {
|
|
TickType_t lastStatusTime = 0;
|
|
TickType_t lastHeartbeatTime = 0;
|
|
TickType_t lastGreetingTime = 0;
|
|
|
|
while (true) {
|
|
TickType_t currentTime = xTaskGetTickCount();
|
|
|
|
// 检查连接状态
|
|
if (!WebSocketManager::getInstance()->isConnected()) {
|
|
ESP_LOGI("APP_TASK", "WebSocket未连接,尝试重新连接...");
|
|
|
|
// 确保WiFi已连接
|
|
if (!WifiConnectors::getInstance()->isWifiConnect()) {
|
|
ESP_LOGI("APP_TASK", "WiFi未连接,等待WiFi连接...");
|
|
vTaskDelay(5000 / portTICK_PERIOD_MS);
|
|
continue;
|
|
}
|
|
|
|
if (WebSocketManager::getInstance()->connect()) {
|
|
ESP_LOGI("APP_TASK", "重新连接成功");
|
|
} else {
|
|
ESP_LOGI("APP_TASK", "重新连接失败");
|
|
}
|
|
vTaskDelay(5000 / portTICK_PERIOD_MS);
|
|
continue;
|
|
}
|
|
// 每10秒发送状态信息
|
|
if (currentTime - lastStatusTime > (10000 / portTICK_PERIOD_MS)) {
|
|
sendStatus();
|
|
lastStatusTime = currentTime;
|
|
}
|
|
// 每60秒发送问候
|
|
if (currentTime - lastGreetingTime > (60000 / portTICK_PERIOD_MS)) {
|
|
sendGreeting();
|
|
lastGreetingTime = currentTime;
|
|
}
|
|
vTaskDelay(100 / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
void createWebSocket() {
|
|
// 等待WiFi连接成功后再连接WebSocket
|
|
ESP_LOGI("APP_TASK", "等待WiFi连接...");
|
|
while (!WifiConnectors::getInstance()->isWifiConnect()) {
|
|
ESP_LOGI("APP_TASK", "WiFi未连接,等待中...");
|
|
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
|
}
|
|
// 保存SN
|
|
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());
|
|
|
|
// 配置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.max_reconnect_attempts = 10; // 最大重连尝试次数
|
|
// TODO: 此处通信类存在线程重复创建bug,似乎是来自esp-idf的bug,待查证
|
|
// 初始化WebSocket管理器
|
|
if (!WebSocketManager::getInstance()->initialize(config)) {
|
|
ESP_LOGE("APP_TASK", "WebSocket管理器初始化失败");
|
|
vTaskDelete(nullptr);
|
|
return;
|
|
}
|
|
|
|
// 设置回调函数
|
|
WebSocketManager::getInstance()->setJsonCallback(onJsonData);
|
|
WebSocketManager::getInstance()->setEventCallback(onWebSocketEvent);
|
|
|
|
// 连接WebSocket服务器
|
|
ESP_LOGI("APP_TASK", "正在连接WebSocket服务器: %s", config.uri.c_str());
|
|
if (!WebSocketManager::getInstance()->connect()) {
|
|
ESP_LOGE("APP_TASK", "WebSocket连接失败");
|
|
}
|
|
|
|
// 创建WebSocket任务
|
|
ThreadConfig websocket_config;
|
|
websocket_config.core_id = 0;
|
|
websocket_config.inherit_cfg = true;
|
|
websocket_config.name = "websocket_task";
|
|
websocket_config.priority = 5;
|
|
websocket_config.stack_size = 4096;
|
|
std::thread websocket_thread = ThreadManager::createThread(websocket_config, websocket_task);
|
|
websocket_thread.detach();
|
|
}
|
|
|
|
#include "LVGLRender.h"
|
|
#include "SimpleI2SForwarder.h"
|
|
|
|
#include "lvpp.h"
|
|
#include "BaseApp.h"
|
|
LV_FONT_DECLARE(SiYuanHeiTiGoogleBan);
|
|
// 使用全局智能指针管理主要对象
|
|
static std::shared_ptr<lvgl_cpp::Screen> g_screen;
|
|
static std::shared_ptr<lvgl_cpp::HomePage> g_home;
|
|
static std::shared_ptr<lvgl_cpp::AppMenu> 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;
|
|
}
|
|
cout << "✅ 读档成功,继续互动:" << endl;
|
|
printPet(*loaded);
|
|
|
|
// 6. 对新对象继续互动
|
|
loaded->neglect(); printPet(*loaded);
|
|
loaded->feed(); printPet(*loaded);
|
|
*/
|
|
return 1;
|
|
}
|
|
|
|
|
|
void Cpp_Hand() {
|
|
// 打印设备信息
|
|
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刷新频率
|
|
|
|
// 创建屏幕
|
|
g_screen = std::make_shared<lvgl_cpp::Screen>();
|
|
lv_scr_load(g_screen->raw());
|
|
|
|
// 延迟创建界面组件
|
|
auto init_timer = std::make_unique<lvgl_cpp::Timer>([&]() {
|
|
ESP_LOGI("MAIN", "Initializing UI components...");
|
|
// 创建主页
|
|
g_home = std::make_shared<lvgl_cpp::HomePage>(*g_screen);
|
|
g_home->bg("pic360.bin", 360, 360)
|
|
.onBattery([]() -> uint8_t {
|
|
return static_cast<uint8_t>(ToolsClass::getInstance()->getBatteryPer());
|
|
})
|
|
.onDateTime([](char *buf, const size_t len) {
|
|
snprintf(buf, len, "06/25 Tue 14:30");
|
|
});
|
|
// 创建菜单
|
|
g_menu = std::make_shared<lvgl_cpp::AppMenu>(*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) {
|
|
lv_obj_add_flag(g_home->raw(), LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_clear_flag(g_menu->raw(), LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
});
|
|
ESP_LOGI("MAIN", "UI initialization complete");
|
|
}, 1000, true); // 1秒后执行一次
|
|
|
|
// 注册应用
|
|
lvgl_cpp::AppFactory::registerApp("Calcu", [](lvgl_cpp::Obj &p) {
|
|
return std::make_unique<AppCalc>(p);
|
|
});
|
|
lvgl_cpp::AppFactory::registerApp("Gif测试", [](lvgl_cpp::Obj &p) {
|
|
return std::make_unique<AppMusic>(p);
|
|
});
|
|
lvgl_cpp::AppFactory::registerApp("Button", [](lvgl_cpp::Obj &p) {
|
|
return std::make_unique<ButtonApp>(p);
|
|
});
|
|
lvgl_cpp::AppFactory::registerApp("AI测试", [](lvgl_cpp::Obj &p) {
|
|
return std::make_unique<WebSocketVoice>(p);
|
|
});
|
|
lvgl_cpp::AppFactory::registerApp("长按录音", [](lvgl_cpp::Obj &p) {
|
|
return std::make_unique<LongPressButtonAnim>(p);
|
|
});
|
|
lvgl_cpp::AppFactory::registerApp("宠物样例", [](lvgl_cpp::Obj &p) {
|
|
return std::make_unique<PetApp>(p);
|
|
});
|
|
|
|
// 连接wifi
|
|
WifiConnectors::getInstance()->connectWifi("Misaki-2.4G", "88888888", 5);
|
|
|
|
// 创建WebSocket
|
|
createWebSocket();
|
|
|
|
// 设置OTA回调
|
|
// setupOtaCallbacks();
|
|
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(std::chrono::seconds(1)); // 休眠 1 秒
|
|
}
|
|
}
|