427 lines
11 KiB
C
427 lines
11 KiB
C
#include "OFFLINE_STORAGE.h"
|
|
#include "FLASH_SPIFS.h"
|
|
#include "cJSON.h"
|
|
#include "esp_log.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include <sys/stat.h>
|
|
#include <dirent.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
#define TAG "OFFLINE_STORAGE"
|
|
#define DATA_DIR "/flash/data"
|
|
#define INDEX_FILE "/flash/index.json"
|
|
|
|
// 最大文件数(防止文件过多)
|
|
#define MAX_FILES 10000
|
|
|
|
// 数据类型目录名
|
|
static const char* DATA_TYPE_DIRS[] = {
|
|
"", // UNKNOWN
|
|
"modbus", // MODBUS
|
|
"status" // DEVICE_STATUS
|
|
};
|
|
|
|
// 索引结构
|
|
static cJSON *index_json = NULL;
|
|
|
|
/**
|
|
* @brief 初始化数据目录
|
|
*/
|
|
static esp_err_t init_data_directories(void)
|
|
{
|
|
struct stat st;
|
|
|
|
// 检查主目录是否存在
|
|
if (stat(DATA_DIR, &st) != 0) {
|
|
mkdir(DATA_DIR, 0777);
|
|
ESP_LOGI(TAG, "创建数据目录: %s", DATA_DIR);
|
|
}
|
|
|
|
// 创建各类型子目录
|
|
for (int i = 1; i < 3; i++) {
|
|
char dir_path[64];
|
|
snprintf(dir_path, sizeof(dir_path), "%s/%s", DATA_DIR, DATA_TYPE_DIRS[i]);
|
|
|
|
if (stat(dir_path, &st) != 0) {
|
|
mkdir(dir_path, 0777);
|
|
ESP_LOGI(TAG, "创建子目录: %s", dir_path);
|
|
}
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief 加载索引文件
|
|
*/
|
|
static esp_err_t load_index(void)
|
|
{
|
|
FILE *f = fopen(INDEX_FILE, "r");
|
|
if (f == NULL) {
|
|
ESP_LOGI(TAG, "索引文件不存在,创建新索引");
|
|
index_json = cJSON_CreateObject();
|
|
if (index_json == NULL) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
// 创建索引数组
|
|
cJSON *index_array = cJSON_CreateArray();
|
|
if (index_array == NULL) {
|
|
cJSON_Delete(index_json);
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
cJSON_AddItemToObject(index_json, "files", index_array);
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
// 读取文件内容
|
|
fseek(f, 0, SEEK_END);
|
|
long file_size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
char *buffer = malloc(file_size + 1);
|
|
if (buffer == NULL) {
|
|
fclose(f);
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
fread(buffer, 1, file_size, f);
|
|
buffer[file_size] = '\0';
|
|
fclose(f);
|
|
|
|
// 解析JSON
|
|
index_json = cJSON_Parse(buffer);
|
|
free(buffer);
|
|
|
|
if (index_json == NULL) {
|
|
ESP_LOGE(TAG, "索引文件解析失败");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief 保存索引文件
|
|
*/
|
|
static esp_err_t save_index(void)
|
|
{
|
|
if (index_json == NULL) {
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
char *json_str = cJSON_PrintUnformatted(index_json);
|
|
if (json_str == NULL) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
FILE *f = fopen(INDEX_FILE, "w");
|
|
if (f == NULL) {
|
|
free(json_str);
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
size_t json_len = strlen(json_str);
|
|
size_t written = fwrite(json_str, 1, json_len, f);
|
|
fclose(f);
|
|
free(json_str);
|
|
|
|
if (written != json_len) {
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief 生成新文件名
|
|
*/
|
|
static esp_err_t generate_filename(char *filename, size_t max_len, offline_data_type_t data_type)
|
|
{
|
|
if (data_type <= OFFLINE_DATA_TYPE_UNKNOWN || data_type > OFFLINE_DATA_TYPE_DEVICE_STATUS) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
// 使用时间戳作为文件名
|
|
uint32_t timestamp = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
|
snprintf(filename, max_len, "%s/%s/%010lu.json",
|
|
DATA_DIR,
|
|
DATA_TYPE_DIRS[data_type],
|
|
(unsigned long)timestamp);
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t offline_storage_init(void)
|
|
{
|
|
ESP_LOGI(TAG, "初始化离线存储模块...");
|
|
|
|
// 检查SPIFFS是否挂载
|
|
if (!flash_spiffs_is_mounted()) {
|
|
ESP_LOGE(TAG, "SPIFFS未挂载");
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
// 初始化目录结构
|
|
ESP_ERROR_CHECK(init_data_directories());
|
|
|
|
// 加载索引
|
|
ESP_ERROR_CHECK(load_index());
|
|
|
|
ESP_LOGI(TAG, "离线存储模块初始化完成");
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t offline_storage_store(const char *data, size_t length, offline_data_type_t data_type)
|
|
{
|
|
if (data == NULL || length == 0) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
if (data_type <= OFFLINE_DATA_TYPE_UNKNOWN || data_type > OFFLINE_DATA_TYPE_DEVICE_STATUS) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
// 生成文件名
|
|
char filename[64];
|
|
esp_err_t ret = generate_filename(filename, sizeof(filename), data_type);
|
|
if (ret != ESP_OK) {
|
|
return ret;
|
|
}
|
|
|
|
// 写入文件
|
|
FILE *f = fopen(filename, "w");
|
|
if (f == NULL) {
|
|
ESP_LOGE(TAG, "无法创建文件: %s", filename);
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
size_t written = fwrite(data, 1, length, f);
|
|
fclose(f);
|
|
|
|
if (written != length) {
|
|
ESP_LOGE(TAG, "写入不完整: %zu/%zu", written, length);
|
|
remove(filename);
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
ESP_LOGD(TAG, "存储数据: %s, 大小: %zu 字节", filename, length);
|
|
|
|
// 更新索引
|
|
cJSON *files_array = cJSON_GetObjectItemCaseSensitive(index_json, "files");
|
|
if (files_array != NULL) {
|
|
cJSON *file_info = cJSON_CreateObject();
|
|
if (file_info != NULL) {
|
|
cJSON_AddStringToObject(file_info, "filename", filename);
|
|
cJSON_AddNumberToObject(file_info, "type", data_type);
|
|
cJSON_AddNumberToObject(file_info, "size", length);
|
|
cJSON_AddItemToArray(files_array, file_info);
|
|
save_index();
|
|
}
|
|
}
|
|
|
|
// 检查文件数量,如果超过限制则删除最旧的
|
|
uint32_t count = offline_storage_get_count();
|
|
if (count > MAX_FILES) {
|
|
ESP_LOGW(TAG, "文件数量过多 (%u),删除最旧的", count);
|
|
offline_storage_delete_oldest();
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t offline_storage_read_oldest(char *buffer, size_t max_len, offline_data_type_t *out_data_type)
|
|
{
|
|
if (buffer == NULL) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
cJSON *files_array = cJSON_GetObjectItemCaseSensitive(index_json, "files");
|
|
if (files_array == NULL || !cJSON_IsArray(files_array)) {
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
if (cJSON_GetArraySize(files_array) == 0) {
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
// 获取第一个元素(最旧的)
|
|
cJSON *first_file = cJSON_GetArrayItem(files_array, 0);
|
|
if (first_file == NULL) {
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
cJSON *filename_item = cJSON_GetObjectItemCaseSensitive(first_file, "filename");
|
|
cJSON *type_item = cJSON_GetObjectItemCaseSensitive(first_file, "type");
|
|
|
|
if (filename_item == NULL || !cJSON_IsString(filename_item)) {
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
const char *filename = cJSON_GetStringValue(filename_item);
|
|
if (filename == NULL) {
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
// 读取文件
|
|
FILE *f = fopen(filename, "r");
|
|
if (f == NULL) {
|
|
ESP_LOGE(TAG, "无法打开文件: %s (索引记录与实际文件不匹配)", filename);
|
|
ESP_LOGW(TAG, "删除无效的索引记录: %s", filename);
|
|
|
|
// 文件不存在,从索引中删除这个无效记录
|
|
offline_storage_delete_oldest();
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long file_size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (file_size > (long)max_len) {
|
|
ESP_LOGE(TAG, "文件过大: %s (%ld bytes)", filename, file_size);
|
|
fclose(f);
|
|
// 删除过大的文件
|
|
offline_storage_delete_oldest();
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
size_t read = fread(buffer, 1, file_size, f);
|
|
fclose(f);
|
|
buffer[read] = '\0';
|
|
|
|
ESP_LOGD(TAG, "读取数据: %s, 大小: %zu 字节", filename, read);
|
|
|
|
// 返回数据类型
|
|
if (out_data_type != NULL && type_item != NULL && cJSON_IsNumber(type_item)) {
|
|
*out_data_type = (offline_data_type_t)cJSON_GetNumberValue(type_item);
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t offline_storage_delete_oldest(void)
|
|
{
|
|
cJSON *files_array = cJSON_GetObjectItemCaseSensitive(index_json, "files");
|
|
if (files_array == NULL || !cJSON_IsArray(files_array)) {
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
if (cJSON_GetArraySize(files_array) == 0) {
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
// 删除第一个元素
|
|
cJSON *first_file = cJSON_DetachItemFromArray(files_array, 0);
|
|
if (first_file == NULL) {
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
cJSON *filename_item = cJSON_GetObjectItemCaseSensitive(first_file, "filename");
|
|
const char *filename = NULL;
|
|
|
|
if (filename_item != NULL && cJSON_IsString(filename_item)) {
|
|
filename = cJSON_GetStringValue(filename_item);
|
|
if (filename != NULL) {
|
|
remove(filename);
|
|
ESP_LOGD(TAG, "删除文件: %s", filename);
|
|
}
|
|
}
|
|
|
|
cJSON_Delete(first_file);
|
|
|
|
// 检查并删除所有重复的索引记录(如果存在)
|
|
if (filename != NULL) {
|
|
int duplicate_count = 0;
|
|
int array_size = cJSON_GetArraySize(files_array);
|
|
for (int i = array_size - 1; i >= 0; i--) {
|
|
cJSON *item = cJSON_GetArrayItem(files_array, i);
|
|
if (item == NULL) {
|
|
continue;
|
|
}
|
|
cJSON *item_filename = cJSON_GetObjectItemCaseSensitive(item, "filename");
|
|
if (item_filename != NULL && cJSON_IsString(item_filename)) {
|
|
const char *item_name = cJSON_GetStringValue(item_filename);
|
|
if (item_name != NULL && strcmp(filename, item_name) == 0) {
|
|
// 找到重复记录,删除它
|
|
cJSON_DeleteItemFromArray(files_array, i);
|
|
duplicate_count++;
|
|
ESP_LOGW(TAG, "删除重复的索引记录: %s", item_name);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (duplicate_count > 0) {
|
|
ESP_LOGW(TAG, "删除了 %d 个重复的索引记录", duplicate_count);
|
|
}
|
|
}
|
|
|
|
save_index();
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
uint32_t offline_storage_get_count(void)
|
|
{
|
|
cJSON *files_array = cJSON_GetObjectItemCaseSensitive(index_json, "files");
|
|
if (files_array == NULL || !cJSON_IsArray(files_array)) {
|
|
return 0;
|
|
}
|
|
|
|
return (uint32_t)cJSON_GetArraySize(files_array);
|
|
}
|
|
|
|
bool offline_storage_has_data(void)
|
|
{
|
|
return offline_storage_get_count() > 0;
|
|
}
|
|
|
|
esp_err_t offline_storage_clear_all(void)
|
|
{
|
|
ESP_LOGW(TAG, "清空所有离线数据...");
|
|
|
|
cJSON *files_array = cJSON_GetObjectItemCaseSensitive(index_json, "files");
|
|
if (files_array == NULL || !cJSON_IsArray(files_array)) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
// 删除所有文件
|
|
cJSON *item = NULL;
|
|
cJSON_ArrayForEach(item, files_array) {
|
|
cJSON *filename_item = cJSON_GetObjectItemCaseSensitive(item, "filename");
|
|
if (filename_item != NULL && cJSON_IsString(filename_item)) {
|
|
const char *filename = cJSON_GetStringValue(filename_item);
|
|
if (filename != NULL) {
|
|
remove(filename);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 清空索引
|
|
cJSON_DeleteItemFromObject(index_json, "files");
|
|
cJSON *new_array = cJSON_CreateArray();
|
|
if (new_array != NULL) {
|
|
cJSON_AddItemToObject(index_json, "files", new_array);
|
|
}
|
|
|
|
save_index();
|
|
|
|
ESP_LOGI(TAG, "所有离线数据已清空");
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t offline_storage_get_usage(size_t *used, size_t *total)
|
|
{
|
|
if (used == NULL || total == NULL) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
*total = flash_spiffs_get_total_size();
|
|
*used = flash_spiffs_get_used_size();
|
|
|
|
return ESP_OK;
|
|
}
|