Rdzleo 14776acb0a feat: 完成 AI/吧唧双模式完全隔离重构 + 触摸坐标日志 + SPIFFS 预烧录
## 核心变更

### 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>
2026-02-28 10:23:04 +08:00

249 lines
7.2 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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);
}