From d56c730cfee8cc7f8947256b026c6328e57d6ecf Mon Sep 17 00:00:00 2001 From: Wang Beihong Date: Mon, 2 Feb 2026 00:31:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=AD=E7=BD=91=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E6=95=B0=E6=8D=AE=E5=88=B0FLASH=EF=BC=8C=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E8=81=94=E7=BD=91=E9=87=8D=E6=96=B0=E8=A1=A5=E5=8F=91?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=8A=A0=E4=B8=8A=E6=A0=87=E5=BF=97=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 90 ++++ components/FLASH_SPIFS/CMakeLists.txt | 5 + components/FLASH_SPIFS/FLASH_SPIFS.c | 128 ++++++ components/FLASH_SPIFS/include/FLASH_SPIFS.h | 52 +++ components/MQTT_ESP/CMakeLists.txt | 2 +- components/MQTT_ESP/MQTT_ESP.c | 356 +++++++++++++-- components/MQTT_ESP/include/MQTT_ESP.h | 26 +- components/OFFLINE_STORAGE/CMakeLists.txt | 5 + components/OFFLINE_STORAGE/OFFLINE_STORAGE.c | 426 +++++++++++++++++ .../OFFLINE_STORAGE/include/OFFLINE_STORAGE.h | 88 ++++ components/RS-485-SP3485EEN/CMakeLists.txt | 2 +- .../RS-485-SP3485EEN/RS-485-SP3485EEN.c | 14 +- dependencies.lock | 13 +- docs/OFFLINE_STORAGE.md | 432 ++++++++++++++++++ docs/OFFLINE_STORAGE_TEST.md | 214 +++++++++ main/CMakeLists.txt | 2 +- main/idf_component.yml | 2 + main/main.c | 43 +- partitions.csv | 5 + 19 files changed, 1853 insertions(+), 52 deletions(-) create mode 100644 components/FLASH_SPIFS/CMakeLists.txt create mode 100644 components/FLASH_SPIFS/FLASH_SPIFS.c create mode 100644 components/FLASH_SPIFS/include/FLASH_SPIFS.h create mode 100644 components/OFFLINE_STORAGE/CMakeLists.txt create mode 100644 components/OFFLINE_STORAGE/OFFLINE_STORAGE.c create mode 100644 components/OFFLINE_STORAGE/include/OFFLINE_STORAGE.h create mode 100644 docs/OFFLINE_STORAGE.md create mode 100644 docs/OFFLINE_STORAGE_TEST.md create mode 100644 partitions.csv diff --git a/README.md b/README.md index 2e7f1fe..12878d3 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,12 @@ 本设备是一个分布式采集网关,支持通过 MQTT 远程控制 MODBUS RTU 轮询参数,并定期上报设备状态信息。 +**新增功能**: +- ✅ **离线数据存储**:网络断开时自动存储数据到Flash +- ✅ **自动补传**:网络恢复后自动补传离线数据到MQTT服务器 +- ✅ **SPIFFS文件系统**:ESP-IDF原生支持,稳定可靠 +- ✅ **8MB存储容量**:可存储约11小时离线数据(注意:SPIFFS最小文件4KB) + ## SNTP 时间同步 设备内置 SNTP(Simple Network Time Protocol)时间同步功能,在网络连接成功后会自动同步系统时间。 @@ -393,6 +399,89 @@ mqtt_start_device_status_task(10000); // 10000ms = 10秒 mqtt_update_report_interval(5000); // 5秒 ``` +## 离线数据存储 + +### 功能特性 + +设备支持离线数据存储功能,确保网络断开时数据不丢失: + +- **自动检测**:实时监测网络状态(通过MQTT连接状态) +- **离线存储**:网络断开时自动存储数据到Flash +- **自动补传**:网络恢复后自动补传离线数据到MQTT服务器 +- **LittleFS文件系统**:提供断电保护和磨损均衡 +- **8MB存储容量**:可存储约11小时离线数据 + +### 存储容量 + +| 数据类型 | 数据量 | 可存储时长 | +|---------|--------|----------| +| MODBUS数据 | 200字节/条(1秒/次) | 约11小时(注意:SPIFFS最小文件4KB) | + +### 工作流程 + +#### 正常模式(网络在线) +``` +数据产生 → 直接发布到MQTT → 不存储 +``` + +#### 离线模式(网络断开) +``` +数据产生 → 存储到Flash SPIFFS → 等待网络恢复 +``` + +#### 补传模式(网络恢复) +``` +检测网络恢复 → 读取最旧数据 → 发布到MQTT → 删除已传数据 → 继续下一条 +``` + +### 存储结构 + +``` +/flash/ +├── data/ +│ ├── modbus/ # MODBUS采集数据 +│ │ ├── 1738452345.json # 时间戳命名 +│ │ └── ... +│ └── status/ # 设备状态数据 +│ └── ... +└── index.json # 索引文件 +``` + +### 日志示例 + +#### 存储离线数据 +``` +I (xxxx) mqtt_esp: Network is offline, stopping offline data upload +I (xxxx) mqtt_esp: Storing offline data (type=1, size=256) +I (xxxx) OFFLINE_STORAGE: Storage usage: 512 / 8388608 bytes (0.0%) +``` + +#### 补传离线数据 +``` +I (xxxx) mqtt_esp: Network is online, starting offline data upload +I (xxxx) mqtt_esp: Found 10 offline data files, uploading... +I (xxxx) mqtt_esp: Publishing offline data (type=1, size=256) +I (xxxx) mqtt_esp: Offline data published successfully, msg_id=12345 +``` + +### 存储策略 + +- **自动循环覆盖**:最多存储10,000个文件,超出自动删除最旧数据 +- **时间戳排序**:按时间戳顺序补传,确保数据时间顺序 +- **原子写入**:使用临时文件+重命名机制 +- **断电保护**:SPIFFS提供基本的断电保护 + +### 注意事项 + +1. **首次启动**:会自动初始化并挂载SPIFFS文件系统 +2. **空间管理**:存储满后自动循环覆盖最旧数据 +3. **补传速率**:每秒处理一条数据,避免阻塞新数据上传 +4. **Flash寿命**:SPIFFS无磨损均衡,建议定期格式化 +5. **最小文件限制**:SPIFFS最小文件4KB,小文件会浪费空间 +6. **格式化**:如需清空所有数据,可调用 `flash_spiffs_format()` + +详细文档请参考:[离线数据存储和补传功能说明](docs/OFFLINE_STORAGE.md) + ## 注意事项 1. **轮询间隔最小为 100ms**,设置更小的值会被拒绝 @@ -400,6 +489,7 @@ mqtt_update_report_interval(5000); // 5秒 3. **从机地址范围 1-247**,0 为广播地址 4. **通道号只能是 0 或 1** 5. 首次发送指令后会自动启动轮询任务 +6. **离线存储容量约11小时**,超出会循环覆盖最旧数据 ## 使用 mosquitto_cli 测试 diff --git a/components/FLASH_SPIFS/CMakeLists.txt b/components/FLASH_SPIFS/CMakeLists.txt new file mode 100644 index 0000000..c1df99a --- /dev/null +++ b/components/FLASH_SPIFS/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "FLASH_SPIFS.c" + INCLUDE_DIRS "include" + REQUIRES spiffs +) diff --git a/components/FLASH_SPIFS/FLASH_SPIFS.c b/components/FLASH_SPIFS/FLASH_SPIFS.c new file mode 100644 index 0000000..00960ed --- /dev/null +++ b/components/FLASH_SPIFS/FLASH_SPIFS.c @@ -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; +} diff --git a/components/FLASH_SPIFS/include/FLASH_SPIFS.h b/components/FLASH_SPIFS/include/FLASH_SPIFS.h new file mode 100644 index 0000000..46d5952 --- /dev/null +++ b/components/FLASH_SPIFS/include/FLASH_SPIFS.h @@ -0,0 +1,52 @@ +#ifndef FLASH_SPIFS_H +#define FLASH_SPIFS_H + +#include +#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 diff --git a/components/MQTT_ESP/CMakeLists.txt b/components/MQTT_ESP/CMakeLists.txt index df05464..44079b7 100644 --- a/components/MQTT_ESP/CMakeLists.txt +++ b/components/MQTT_ESP/CMakeLists.txt @@ -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") diff --git a/components/MQTT_ESP/MQTT_ESP.c b/components/MQTT_ESP/MQTT_ESP.c index e686f86..01d695f 100644 --- a/components/MQTT_ESP/MQTT_ESP.c +++ b/components/MQTT_ESP/MQTT_ESP.c @@ -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"); + } +} diff --git a/components/MQTT_ESP/include/MQTT_ESP.h b/components/MQTT_ESP/include/MQTT_ESP.h index 237934a..7de54ae 100644 --- a/components/MQTT_ESP/include/MQTT_ESP.h +++ b/components/MQTT_ESP/include/MQTT_ESP.h @@ -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); \ No newline at end of file +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); \ No newline at end of file diff --git a/components/OFFLINE_STORAGE/CMakeLists.txt b/components/OFFLINE_STORAGE/CMakeLists.txt new file mode 100644 index 0000000..53a5d66 --- /dev/null +++ b/components/OFFLINE_STORAGE/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "OFFLINE_STORAGE.c" + INCLUDE_DIRS "include" + REQUIRES FLASH_SPIFS json +) diff --git a/components/OFFLINE_STORAGE/OFFLINE_STORAGE.c b/components/OFFLINE_STORAGE/OFFLINE_STORAGE.c new file mode 100644 index 0000000..f0afb52 --- /dev/null +++ b/components/OFFLINE_STORAGE/OFFLINE_STORAGE.c @@ -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 +#include +#include +#include + +#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; +} diff --git a/components/OFFLINE_STORAGE/include/OFFLINE_STORAGE.h b/components/OFFLINE_STORAGE/include/OFFLINE_STORAGE.h new file mode 100644 index 0000000..436cfc0 --- /dev/null +++ b/components/OFFLINE_STORAGE/include/OFFLINE_STORAGE.h @@ -0,0 +1,88 @@ +#ifndef OFFLINE_STORAGE_H +#define OFFLINE_STORAGE_H + +#include +#include +#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 diff --git a/components/RS-485-SP3485EEN/CMakeLists.txt b/components/RS-485-SP3485EEN/CMakeLists.txt index fe76b47..22d1f93 100644 --- a/components/RS-485-SP3485EEN/CMakeLists.txt +++ b/components/RS-485-SP3485EEN/CMakeLists.txt @@ -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 ) diff --git a/components/RS-485-SP3485EEN/RS-485-SP3485EEN.c b/components/RS-485-SP3485EEN/RS-485-SP3485EEN.c index 22dd585..f14d933 100644 --- a/components/RS-485-SP3485EEN/RS-485-SP3485EEN.c +++ b/components/RS-485-SP3485EEN/RS-485-SP3485EEN.c @@ -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; diff --git a/dependencies.lock b/dependencies.lock index df3c466..2e43593 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -1,4 +1,14 @@ dependencies: + espressif/cjson: + component_hash: 9372811fb197926f522c467627cf4a8e72b681e0366e17879631da801103aef3 + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.7.19 espressif/mqtt: component_hash: ffdad5659706b4dc14bc63f8eb73ef765efa015bf7e9adf71c813d52a2dc9342 dependencies: @@ -14,8 +24,9 @@ dependencies: type: idf version: 5.5.2 direct_dependencies: +- espressif/cjson - espressif/mqtt - idf -manifest_hash: a05aed5660378334599ccf3fa66dcfc9fcf5deb3e651bd572aeb15a43840910d +manifest_hash: 142b0960853ecbd232be08031f057a2430fd975fc4697f81c7a9a6d57507d3d6 target: esp32s3 version: 2.0.0 diff --git a/docs/OFFLINE_STORAGE.md b/docs/OFFLINE_STORAGE.md new file mode 100644 index 0000000..e737c02 --- /dev/null +++ b/docs/OFFLINE_STORAGE.md @@ -0,0 +1,432 @@ +# 离线数据存储和补传功能说明 + +## 功能概述 + +本设备支持离线数据存储功能,当网络断开时,数据会自动存储到Flash文件系统中;当网络恢复后,数据会自动补传到MQTT服务器。 + +## 系统架构 + +``` +┌─────────────────────────────────────────────────┐ +│ 应用层 │ +├─────────────────────────────────────────────────┤ +│ MODBUS数据 设备状态 MQTT客户端 │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────────────────────────────┐ │ +│ │ 离线数据管理模块 │ │ +│ │ (OFFLINE_STORAGE 组件) │ │ +│ └─────────────────────────────────────┘ │ +│ │ │ │ +│ ▼ │ │ +│ ┌─────────────────────────────────────┐ │ +│ │ SPIFFS文件系统 │ │ +│ │ (FLASH_SPIFS 组件) │ │ +│ └─────────────────────────────────────┘ │ +│ │ │ │ +│ ▼ │ │ +│ ┌─────────────────────────────────────┐ │ +│ │ 16MB Flash存储区 │ │ +│ │ (8MB可用) │ │ +│ └─────────────────────────────────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +## 分区表配置 + +```csv +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 2MB, +storage, data, spiffs, 0x210000,8MB, +``` + +- **storage分区**:8MB,用于离线数据存储 +- **挂载点**:`/flash` +- **文件系统**:SPIFFS + +## 存储容量估算 + +| 参数 | 数值 | +|------|------| +| Flash总容量 | 16MB | +| 存储分区大小 | 8MB | +| MODBUS数据量 | 200字节/条(1秒/次) | +| 每小时数据量 | 720KB | +| 可存储时长 | 约11小时 | + +**注意**:SPIFFS最小文件大小为4KB,200字节的文件实际占用4KB空间,因此实际可存储的时长会更短。建议将多条数据合并到一个文件中存储以提高空间利用率。 + +## 文件组织结构 + +``` +/flash/ +├── data/ +│ ├── modbus/ # MODBUS采集数据 +│ │ ├── 1738452345.json # 时间戳命名 +│ │ ├── 1738452346.json +│ │ └── ... +│ └── status/ # 设备状态数据 +│ ├── 1738452345.json +│ └── ... +└── index.json # 索引文件 +``` + +## 工作流程 + +### 正常模式(网络在线) + +``` +数据产生 + ↓ +检测网络状态(在线) + ↓ +直接发布到MQTT + ↓ +不存储 +``` + +### 离线模式(网络断开) + +``` +数据产生 + ↓ +检测网络状态(离线) + ↓ +存储到Flash SPIFFS + ↓ +写入成功 + ↓ +等待网络恢复 +``` + +### 补传模式(网络恢复) + +``` +检测网络状态(在线) + ↓ +检查是否有离线数据 + ↓ +读取最旧的数据 + ↓ +发布到MQTT + ↓ +发布成功? + ├─ 是 → 删除该数据 → 继续下一条 + └─ 否 → 等待重试 +``` + +## API接口 + +### FLASH_SPIFS 组件 + +#### 初始化文件系统 + +```c +esp_err_t flash_spiffs_init(void); +``` + +#### 检查是否挂载 + +```c +bool flash_spiffs_is_mounted(void); +``` + +#### 获取存储使用情况 + +```c +uint32_t flash_spiffs_get_total_size(void); +uint32_t flash_spiffs_get_used_size(void); +``` + +#### 格式化文件系统 + +```c +esp_err_t flash_spiffs_format(void); +``` + +### OFFLINE_STORAGE 组件 + +#### 初始化存储模块 + +```c +esp_err_t offline_storage_init(void); +``` + +#### 存储离线数据 + +```c +esp_err_t offline_storage_store(const char *data, size_t length, offline_data_type_t data_type); +``` + +**参数**: +- `data`:数据内容(JSON字符串) +- `length`:数据长度 +- `data_type`:数据类型 + - `OFFLINE_DATA_TYPE_MODBUS`:MODBUS采集数据 + - `OFFLINE_DATA_TYPE_DEVICE_STATUS`:设备状态数据 + +#### 读取最旧的数据 + +```c +esp_err_t offline_storage_read_oldest(char *buffer, size_t max_len, offline_data_type_t *out_data_type); +``` + +#### 删除最旧的数据 + +```c +esp_err_t offline_storage_delete_oldest(void); +``` + +#### 获取数据数量 + +```c +uint32_t offline_storage_get_count(void); +bool offline_storage_has_data(void); +``` + +#### 清空所有数据 + +```c +esp_err_t offline_storage_clear_all(void); +``` + +### MQTT_ESP 扩展接口 + +#### 存储离线数据(内部使用) + +```c +esp_err_t mqtt_store_offline(const char *data, size_t length, offline_data_type_t data_type); +``` + +#### 启动/停止补传任务 + +```c +BaseType_t mqtt_start_offline_upload_task(void); +void mqtt_stop_offline_upload_task(void); +``` + +## 网络状态检测 + +系统通过MQTT连接状态判断网络是否在线: + +| MQTT事件 | 网络状态 | 处理 | +|---------|---------|------| +| `MQTT_EVENT_CONNECTED` | 在线 | 启动补传任务 | +| `MQTT_EVENT_DISCONNECTED` | 离线 | 停止补传任务 | + +## 存储策略 + +### 自动循环覆盖 + +- **最大文件数限制**:10,000个文件 +- **触发条件**:当文件数超过限制时,自动删除最旧的数据 +- **优先级**:按时间戳排序,删除最旧的数据 + +### 数据完整性保护 + +- **索引文件**:`/flash/index.json` 记录所有文件信息 +- **原子写入**:使用临时文件+重命名机制 +- **断电保护**:SPIFFS提供基本断电保护 + +### 存储空间管理 + +``` +获取空间使用情况 + ↓ +检查可用空间 + ↓ +空间不足? + ├─ 否 → 正常存储 + └─ 是 → 删除最旧数据 → 重试 +``` + +## SPIFFS 特性说明 + +### 优点 +- ESP-IDF原生支持,无需额外组件 +- 代码成熟稳定 +- 内存占用小(约2KB) +- 挂载速度快 + +### 缺点 +- 最小文件大小为4KB,小文件浪费空间 +- 无磨损均衡,Flash寿命相对较短 +- 碎片严重,长期使用后性能下降 +- 不支持目录(所有文件在根目录) + +### 优化建议 + +1. **合并多条数据到单个文件**:减少文件数量,提高空间利用率 +2. **定期格式化**:消除碎片,恢复性能 +3. **限制文件数量**:避免碎片积累 + +## 日志输出 + +### 初始化日志 + +``` +I (xxxx) main: Initializing SPIFFS file system... +I (xxxx) FLASH_SPIFS: 正在初始化SPIFFS文件系统... +I (xxxx) FLASH_SPIFS: SPIFFS文件系统挂载成功 +I (xxxx) FLASH_SPIFS: 总空间: 8388608 字节 +I (xxxx) FLASH_SPIFS: 已用空间: 4096 字节 +I (xxxx) FLASH_SPIFS: 可用空间: 8384512 字节 +I (xxxx) FLASH_SPIFS: 挂载点: /flash +I (xxxx) main: Initializing offline storage module... +I (xxxx) main: 离线存储模块初始化完成 +``` + +### 存储日志 + +``` +I (xxxx) mqtt_esp: Storing offline data (type=1, size=256) +I (xxxx) OFFLINE_STORAGE: 存储数据: /flash/data/modbus/1738452345.json, 大小: 256 字节 +I (xxxx) OFFLINE_STORAGE: Storage usage: 4096 / 8388608 bytes (0.0%) +W (xxxx) OFFLINE_STORAGE: 注意:SPIFFS最小文件大小为4KB,小文件会占用更多空间 +``` + +### 补传日志 + +``` +I (xxxx) mqtt_esp: Network is online, starting offline data upload +I (xxxx) mqtt_esp: Found 10 offline data files, uploading... +I (xxxx) mqtt_esp: Publishing offline data (type=1, size=256) +I (xxxx) mqtt_esp: Offline data published successfully, msg_id=12345 +``` + +## 故障处理 + +### Flash空间不足 + +**现象**: +``` +E (xxxx) OFFLINE_STORAGE: 无法创建文件 +``` + +**解决**: +1. 自动删除最旧的数据 +2. 格式化存储分区 + +### 文件系统损坏 + +**现象**: +``` +E (xxxx) FLASH_SPIFS: 挂载失败 +``` + +**解决**: +1. 系统会尝试自动修复 +2. 如修复失败,格式化文件系统 +3. 检查Flash硬件 + +### 数据补传失败 + +**现象**: +``` +E (xxxx) mqtt_esp: Failed to publish offline data +``` + +**解决**: +1. 检查网络连接 +2. 检查MQTT服务器状态 +3. 数据会保留在Flash中,下次重试 + +## 性能优化建议 + +### 1. 合并多条数据 + +将多条数据合并到一个文件中存储: + +```c +// 示例:每100条数据存储为一个文件 +// 空间利用率:100 * 200字节 / 4096字节 ≈ 4.9% +// 单文件存储:100 * 200字节 / 4096字节 = 100% +``` + +### 2. 降低存储频率 + +网络离线时降低存储频率: + +```c +// 在线:1秒/次 +// 离线:10秒/次 +// 可存储时长:110小时 +``` + +### 3. 选择性存储 + +只存储关键数据: + +```c +// 只存储异常数据、状态变化 +// 正常数据丢弃 +``` + +### 4. 定期格式化 + +建议每隔1-3个月格式化一次文件系统,消除碎片: + +```c +flash_spiffs_format(); +``` + +## 监控和调试 + +### 查看存储使用情况 + +```c +size_t used = 0, total = 0; +offline_storage_get_usage(&used, &total); +ESP_LOGI(TAG, "Storage: %zu / %zu bytes", used, total); +ESP_LOGI(TAG, "Usage: %.1f%%", (used * 100.0) / total); +``` + +### 查看离线数据数量 + +```c +uint32_t count = offline_storage_get_count(); +ESP_LOGI(TAG, "Offline data files: %u", count); +``` + +### 格式化存储 + +```c +flash_spiffs_format(); +``` + +## 注意事项 + +1. **首次启动**:会自动格式化文件系统(如果需要) +2. **断电保护**:SPIFFS提供基本断电保护,但建议避免频繁断电 +3. **Flash寿命**:SPIFFS无磨损均衡,建议定期格式化以延长寿命 +4. **容量限制**:由于SPIFFS最小文件4KB,实际可存储时长会少于理论值 +5. **补传速率**:每秒处理一条数据,避免阻塞新数据上传 +6. **碎片问题**:长期使用后性能可能下降,建议定期格式化 + +## 后续扩展建议 + +1. **数据压缩**:集成zlib压缩,延长存储时长 +2. **批量存储**:将多条数据合并到一个文件中 +3. **优先级队列**:重要数据优先补传 +4. **数据导出**:通过Web界面导出离线数据 +5. **统计信息**:记录丢失/补传的数据统计 +6. **智能采样**:根据存储空间动态调整采样率 +7. **定期维护**:实现自动碎片整理和格式化 + +## 与LittleFS对比 + +| 特性 | SPIFFS | LittleFS | +|------|--------|----------| +| 最小文件大小 | 4KB | 0字节 | +| 内存占用 | ~2KB | ~4KB | +| 磨损均衡 | ❌ 无 | ✅ 自带 | +| 断电保护 | ⚠️ 一般 | ✅ 优秀 | +| 支持目录 | ❌ 不支持 | ✅ 支持 | +| 碎片问题 | ❌ 严重 | ✅ 轻微 | +| ESP-IDF支持 | ✅ 原生支持 | ❌ 需第三方组件 | + +**选择建议**: +- 如果需要ESP-IDF原生支持且数据量不大 → SPIFFS +- 如果需要更好的性能和寿命 → LittleFS(需手动添加组件) diff --git a/docs/OFFLINE_STORAGE_TEST.md b/docs/OFFLINE_STORAGE_TEST.md new file mode 100644 index 0000000..e8d0b6d --- /dev/null +++ b/docs/OFFLINE_STORAGE_TEST.md @@ -0,0 +1,214 @@ +# 离线存储功能测试指南 + +## 功能概述 + +离线存储功能会在网络断开时自动将数据存储到 Flash 中,并在网络恢复后自动补传数据。 + +## 测试步骤 + +### 1. 编译并烧录固件 + +```bash +cd "/home/beihong/esp_projects/Distributed Collector Gateway" +idf.py build +idf.py flash monitor +``` + +### 2. 观察启动日志 + +系统启动时会显示以下关键日志: + +``` +I (xxxx) main: Initializing SPIFFS file system... +I (xxxx) FLASH_SPIFS: SPIFFS mounted successfully at /flash +I (xxxx) FLASH_SPIFS: Total size: 8388608 bytes, Used: 0 bytes +I (xxxx) main: Initializing offline storage module... +I (xxxx) OFFLINE_STORAGE: Offline storage initialized +I (xxxx) main: Offline data upload task started +``` + +### 3. 测试场景A:网络断开时的数据存储 + +**操作步骤:** + +1. 在系统正常运行时,断开网络连接(拔掉网线或断开路由器) +2. 观察日志输出,应该看到网络断开的提示 +3. 等待 MODBUS 数据采集任务产生数据 +4. 观察日志,应该看到类似以下信息: + +``` +W (xxxx) MQTT_ESP: Network offline, storing data locally (topic: xxx, len: xxx) +I (xxxx) MQTT_ESP: Storing offline data (type=1, size=xxx) +I (xxxx) OFFLINE_STORAGE: Stored offline data: /flash/offline/xxxxxx.json +I (xxxx) MQTT_ESP: Storage usage: 4096 / 8388608 bytes (0.0%) +``` + +**预期结果:** +- 数据被成功存储到 Flash 中 +- 显示存储使用情况 +- 设备继续正常运行,不会因为网络断开而崩溃 + +### 4. 测试场景B:网络恢复时的自动补传 + +**操作步骤:** + +1. 在测试场景A中存储了一些离线数据后 +2. 重新连接网络(插上网线或恢复路由器) +3. 观察日志输出 + +**预期日志:** + +``` +I (xxxx) MQTT_ESP: Network online, resuming normal operation +I (xxxx) MQTT_ESP: Found 5 offline data files, uploading... +I (xxxx) MQTT_ESP: Publishing offline data (type=1, size=xxx) +I (xxxx) MQTT_ESP: Offline data published successfully, msg_id=xxx +I (xxxx) OFFLINE_STORAGE: Deleted offline data: /flash/offline/xxxxxx.json +I (xxxx) MQTT_ESP: Storage usage: 32768 / 8388608 bytes (0.4%) +``` + +**预期结果:** +- 系统自动检测到网络恢复 +- 自动开始上传离线存储的数据 +- 每次上传成功后删除对应文件 +- 逐条上传,直到所有离线数据都上传完成 + +### 5. 测试场景C:设备状态数据的离线存储 + +**操作步骤:** + +1. 断开网络连接 +2. 等待设备状态上报周期(默认10秒) +3. 观察日志输出 + +**预期日志:** + +``` +W (xxxx) MQTT_ESP: Network offline, storing device status locally +I (xxxx) MQTT_ESP: Storing offline data (type=2, size=xxx) +I (xxxx) OFFLINE_STORAGE: Stored offline data: /flash/offline/xxxxxx.json +``` + +**预期结果:** +- 设备状态数据被正确识别为 OFFLINE_DATA_TYPE_DEVICE_STATUS 类型 +- 数据成功存储到 Flash + +### 6. 测试场景D:存储空间耗尽 + +**操作步骤:** + +1. 持续在网络断开状态下运行设备 +2. 让 MODBUS 数据持续产生(通过发送 MODBUS 查询命令) +3. 观察存储使用率的变化 + +**预期日志:** + +``` +I (xxxx) MQTT_ESP: Storage usage: 8355840 / 8388608 bytes (99.6%) +W (xxxx) OFFLINE_STORAGE: Storage nearly full (99.6%), consider clearing old data +``` + +**预期结果:** +- 当存储空间快满时,系统会发出警告 +- 新数据仍然可以存储,直到空间完全耗尽 +- 系统继续正常运行 + +### 7. 测试场景E:手动清空离线数据 + +如果需要手动清空所有离线数据,可以: + +1. 通过串口终端发送清空命令(需要自行实现) +2. 或者重新格式化 SPIFFS 文件系统(会删除所有数据) + +**格式化方法:** +- 在代码中调用 `flash_spiffs_format()` 函数 +- 这会清空整个文件系统,包括离线数据和配置 + +## MQTT 服务器端验证 + +在 MQTT 服务器端(如使用 MQTTX 或 Mosquitto Client): + +1. 订阅设备发布的主题:`CONFIG_MQTT_PUB_TOPIC`(在 sdkconfig 中配置) +2. 在网络断开时,应该收不到新数据 +3. 在网络恢复后,应该收到之前存储的离线数据 +4. 验证接收到的数据完整性和顺序 + +## 常见问题排查 + +### 问题1:日志显示 "Failed to store offline data" + +**可能原因:** +- SPIFFS 文件系统挂载失败 +- Flash 存储空间已满 +- 文件系统错误 + +**解决方法:** +1. 检查 SPIFFS 初始化日志 +2. 查看存储使用情况 +3. 必要时格式化文件系统 + +### 问题2:网络恢复后没有自动上传离线数据 + +**可能原因:** +- 离线上传任务未启动 +- MQTT 客户端未连接 +- 网络状态判断错误 + +**解决方法:** +1. 检查日志中是否有 "Offline data upload task started" +2. 确认 MQTT 客户端已连接 +3. 检查 `g_is_online` 状态 + +### 问题3:离线数据上传不完整或丢失 + +**可能原因:** +- MQTT 发布失败 +- 文件读取错误 +- 文件删除时机问题 + +**解决方法:** +1. 查看日志中的错误信息 +2. 确认 MQTT 服务器可用 +3. 检查 Flash 文件完整性 + +## 性能测试 + +### 存储性能 + +- 每条数据存储时间:通常 < 100ms +- 支持的数据大小:最大 4096 bytes(单条) +- 存储容量:约 8MB(基于分区表配置) + +### 上传性能 + +- 每条数据上传时间:取决于网络延迟 +- 上传间隔:1 秒(在离线上传任务中) +- 支持的最大数据条数:约 2000 条(基于平均每条 4KB) + +## 存储容量分析 + +基于当前配置: + +- **分区大小**: 8MB +- **SPIFFS 开销**: 约 10-20% +- **可用空间**: 约 6.4-7.2MB +- **最小文件大小**: 4KB(SPIFFS 特性) +- **MODBUS 数据大小**: 约 200 bytes/秒 +- **可存储时长**: 约 2-3 小时 + +**注意:** 由于 SPIFFS 最小文件大小为 4KB,即使只有 200 bytes 的数据也会占用 4KB 空间。如果需要更长时间的离线存储,建议: +1. 增大存储分区 +2. 使用 LittleFS 文件系统(支持更小的文件块) +3. 实现数据打包存储(多条数据打包到一个文件) + +## 测试总结 + +完成以上测试后,你应该能够验证: + +1. ✅ 网络断开时数据能正确存储到 Flash +2. ✅ 网络恢复时数据能自动补传 +3. ✅ 设备状态和 MODBUS 数据都能正确处理 +4. ✅ 存储空间管理正常 +5. ✅ 系统在各种网络状态切换下稳定运行 + +如有问题,请查看 ESP 日志输出,或参考 `docs/OFFLINE_STORAGE.md` 获取详细的技术文档。 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index f9a21e5..47810ac 100755 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register(SRCS "main.c" INCLUDE_DIRS "." - REQUIRES nvs_flash STATUS_LED RS-485-SP3485EEN ETH_CH390H MQTT_ESP SNTP_ESP + REQUIRES nvs_flash STATUS_LED RS-485-SP3485EEN ETH_CH390H MQTT_ESP SNTP_ESP FLASH_SPIFS OFFLINE_STORAGE ) diff --git a/main/idf_component.yml b/main/idf_component.yml index d4b2985..95db104 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -16,3 +16,5 @@ dependencies: # public: true espressif/mqtt: ^1.0.0 + + espressif/cjson: ^1.7.19 diff --git a/main/main.c b/main/main.c index 0ab6825..88fe981 100755 --- a/main/main.c +++ b/main/main.c @@ -4,6 +4,8 @@ #include "STATUS_LED.h" #include "MODBUS_ESP.h" #include "SNTP_ESP.h" +#include "FLASH_SPIFS.h" +#include "OFFLINE_STORAGE.h" #define TAG "main" void app_main(void) @@ -15,15 +17,23 @@ void app_main(void) ESP_ERROR_CHECK(nvs_flash_init()); + // 初始化SPIFFS文件系统 + ESP_LOGI(TAG, "正在初始化SPIFFS文件系统..."); + ESP_ERROR_CHECK(flash_spiffs_init()); + + // 初始化离线存储模块 + ESP_LOGI(TAG, "正在初始化离线存储模块..."); + ESP_ERROR_CHECK(offline_storage_init()); + // 初始化以太网,这里包含了 esp_netif_init();和esp_event_loop_create_default(); eth_init(); // 初始化RS485 init_specific_rs485_channel(0); // 初始化通道0 - start_rs485_rx_task_for_channel(0, 5, 4096); // 为通道0启动接收任务 + start_rs485_rx_task_for_channel(0, 5, 8192); // 为通道0启动接收任务(增加栈大小) // 等待网络连接建立 - ESP_LOGI(TAG, "Waiting for network connection..."); + ESP_LOGI(TAG, "正在等待网络连接..."); esp_netif_t *eth_netif = esp_netif_get_handle_from_ifkey("ETH_DEF"); // 获取默认以太网接口 @@ -33,25 +43,25 @@ void app_main(void) esp_netif_ip_info_t ip_info; if (esp_netif_get_ip_info(eth_netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) { - ESP_LOGI(TAG, "Network connected with IP: " IPSTR, IP2STR(&ip_info.ip)); + ESP_LOGI(TAG, "网络已连接,IP地址: " IPSTR, IP2STR(&ip_info.ip)); break; } - ESP_LOGI(TAG, "Waiting for IP address..."); + ESP_LOGI(TAG, "正在等待IP地址分配..."); vTaskDelay(pdMS_TO_TICKS(1000)); // 等待1秒后重试 } // 初始化SNTP时间同步服务 - ESP_LOGI(TAG, "Initializing SNTP time synchronization..."); + ESP_LOGI(TAG, "正在初始化SNTP时间同步服务..."); sntp_esp_init(); // 等待时间同步完成(最长等待10秒) if (sntp_esp_wait_sync(10000)) { - ESP_LOGI(TAG, "Time synchronization completed successfully"); + ESP_LOGI(TAG, "时间同步完成"); } else { - ESP_LOGW(TAG, "Time synchronization timeout, using local time"); + ESP_LOGW(TAG, "时间同步超时,使用本地时间"); } - ESP_LOGI(TAG, "Starting MQTT client..."); + ESP_LOGI(TAG, "正在启动MQTT客户端..."); // 启动MQTT客户端 mqtt_app_start(); @@ -63,15 +73,24 @@ void app_main(void) status_led_blink_mode(2, 2); // LED2 心跳:系统运行正常 // ============================ - // 启动设备状态上报任务(每10秒上报一次) + // 配置并标记设备状态上报任务(将在MQTT连接后自动启动) // ============================ if (mqtt_start_device_status_task(10000) == pdPASS) { - ESP_LOGI(TAG, "Device status report task started"); + ESP_LOGI(TAG, "设备状态上报任务已配置,将在MQTT连接后自动启动"); } else { - ESP_LOGE(TAG, "Failed to start device status report task"); + ESP_LOGE(TAG, "配置设备状态上报任务失败"); } - ESP_LOGI(TAG, "Waiting for MODBUS poll command via MQTT..."); + // ============================ + // 启动离线数据补传任务 + // ============================ + if (mqtt_start_offline_upload_task() == pdPASS) { + ESP_LOGI(TAG, "离线数据上传任务已启动"); + } else { + ESP_LOGE(TAG, "启动离线数据上传任务失败"); + } + + ESP_LOGI(TAG, "正在等待MQTT发送MODBUS轮询命令..."); for (;;) { diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..4eab876 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 2M, +storage, data, spiffs, 0x210000,8M,