1. 完整封装并拓展了SD卡文件管理类,支持基本文件管理功能
2. 简单封装了LVGL渲染类,已经封装好了gif渲染功能 3. 修复了硬件厂商提供的驱动的Bug 4. 初步定义了宠物基类的抽象信息
This commit is contained in:
@@ -63,15 +63,19 @@ esp_pthread_cfg_t create_config(const char *name, int core_id, int stack, int pr
|
||||
#include "ThreadManager.h"
|
||||
#include "WifiConnectors.h"
|
||||
#include <string>
|
||||
|
||||
#include "LVGLRender.h"
|
||||
#include "SDFileManager.h"
|
||||
void OTAClass::Init() {
|
||||
ESP_LOGI("OTA", "Init");
|
||||
|
||||
ESP_LOGI("OTAClass::Init", "当前固件版本 1.0.1");
|
||||
|
||||
// 测试Wifi
|
||||
// WifiConnectors::getInstance()->log();
|
||||
//
|
||||
// WifiConnectors::getInstance()->connectWifi("Misaki-2.4G", "88888888");
|
||||
// 列出当前目录内容
|
||||
std::string listing = SDFileManager::getInstance()->lsCommand(".", false, true);
|
||||
ESP_LOGI("SD", "%s", listing.c_str());
|
||||
|
||||
LVGLRender::getInstance()->RenderGif("sequence01.gif");
|
||||
|
||||
// 1. 创建普通函数一个可以运行在任意核上的线程
|
||||
ThreadConfig config1;
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
// Created by misaki on 2025/9/2.
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "PetInterface.h"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//
|
||||
// Created by misaki on 2025/9/8.
|
||||
//
|
||||
|
||||
#include "PetInterface.h"
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Created by misaki on 2025/9/8.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
// 资源句柄,防止裸指针到处飞
|
||||
template<class T>
|
||||
using Handle = std::shared_ptr<T>; /// 宠物句柄
|
||||
|
||||
// 前向声明
|
||||
class IPet; /// 宠物接口
|
||||
using IPetPtr = Handle<IPet>; /// 宠物句柄
|
||||
|
||||
// 事件类型
|
||||
enum class Emotion {
|
||||
Feed, /// 喂食
|
||||
Happy, /// 高兴
|
||||
Angry, /// 生气
|
||||
Upset /// 沮丧
|
||||
};
|
||||
|
||||
// 成长阶段
|
||||
enum class Stage {
|
||||
Baby, /// 幼年
|
||||
Teen, /// 青年
|
||||
Adult /// 成长
|
||||
};
|
||||
|
||||
// 数值封装
|
||||
struct Vitals {
|
||||
int hp = 100; // 0-100 /// 生命值
|
||||
int intimacy = 0; // 0-100 /// 亲密度
|
||||
};
|
||||
|
||||
// 身份设定
|
||||
struct Persona {
|
||||
std::string systemPrompt; /// 系统提示
|
||||
std::string greet; /// 默认欢迎语
|
||||
// …可扩展
|
||||
};
|
||||
|
||||
// 动物元数据(只读)
|
||||
struct AnimalManifest {
|
||||
std::string id; // "snow_leopard" /// ID
|
||||
std::string displayName; /// 显示名称
|
||||
Persona persona; /// 身份设定
|
||||
std::unordered_map<Stage, std::string> modelPath; /// 阶段→模型
|
||||
std::unordered_map<Emotion, std::string> audioPath; /// 阶段→音频
|
||||
std::string version; /// 版本
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// Created by misaki on 2025/9/8.
|
||||
//
|
||||
|
||||
#include "ThreadManager.h"
|
||||
#include "LVGLRender.h"
|
||||
#include "I2C_Driver.h"
|
||||
#include "ST77916.h"
|
||||
#include "LVGL_Driver.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
|
||||
LVGLRender* LVGLRender::LVGLRenderInstance = nullptr;
|
||||
std::mutex LVGLRender::instance_mutex;
|
||||
uint16_t LVGLRender::fps = 30;
|
||||
|
||||
LVGLRender* LVGLRender::getInstance() {
|
||||
// 双检锁(DCLP),C++11 起 atomic+mutex 组合保证线程安全
|
||||
LVGLRender* tmp = LVGLRenderInstance;
|
||||
if (tmp == nullptr) {
|
||||
std::lock_guard<std::mutex> lock(instance_mutex);
|
||||
tmp = LVGLRenderInstance;
|
||||
if (tmp == nullptr) {
|
||||
tmp = new LVGLRender();
|
||||
LVGLRenderInstance = tmp;
|
||||
}
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
void LVGLRender::LVGL_Update() {
|
||||
while (true) {
|
||||
vTaskDelay(LVGL_DELAY_FROM_FPS(LVGLRender::fps));
|
||||
lv_timer_handler();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 构造函数
|
||||
LVGLRender::LVGLRender() {
|
||||
ESP_LOGI("LVGL_Render", "LVGL_Render构造函数...初始化媒体驱动...");
|
||||
I2C_Init();
|
||||
LCD_Init();
|
||||
LVGL_Init();
|
||||
ESP_LOGI("LVGL_Render", "LVGL_Render构造函数...初始化媒体驱动成功...");
|
||||
|
||||
ESP_LOGI("LVGL_Render", "LVGL_Render构造函数...创建LVGL心跳...");
|
||||
|
||||
ThreadConfig trickConfig;
|
||||
trickConfig.core_id = 1; // 渲染分配给核1
|
||||
trickConfig.name = "LVGL_Render_Heartbeat";
|
||||
trickConfig.priority = 5;
|
||||
trickConfig.stack_size = 4096; // 给LVGL一个较大的堆栈,避免栈溢出
|
||||
|
||||
std::thread tick_thread = ThreadManager::createMemberThread(trickConfig, this, &LVGLRender::LVGL_Update);
|
||||
|
||||
tick_thread.detach(); // 线程分离 生命周期跟随主线程结束,线程结束后自动销毁
|
||||
|
||||
ESP_LOGI("LVGL_Render", "LVGL_Render构造函数...创建LVGL心跳成功...");
|
||||
}
|
||||
|
||||
LVGLRender::~LVGLRender() {
|
||||
|
||||
}
|
||||
|
||||
void LVGLRender::log() {
|
||||
ESP_LOGI("LVGL_Render", "LVGL_Render log...");
|
||||
}
|
||||
|
||||
// 静态:拼出完整路径
|
||||
std::string LVGLRender::makeFullPath(const std::string& filename)
|
||||
{
|
||||
return "/sdcard/" + filename;
|
||||
}
|
||||
|
||||
#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)
|
||||
{
|
||||
// 构造 lv_img_dsc_t —— 数据指针直接指向 vector 内部
|
||||
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 = (lv_coord_t)w;
|
||||
gif_desc.header.h = (lv_coord_t)h;
|
||||
gif_desc.data_size = data.size();
|
||||
gif_desc.data = data.data(); // 指向 vector 内部
|
||||
|
||||
// 创建 lv_gif 对象
|
||||
lv_obj_t* gif = lv_gif_create(lv_scr_act());
|
||||
lv_gif_set_src(gif, &gif_desc);
|
||||
lv_obj_center(gif);
|
||||
|
||||
ESP_LOGI("LVGLRender", "GIF 已渲染到屏幕");
|
||||
}
|
||||
|
||||
void LVGLRender::RenderGif(const std::string &filename) {
|
||||
std::string fullPath = 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;
|
||||
|
||||
// LVGL 渲染函数
|
||||
renderGifInternal(gifBin, w, h);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// Created by misaki on 2025/9/8.
|
||||
//
|
||||
/**
|
||||
* 本类为单例类,用于实现LVGL的渲染
|
||||
* 封装了一整套的LVGL渲染流
|
||||
* 同时兼顾了底层的显示驱动
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#define LVGL_DELAY_FROM_FPS(fps) pdMS_TO_TICKS(1000 / (fps))
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
class LVGLRender {
|
||||
public:
|
||||
static std::string makeFullPath(const std::string& filename);
|
||||
|
||||
public:
|
||||
static LVGLRender* getInstance();
|
||||
|
||||
/**
|
||||
* 全屏渲染gif动画
|
||||
* @brief 渲染GIF文件
|
||||
* @param filename GIF文件路径
|
||||
*/
|
||||
void RenderGif(const std::string &filename);
|
||||
|
||||
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();
|
||||
|
||||
LVGLRender(LVGLRender const&) = delete; // 拷贝构造函数私有化
|
||||
LVGLRender& operator=(LVGLRender const&) = delete;// 赋值运算符私有化
|
||||
|
||||
void LVGL_Update(); // 渲染lvgl上下文(持久性线程)
|
||||
|
||||
private:
|
||||
static LVGLRender* LVGLRenderInstance; /// 单例实例
|
||||
static std::mutex instance_mutex; /// 单例锁
|
||||
static uint16_t fps; /// 帧率
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
//
|
||||
// Created by misaki on 2025/9/9.
|
||||
//
|
||||
|
||||
#include "SDFileManager.h"
|
||||
#include "SD_MMC.h"
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <iomanip>
|
||||
|
||||
SDFileManager* SDFileManager::SDFileInstance = nullptr;
|
||||
std::mutex SDFileManager::instance_mutex;
|
||||
|
||||
SDFileManager *SDFileManager::getInstance() {
|
||||
// 双检锁(DCLP),C++11 起 atomic+mutex 组合保证线程安全
|
||||
SDFileManager* tmp = SDFileInstance;
|
||||
if (tmp == nullptr) {
|
||||
std::lock_guard<std::mutex> lock(instance_mutex);
|
||||
tmp = SDFileInstance;
|
||||
if (tmp == nullptr) {
|
||||
tmp = new SDFileManager();
|
||||
SDFileInstance = tmp;
|
||||
}
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
SDFileManager::SDFileManager() : is_initialized(false), current_directory(MOUNT_POINT) {
|
||||
// 初始化SD卡
|
||||
init();
|
||||
// 获取Flash大小
|
||||
Flash_Searching();
|
||||
}
|
||||
|
||||
SDFileManager::~SDFileManager() {
|
||||
// 清理资源
|
||||
}
|
||||
|
||||
void SDFileManager::init() {
|
||||
SD_Init();
|
||||
is_initialized = true;
|
||||
}
|
||||
|
||||
bool SDFileManager::writeFileSync(const char* path, const char* data) {
|
||||
std::lock_guard<std::mutex> lock(file_operation_mutex);
|
||||
|
||||
if (!is_initialized) {
|
||||
ESP_LOGE("SDFileManager", "SD card not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t result = s_example_write_file(path, const_cast<char*>(data));
|
||||
return result == ESP_OK;
|
||||
}
|
||||
|
||||
std::string SDFileManager::readFileSync(const char* path) {
|
||||
std::lock_guard<std::mutex> lock(file_operation_mutex);
|
||||
|
||||
if (!is_initialized) {
|
||||
ESP_LOGE("SDFileManager", "SD card not initialized");
|
||||
return "";
|
||||
}
|
||||
|
||||
FILE* file = fopen(path, "r");
|
||||
if (file == nullptr) {
|
||||
ESP_LOGE("SDFileManager", "Failed to open file: %s", path);
|
||||
return "";
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
fseek(file, 0, SEEK_END);
|
||||
long size = ftell(file);
|
||||
fseek(file, 0, SEEK_SET);
|
||||
|
||||
// 读取文件内容
|
||||
std::string content;
|
||||
content.resize(size);
|
||||
size_t bytes_read = fread(&content[0], 1, size, file);
|
||||
fclose(file);
|
||||
|
||||
if (bytes_read != static_cast<size_t>(size)) {
|
||||
ESP_LOGE("SDFileManager", "Failed to read entire file: %s", path);
|
||||
return "";
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
std::vector<std::string> SDFileManager::listFilesSync(const char* directory, const char* extension) {
|
||||
std::lock_guard<std::mutex> lock(file_operation_mutex);
|
||||
|
||||
if (!is_initialized) {
|
||||
ESP_LOGE("SDFileManager", "SD card not initialized");
|
||||
return {};
|
||||
}
|
||||
|
||||
const int max_files = 50;
|
||||
char file_names[max_files][100];
|
||||
uint16_t file_count = Folder_retrieval(directory, extension, file_names, max_files);
|
||||
|
||||
std::vector<std::string> files;
|
||||
for (uint16_t i = 0; i < file_count; ++i) {
|
||||
files.emplace_back(file_names[i]);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
FILE* SDFileManager::openFileSync(const char* path, const char* mode) {
|
||||
std::lock_guard<std::mutex> lock(file_operation_mutex);
|
||||
|
||||
if (!is_initialized) {
|
||||
ESP_LOGE("SDFileManager", "SD card not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return Open_File(path);
|
||||
}
|
||||
|
||||
bool SDFileManager::closeFileSync(FILE* file) {
|
||||
if (file != nullptr) {
|
||||
return fclose(file) == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SDFileManager::asyncWriteFile(const char* path, const char* data, WriteCallback callback) {
|
||||
ThreadConfig config = getThreadConfig("write_file");
|
||||
|
||||
ThreadManager::createThread(config, [this, path = std::string(path),
|
||||
data = std::string(data), callback]() {
|
||||
bool success = this->writeFileSync(path.c_str(), data.c_str());
|
||||
if (callback) {
|
||||
callback(success, path.c_str());
|
||||
}
|
||||
}).detach(); // 分离线程,使其独立运行
|
||||
}
|
||||
|
||||
void SDFileManager::asyncReadFile(const char* path, ReadCallback callback) {
|
||||
if (!callback) {
|
||||
return; // 没有回调函数,异步读取无意义
|
||||
}
|
||||
|
||||
ThreadConfig config = getThreadConfig("read_file");
|
||||
|
||||
ThreadManager::createThread(config, [this, path = std::string(path), callback]() {
|
||||
std::string content = this->readFileSync(path.c_str());
|
||||
bool success = !content.empty();
|
||||
callback(success, path.c_str(), content);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void SDFileManager::asyncListFiles(const char* directory, const char* extension, ListCallback callback) {
|
||||
if (!callback) {
|
||||
return; // 没有回调函数,异步列出文件无意义
|
||||
}
|
||||
|
||||
ThreadConfig config = getThreadConfig("list_files");
|
||||
|
||||
ThreadManager::createThread(config, [this, directory = std::string(directory),
|
||||
extension = std::string(extension ? extension : ""),
|
||||
callback]() {
|
||||
std::vector<std::string> files = this->listFilesSync(
|
||||
directory.c_str(),
|
||||
extension.empty() ? nullptr : extension.c_str()
|
||||
);
|
||||
bool success = !files.empty();
|
||||
callback(success, files);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void SDFileManager::asyncOpenFile(const char* path, const char* mode, OpenCallback callback) {
|
||||
if (!callback) {
|
||||
return; // 没有回调函数,异步打开文件无意义
|
||||
}
|
||||
|
||||
ThreadConfig config = getThreadConfig("open_file");
|
||||
|
||||
ThreadManager::createThread(config, [this, path = std::string(path),
|
||||
mode = std::string(mode), callback]() {
|
||||
FILE* file = this->openFileSync(path.c_str(), mode.c_str());
|
||||
bool success = (file != nullptr);
|
||||
callback(success, file);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
ThreadConfig SDFileManager::getThreadConfig(const char* operation) {
|
||||
ThreadConfig config;
|
||||
config.name = "sd_" + std::string(operation);
|
||||
config.core_id = -1; // 不绑定核心
|
||||
config.stack_size = 4096;
|
||||
config.priority = 5;
|
||||
config.inherit_cfg = false;
|
||||
return config;
|
||||
}
|
||||
|
||||
// 实现ls命令
|
||||
std::string SDFileManager::lsCommand(const char* path, bool recursive, bool showDetails) {
|
||||
std::lock_guard<std::mutex> lock(file_operation_mutex);
|
||||
|
||||
if (!is_initialized) {
|
||||
ESP_LOGE("SDFileManager", "SD card not initialized");
|
||||
return "Error: SD card not initialized";
|
||||
}
|
||||
|
||||
std::string fullPath = path;
|
||||
if (fullPath.empty() || fullPath == ".") {
|
||||
fullPath = current_directory;
|
||||
} else if (fullPath[0] != '/') {
|
||||
// 相对路径
|
||||
fullPath = current_directory + "/" + fullPath;
|
||||
}
|
||||
|
||||
DIR* dir = opendir(fullPath.c_str());
|
||||
if (dir == nullptr) {
|
||||
return "Error: Cannot open directory " + fullPath;
|
||||
}
|
||||
|
||||
std::stringstream result;
|
||||
struct dirent* entry;
|
||||
struct stat st;
|
||||
|
||||
result << "Directory listing for: " << fullPath << "\n";
|
||||
result << "----------------------------------------\n";
|
||||
|
||||
while ((entry = readdir(dir)) != nullptr) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string entryPath = fullPath + "/" + entry->d_name;
|
||||
if (stat(entryPath.c_str(), &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (showDetails) {
|
||||
result << getFileInfoString(entry->d_name, &st);
|
||||
} else {
|
||||
result << entry->d_name;
|
||||
}
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
result << "/";
|
||||
}
|
||||
result << "\n";
|
||||
|
||||
// 递归列出子目录
|
||||
if (recursive && S_ISDIR(st.st_mode)) {
|
||||
std::string subDirResult = lsCommand(entryPath.c_str(), true, showDetails);
|
||||
result << subDirResult;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return result.str();
|
||||
}
|
||||
|
||||
// 实现cat命令
|
||||
std::string SDFileManager::catCommand(const char* path, bool lineNumbers) {
|
||||
std::lock_guard<std::mutex> lock(file_operation_mutex);
|
||||
|
||||
if (!is_initialized) {
|
||||
ESP_LOGE("SDFileManager", "SD card not initialized");
|
||||
return "Error: SD card not initialized";
|
||||
}
|
||||
|
||||
std::string fullPath = path;
|
||||
if (fullPath[0] != '/') {
|
||||
// 相对路径
|
||||
fullPath = current_directory + "/" + fullPath;
|
||||
}
|
||||
|
||||
FILE* file = fopen(fullPath.c_str(), "r");
|
||||
if (file == nullptr) {
|
||||
return "Error: Cannot open file " + fullPath;
|
||||
}
|
||||
|
||||
std::stringstream result;
|
||||
char buffer[256];
|
||||
int lineNumber = 1;
|
||||
|
||||
while (fgets(buffer, sizeof(buffer), file) != nullptr) {
|
||||
if (lineNumbers) {
|
||||
result << std::setw(4) << lineNumber << ": ";
|
||||
}
|
||||
result << buffer;
|
||||
lineNumber++;
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
return result.str();
|
||||
}
|
||||
|
||||
// 实现pwd命令
|
||||
std::string SDFileManager::pwdCommand() {
|
||||
return current_directory;
|
||||
}
|
||||
|
||||
// 实现cd命令
|
||||
bool SDFileManager::cdCommand(const char* path) {
|
||||
std::lock_guard<std::mutex> lock(file_operation_mutex);
|
||||
|
||||
if (!is_initialized) {
|
||||
ESP_LOGE("SDFileManager", "SD card not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string newPath;
|
||||
if (path[0] == '/') {
|
||||
// 绝对路径
|
||||
newPath = path;
|
||||
} else if (strcmp(path, "..") == 0) {
|
||||
// 上级目录
|
||||
size_t pos = current_directory.find_last_of('/');
|
||||
if (pos != std::string::npos && pos > 0) {
|
||||
newPath = current_directory.substr(0, pos);
|
||||
} else {
|
||||
newPath = "/";
|
||||
}
|
||||
} else {
|
||||
// 相对路径
|
||||
newPath = current_directory + "/" + path;
|
||||
}
|
||||
|
||||
// 检查路径是否存在且是目录
|
||||
struct stat st;
|
||||
if (stat(newPath.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
current_directory = newPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 实现mkdir命令
|
||||
bool SDFileManager::mkdirCommand(const char* path) {
|
||||
std::lock_guard<std::mutex> lock(file_operation_mutex);
|
||||
|
||||
if (!is_initialized) {
|
||||
ESP_LOGE("SDFileManager", "SD card not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string fullPath = path;
|
||||
if (fullPath[0] != '/') {
|
||||
// 相对路径
|
||||
fullPath = current_directory + "/" + fullPath;
|
||||
}
|
||||
|
||||
// 创建目录
|
||||
int result = mkdir(fullPath.c_str(), 0777);
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
// 实现rm命令
|
||||
bool SDFileManager::rmCommand(const char* path, bool recursive) {
|
||||
std::lock_guard<std::mutex> lock(file_operation_mutex);
|
||||
|
||||
if (!is_initialized) {
|
||||
ESP_LOGE("SDFileManager", "SD card not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string fullPath = path;
|
||||
if (fullPath[0] != '/') {
|
||||
// 相对路径
|
||||
fullPath = current_directory + "/" + fullPath;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (stat(fullPath.c_str(), &st) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (recursive) {
|
||||
return removeDirectoryRecursive(fullPath.c_str());
|
||||
} else {
|
||||
// 非递归删除空目录
|
||||
return rmdir(fullPath.c_str()) == 0;
|
||||
}
|
||||
} else {
|
||||
// 删除文件
|
||||
return remove(fullPath.c_str()) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 实现stat命令
|
||||
std::string SDFileManager::statCommand(const char* path) {
|
||||
std::lock_guard<std::mutex> lock(file_operation_mutex);
|
||||
|
||||
if (!is_initialized) {
|
||||
ESP_LOGE("SDFileManager", "SD card not initialized");
|
||||
return "Error: SD card not initialized";
|
||||
}
|
||||
|
||||
std::string fullPath = path;
|
||||
if (fullPath[0] != '/') {
|
||||
// 相对路径
|
||||
fullPath = current_directory + "/" + fullPath;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (stat(fullPath.c_str(), &st) != 0) {
|
||||
return "Error: Cannot get file status for " + fullPath;
|
||||
}
|
||||
|
||||
return getFileInfoString(fullPath.c_str(), &st);
|
||||
}
|
||||
|
||||
// 辅助方法:递归删除目录
|
||||
bool SDFileManager::removeDirectoryRecursive(const char* path) {
|
||||
DIR* dir = opendir(path);
|
||||
if (dir == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(dir)) != nullptr) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string entryPath = std::string(path) + "/" + entry->d_name;
|
||||
struct stat st;
|
||||
if (stat(entryPath.c_str(), &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (!removeDirectoryRecursive(entryPath.c_str())) {
|
||||
closedir(dir);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (remove(entryPath.c_str()) != 0) {
|
||||
closedir(dir);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return rmdir(path) == 0;
|
||||
}
|
||||
|
||||
// 辅助方法:获取文件信息字符串
|
||||
std::string SDFileManager::getFileInfoString(const char* path, const struct stat* st) {
|
||||
std::stringstream info;
|
||||
|
||||
// 文件类型和权限
|
||||
info << getFileTypeString(st->st_mode);
|
||||
info << ((st->st_mode & S_IRUSR) ? "r" : "-");
|
||||
info << ((st->st_mode & S_IWUSR) ? "w" : "-");
|
||||
info << ((st->st_mode & S_IXUSR) ? "x" : "-");
|
||||
info << ((st->st_mode & S_IRGRP) ? "r" : "-");
|
||||
info << ((st->st_mode & S_IWGRP) ? "w" : "-");
|
||||
info << ((st->st_mode & S_IXGRP) ? "x" : "-");
|
||||
info << ((st->st_mode & S_IROTH) ? "r" : "-");
|
||||
info << ((st->st_mode & S_IWOTH) ? "w" : "-");
|
||||
info << ((st->st_mode & S_IXOTH) ? "x" : "-");
|
||||
|
||||
// 文件大小
|
||||
info << " " << std::setw(10) << formatFileSize(st->st_size);
|
||||
|
||||
// 最后修改时间
|
||||
struct tm* timeinfo = localtime(&st->st_mtime);
|
||||
char timeBuf[20];
|
||||
strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M", timeinfo);
|
||||
info << " " << timeBuf;
|
||||
|
||||
// 文件名
|
||||
info << " " << path;
|
||||
|
||||
return info.str();
|
||||
}
|
||||
|
||||
// 辅助方法:获取文件类型字符串
|
||||
std::string SDFileManager::getFileTypeString(mode_t mode) {
|
||||
if (S_ISREG(mode)) return "-"; // 普通文件
|
||||
if (S_ISDIR(mode)) return "d"; // 目录
|
||||
if (S_ISCHR(mode)) return "c"; // 字符设备
|
||||
if (S_ISBLK(mode)) return "b"; // 块设备
|
||||
if (S_ISFIFO(mode)) return "p"; // 管道
|
||||
if (S_ISLNK(mode)) return "l"; // 符号链接
|
||||
if (S_ISSOCK(mode)) return "s"; // 套接字
|
||||
return "?"; // 未知类型
|
||||
}
|
||||
|
||||
// 辅助方法:格式化文件大小
|
||||
std::string SDFileManager::formatFileSize(size_t size) {
|
||||
std::stringstream ss;
|
||||
if (size < 1024) {
|
||||
ss << size << " B";
|
||||
} else if (size < 1024 * 1024) {
|
||||
ss << std::fixed << std::setprecision(1) << (size / 1024.0) << " KB";
|
||||
} else if (size < 1024 * 1024 * 1024) {
|
||||
ss << std::fixed << std::setprecision(1) << (size / (1024.0 * 1024.0)) << " MB";
|
||||
} else {
|
||||
ss << std::fixed << std::setprecision(1) << (size / (1024.0 * 1024.0 * 1024.0)) << " GB";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
//
|
||||
// Created by misaki on 2025/9/9.
|
||||
//
|
||||
/**
|
||||
* 本模块用于管理SD卡文件
|
||||
* 同样为单例类
|
||||
* 支持异步读取和写入
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include "ThreadManager.h"
|
||||
#include "SD_MMC.h"
|
||||
|
||||
class SDFileManager {
|
||||
public:
|
||||
// 文件操作回调函数类型定义
|
||||
using WriteCallback = std::function<void(bool success, const char* path)>;
|
||||
using ReadCallback = std::function<void(bool success, const char* path, const std::string& content)>;
|
||||
using ListCallback = std::function<void(bool success, const std::vector<std::string>& files)>;
|
||||
using OpenCallback = std::function<void(bool success, FILE* file)>;
|
||||
|
||||
static SDFileManager* getInstance();
|
||||
|
||||
// 同步文件操作
|
||||
/**
|
||||
* 同步写入文件
|
||||
* @param path 文件路径
|
||||
* @param data 数据
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool writeFileSync(const char* path, const char* data);
|
||||
/**
|
||||
* 同步读取文件
|
||||
* @param path 文件路径
|
||||
* @return 文件内容
|
||||
*/
|
||||
std::string readFileSync(const char* path);
|
||||
|
||||
/**
|
||||
* 同步列出目录下的文件
|
||||
* @param directory 目录路径
|
||||
* @param extension 文件扩展名
|
||||
* @return 文件列表
|
||||
*/
|
||||
std::vector<std::string> listFilesSync(const char* directory, const char* extension = nullptr);
|
||||
|
||||
/**
|
||||
* 同步打开文件
|
||||
* @param path 文件路径
|
||||
* @param mode 模式
|
||||
* @return 文件指针
|
||||
*/
|
||||
FILE* openFileSync(const char* path, const char* mode = "rb");
|
||||
|
||||
/**
|
||||
* 同步关闭文件
|
||||
* @param file 文件指针
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool closeFileSync(FILE* file);
|
||||
|
||||
// 异步文件操作
|
||||
/**
|
||||
* 异步写入文件
|
||||
* @param path 文件路径
|
||||
* @param data 数据
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
void asyncWriteFile(const char* path, const char* data, WriteCallback callback = nullptr);
|
||||
/**
|
||||
* 异步读取文件
|
||||
* @param path 文件路径
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
void asyncReadFile(const char* path, ReadCallback callback = nullptr);
|
||||
/**
|
||||
* 异步列出目录下的文件
|
||||
* @param directory 目录路径
|
||||
* @param extension 文件扩展名
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
void asyncListFiles(const char* directory, const char* extension, ListCallback callback = nullptr);
|
||||
/**
|
||||
* 异步打开文件
|
||||
* @param path 文件路径
|
||||
* @param mode 模式
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
void asyncOpenFile(const char* path, const char* mode, OpenCallback callback = nullptr);
|
||||
|
||||
/**
|
||||
* 类似Linux的ls命令,列出目录内容
|
||||
* @param path 目录路径
|
||||
* @param recursive 是否递归列出子目录
|
||||
* @param showDetails 是否显示详细信息(大小、类型等)
|
||||
* @return 格式化后的目录列表字符串
|
||||
*/
|
||||
std::string lsCommand(const char* path, bool recursive = false, bool showDetails = false);
|
||||
|
||||
/**
|
||||
* 类似Linux的cat命令,显示文件内容
|
||||
* @param path 文件路径
|
||||
* @param lineNumbers 是否显示行号
|
||||
* @return 文件内容字符串
|
||||
*/
|
||||
std::string catCommand(const char* path, bool lineNumbers = false);
|
||||
|
||||
/**
|
||||
* 类似Linux的pwd命令,获取当前工作目录
|
||||
* @return 当前工作目录
|
||||
*/
|
||||
std::string pwdCommand();
|
||||
|
||||
/**
|
||||
* 类似Linux的cd命令,改变当前工作目录
|
||||
* @param path 目标目录路径
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool cdCommand(const char* path);
|
||||
|
||||
/**
|
||||
* 类似Linux的mkdir命令,创建目录
|
||||
* @param path 目录路径
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool mkdirCommand(const char* path);
|
||||
|
||||
/**
|
||||
* 类似Linux的rm命令,删除文件或目录
|
||||
* @param path 文件或目录路径
|
||||
* @param recursive 是否递归删除目录
|
||||
* @return 是否成功
|
||||
*/
|
||||
bool rmCommand(const char* path, bool recursive = false);
|
||||
|
||||
/**
|
||||
* 类似Linux的stat命令,获取文件/目录信息
|
||||
* @param path 文件或目录路径
|
||||
* @return 文件信息字符串
|
||||
*/
|
||||
std::string statCommand(const char* path);
|
||||
|
||||
// 获取SD卡和Flash信息
|
||||
uint32_t getSDCardSize() const { return SDCard_Size; }
|
||||
uint32_t getFlashSize() const { return Flash_Size; }
|
||||
|
||||
private:
|
||||
explicit SDFileManager(); // 私有构造函数,确保单例
|
||||
~SDFileManager();
|
||||
|
||||
SDFileManager(SDFileManager const&) = delete; // 禁止拷贝构造
|
||||
SDFileManager& operator=(SDFileManager const&) = delete; // 禁止赋值构造
|
||||
|
||||
// 初始化SD卡
|
||||
void init();
|
||||
|
||||
|
||||
// 辅助方法
|
||||
std::string getFileInfoString(const char* path, const struct stat* st);
|
||||
std::string getFileTypeString(mode_t mode);
|
||||
std::string formatFileSize(size_t size);
|
||||
bool removeDirectoryRecursive(const char* path);
|
||||
|
||||
// 线程配置
|
||||
ThreadConfig getThreadConfig(const char* operation);
|
||||
|
||||
private:
|
||||
static SDFileManager* SDFileInstance; // 单例实例
|
||||
static std::mutex instance_mutex; // 实例互斥锁
|
||||
std::mutex file_operation_mutex; // 文件操作互斥锁
|
||||
bool is_initialized; // SD卡初始化状态
|
||||
|
||||
std::string current_directory; // 当前工作目录
|
||||
};
|
||||
@@ -124,9 +124,11 @@ public:
|
||||
if (extra) {
|
||||
ss << extra;
|
||||
}
|
||||
ss << "Core id: " << xPortGetCoreID()
|
||||
<< ", prio: " << uxTaskPriorityGet(nullptr)
|
||||
<< ", minimum free stack: " << uxTaskGetStackHighWaterMark(nullptr) << " bytes.";
|
||||
ss
|
||||
<< ", 任务名: " << pcTaskGetName(nullptr)
|
||||
<< "核心 id: " << xPortGetCoreID()
|
||||
<< ", 优先级: " << uxTaskPriorityGet(nullptr)
|
||||
<< ", 栈剩余空间: " << uxTaskGetStackHighWaterMark(nullptr) << " bytes.";
|
||||
ESP_LOGI(pcTaskGetName(nullptr), "%s", ss.str().c_str());
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,5 @@ bool WifiConnectors::isWifiConnect() {
|
||||
|
||||
void WifiConnectors::log() {
|
||||
ESP_LOGI("WifiConnectors", "WifiConnectors log");
|
||||
|
||||
WIFI_Scan();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
|
||||
|
||||
class WifiConnectors{
|
||||
// 显然,Wifi连接必须是单例的,否则必然出现冲突
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "ST77916.h"
|
||||
|
||||
#include "esp_lcd_panel_io.h"
|
||||
|
||||
#define LCD_OPCODE_WRITE_CMD (0x02ULL)
|
||||
#define LCD_OPCODE_READ_CMD (0x0BULL)
|
||||
|
||||
@@ -14,7 +14,6 @@ extern "C" {
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_lcd_panel_io.h"
|
||||
#include "esp_lcd_panel_io_interface.h"
|
||||
#include "esp_intr_alloc.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "LVGL_Driver.h"
|
||||
|
||||
#include "ST77916.h"
|
||||
static const char *TAG_LVGL = "LVGL";
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ extern "C" {
|
||||
#include "lvgl.h"
|
||||
#include "demos/lv_demos.h"
|
||||
|
||||
#include "ST77916.h"
|
||||
|
||||
|
||||
#define LVGL_BUF_LEN (EXAMPLE_LCD_WIDTH * EXAMPLE_LCD_HEIGHT / 20)
|
||||
#define EXAMPLE_LVGL_TICK_PERIOD_MS 2
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "LVGL_Example.h"
|
||||
#include "LVGL_Music.h"
|
||||
#include "ST77916.h"
|
||||
#include <demos/lv_demos.h>
|
||||
// #include <demos/music/lv_demo_music_main.h>
|
||||
// #include <demos/music/lv_demo_music_list.h>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "CST816.h"
|
||||
|
||||
#include "esp_lcd_panel_io.h"
|
||||
|
||||
#define POINT_NUM_MAX (1)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ extern "C" {
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_lcd_panel_io.h"
|
||||
#include "esp_lcd_panel_io_interface.h"
|
||||
#include "esp_lcd_touch.h"
|
||||
|
||||
#include "TCA9554PWR.h"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_err.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_lcd_panel_io.h"
|
||||
#include "esp_lcd_panel_io_interface.h" // Misaki 2025.9 fixed the bus is #include "esp_lcd_panel_io.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ static void ota_start(const char *url)
|
||||
.url = url,
|
||||
.keep_alive_enable = true,
|
||||
};
|
||||
esp_https_ota_config_t ota_cfg = { .http_config = &config };
|
||||
esp_https_ota_config_t ota_cfg = {
|
||||
.http_config = &config
|
||||
};
|
||||
ESP_LOGI(TAG, "开始下载固件...");
|
||||
esp_err_t ret = esp_https_ota(&ota_cfg);
|
||||
if (ret == ESP_OK) {
|
||||
|
||||
@@ -11,7 +11,7 @@ wifi_ap_info_t *wifi_ap_list = NULL;
|
||||
uint16_t wifi_ap_count = 0;
|
||||
ble_device_info_t *ble_device_list = NULL;
|
||||
|
||||
void Wireless_Init(void)
|
||||
esp_err_t Wireless_Init(void)
|
||||
{
|
||||
// Initialize NVS.
|
||||
esp_err_t ret = nvs_flash_init(); // 初始化Flash
|
||||
@@ -20,6 +20,7 @@ void Wireless_Init(void)
|
||||
ret = nvs_flash_init(); // 重新初始化Flash
|
||||
}
|
||||
ESP_ERROR_CHECK( ret ); // 检查错误
|
||||
|
||||
// WiFi 手动调用Wifi初始化
|
||||
// xTaskCreatePinnedToCore(
|
||||
// WIFI_Init,
|
||||
@@ -38,9 +39,10 @@ void Wireless_Init(void)
|
||||
// 2,
|
||||
// NULL,
|
||||
// 0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void WIFI_Init(void *arg)
|
||||
esp_err_t WIFI_Init(void *arg)
|
||||
{
|
||||
esp_netif_init(); // 初始化网络接口
|
||||
esp_event_loop_create_default(); // 创建事件循环
|
||||
@@ -48,7 +50,8 @@ void WIFI_Init(void *arg)
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); // 创建WiFi初始化配置(使用默认配置)
|
||||
esp_wifi_init(&cfg); // 初始化WiFi
|
||||
esp_wifi_set_mode(WIFI_MODE_STA); // 设置WiFi模式为Station
|
||||
esp_wifi_start(); // 启动WiFi
|
||||
esp_err_t ret = esp_wifi_start(); // 启动WiFi
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ extern uint16_t BLE_NUM;
|
||||
extern uint16_t WIFI_NUM;
|
||||
extern bool Scan_finish;
|
||||
|
||||
void Wireless_Init(void);
|
||||
void WIFI_Init(void *arg);
|
||||
esp_err_t Wireless_Init(void);
|
||||
esp_err_t WIFI_Init(void *arg);
|
||||
uint16_t WIFI_Scan(void);
|
||||
void BLE_Init(void *arg);
|
||||
uint16_t BLE_Scan(void);
|
||||
|
||||
@@ -29,9 +29,12 @@ idf_component_register(SRCS "Bionic_sphere.c"
|
||||
"../Lib/OTA_Driver/ota_ws.c"
|
||||
# 业务代码(使用Cpp编写)
|
||||
"../Bionic_Core/PetBaseClass/PetBaseClass.cpp" # 宠物基类库
|
||||
"../Bionic_Core/PetBaseClass/PetInterface.cpp" # 宠物接口层
|
||||
"../Bionic_Core/OTAClass/OTAClass.cpp" # OTA类库
|
||||
"../Bionic_Core/CommClass/CommClass.cpp" # 通信类库
|
||||
"../Bionic_Core/ToolsClass/ToolsClass.cpp" # 工具类库
|
||||
"../Bionic_Core/ToolsClass/LVGL_Render/LVGLRender.cpp" # LVGL渲染类库
|
||||
"../Bionic_Core/ToolsClass/SDFileManager/SDFileManager.cpp" # SD文件管理类库
|
||||
"../Bionic_Core/ToolsClass/WifiConnectors/WifiConnectors.cpp" # WIFI连接类库
|
||||
"../Bionic_Core/ToolsClass/ThreadManager/ThreadManager.cpp" # 线程管理类库
|
||||
"../Bionic_Core/CppHandle/CppHandle.cpp" # C++&C兼容库
|
||||
@@ -64,6 +67,8 @@ idf_component_register(SRCS "Bionic_sphere.c"
|
||||
"../Bionic_Core/OTAClass"
|
||||
"../Bionic_Core/CommClass"
|
||||
"../Bionic_Core/ToolsClass"
|
||||
"../Bionic_Core/ToolsClass/LVGL_Render"
|
||||
"../Bionic_Core/ToolsClass/SDFileManager"
|
||||
"../Bionic_Core/ToolsClass/WifiConnectors"
|
||||
"../Bionic_Core/ToolsClass/ThreadManager"
|
||||
"../Bionic_Core/CppHandle"
|
||||
|
||||
@@ -195,3 +195,14 @@
|
||||
- [x] 2. 封装了一个模板线程类,支持创建来自单例类的成员函数线程,普通类的线程,普通函数线程
|
||||
|
||||
- [x] 3. 封装了一个Wifi模块类,支持Wifi的各种基本配置
|
||||
|
||||
#### Day12 2025.9.9(前三天在打数学建模国赛)
|
||||
##### 主要目标:完成具体业务开发&各种优化
|
||||
实际完成任务:
|
||||
- [x] 1. 完整封装并拓展了SD卡文件管理类,支持基本文件管理功能
|
||||
|
||||
- [x] 2. 简单封装了LVGL渲染类,已经封装好了gif渲染功能
|
||||
|
||||
- [x] 3. 修复了硬件厂商提供的驱动的Bug
|
||||
|
||||
- [x] 4. 初步定义了宠物基类的抽象信息
|
||||
|
||||
Reference in New Issue
Block a user