增加断网保存数据到FLASH,恢复联网重新补发,并加上标志位
This commit is contained in:
5
components/FLASH_SPIFS/CMakeLists.txt
Normal file
5
components/FLASH_SPIFS/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "FLASH_SPIFS.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES spiffs
|
||||
)
|
||||
128
components/FLASH_SPIFS/FLASH_SPIFS.c
Normal file
128
components/FLASH_SPIFS/FLASH_SPIFS.c
Normal file
@@ -0,0 +1,128 @@
|
||||
#include "FLASH_SPIFS.h"
|
||||
#include "esp_spiffs.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#define TAG "FLASH_SPIFS"
|
||||
#define MOUNT_POINT "/flash"
|
||||
|
||||
static bool is_mounted = false;
|
||||
|
||||
esp_err_t flash_spiffs_init(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "正在初始化SPIFFS文件系统...");
|
||||
|
||||
esp_vfs_spiffs_conf_t conf = {
|
||||
.base_path = MOUNT_POINT,
|
||||
.partition_label = "storage",
|
||||
.max_files = 20,
|
||||
.format_if_mount_failed = true
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_vfs_spiffs_register(&conf);
|
||||
if (ret != ESP_OK) {
|
||||
if (ret == ESP_FAIL) {
|
||||
ESP_LOGE(TAG, "无法挂载或格式化文件系统");
|
||||
} else if (ret == ESP_ERR_NOT_FOUND) {
|
||||
ESP_LOGE(TAG, "找不到分区 'storage'");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "初始化失败 (%s)", esp_err_to_name(ret));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t total = 0, used = 0;
|
||||
ret = esp_spiffs_info(conf.partition_label, &total, &used);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "获取文件系统信息失败: %s", esp_err_to_name(ret));
|
||||
esp_vfs_spiffs_unregister(conf.partition_label);
|
||||
return ret;
|
||||
}
|
||||
|
||||
is_mounted = true;
|
||||
|
||||
ESP_LOGI(TAG, "SPIFFS文件系统挂载成功");
|
||||
ESP_LOGI(TAG, "总空间: %u 字节", total);
|
||||
ESP_LOGI(TAG, "已用空间: %u 字节", used);
|
||||
ESP_LOGI(TAG, "可用空间: %u 字节", total - used);
|
||||
ESP_LOGI(TAG, "挂载点: %s", MOUNT_POINT);
|
||||
|
||||
// 注意: 文件系统检查 (esp_spiffs_check) 在初始化阶段耗时过长,可能触发看门狗
|
||||
// 如果需要检查文件系统完整性,可以在单独的任务中调用 flash_spiffs_format() 格式化
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool flash_spiffs_is_mounted(void)
|
||||
{
|
||||
return is_mounted;
|
||||
}
|
||||
|
||||
uint32_t flash_spiffs_get_total_size(void)
|
||||
{
|
||||
if (!is_mounted) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t total = 0, used = 0;
|
||||
if (esp_spiffs_info("storage", &total, &used) == ESP_OK) {
|
||||
return total;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t flash_spiffs_get_used_size(void)
|
||||
{
|
||||
if (!is_mounted) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t total = 0, used = 0;
|
||||
if (esp_spiffs_info("storage", &total, &used) == ESP_OK) {
|
||||
return used;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
esp_err_t flash_spiffs_format(void)
|
||||
{
|
||||
if (!is_mounted) {
|
||||
ESP_LOGE(TAG, "文件系统未挂载");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "正在格式化文件系统,所有数据将被删除...");
|
||||
|
||||
// 先卸载
|
||||
esp_vfs_spiffs_unregister("storage");
|
||||
is_mounted = false;
|
||||
|
||||
// 重新格式化并挂载
|
||||
esp_vfs_spiffs_conf_t conf = {
|
||||
.base_path = MOUNT_POINT,
|
||||
.partition_label = "storage",
|
||||
.max_files = 20,
|
||||
.format_if_mount_failed = true
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_vfs_spiffs_register(&conf);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "格式化失败: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
is_mounted = true;
|
||||
|
||||
// 重新获取信息
|
||||
size_t total = 0, used = 0;
|
||||
esp_spiffs_info("storage", &total, &used);
|
||||
ESP_LOGI(TAG, "文件系统格式化完成");
|
||||
ESP_LOGI(TAG, "总空间: %u 字节", total);
|
||||
ESP_LOGI(TAG, "已用空间: %u 字节", used);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
const char* flash_spiffs_get_mount_point(void)
|
||||
{
|
||||
return MOUNT_POINT;
|
||||
}
|
||||
52
components/FLASH_SPIFS/include/FLASH_SPIFS.h
Normal file
52
components/FLASH_SPIFS/include/FLASH_SPIFS.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#ifndef FLASH_SPIFS_H
|
||||
#define FLASH_SPIFS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
/**
|
||||
* @brief 初始化SPIFFS文件系统
|
||||
*
|
||||
* @return ESP_OK 成功
|
||||
* 其他 失败
|
||||
*/
|
||||
esp_err_t flash_spiffs_init(void);
|
||||
|
||||
/**
|
||||
* @brief 检查文件系统是否已挂载
|
||||
*
|
||||
* @return true 已挂载
|
||||
* false 未挂载
|
||||
*/
|
||||
bool flash_spiffs_is_mounted(void);
|
||||
|
||||
/**
|
||||
* @brief 获取文件系统总大小
|
||||
*
|
||||
* @return 总大小(字节)
|
||||
*/
|
||||
uint32_t flash_spiffs_get_total_size(void);
|
||||
|
||||
/**
|
||||
* @brief 获取文件系统已使用大小
|
||||
*
|
||||
* @return 已使用大小(字节)
|
||||
*/
|
||||
uint32_t flash_spiffs_get_used_size(void);
|
||||
|
||||
/**
|
||||
* @brief 格式化文件系统(清空所有数据)
|
||||
*
|
||||
* @return ESP_OK 成功
|
||||
* 其他 失败
|
||||
*/
|
||||
esp_err_t flash_spiffs_format(void);
|
||||
|
||||
/**
|
||||
* @brief 获取文件系统挂载路径
|
||||
*
|
||||
* @return 挂载路径字符串(如 "/flash")
|
||||
*/
|
||||
const char* flash_spiffs_get_mount_point(void);
|
||||
|
||||
#endif // FLASH_SPIFS_H
|
||||
@@ -1,3 +1,3 @@
|
||||
idf_component_register(SRCS "MQTT_ESP.c"
|
||||
PRIV_REQUIRES mqtt log STATUS_LED MODBUS_ESP SNTP_ESP json esp_wifi esp_system
|
||||
PRIV_REQUIRES mqtt log STATUS_LED MODBUS_ESP SNTP_ESP OFFLINE_STORAGE json esp_wifi esp_system
|
||||
INCLUDE_DIRS "include")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "STATUS_LED.h"
|
||||
#include "MODBUS_ESP.h"
|
||||
#include "SNTP_ESP.h"
|
||||
#include "OFFLINE_STORAGE.h"
|
||||
#include "cJSON.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_mac.h"
|
||||
@@ -23,6 +24,16 @@ static esp_mqtt_client_handle_t g_client = NULL;
|
||||
static TaskHandle_t device_status_task_handle = NULL;
|
||||
static uint32_t g_report_interval_ms = 10000; // 默认10秒上报一次
|
||||
static SemaphoreHandle_t report_interval_mutex = NULL;
|
||||
static bool g_device_status_task_started = false; // 设备状态任务是否已启动
|
||||
static bool g_device_status_task_auto_start = false; // 是否在MQTT连接后自动启动
|
||||
|
||||
// ============================
|
||||
// 离线数据补传任务相关
|
||||
// ============================
|
||||
|
||||
static TaskHandle_t offline_upload_task_handle = NULL;
|
||||
static bool g_is_online = false; // 网络在线状态
|
||||
static char offline_data_buffer[2048]; // 静态缓冲区,避免栈溢出
|
||||
|
||||
/**
|
||||
* @brief 获取设备MAC地址字符串
|
||||
@@ -194,7 +205,7 @@ static char* build_device_status_json(void)
|
||||
*/
|
||||
static void device_status_report_task(void *arg)
|
||||
{
|
||||
ESP_LOGI(TAG, "Device status report task started");
|
||||
ESP_LOGI(TAG, "设备状态上报任务已启动");
|
||||
|
||||
while (1) {
|
||||
// 检查是否应该退出
|
||||
@@ -202,22 +213,32 @@ static void device_status_report_task(void *arg)
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查MQTT是否已连接
|
||||
if (g_client != NULL) {
|
||||
// 构建设备状态JSON
|
||||
char *status_json = build_device_status_json();
|
||||
if (status_json != NULL) {
|
||||
// 构建设备状态JSON
|
||||
char *status_json = build_device_status_json();
|
||||
if (status_json != NULL) {
|
||||
// 如果网络在线,直接发布;如果离线,存储到本地Flash
|
||||
if (g_is_online && g_client != NULL) {
|
||||
// 发布到MQTT
|
||||
int ret = esp_mqtt_client_publish(g_client, CONFIG_MQTT_PUB_TOPIC,
|
||||
status_json, strlen(status_json), 0, 0);
|
||||
if (ret >= 0) {
|
||||
ESP_LOGI(TAG, "Device status published, msg_id=%d", ret);
|
||||
ESP_LOGD(TAG, "Status: %s", status_json);
|
||||
ESP_LOGI(TAG, "设备状态已发布,消息ID=%d", ret);
|
||||
ESP_LOGD(TAG, "状态: %s", status_json);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to publish device status");
|
||||
ESP_LOGW(TAG, "发布设备状态失败");
|
||||
}
|
||||
} else {
|
||||
// 网络离线,存储到本地Flash
|
||||
ESP_LOGW(TAG, "网络离线,设备状态存储到本地");
|
||||
esp_err_t ret = mqtt_store_offline(status_json, strlen(status_json),
|
||||
OFFLINE_DATA_TYPE_DEVICE_STATUS);
|
||||
if (ret == ESP_OK) {
|
||||
ESP_LOGI(TAG, "设备状态已成功存储到离线存储");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "存储设备状态到离线存储失败");
|
||||
}
|
||||
free(status_json);
|
||||
}
|
||||
free(status_json);
|
||||
}
|
||||
|
||||
// 获取上报间隔
|
||||
@@ -230,13 +251,35 @@ static void device_status_report_task(void *arg)
|
||||
vTaskDelay(pdMS_TO_TICKS(interval));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Device status report task exiting");
|
||||
ESP_LOGI(TAG, "设备状态上报任务已退出");
|
||||
device_status_task_handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
BaseType_t mqtt_start_device_status_task(uint32_t report_interval_ms)
|
||||
{
|
||||
// 如果网络不在线,只保存配置,延迟到MQTT连接后启动
|
||||
if (!g_is_online) {
|
||||
// 创建互斥量
|
||||
if (report_interval_mutex == NULL) {
|
||||
report_interval_mutex = xSemaphoreCreateMutex();
|
||||
if (report_interval_mutex == NULL) {
|
||||
ESP_LOGE(TAG, "创建报告间隔互斥锁失败");
|
||||
return pdFALSE;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
xSemaphoreTake(report_interval_mutex, portMAX_DELAY);
|
||||
g_report_interval_ms = report_interval_ms > 0 ? report_interval_ms : 10000;
|
||||
xSemaphoreGive(report_interval_mutex);
|
||||
|
||||
// 标记为自动启动
|
||||
g_device_status_task_auto_start = true;
|
||||
ESP_LOGI(TAG, "设备状态任务将在MQTT连接后启动 (间隔=%dms)", g_report_interval_ms);
|
||||
return pdPASS;
|
||||
}
|
||||
|
||||
// 如果任务已存在,先停止
|
||||
if (device_status_task_handle != NULL) {
|
||||
mqtt_stop_device_status_task();
|
||||
@@ -247,7 +290,7 @@ BaseType_t mqtt_start_device_status_task(uint32_t report_interval_ms)
|
||||
if (report_interval_mutex == NULL) {
|
||||
report_interval_mutex = xSemaphoreCreateMutex();
|
||||
if (report_interval_mutex == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create report interval mutex");
|
||||
ESP_LOGE(TAG, "创建报告间隔互斥锁失败");
|
||||
return pdFALSE;
|
||||
}
|
||||
}
|
||||
@@ -259,12 +302,13 @@ BaseType_t mqtt_start_device_status_task(uint32_t report_interval_ms)
|
||||
|
||||
// 创建任务
|
||||
BaseType_t ret = xTaskCreate(device_status_report_task, "dev_status",
|
||||
4096, NULL, 5, &device_status_task_handle);
|
||||
8192, NULL, 5, &device_status_task_handle);
|
||||
|
||||
if (ret == pdPASS) {
|
||||
ESP_LOGI(TAG, "Device status report task started (interval=%dms)", g_report_interval_ms);
|
||||
g_device_status_task_started = true;
|
||||
ESP_LOGI(TAG, "设备状态报告任务已创建 (间隔=%dms)", g_report_interval_ms);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to create device status report task");
|
||||
ESP_LOGE(TAG, "创建设备状态报告任务失败");
|
||||
}
|
||||
|
||||
return ret;
|
||||
@@ -273,14 +317,14 @@ BaseType_t mqtt_start_device_status_task(uint32_t report_interval_ms)
|
||||
void mqtt_stop_device_status_task(void)
|
||||
{
|
||||
if (device_status_task_handle != NULL) {
|
||||
ESP_LOGI(TAG, "Stopping device status report task...");
|
||||
ESP_LOGI(TAG, "正在停止设备状态报告任务...");
|
||||
|
||||
TaskHandle_t temp_handle = device_status_task_handle;
|
||||
device_status_task_handle = NULL;
|
||||
vTaskDelete(temp_handle);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
ESP_LOGI(TAG, "Device status report task stopped");
|
||||
ESP_LOGI(TAG, "设备状态报告任务已停止");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,15 +357,40 @@ static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_
|
||||
int msg_id;
|
||||
switch ((esp_mqtt_event_id_t)event_id) {
|
||||
case MQTT_EVENT_CONNECTED:
|
||||
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
|
||||
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED - MQTT已连接");
|
||||
// 订阅并取消订阅测试主题
|
||||
msg_id = esp_mqtt_client_subscribe(client, CONFIG_MQTT_SUB_TOPIC, 1);
|
||||
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
|
||||
status_led_blink_mode(2, 2); // LED2 心跳:MQTT连接正常
|
||||
ESP_LOGI(TAG, "订阅发送成功, 消息ID=%d", msg_id);
|
||||
status_led_blink_mode(2, 2); // LED2 心跳:MQTT连接正常
|
||||
|
||||
// 网络在线,启动离线数据补传任务
|
||||
g_is_online = true;
|
||||
ESP_LOGI(TAG, "网络在线,开始离线数据上传");
|
||||
|
||||
// 启动设备状态上报任务(如果配置了自动启动)
|
||||
if (g_device_status_task_auto_start && !g_device_status_task_started) {
|
||||
if (mqtt_start_device_status_task(g_report_interval_ms) == pdPASS) {
|
||||
g_device_status_task_started = true;
|
||||
g_device_status_task_auto_start = false;
|
||||
ESP_LOGI(TAG, "MQTT连接时自动启动设备状态任务");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MQTT_EVENT_DISCONNECTED:
|
||||
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
|
||||
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED - MQTT已断开连接");
|
||||
status_led_blink_mode(2, 1); // LED2 快闪:MQTT断开,正在重连
|
||||
|
||||
// 网络离线,停止离线数据补传任务
|
||||
g_is_online = false;
|
||||
ESP_LOGI(TAG, "网络离线,停止离线数据上传");
|
||||
|
||||
// 停止设备状态上报任务(避免存储新数据)
|
||||
if (g_device_status_task_started) {
|
||||
mqtt_stop_device_status_task();
|
||||
g_device_status_task_started = false;
|
||||
g_device_status_task_auto_start = true; // 重连后自动启动
|
||||
ESP_LOGI(TAG, "设备状态任务已停止,将在重连时自动重启");
|
||||
}
|
||||
break;
|
||||
|
||||
case MQTT_EVENT_SUBSCRIBED:
|
||||
@@ -388,12 +457,12 @@ status_led_blink_mode(2, 2); // LED2 心跳:MQTT连接正常
|
||||
.enabled = (enabled != NULL && cJSON_IsBool(enabled)) ? cJSON_IsTrue(enabled) : true
|
||||
};
|
||||
|
||||
// 更新轮询配置
|
||||
if (modbus_update_poll_config(&poll_config)) {
|
||||
ESP_LOGI(TAG, "MODBUS poll config updated via MQTT");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to update MODBUS poll config");
|
||||
}
|
||||
// 更新轮询配置
|
||||
if (modbus_update_poll_config(&poll_config)) {
|
||||
ESP_LOGI(TAG, "通过MQTT更新MODBUS轮询配置成功");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "通过MQTT更新MODBUS轮询配置失败");
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Missing required fields in MODBUS poll command");
|
||||
}
|
||||
@@ -476,12 +545,243 @@ int mqtt_publish_message(const char* topic, const char* data, int len, int qos,
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 如果网络离线,存储数据到本地Flash
|
||||
if (!g_is_online) {
|
||||
ESP_LOGW(TAG, "网络离线,数据存储到本地 (主题: %s, 长度: %d)", topic, len);
|
||||
|
||||
// 根据主题判断数据类型
|
||||
// 设备状态数据主题通常包含 "status" 或特定的设备状态主题
|
||||
// 传感器数据使用默认的发布主题,不包含特定状态关键字
|
||||
offline_data_type_t data_type = OFFLINE_DATA_TYPE_MODBUS;
|
||||
// 只有当主题明确包含"status"关键字时才认为是设备状态数据
|
||||
// 避免将包含"device"的传感器数据主题误判为设备状态
|
||||
if (strstr(topic, "status") != NULL) {
|
||||
data_type = OFFLINE_DATA_TYPE_DEVICE_STATUS;
|
||||
}
|
||||
|
||||
// 存储离线数据
|
||||
esp_err_t ret = mqtt_store_offline(data, len, data_type);
|
||||
if (ret == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Data stored to offline storage successfully");
|
||||
return 0; // 返回0表示已存储(模拟成功)
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to store data to offline storage");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// 网络在线,正常发布
|
||||
int msg_id = esp_mqtt_client_publish(g_client, topic, data, len, qos, retain);
|
||||
if (msg_id < 0) {
|
||||
ESP_LOGE(TAG, "Failed to publish message to topic: %s", topic);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Published message to topic: %s, msg_id: %d", topic, msg_id);
|
||||
return msg_id;
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 离线数据补传任务
|
||||
// ============================
|
||||
|
||||
/**
|
||||
* @brief 检查并发布离线数据(网络在线时)
|
||||
*
|
||||
* @return ESP_OK 成功处理(包括没有数据的情况)
|
||||
* ESP_FAIL 处理失败
|
||||
*/
|
||||
static esp_err_t publish_offline_data(void)
|
||||
{
|
||||
if (!offline_storage_has_data()) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// 使用静态缓冲区,避免栈溢出
|
||||
offline_data_type_t data_type;
|
||||
|
||||
// 读取最旧的离线数据
|
||||
esp_err_t ret = offline_storage_read_oldest(offline_data_buffer, sizeof(offline_data_buffer), &data_type);
|
||||
if (ret != ESP_OK) {
|
||||
if (ret == ESP_ERR_NOT_FOUND) {
|
||||
return ESP_OK; // 没有数据,不是错误
|
||||
}
|
||||
ESP_LOGE(TAG, "Failed to read offline data: %s", esp_err_to_name(ret));
|
||||
// 读取失败,删除可能损坏的数据记录,避免死循环
|
||||
ESP_LOGW(TAG, "Deleting potentially corrupted data record");
|
||||
offline_storage_delete_oldest();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// 优先上传传感器数据,设备状态数据丢弃
|
||||
if (data_type == OFFLINE_DATA_TYPE_DEVICE_STATUS) {
|
||||
ESP_LOGW(TAG, "Discarding device status offline data (priority: sensor data only)");
|
||||
offline_storage_delete_oldest();
|
||||
return ESP_OK; // 返回成功,继续处理下一条
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Publishing offline data (type=%d, size=%zu)", data_type, strlen(offline_data_buffer));
|
||||
|
||||
// 为补发数据添加标识:is_retrospective = true
|
||||
// 解析JSON,添加字段后重新序列化
|
||||
cJSON *root = cJSON_Parse(offline_data_buffer);
|
||||
if (root != NULL) {
|
||||
// 添加补发标识
|
||||
cJSON_AddBoolToObject(root, "is_retrospective", true);
|
||||
|
||||
// 重新序列化为字符串
|
||||
char *json_with_flag = cJSON_PrintUnformatted(root);
|
||||
if (json_with_flag != NULL) {
|
||||
ESP_LOGI(TAG, "Publishing offline data with retrospective flag: %s", json_with_flag);
|
||||
|
||||
// 使用新字符串发布
|
||||
int msg_id = esp_mqtt_client_publish(g_client, CONFIG_MQTT_PUB_TOPIC,
|
||||
json_with_flag, strlen(json_with_flag), 0, 0);
|
||||
|
||||
free(json_with_flag);
|
||||
cJSON_Delete(root);
|
||||
|
||||
if (msg_id >= 0) {
|
||||
ESP_LOGI(TAG, "Offline data with retrospective flag published successfully, msg_id=%d", msg_id);
|
||||
|
||||
// 发布成功,删除已上传的数据
|
||||
ret = offline_storage_delete_oldest();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to delete published offline data");
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to publish offline data with retrospective flag");
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to re-serialize JSON with retrospective flag");
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to parse JSON for adding retrospective flag, publishing raw data: %s", offline_data_buffer);
|
||||
|
||||
// 如果JSON解析失败,直接发布原始数据(不添加标识)
|
||||
int msg_id = esp_mqtt_client_publish(g_client, CONFIG_MQTT_PUB_TOPIC,
|
||||
offline_data_buffer, strlen(offline_data_buffer), 0, 0);
|
||||
|
||||
if (msg_id >= 0) {
|
||||
ESP_LOGI(TAG, "Raw offline data published successfully, msg_id=%d", msg_id);
|
||||
|
||||
// 发布成功,删除已上传的数据
|
||||
ret = offline_storage_delete_oldest();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to delete published offline data");
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to publish raw offline data");
|
||||
}
|
||||
}
|
||||
|
||||
// 发布失败也删除数据,避免重复尝试
|
||||
ESP_LOGW(TAG, "Deleting failed data to avoid retry loop");
|
||||
offline_storage_delete_oldest();
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 存储离线数据(网络离线时)
|
||||
*
|
||||
* @param data 数据内容
|
||||
* @param length 数据长度
|
||||
* @param data_type 数据类型
|
||||
* @return ESP_OK 成功
|
||||
* ESP_FAIL 失败
|
||||
*/
|
||||
esp_err_t mqtt_store_offline(const char *data, size_t length, offline_data_type_t data_type)
|
||||
{
|
||||
if (g_is_online) {
|
||||
ESP_LOGW(TAG, "Network is online, storing data not needed");
|
||||
return ESP_OK; // 网络在线,不需要存储
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Storing offline data (type=%d, size=%zu)", data_type, length);
|
||||
|
||||
esp_err_t ret = offline_storage_store(data, length, data_type);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to store offline data: %s", esp_err_to_name(ret));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// 获取存储使用情况
|
||||
size_t used = 0, total = 0;
|
||||
if (offline_storage_get_usage(&used, &total) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Storage usage: %zu / %zu bytes (%.1f%%)",
|
||||
used, total, (used * 100.0) / total);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 离线数据补传任务
|
||||
*
|
||||
* 当网络在线时,持续检查并上传离线存储的数据
|
||||
*/
|
||||
static void offline_upload_task(void *pvParameters)
|
||||
{
|
||||
ESP_LOGI(TAG, "Offline data upload task started");
|
||||
|
||||
while (true) {
|
||||
if (g_is_online && g_client != NULL) {
|
||||
// 检查并上传离线数据
|
||||
if (offline_storage_has_data()) {
|
||||
ESP_LOGI(TAG, "Found %u offline data files, uploading...",
|
||||
offline_storage_get_count());
|
||||
|
||||
// 尝试发布数据,每次处理一条
|
||||
publish_offline_data();
|
||||
}
|
||||
}
|
||||
|
||||
// 每100毫秒检查一次(提高上传速率)
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 启动离线数据补传任务
|
||||
*
|
||||
* @return pdTRUE 成功, pdFALSE 失败
|
||||
*/
|
||||
BaseType_t mqtt_start_offline_upload_task(void)
|
||||
{
|
||||
if (offline_upload_task_handle != NULL) {
|
||||
ESP_LOGW(TAG, "Offline upload task already running");
|
||||
return pdTRUE;
|
||||
}
|
||||
|
||||
BaseType_t ret = xTaskCreate(offline_upload_task, "offline_upload",
|
||||
8192, NULL, 4, &offline_upload_task_handle);
|
||||
|
||||
if (ret == pdPASS) {
|
||||
ESP_LOGI(TAG, "Offline data upload task started");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to create offline data upload task");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 停止离线数据补传任务
|
||||
*/
|
||||
void mqtt_stop_offline_upload_task(void)
|
||||
{
|
||||
if (offline_upload_task_handle != NULL) {
|
||||
ESP_LOGI(TAG, "Stopping offline data upload task...");
|
||||
|
||||
TaskHandle_t temp_handle = offline_upload_task_handle;
|
||||
offline_upload_task_handle = NULL;
|
||||
vTaskDelete(temp_handle);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
ESP_LOGI(TAG, "Offline data upload task stopped");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "mqtt_client.h"
|
||||
#include "esp_log.h"
|
||||
#include "OFFLINE_STORAGE.h"
|
||||
|
||||
void mqtt_app_start(void);
|
||||
int mqtt_publish_message(const char* topic, const char* data, int len, int qos, int retain);
|
||||
@@ -22,4 +23,27 @@ void mqtt_stop_device_status_task(void);
|
||||
*
|
||||
* @param report_interval_ms 新的上报间隔(毫秒)
|
||||
*/
|
||||
void mqtt_update_report_interval(uint32_t report_interval_ms);
|
||||
void mqtt_update_report_interval(uint32_t report_interval_ms);
|
||||
|
||||
/**
|
||||
* @brief 存储离线数据(网络离线时使用)
|
||||
*
|
||||
* @param data 数据内容
|
||||
* @param length 数据长度
|
||||
* @param data_type 数据类型
|
||||
* @return ESP_OK 成功
|
||||
* ESP_FAIL 失败
|
||||
*/
|
||||
esp_err_t mqtt_store_offline(const char *data, size_t length, offline_data_type_t data_type);
|
||||
|
||||
/**
|
||||
* @brief 启动离线数据补传任务
|
||||
*
|
||||
* @return pdTRUE 成功, pdFALSE 失败
|
||||
*/
|
||||
BaseType_t mqtt_start_offline_upload_task(void);
|
||||
|
||||
/**
|
||||
* @brief 停止离线数据补传任务
|
||||
*/
|
||||
void mqtt_stop_offline_upload_task(void);
|
||||
5
components/OFFLINE_STORAGE/CMakeLists.txt
Normal file
5
components/OFFLINE_STORAGE/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "OFFLINE_STORAGE.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES FLASH_SPIFS json
|
||||
)
|
||||
426
components/OFFLINE_STORAGE/OFFLINE_STORAGE.c
Normal file
426
components/OFFLINE_STORAGE/OFFLINE_STORAGE.c
Normal file
@@ -0,0 +1,426 @@
|
||||
#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;
|
||||
}
|
||||
88
components/OFFLINE_STORAGE/include/OFFLINE_STORAGE.h
Normal file
88
components/OFFLINE_STORAGE/include/OFFLINE_STORAGE.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#ifndef OFFLINE_STORAGE_H
|
||||
#define OFFLINE_STORAGE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
// 数据类型
|
||||
typedef enum {
|
||||
OFFLINE_DATA_TYPE_UNKNOWN = 0,
|
||||
OFFLINE_DATA_TYPE_MODBUS = 1, // MODBUS采集数据
|
||||
OFFLINE_DATA_TYPE_DEVICE_STATUS = 2, // 设备状态数据
|
||||
} offline_data_type_t;
|
||||
|
||||
/**
|
||||
* @brief 初始化离线存储模块
|
||||
*
|
||||
* @return ESP_OK 成功
|
||||
* 其他 失败
|
||||
*/
|
||||
esp_err_t offline_storage_init(void);
|
||||
|
||||
/**
|
||||
* @brief 存储离线数据
|
||||
*
|
||||
* @param data 数据内容(JSON字符串)
|
||||
* @param length 数据长度
|
||||
* @param data_type 数据类型
|
||||
* @return ESP_OK 成功
|
||||
* ESP_FAIL 失败
|
||||
*/
|
||||
esp_err_t offline_storage_store(const char *data, size_t length, offline_data_type_t data_type);
|
||||
|
||||
/**
|
||||
* @brief 读取最旧的离线数据
|
||||
*
|
||||
* @param buffer 输出缓冲区
|
||||
* @param max_len 缓冲区最大长度
|
||||
* @param out_data_type 输出数据类型
|
||||
* @return ESP_OK 成功
|
||||
* ESP_ERR_NOT_FOUND 没有数据
|
||||
* 其他 失败
|
||||
*/
|
||||
esp_err_t offline_storage_read_oldest(char *buffer, size_t max_len, offline_data_type_t *out_data_type);
|
||||
|
||||
/**
|
||||
* @brief 删除最旧的离线数据
|
||||
*
|
||||
* @return ESP_OK 成功
|
||||
* ESP_ERR_NOT_FOUND 没有数据
|
||||
* 其他 失败
|
||||
*/
|
||||
esp_err_t offline_storage_delete_oldest(void);
|
||||
|
||||
/**
|
||||
* @brief 获取离线数据数量
|
||||
*
|
||||
* @return 数据数量
|
||||
*/
|
||||
uint32_t offline_storage_get_count(void);
|
||||
|
||||
/**
|
||||
* @brief 检查是否有离线数据
|
||||
*
|
||||
* @return true 有数据
|
||||
* false 无数据
|
||||
*/
|
||||
bool offline_storage_has_data(void);
|
||||
|
||||
/**
|
||||
* @brief 清空所有离线数据
|
||||
*
|
||||
* @return ESP_OK 成功
|
||||
* 其他 失败
|
||||
*/
|
||||
esp_err_t offline_storage_clear_all(void);
|
||||
|
||||
/**
|
||||
* @brief 获取存储使用情况
|
||||
*
|
||||
* @param used 已使用字节数
|
||||
* @param total 总字节数
|
||||
* @return ESP_OK 成功
|
||||
* 其他 失败
|
||||
*/
|
||||
esp_err_t offline_storage_get_usage(size_t *used, size_t *total);
|
||||
|
||||
#endif // OFFLINE_STORAGE_H
|
||||
@@ -1,4 +1,4 @@
|
||||
idf_component_register(SRCS "RS-485-SP3485EEN.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES nvs_flash driver MQTT_ESP mqtt STATUS_LED MODBUS_ESP json
|
||||
REQUIRES nvs_flash driver MQTT_ESP mqtt STATUS_LED MODBUS_ESP json OFFLINE_STORAGE
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ void rs485_send(uart_port_t uart_num, const uint8_t *data, size_t len)
|
||||
{
|
||||
if (data == NULL || len == 0)
|
||||
{
|
||||
ESP_LOGW(TAG, "rs485_send: empty payload");
|
||||
ESP_LOGW(TAG, "RS485发送: 数据为空");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,12 +34,12 @@ void rs485_send(uart_port_t uart_num, const uint8_t *data, size_t len)
|
||||
int written = uart_write_bytes(uart_num, (const char *)data, len);
|
||||
if (written < 0)
|
||||
{
|
||||
ESP_LOGE(TAG, "UART%d TX write error (%d)", uart_num, written);
|
||||
ESP_LOGE(TAG, "UART%d 发送写入错误 (%d)", uart_num, written);
|
||||
return;
|
||||
}
|
||||
if ((size_t)written != len)
|
||||
{
|
||||
ESP_LOGW(TAG, "UART%d TX partial (%d/%d)", uart_num, written, len);
|
||||
ESP_LOGW(TAG, "UART%d 发送部分数据 (%d/%d)", uart_num, written, len);
|
||||
}
|
||||
|
||||
// RS485 半双工模式下,uart_write_bytes 会自动等待发送完成
|
||||
@@ -51,7 +51,7 @@ void rs485_send(uart_port_t uart_num, const uint8_t *data, size_t len)
|
||||
// Modbus RTU 3.5T 帧间静默(保留短延时)
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
|
||||
ESP_LOGI(TAG, "UART%d TX done (%d bytes)", uart_num, written);
|
||||
ESP_LOGI(TAG, "UART%d 发送完成 (%d 字节)", uart_num, written);
|
||||
}
|
||||
|
||||
// ============================
|
||||
@@ -61,7 +61,7 @@ int rs485_receive(uart_port_t uart_num, uint8_t *buffer, size_t buf_size, uint32
|
||||
{
|
||||
if (buffer == NULL || buf_size == 0)
|
||||
{
|
||||
ESP_LOGW(TAG, "rs485_receive: invalid buffer");
|
||||
ESP_LOGW(TAG, "RS485接收: 缓冲区无效");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -73,11 +73,11 @@ int rs485_receive(uart_port_t uart_num, uint8_t *buffer, size_t buf_size, uint32
|
||||
}
|
||||
else if (len == 0)
|
||||
{
|
||||
ESP_LOGD(TAG, "UART%d RX timeout", uart_num);
|
||||
ESP_LOGD(TAG, "UART%d 接收超时", uart_num);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW(TAG, "UART%d RX error (%d)", uart_num, len);
|
||||
ESP_LOGW(TAG, "UART%d 接收错误 (%d)", uart_num, len);
|
||||
}
|
||||
|
||||
return len;
|
||||
|
||||
Reference in New Issue
Block a user