这是一次长久的提交:
1. 应用界面增加了返回主页的按钮 2. 修复了gif渲染内存泄漏的严重bug 3. 将PetDao当中的cJSON API替换为cpp_json,完美通过测试 4. 整合已经实现的各种上层建筑,实现了一个宠物对话基本业务应用,用于样品测试展示用 5. 重构了音频播放类,使其更modern,更加便于移植和拓展
This commit is contained in:
@@ -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<uint8_t> 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<uint8_t>& 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<lv_coord_t>(w);
|
||||
gif_desc.header.h = static_cast<lv_coord_t>(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<uint8_t> gifBin = readWholeFile(fullPath);
|
||||
if (gifBin.empty()) return;
|
||||
|
||||
uint32_t w = 0, h = 0;
|
||||
if (!getGifWH(gifBin.data(), w, h)) return;
|
||||
|
||||
renderGifInternal(gifBin, w, 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<uint8_t> readWholeFile(const std::string& path);
|
||||
|
||||
/* 从原始数据解析 GIF 宽高 */
|
||||
bool getGifWH(const uint8_t* raw, uint32_t& w, uint32_t& h);
|
||||
|
||||
/* 真正的渲染实现(空壳,先打印日志) */
|
||||
void renderGifInternal(const std::vector<uint8_t>& 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<uint8_t> current_gif_data; /// 当前GIF数据
|
||||
std::string last_gif_filename; /// 最后一次渲染的GIF文件名
|
||||
|
||||
std::mutex mtx;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
// Created by misaki on 2025/9/26.
|
||||
//
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_log.h>
|
||||
#include <lvgl.h>
|
||||
#include <font/lv_font.h>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#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<std::mutex> 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<EventCb*>(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<EventCb*> 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<void()> cb)
|
||||
{ short_cb_ = std::move(cb); return *this; }
|
||||
// 长按回调(工作线程循环,参数为停止标志)
|
||||
LongPressButton& onLong(std::function<void(std::atomic<bool>&)> 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<LongPressButton*>(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<void()> short_cb_;
|
||||
std::function<void(std::atomic<bool>&)> long_cb_;
|
||||
uint32_t long_timeout_ms_ = 300; /// 默认 300 ms
|
||||
|
||||
lv_timer_t* long_timer_ = nullptr;
|
||||
std::atomic<bool> pressed_{false};
|
||||
std::atomic<bool> 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<lv_img_dsc_t*>(p);
|
||||
if (raw_dsc) {
|
||||
if (raw_dsc->data) heap_caps_free(const_cast<uint8_t*>(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<lv_img_dsc_t, DscDeleter>(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<lv_coord_t>(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<lv_img_dsc_t, DscDeleter> 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<GifPlayer*>(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<Gif>(*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<Gif>(*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<Gif> loop_gif_;
|
||||
std::unique_ptr<Gif> 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<Button>(*this);
|
||||
label_ = std::make_unique<Label>(*btn_exit_);
|
||||
|
||||
// 设置按钮位置和样式
|
||||
btn_exit_->size(40, 40)
|
||||
.align(LV_ALIGN_CENTER, nullptr, 0, -100)
|
||||
.add_style(&getExitButtonStyle());
|
||||
|
||||
label_->text(LV_SYMBOL_CLOSE);
|
||||
label_->center();
|
||||
lv_obj_set_style_text_color(label_->raw(), lv_color_white(), 0);
|
||||
lv_obj_set_style_text_font(label_->raw(), &lv_font_montserrat_16, 0);
|
||||
|
||||
// 使用更安全的事件绑定方式
|
||||
setupExitButton();
|
||||
}
|
||||
|
||||
// 子类重写:界面内容
|
||||
virtual void onShow() {} // 进入时调用
|
||||
virtual void onHide() {} // 退出时调用
|
||||
~BaseApp() override {
|
||||
// 标记为正在销毁
|
||||
is_destroying_ = true;
|
||||
|
||||
// 先清理子对象
|
||||
label_.reset();
|
||||
btn_exit_.reset();
|
||||
|
||||
// 如果ptr_仍然有效,安全删除
|
||||
if (ptr_ && lv_obj_is_valid(ptr_)) {
|
||||
// 先隐藏对象,停止所有交互
|
||||
lv_obj_add_flag(ptr_, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
// 延迟删除,确保所有事件处理完成
|
||||
lv_timer_create([](lv_timer_t* timer) {
|
||||
lv_obj_t* obj = static_cast<lv_obj_t*>(timer->user_data);
|
||||
if (obj && lv_obj_is_valid(obj)) {
|
||||
lv_obj_del(obj);
|
||||
}
|
||||
lv_timer_del(timer);
|
||||
}, 100, ptr_); // 延迟100ms删除
|
||||
}
|
||||
}
|
||||
|
||||
// 主动退出
|
||||
void exit() {
|
||||
if (is_exiting_) return;
|
||||
is_exiting_ = true;
|
||||
onCleanup();
|
||||
onHide();
|
||||
// 禁止自己再被点击
|
||||
lv_obj_clear_flag(raw(), LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
// 异步自毁:让 LVGL 在下一轮循环再删我们
|
||||
lv_async_call([](void* self) {
|
||||
lv_obj_del(static_cast<lv_obj_t*>(self));
|
||||
}, raw());
|
||||
// 通知工厂回到主页 → 由工厂/菜单负责
|
||||
if (exit_cb_) exit_cb_();
|
||||
// 立即隐藏并禁用
|
||||
if (ptr_ && lv_obj_is_valid(ptr_)) {
|
||||
lv_obj_add_flag(ptr_, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(ptr_, LV_OBJ_FLAG_CLICKABLE);
|
||||
}
|
||||
|
||||
// 通知退出回调
|
||||
if (exit_cb_) {
|
||||
exit_cb_();
|
||||
}
|
||||
|
||||
// 延迟自毁
|
||||
auto self = this;
|
||||
lv_timer_create([](lv_timer_t* timer) {
|
||||
const auto* app = static_cast<BaseApp*>(timer->user_data);
|
||||
if (app && app->ptr_ && lv_obj_is_valid(app->ptr_)) {
|
||||
lv_obj_del(app->ptr_);
|
||||
}
|
||||
lv_timer_del(timer);
|
||||
}, 50, this); // 50ms后删除
|
||||
}
|
||||
|
||||
// 注册退出通知回调(由 AppFactory 设置)
|
||||
void onExit(std::function<void()> cb) { exit_cb_ = std::move(cb); }
|
||||
virtual void onShow() {}
|
||||
virtual void onHide() {}
|
||||
|
||||
void onExit(std::function<void()> cb) { exit_cb_ = std::move(cb); }
|
||||
protected:
|
||||
Button btn_exit_{*this}; // 退出按钮
|
||||
Label label_{btn_exit_}; // 退出图标
|
||||
// 子类有需要就重写, 用于释放资源
|
||||
virtual void onCleanup() {}
|
||||
protected:
|
||||
std::unique_ptr<Button> btn_exit_;
|
||||
std::unique_ptr<Label> label_;
|
||||
|
||||
private:
|
||||
std::function<void()> exit_cb_;
|
||||
std::atomic<bool> is_exiting_{false};
|
||||
std::atomic<bool> is_destroying_{false};
|
||||
|
||||
void setupExitButton() {
|
||||
// 使用原始LVGL事件,避免C++回调问题
|
||||
lv_obj_add_event_cb(btn_exit_->raw(), [](lv_event_t* e) {
|
||||
lv_obj_t* btn = lv_event_get_target(e);
|
||||
if (!btn || !lv_obj_is_valid(btn)) return;
|
||||
|
||||
lv_obj_t* parent = lv_obj_get_parent(btn);
|
||||
if (!parent || !lv_obj_is_valid(parent)) return;
|
||||
|
||||
// 通过用户数据找到BaseApp实例
|
||||
auto* app = static_cast<BaseApp*>(lv_obj_get_user_data(parent));
|
||||
if (app && !app->is_exiting_ && !app->is_destroying_) {
|
||||
app->exit();
|
||||
}
|
||||
}, LV_EVENT_CLICKED, this);
|
||||
|
||||
// 保存this指针到用户数据
|
||||
lv_obj_set_user_data(raw(), this);
|
||||
}
|
||||
|
||||
static lv_style_t& getExitButtonStyle() {
|
||||
static lv_style_t style;
|
||||
static bool style_initialized = false;
|
||||
|
||||
if (!style_initialized) {
|
||||
lv_style_init(&style);
|
||||
lv_style_set_bg_color(&style, lv_color_hex(0xFF3B30));
|
||||
lv_style_set_bg_opa(&style, LV_OPA_COVER);
|
||||
lv_style_set_radius(&style, 20);
|
||||
lv_style_set_border_width(&style, 0);
|
||||
style_initialized = true;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
};
|
||||
|
||||
// 子应用工厂
|
||||
@@ -731,11 +1125,12 @@ public:
|
||||
lv_obj_set_style_bg_opa(raw(), LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_border_width(raw(), 0, 0);
|
||||
lv_obj_clear_flag(raw(), LV_OBJ_FLAG_SCROLLABLE); // 圆形屏建议不要滚动
|
||||
lv_obj_set_style_pad_top(raw(), 60, 0); // ← 新增
|
||||
lv_obj_set_style_pad_top(raw(), 100, 0); // 增加顶部内边距为返回按钮留空间
|
||||
lv_obj_set_style_pad_bottom(raw(),60, 0); // ← 新增
|
||||
lv_obj_set_style_pad_left(raw(), 20, 0);
|
||||
lv_obj_set_style_pad_right(raw(), 20, 0);
|
||||
|
||||
// 创建返回主页按钮(放在顶部)
|
||||
createBackButton();
|
||||
// 横向 2 列,居中排列
|
||||
lv_obj_set_flex_flow(raw(), LV_FLEX_FLOW_ROW_WRAP);
|
||||
lv_obj_set_flex_align(raw(), LV_FLEX_ALIGN_SPACE_EVENLY,
|
||||
@@ -775,9 +1170,81 @@ public:
|
||||
click_cb_ = std::move(cb);
|
||||
return *this;
|
||||
}
|
||||
// 注册返回主页回调
|
||||
AppMenu& onBack(std::function<void()> cb) {
|
||||
back_cb_ = std::move(cb);
|
||||
return *this;
|
||||
}
|
||||
private:
|
||||
void createBackButton() {
|
||||
// 创建返回按钮容器(固定在顶部)
|
||||
back_btn_ = std::make_unique<Button>(*this);
|
||||
back_label_ = std::make_unique<Label>(*back_btn_);
|
||||
|
||||
// 设置返回按钮样式
|
||||
back_btn_->size(120, 40)
|
||||
.align(LV_ALIGN_TOP_MID, nullptr, 0, 20) // 顶部中间,向下偏移20px
|
||||
.add_style(&getBackButtonStyle());
|
||||
|
||||
back_label_->text("返回主页");
|
||||
back_label_->center();
|
||||
lv_obj_set_style_text_color(back_label_->raw(), lv_color_white(), 0);
|
||||
lv_obj_set_style_text_font(back_label_->raw(), &SiYuanHeiTiGoogleBan, LV_PART_MAIN);
|
||||
|
||||
// 返回按钮点击事件
|
||||
lv_obj_add_event_cb(back_btn_->raw(), [](lv_event_t* e) {
|
||||
// lv_obj_t* target = lv_event_get_target(e);
|
||||
// ESP_LOGI("BackButton", "收到事件: %d, 对象: %p", code, target);
|
||||
switch(lv_event_get_code(e)) {
|
||||
case LV_EVENT_PRESSED: {
|
||||
ESP_LOGI("BackButton", "按钮按下");
|
||||
break;
|
||||
}
|
||||
case LV_EVENT_RELEASED: {
|
||||
ESP_LOGI("BackButton", "按钮释放");
|
||||
break;
|
||||
}
|
||||
case LV_EVENT_CLICKED: {
|
||||
ESP_LOGI("BackButton", "按钮点击!!!");
|
||||
// 处理点击事件
|
||||
lv_obj_t* btn = lv_event_get_target(e);
|
||||
lv_obj_t* menu_obj = lv_obj_get_parent(btn);
|
||||
auto* menu = static_cast<AppMenu*>(lv_obj_get_user_data(menu_obj));
|
||||
if (menu && menu->back_cb_) {
|
||||
ESP_LOGI("BackButton", "执行返回回调");
|
||||
menu->back_cb_();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, LV_EVENT_CLICKED, nullptr);
|
||||
// 确保设置用户数据
|
||||
lv_obj_set_user_data(raw(), this);
|
||||
// ESP_LOGI("AppMenu", "返回按钮创建完成, 按钮地址: %p", back_btn_->raw());
|
||||
}
|
||||
|
||||
static lv_style_t& getBackButtonStyle() {
|
||||
static lv_style_t style;
|
||||
static bool style_initialized = false;
|
||||
|
||||
if (!style_initialized) {
|
||||
lv_style_init(&style);
|
||||
lv_style_set_bg_color(&style, lv_color_hex(0x4CAF50)); // 绿色背景
|
||||
lv_style_set_bg_opa(&style, LV_OPA_COVER);
|
||||
lv_style_set_radius(&style, 20);
|
||||
lv_style_set_border_width(&style, 0);
|
||||
style_initialized = true;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
private:
|
||||
std::function<void(const char*)> click_cb_;
|
||||
std::function<void()> back_cb_;
|
||||
std::unique_ptr<Button> back_btn_;
|
||||
std::unique_ptr<Label> back_label_;
|
||||
};
|
||||
|
||||
// 主页
|
||||
@@ -794,44 +1261,37 @@ public:
|
||||
|
||||
// 背景图片(默认空,可热切换)
|
||||
bg_img_ = std::make_unique<Image>(*this);
|
||||
|
||||
// 顶栏电池
|
||||
battery_ = std::make_unique<Battery>(*this);
|
||||
battery_->size(60, 30)
|
||||
.percent(true)
|
||||
.align(LV_ALIGN_CENTER, -10, -150);
|
||||
|
||||
// 底栏时间
|
||||
datetime_ = std::make_unique<DateTime>(*this);
|
||||
datetime_->format("%m/%d %a %H:%M")
|
||||
.align(LV_ALIGN_BOTTOM_MID, nullptr, 0, -10);
|
||||
|
||||
// 长按识别:按住 500 ms 即进入应用菜单
|
||||
lv_obj_add_flag(raw(), LV_OBJ_FLAG_CLICKABLE); // 必须置位,否则收不到输入事件
|
||||
on(LV_EVENT_LONG_PRESSED, [this](lv_event_t*) {
|
||||
if (menu_open_cb_) menu_open_cb_();
|
||||
});
|
||||
}
|
||||
|
||||
// 热切换背景
|
||||
HomePage& bg(const char* bin_path, const uint32_t w, const uint32_t h) {
|
||||
bg_img_->bin(bin_path, w, h);
|
||||
bg_img_->center();
|
||||
return *this;
|
||||
}
|
||||
|
||||
// 注册电量回调
|
||||
HomePage& onBattery(std::function<uint8_t()> cb) {
|
||||
battery_->onRead(std::move(cb));
|
||||
return *this;
|
||||
}
|
||||
|
||||
// 注册时间回调
|
||||
HomePage& onDateTime(std::function<void(char*, size_t)> cb) {
|
||||
datetime_->onRead(std::move(cb));
|
||||
return *this;
|
||||
}
|
||||
|
||||
// 注册右滑 → 打开菜单的回调
|
||||
HomePage& onOpenMenu(std::function<void()> cb) {
|
||||
menu_open_cb_ = std::move(cb);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//
|
||||
// Created by misaki on 2025/9/28.
|
||||
//
|
||||
|
||||
#include "BaseApp.h"
|
||||
@@ -0,0 +1,415 @@
|
||||
//
|
||||
// Created by misaki on 2025/9/28.
|
||||
//
|
||||
#pragma once
|
||||
#include <optional>
|
||||
#include <iostream>
|
||||
|
||||
#include "lvpp.h"
|
||||
#include "cpp_json.h"
|
||||
#include "CommClass.h"
|
||||
LV_FONT_DECLARE(SiYuanHeiTiGoogleBan); // 引入字体
|
||||
struct AppCalc : lvgl_cpp::BaseApp {
|
||||
using BaseApp::BaseApp;
|
||||
void onShow() override {
|
||||
lvgl_cpp::Label(*this).text("Calculator").center();
|
||||
}
|
||||
};
|
||||
struct AppMusic : lvgl_cpp::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_clear_flag(this->raw(), LV_OBJ_FLAG_SCROLLABLE); // 禁止滚动
|
||||
lv_obj_set_scroll_dir(this->raw(), LV_DIR_NONE);
|
||||
|
||||
lv_obj_set_style_bg_opa(this->raw(), LV_OPA_COVER, 0);
|
||||
gif->src("sequence02mmm.gif")
|
||||
.center();
|
||||
});
|
||||
|
||||
// 按钮文字
|
||||
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);
|
||||
}
|
||||
|
||||
void onCleanup() override {
|
||||
if (gif) gif->drop(); // 对象还活着,立即释放
|
||||
}
|
||||
|
||||
~AppMusic() override{
|
||||
gif.reset();
|
||||
btn_.reset();
|
||||
}
|
||||
private:
|
||||
std::optional<lvgl_cpp::Button> btn_; // c++17 轻量 RAII
|
||||
std::optional<lvgl_cpp::Gif> gif;
|
||||
};
|
||||
|
||||
class ButtonApp : public lvgl_cpp::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_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<lvgl_cpp::Button> btn_; // c++17 轻量 RAII
|
||||
};
|
||||
|
||||
class WebSocketVoice : public lvgl_cpp::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::ERROR,
|
||||
1000);
|
||||
});
|
||||
/* 按钮文字 */
|
||||
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<lvgl_cpp::Button> btn_; // c++17 轻量 RAII
|
||||
};
|
||||
|
||||
#include "SDFileManager.h"
|
||||
#include "SimpleI2SForwarder.h"
|
||||
#include <mbedtls/base64.h> // base64 编解码
|
||||
class LongPressButtonAnim : public lvgl_cpp::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);
|
||||
btn_->onLong([](const std::atomic<bool>& stop) { // 长按循环按钮
|
||||
std::vector<int16_t> audio_data;
|
||||
std::vector<int16_t> audio_data_sum;
|
||||
constexpr int SHIFT_AVE = 14; // 你喂 AFE 时右移的位数
|
||||
constexpr int GAIN_SHIFT = 14 - 9; // 想拉回 0 dB 需要左移多少
|
||||
ESP_LOGI("LP","开始录音");
|
||||
while(!stop){
|
||||
// 获取所有可用的最新数据,并累加到 audio_data_sum 当中
|
||||
if (LatestDataForwarder::getInstance()->retrieveLatestData(audio_data, 100)) {
|
||||
// 放大到正常幅度再累加(放大声音)
|
||||
for (auto &s : audio_data) {
|
||||
int32_t tmp = static_cast<int32_t>(s) << GAIN_SHIFT; // 能量拉回 0 dB
|
||||
if (tmp > 32767) tmp = 32767;
|
||||
if (tmp < -32768) tmp = -32768;
|
||||
audio_data_sum.push_back(static_cast<int16_t>(tmp));
|
||||
}
|
||||
// ESP_LOGI("LP", "got %zu samples, total now %zu",
|
||||
// audio_data.size(), audio_data_sum.size());
|
||||
} else {
|
||||
ESP_LOGW("LP", "no data within 100 ms");
|
||||
}
|
||||
// vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
ESP_LOGI("LP","结束录音");
|
||||
// 在SD卡根目录创建一个pcm文件,向其中写入数据
|
||||
// ESP_LOGI("LP","开始写入文件");
|
||||
// SDFileManager::getInstance()->writeFileSync(
|
||||
// ToolsClass::makeFullPath("temp_little_endian_16bit_16khz.pcm").c_str(),
|
||||
// reinterpret_cast<const char *>(audio_data_sum.data()),
|
||||
// audio_data_sum.size() * sizeof(int16_t),
|
||||
// "wb");
|
||||
// ESP_LOGI("LP","写入文件成功");
|
||||
// 将数据封装成 json 后通过 websocket 发送给服务器
|
||||
ESP_LOGI("LP","开始发送数据");
|
||||
size_t pcm_bytes = audio_data_sum.size() * sizeof(int16_t);
|
||||
size_t b64_len = 0;
|
||||
mbedtls_base64_encode(nullptr, 0, &b64_len,
|
||||
reinterpret_cast<const unsigned char*>(audio_data_sum.data()),
|
||||
pcm_bytes);
|
||||
/* 再真正编码 */
|
||||
std::vector<unsigned char> b64_buf(b64_len);
|
||||
mbedtls_base64_encode(b64_buf.data(), b64_len, &b64_len,
|
||||
reinterpret_cast<const unsigned char*>(audio_data_sum.data()),
|
||||
pcm_bytes);
|
||||
/* 转成 std::string(不含 \0) */
|
||||
std::string b64_str(reinterpret_cast<char*>(b64_buf.data()), b64_len);
|
||||
cppjson::Json audio_json = cppjson::Json::object(); // 创建 json 对象
|
||||
cppjson::Json audio_json_body = cppjson::Json::object();
|
||||
audio_json.set("type", cppjson::Json("audio"));
|
||||
audio_json_body
|
||||
.set("data", cppjson::Json(b64_str))
|
||||
.set("size", cppjson::Json(audio_data_sum.size()));
|
||||
audio_json.set("body", audio_json_body);
|
||||
WebSocketManager::getInstance()->sendJson(audio_json);
|
||||
ESP_LOGI("LP","发送数据 OVER!");
|
||||
ESP_LOGI("LP","long exit");
|
||||
});
|
||||
btn_->onShort([]() { // 短按(点一下)
|
||||
ESP_LOGI("Btn", "short click");
|
||||
});
|
||||
|
||||
/* 按钮文字 */
|
||||
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<lvgl_cpp::LongPressButton> btn_; // c++17 轻量 RAII
|
||||
};
|
||||
|
||||
|
||||
#include "SpeechRecognizer.h"
|
||||
#include "AudioOutput.h"
|
||||
#include "PetBaseClass.h"
|
||||
#include "PetDao.h"
|
||||
class PetApp : public lvgl_cpp::BaseApp {
|
||||
public:
|
||||
using BaseApp::BaseApp; // 继承 exit 按钮机制
|
||||
|
||||
// 打印宠物当前状态
|
||||
static void printPet(const PetBase& pet)
|
||||
{
|
||||
const auto& info = pet.getPetInfo();
|
||||
std::cout << "【状态】"
|
||||
<< " 名称:" << info.pet_name
|
||||
<< " HP:" << info.pet_hp
|
||||
<< " 亲密度:" << info.pet_density
|
||||
<< " 身份:" << info.pet_identity
|
||||
<< " 阶段:" << static_cast<int>(pet.getCurrentStage())
|
||||
<< " 动作:" << static_cast<int>(pet.getCurrentAction())
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
// 初始化测试宠物信息
|
||||
void initPet() {
|
||||
// 创建雪豹
|
||||
PetBaseInfo info{
|
||||
.pet_name = "芝士雪豹",
|
||||
.pet_hp = 100,
|
||||
.pet_density = 45, // 故意设低,方便看进化
|
||||
.pet_identity = "我是顶真,来自妈妈省"
|
||||
};
|
||||
const auto stageStrategy = std::make_shared<PetStageStrategy>();
|
||||
const auto actionStrategy = std::make_shared<PetActionStrategy>();
|
||||
|
||||
// 填阶段资源(模型/音频)
|
||||
stageStrategy->addStage(PetStageType::PET_STAGE_YOUNG, "pet_data/cheese_snow_leopard/models/setdown30.gif");
|
||||
stageStrategy->addStage(PetStageType::PET_STAGE_ADULT, "pet_data/cheese_snow_leopard/models/setdown10.gif");
|
||||
stageStrategy->addStage(PetStageType::PET_STAGE_OLD, "pet_data/cheese_snow_leopard/models/setdown20.gif");
|
||||
stageStrategy->addStageAudio(PetStageType::PET_STAGE_YOUNG, "pet_data/cheese_snow_leopard/audio/dog.pcm");
|
||||
stageStrategy->addStageAudio(PetStageType::PET_STAGE_ADULT, "/audio/snow_adult.mp3");
|
||||
stageStrategy->addStageAudio(PetStageType::PET_STAGE_OLD, "/audio/snow_old.mp3");
|
||||
|
||||
// 填动作资源
|
||||
actionStrategy->addAction(PetActionType::PET_ACTION_EAT, "pet_data/cheese_snow_leopard/models/eat1.gif");
|
||||
actionStrategy->addActionAudio(PetActionType::PET_ACTION_EAT, "pet_data/cheese_snow_leopard/audio/cat.pcm");
|
||||
// 创建宠物
|
||||
pet = std::make_shared<PetBase>(info, stageStrategy, actionStrategy);
|
||||
// 4. 构造观察者
|
||||
audioObserver = std::make_shared<PetAudioStrategy>(stageStrategy, actionStrategy);
|
||||
audioObserver->setAudioCallback([&](PetType petType, const std::string& path){
|
||||
std::cout << "🔊 播放音频: " << path << std::endl;
|
||||
if (petType == PetType::Action) { // 如果播放的是动作
|
||||
AudioOutput::getInstance()->playPcmFile(ToolsClass::makeFullPath(path).c_str());
|
||||
}
|
||||
if (petType == PetType::Stage) { // 如果播放的是阶段
|
||||
// 暂时nothing to do
|
||||
}
|
||||
});
|
||||
renderObserver = std::make_shared<PetRendererStrategy>(stageStrategy, actionStrategy);
|
||||
renderObserver->setRenderCallback([&](PetType petType, const std::string& path){
|
||||
std::cout << "🖼️ 渲染模型: " << path << std::endl;
|
||||
if (petType == PetType::Action) { // 如果播放的是动作,则突发播放
|
||||
gif_->burst(path.c_str(), 7000);
|
||||
}
|
||||
if (petType == PetType::Stage) { // 如果播放的是阶段,则持久循环播放
|
||||
std::cout << "渲染模型路径: " << path << std::endl;
|
||||
gif_->loop(path.c_str());
|
||||
}
|
||||
});
|
||||
audioObserver->subscribe(pet);
|
||||
renderObserver->subscribe(pet);
|
||||
|
||||
// 触发初始渲染
|
||||
pet->notifyStageChange(PetStageType::PET_STAGE_YOUNG, pet->getCurrentStage());
|
||||
printPet(*pet);
|
||||
}
|
||||
|
||||
// 初始化语音识别
|
||||
void initSpeech() {
|
||||
// 获取 SpeechRecognizer 全局单例实例
|
||||
recognizer = SpeechRecognizer::getInstance();
|
||||
// 初始化
|
||||
recognizer->init(false, VAD_MODE_3, "/sdcard/srmodels");
|
||||
// 添加自定义命令
|
||||
std::vector<std::pair<int, std::string> > commands = {
|
||||
{0, "mo tou"}, // 摸头
|
||||
{1, "wei shi"}, // 喂食
|
||||
{2, "jiao liang sheng"}, // 叫两声
|
||||
};
|
||||
// 添加命令
|
||||
recognizer->addCommands(commands);
|
||||
// 注册回调函数
|
||||
recognizer->registerCommandCallback([this](int command_id, const std::string& phrase, float probability) {
|
||||
this->commandCallback(command_id, phrase, probability);
|
||||
});
|
||||
recognizer->registerStateCallback([this](const std::string& state) {
|
||||
this->stateCallback(state);
|
||||
});
|
||||
recognizer->start(); // 启动识别
|
||||
}
|
||||
// 状态回调函数
|
||||
void stateCallback(const std::string &state) {
|
||||
ESP_LOGI("Example", "状态改变到: %s", state.c_str());
|
||||
}
|
||||
// 命令回调函数
|
||||
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:
|
||||
pet->play();
|
||||
info_->text_fmt("HP: %d \n Dens: %d", pet->getPetInfo().pet_hp, pet->getPetInfo().pet_density);
|
||||
printPet(*pet);
|
||||
break;
|
||||
case 1:
|
||||
pet->feed();
|
||||
info_->text_fmt("HP: %d \n Dens: %d", pet->getPetInfo().pet_hp, pet->getPetInfo().pet_density);
|
||||
printPet(*pet);
|
||||
break;
|
||||
case 2:
|
||||
pet->play();
|
||||
info_->text_fmt("HP: %d \n Dens: %d", pet->getPetInfo().pet_hp, pet->getPetInfo().pet_density);
|
||||
printPet(*pet);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGI("Example", "未知的命令ID: %d", command_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
void onShow() override {
|
||||
lv_obj_set_style_bg_color(this->raw(), lv_color_black(), 0); // 背景
|
||||
lv_obj_clear_flag(this->raw(), LV_OBJ_FLAG_SCROLLABLE); // 禁止滚动
|
||||
gif_ = std::make_unique<lvgl_cpp::GifPlayer>(*this); // 注意初始化顺序
|
||||
gif_->size(360, 360);
|
||||
initPet(); // 初始化测试宠物信息
|
||||
initSpeech(); // 初始化语音识别
|
||||
|
||||
// 居中按钮:180×60 px,圆角,现代蓝
|
||||
btn_.emplace(*this);
|
||||
btn_->size(120, 40)
|
||||
.align(LV_ALIGN_CENTER, nullptr, 0, 80);
|
||||
// 按钮文字
|
||||
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);
|
||||
btn_->onShort([]() { // 短按事件(点一下)
|
||||
ESP_LOGI("Btn", "short click");
|
||||
});
|
||||
|
||||
btn_->onLong([](const std::atomic<bool>& stop) { // 长按循环按钮
|
||||
std::vector<int16_t> audio_data;
|
||||
std::vector<int16_t> audio_data_sum;
|
||||
constexpr int SHIFT_AVE = 14; // 喂 AFE 时右移的位数
|
||||
constexpr int GAIN_SHIFT = 14 - 9; // 想拉回 0 dB 需要左移多少
|
||||
ESP_LOGI("LP","开始录音");
|
||||
while(!stop){
|
||||
// 获取所有可用的最新数据,并累加到 audio_data_sum 当中
|
||||
if (LatestDataForwarder::getInstance()->retrieveLatestData(audio_data, 100)) {
|
||||
// 放大到正常幅度再累加(放大声音)
|
||||
for (auto &s : audio_data) {
|
||||
int32_t tmp = static_cast<int32_t>(s) << GAIN_SHIFT; // 能量拉回 0 dB
|
||||
if (tmp > 32767) tmp = 32767;
|
||||
if (tmp < -32768) tmp = -32768;
|
||||
audio_data_sum.push_back(static_cast<int16_t>(tmp));
|
||||
}
|
||||
// ESP_LOGI("LP", "got %zu samples, total now %zu",
|
||||
// audio_data.size(), audio_data_sum.size());
|
||||
} else {
|
||||
ESP_LOGW("LP", "no data within 100 ms");
|
||||
}
|
||||
}
|
||||
ESP_LOGI("LP","结束录音");
|
||||
// 将数据封装成 json 后通过 websocket 发送给服务器
|
||||
ESP_LOGI("LP","开始发送数据");
|
||||
size_t pcm_bytes = audio_data_sum.size() * sizeof(int16_t);
|
||||
size_t b64_len = 0;
|
||||
mbedtls_base64_encode(nullptr, 0, &b64_len,
|
||||
reinterpret_cast<const unsigned char*>(audio_data_sum.data()),
|
||||
pcm_bytes);
|
||||
// 再真正编码
|
||||
std::vector<unsigned char> b64_buf(b64_len);
|
||||
mbedtls_base64_encode(b64_buf.data(), b64_len, &b64_len,
|
||||
reinterpret_cast<const unsigned char*>(audio_data_sum.data()),
|
||||
pcm_bytes);
|
||||
// 转成 std::string(不含 \0)
|
||||
std::string b64_str(reinterpret_cast<char*>(b64_buf.data()), b64_len);
|
||||
cppjson::Json audio_json = cppjson::Json::object(); // 创建 json 对象
|
||||
cppjson::Json audio_json_body = cppjson::Json::object();
|
||||
audio_json.set("type", "audio");
|
||||
audio_json_body
|
||||
.set("data", b64_str)
|
||||
.set("size", cppjson::Json(audio_data_sum.size()));
|
||||
audio_json.set("body", audio_json_body);
|
||||
WebSocketManager::getInstance()->sendJson(audio_json);
|
||||
ESP_LOGI("LP","发送数据 OVER!");
|
||||
ESP_LOGI("LP","long exit");
|
||||
});
|
||||
|
||||
// 透明文字浮层(初始提示)
|
||||
info_.emplace(*this);
|
||||
info_->text_fmt("HP: %d \n Dens: %d", pet->getPetInfo().pet_hp, pet->getPetInfo().pet_density)
|
||||
.textColor(lv_color_black())
|
||||
.font(&SiYuanHeiTiGoogleBan)
|
||||
.alignText(LV_TEXT_ALIGN_CENTER)
|
||||
.place(LV_ALIGN_CENTER, nullptr, 0, -120); // 屏幕正中央
|
||||
}
|
||||
|
||||
void onCleanup() override {
|
||||
if (gif_) gif_->stop(); // 对象还活着,立即释放
|
||||
}
|
||||
~PetApp() override = default; // unique_ptr 自动析构
|
||||
|
||||
private:
|
||||
std::shared_ptr<PetBase> pet;
|
||||
std::shared_ptr<PetAudioStrategy> audioObserver;
|
||||
std::shared_ptr<PetRendererStrategy> renderObserver;
|
||||
|
||||
private:
|
||||
SpeechRecognizer *recognizer; // 语音识别
|
||||
std::unique_ptr<lvgl_cpp::GifPlayer> gif_; // gif 播放器
|
||||
std::optional<lvgl_cpp::TextOverlay> info_; // 透明文字
|
||||
std::optional<lvgl_cpp::LongPressButton> btn_; // 长按按钮
|
||||
};
|
||||
Reference in New Issue
Block a user