## 核心变更 ### 1. 双模式完全隔离 (Phase 2+4) - 拆分 InitializeButtons() 为 InitializeBadgeModeButtons() + InitializeAiModeButtons() - 构造函数按 device_mode 分支:吧唧模式不创建 PowerSaveTimer/BackgroundTask - 吧唧模式不注册音量/故事按键回调,避免调用 GetAudioCodec() 崩溃 - GPIO0 由 iot_button 统一处理,dzbj_button 仅注册 KEY2(GPIO4) - SetDeviceState() 中 background_task_ 空指针保护 ### 2. 吧唧模式 BOOT 按键崩溃修复 - 新增 dzbj_boot_click_handler()(C 函数,避免 lvgl.h 与 display.h 冲突) - 移植 dzbj 的唤醒屏幕/退出手电筒/返回Home 完整逻辑 ### 3. esp_timer 阻塞 LVGL 渲染修复 - iot_button 回调在 esp_timer 任务中执行,vTaskDelay 会阻塞 lv_tick_inc - 改为 xTaskCreate 派发到独立 FreeRTOS 任务,避免冻结 LVGL 渲染 ### 4. 触摸坐标日志 + SPIFFS 预烧录 - esp_lvgl_port_touch.c 添加触摸坐标打印 - CMakeLists.txt 添加 spiffs_create_partition_image 自动打包 spiffs_image/ ### 5. dzbj 模块文件新增 - device_mode: NVS 设备模式管理 (AI=0/吧唧=1) - dzbj_button: GPIO4 KEY2 中断 + BOOT 点击处理 - dzbj_ble: BLE GATT 图传服务 (0x0B00) - dzbj_battery: ADC 电池电压监测 - sleep_mgr: 10s 超时熄屏低功耗管理 - pages: 图片浏览/GIF播放/PWM亮度 - fatfs: SPIFFS 文件管理 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
249 lines
7.2 KiB
C
249 lines
7.2 KiB
C
/**
|
||
* @file fatfs.c
|
||
* @brief SPIFFS 文件系统管理模块(从 dzbj 移植)
|
||
*
|
||
* 提供 SPIFFS 挂载、文件读写、JPEG 解码等功能。
|
||
*/
|
||
|
||
#include "esp_err.h"
|
||
#include "esp_log.h"
|
||
#include "esp_spiffs.h"
|
||
#include "fatfs.h"
|
||
#include <dirent.h>
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
|
||
static const char *TAG = "FATFS";
|
||
|
||
// 初始化SPIFFS文件系统
|
||
void fatfs_init(void) {
|
||
esp_vfs_spiffs_conf_t conf = {
|
||
.base_path = "/spiflash",
|
||
.partition_label = "storage",
|
||
.max_files = 5,
|
||
.format_if_mount_failed = true,
|
||
};
|
||
esp_err_t err = esp_vfs_spiffs_register(&conf);
|
||
if (err != ESP_OK) {
|
||
ESP_LOGE(TAG, "Failed to mount SPIFFS (%s)", esp_err_to_name(err));
|
||
return;
|
||
}
|
||
size_t total = 0, used = 0;
|
||
err = esp_spiffs_info("storage", &total, &used);
|
||
if (err != ESP_OK) {
|
||
ESP_LOGE(TAG, "Failed to get SPIFFS info (%s)", esp_err_to_name(err));
|
||
} else {
|
||
ESP_LOGI(TAG, "SPIFFS: Total size: %d, Used: %d", total, used);
|
||
}
|
||
}
|
||
|
||
// 读取图片数据到内存
|
||
void read_img(uint8_t *img_p) {
|
||
FILE *f = fopen("/spiflash/img.bin", "r");
|
||
if (f == NULL) {
|
||
ESP_LOGE(TAG, "OPEN ERROR");
|
||
return;
|
||
}
|
||
size_t size = fread(img_p, sizeof(uint8_t), 129600 * 2, f);
|
||
fclose(f);
|
||
if (size != 0) {
|
||
ESP_LOGI(TAG, "read success!");
|
||
}
|
||
}
|
||
|
||
// 测试FATFS文件系统
|
||
void fs_test(void) {
|
||
FILE *f = fopen("/spiflash/img.bin", "r");
|
||
if (f == NULL) {
|
||
ESP_LOGE(TAG, "Failed to open file for reading");
|
||
return;
|
||
}
|
||
uint8_t line[2];
|
||
fread(line, sizeof(uint8_t), 2, f);
|
||
fclose(f);
|
||
ESP_LOGI(TAG, "Read from file: %x %x", line[0], line[1]);
|
||
}
|
||
|
||
// 列出目录下所有文件名
|
||
void fatfs_list_all_filenames(const char *dir_path, bool recursive) {
|
||
DIR *dir = opendir(dir_path);
|
||
if (dir == NULL) {
|
||
ESP_LOGE(TAG, "无法打开目录: %s", dir_path);
|
||
return;
|
||
}
|
||
struct dirent *entry;
|
||
while ((entry = readdir(dir)) != NULL) {
|
||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||
continue;
|
||
}
|
||
char full_path[512];
|
||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
|
||
struct stat file_stat;
|
||
if (stat(full_path, &file_stat) == 0 && S_ISDIR(file_stat.st_mode)) {
|
||
if (recursive) {
|
||
fatfs_list_all_filenames(full_path, recursive);
|
||
}
|
||
} else if (stat(full_path, &file_stat) == 0 && S_ISREG(file_stat.st_mode)) {
|
||
ESP_LOGI(TAG, "文件名: %s, 大小:%d", full_path, (int)file_stat.st_size);
|
||
}
|
||
}
|
||
closedir(dir);
|
||
}
|
||
|
||
// 删除目录下所有空文件
|
||
void fatfs_remove_nullData(const char *dir_path) {
|
||
DIR *dir = opendir(dir_path);
|
||
if (dir == NULL) {
|
||
ESP_LOGE(TAG, "无法打开目录: %s", dir_path);
|
||
return;
|
||
}
|
||
struct dirent *entry;
|
||
while ((entry = readdir(dir)) != NULL) {
|
||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||
continue;
|
||
}
|
||
char full_path[512];
|
||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, (char*)entry->d_name);
|
||
struct stat file_stat;
|
||
stat(full_path, &file_stat);
|
||
if ((int)file_stat.st_size == 0) {
|
||
remove(full_path);
|
||
ESP_LOGE(TAG, "删除空文件: %s", full_path);
|
||
}
|
||
}
|
||
closedir(dir);
|
||
}
|
||
|
||
// 删除目录下所有文件
|
||
void fatfs_remove_allData(const char *dir_path) {
|
||
DIR *dir = opendir(dir_path);
|
||
if (dir == NULL) {
|
||
ESP_LOGE(TAG, "无法打开目录: %s", dir_path);
|
||
return;
|
||
}
|
||
struct dirent *entry;
|
||
while ((entry = readdir(dir)) != NULL) {
|
||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||
continue;
|
||
}
|
||
char full_path[512];
|
||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, (char*)entry->d_name);
|
||
remove(full_path);
|
||
ESP_LOGE(TAG, "删除文件: %s", full_path);
|
||
}
|
||
closedir(dir);
|
||
}
|
||
|
||
// 检查图片是否有效(文件大小不为0)
|
||
bool fats_img_isOK(char* img_path) {
|
||
struct stat file_stat;
|
||
stat(img_path, &file_stat);
|
||
return file_stat.st_size > 0;
|
||
}
|
||
|
||
// JPEG 解码:从 SPIFFS 读取 JPEG 并解码为 RGB565
|
||
esp_err_t DecodeImg(char *imgpath, uint8_t** imgData, esp_jpeg_image_output_t *outimage) {
|
||
FILE *f = fopen(imgpath, "rb");
|
||
if (f == NULL) {
|
||
ESP_LOGE(TAG, "OPEN ERROR: %s", imgpath);
|
||
return ESP_FAIL;
|
||
}
|
||
struct stat file_stat;
|
||
stat(imgpath, &file_stat);
|
||
|
||
// 分配输出缓冲区(360×360 RGB565)
|
||
*imgData = malloc(360 * 360 * 2);
|
||
if (*imgData == NULL) {
|
||
ESP_LOGE(TAG, "输出缓冲区分配失败");
|
||
fclose(f);
|
||
return ESP_FAIL;
|
||
}
|
||
|
||
// 分配输入缓冲区(JPEG 原始数据)
|
||
uint8_t *imgEncoderData = malloc(file_stat.st_size);
|
||
if (imgEncoderData == NULL) {
|
||
ESP_LOGE(TAG, "输入缓冲区分配失败(需%d字节)", (int)file_stat.st_size);
|
||
free(*imgData);
|
||
*imgData = NULL;
|
||
fclose(f);
|
||
return ESP_FAIL;
|
||
}
|
||
|
||
size_t read_len = fread(imgEncoderData, sizeof(uint8_t), file_stat.st_size, f);
|
||
fclose(f);
|
||
|
||
if (read_len != (size_t)file_stat.st_size) {
|
||
ESP_LOGE(TAG, "文件读取不完整(预期:%d,实际:%zu)",
|
||
(int)file_stat.st_size, read_len);
|
||
free(imgEncoderData);
|
||
free(*imgData);
|
||
*imgData = NULL;
|
||
return ESP_FAIL;
|
||
}
|
||
|
||
// 验证 JPEG 头
|
||
if (file_stat.st_size < 2 || imgEncoderData[0] != 0xFF || imgEncoderData[1] != 0xD8) {
|
||
ESP_LOGE(TAG, "不是有效JPEG文件: %s", imgpath);
|
||
free(imgEncoderData);
|
||
free(*imgData);
|
||
*imgData = NULL;
|
||
return ESP_FAIL;
|
||
}
|
||
|
||
uint32_t outbuf_size = 360 * 360 * sizeof(uint8_t) * 2;
|
||
esp_jpeg_image_cfg_t jpeg_cfg = {
|
||
.indata = imgEncoderData,
|
||
.indata_size = file_stat.st_size,
|
||
.outbuf = *imgData,
|
||
.outbuf_size = outbuf_size,
|
||
.out_format = JPEG_IMAGE_FORMAT_RGB565,
|
||
.flags = {
|
||
.swap_color_bytes = true,
|
||
},
|
||
};
|
||
esp_err_t ret = esp_jpeg_decode(&jpeg_cfg, outimage);
|
||
free(imgEncoderData);
|
||
return ret;
|
||
}
|
||
|
||
// 测试读取图片数据
|
||
void test_readimg(char *imgpath, uint16_t size) {
|
||
FILE *f = fopen(imgpath, "r");
|
||
if (f == NULL) {
|
||
ESP_LOGE(TAG, "OPEN ERROR");
|
||
return;
|
||
}
|
||
uint8_t *head = malloc(size);
|
||
if (head == NULL) {
|
||
fclose(f);
|
||
return;
|
||
}
|
||
fread(head, sizeof(uint8_t), size, f);
|
||
fclose(f);
|
||
for (int i = 0; i < size; i++) {
|
||
printf("%x ", *(head + i));
|
||
}
|
||
printf("\n");
|
||
free(head);
|
||
}
|
||
|
||
// 获取目录下所有图片文件名
|
||
void fat_getAllimgList(const char *dir_path, char** list, uint8_t* num) {
|
||
*num = 0;
|
||
DIR *dir = opendir(dir_path);
|
||
if (dir == NULL) {
|
||
ESP_LOGE(TAG, "无法打开目录: %s", dir_path);
|
||
return;
|
||
}
|
||
struct dirent *entry;
|
||
while ((entry = readdir(dir)) != NULL) {
|
||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||
continue;
|
||
}
|
||
list[*num] = strdup(entry->d_name);
|
||
(*num)++;
|
||
}
|
||
closedir(dir);
|
||
}
|