ESP32物联网开发实战:用cJSON处理传感器数据的5个经典场景
在ESP32的物联网项目里,数据是设备与云端、设备与设备之间沟通的“语言”。而JSON,无疑是当下最通用、最受欢迎的语言格式。它结构清晰、易于阅读,无论是上报温湿度,还是接收云端指令,JSON都能优雅地胜任。但很多开发者,尤其是刚从Arduino转向ESP-IDF的朋友,在面对如何高效、可靠地生成和解析JSON数据时,往往会感到一丝困惑——内存怎么管理?数据结构复杂了怎么办?解析失败了如何优雅地处理?
这篇文章,我想和你聊聊我在多个ESP32物联网项目中,使用cJSON库处理传感器数据的五个最经典、也最实用的场景。我不会重复那些基础的API调用手册,而是聚焦于如何将cJSON无缝嵌入到真实的设备数据流中,从DHT11的数据打包,到WiFi扫描结果的格式化,再到与云平台通信的全流程。你会发现,用好cJSON,能让你的设备代码变得更清晰、更健壮,数据交互也变得前所未有的简单。
1. 场景一:从传感器到JSON字符串——DHT11温湿度数据上报
物联网设备最基础的功能就是采集并上报数据。我们以最常见的DHT11温湿度传感器为例。假设我们已经通过某个GPIO口读取到了温度和湿度值,接下来的任务不是简单地用printf打印,而是将它们封装成云平台期待的JSON格式。
1.1 构建一个包含元数据的传感器报文
直接上报{"temperature": 25, "humidity": 60}虽然可以,但在实际项目中显得过于单薄。云端通常需要更多的上下文信息来识别和处理这条数据。一个更健壮的报文应该包含设备标识、时间戳、数据本身以及状态信息。
#include "cJSON.h" #include <time.h> // 假设从DHT11读取的数据 float current_temp = 25.6; float current_humi = 45.7; const char* device_id = "ESP32_S3_Kitchen_01"; char* create_sensor_json_packet(float temp, float humi, const char* dev_id) { cJSON *root = cJSON_CreateObject(); if (root == NULL) { ESP_LOGE("JSON", "Failed to create root object"); return NULL; } // 1. 设备标识与报文元信息 cJSON_AddStringToObject(root, "deviceId", dev_id); cJSON_AddStringToObject(root, "msgType", "sensorData"); // 获取当前时间戳(需要配置SNTP,这里简化处理) time_t now; time(&now); cJSON_AddNumberToObject(root, "timestamp", (double)now); // 2. 核心传感器数据作为一个独立对象 cJSON *data = cJSON_CreateObject(); cJSON_AddItemToObject(root, "data", data); cJSON_AddNumberToObject(data, "temperature", temp); cJSON_AddNumberToObject(data, "humidity", humi); // 可以添加单位信息 cJSON_AddStringToObject(data, "temperatureUnit", "Celsius"); cJSON_AddStringToObject(data, "humidityUnit", "%"); // 3. 设备状态信息 cJSON *status = cJSON_CreateObject(); cJSON_AddItemToObject(root, "status", status); cJSON_AddNumberToObject(status, "rssi", -65); // 假设的WiFi信号强度 cJSON_AddBoolToObject(status, "batteryOk", true); // 4. 序列化为字符串(使用非格式化版本以节省空间) char *json_str = cJSON_PrintUnformatted(root); cJSON_Delete(root); // 释放cJSON对象树,json_str仍然有效 return json_str; // 注意:调用者需要负责free这个字符串 }注意:
cJSON_Print返回的字符串是动态分配的内存,使用完毕后必须用cJSON_free()或标准的free()来释放,否则会导致内存泄漏。这是新手最容易踩的坑。
这个函数最终生成的JSON字符串如下(为了可读性,这里做了格式化):
{ "deviceId":"ESP32_S3_Kitchen_01", "msgType":"sensorData", "timestamp":1698765432, "data":{"temperature":25.6,"humidity":45.7,"temperatureUnit":"Celsius","humidityUnit":"%"}, "status":{"rssi":-65,"batteryOk":true} }这样的结构,云端解析起来一目了然,也便于后期根据deviceId或msgType进行路由和处理。
1.2 内存管理的实战技巧
在资源受限的ESP32上,频繁创建和释放JSON对象与字符串容易导致内存碎片。对于周期性上报的数据,我们可以采用内存池或静态缓冲区的思路进行优化。
#define JSON_BUFFER_SIZE 512 static char json_buffer[JSON_BUFFER_SIZE]; bool create_sensor_json_to_buffer(float temp, float humi, const char* dev_id, char* out_buf, int buf_size) { cJSON *root = cJSON_CreateObject(); // ... 构建JSON对象的过程同上 ... // 关键:使用预分配的缓冲区进行打印 bool print_ok = cJSON_PrintPreallocated(root, out_buf, buf_size, true); cJSON_Delete(root); if (!print_ok) { ESP_LOGE("JSON", "Buffer too small for JSON serialization."); out_buf[0] = '\0'; // 清空缓冲区 return false; } return true; } // 在任务循环中调用 void sensor_task(void *pvParameters) { while(1) { float t, h; // ... 读取DHT11数据 ... if (create_sensor_json_to_buffer(t, h, device_id, json_buffer, JSON_BUFFER_SIZE)) { // 现在 json_buffer 中就是JSON字符串,可以直接通过MQTT或HTTP发送 mqtt_publish("sensor/data", json_buffer); } vTaskDelay(pdMS_TO_TICKS(10000)); // 每10秒上报一次 } }使用cJSON_PrintPreallocated能有效避免频繁的动态内存分配,特别适合在实时任务中稳定运行。
2. 场景二:动态配置的载体——GPIO配置数组解析
物联网设备的一个常见需求是能够远程配置。比如,我们需要通过云端下发的指令,动态设置多个GPIO口的工作模式(输入/输出)和初始电平。用JSON来传递这样的配置数组再合适不过。
2.1 解析云端下发的配置指令
假设我们收到这样一条来自云端的配置JSON字符串:
{ "configVersion": 2, "gpioConfig": [ {"pin": 12, "mode": "output", "level": 1}, {"pin": 13, "mode": "input", "pull": "up"}, {"pin": 14, "mode": "output", "level": 0} ] }我们的代码需要安全地解析它,并应用到硬件上。
#include "driver/gpio.h" typedef struct { int pin; char mode[10]; // "input", "output" int level; // 0 or 1, for output char pull[10]; // "up", "down", "none", for input } gpio_config_item_t; bool parse_gpio_config(const char *json_str) { cJSON *root = cJSON_Parse(json_str); if (root == NULL) { const char *error_ptr = cJSON_GetErrorPtr(); ESP_LOGE("CONFIG", "JSON parse error before: %s", error_ptr ? error_ptr : "unknown"); return false; } // 1. 检查配置版本(用于兼容性处理) cJSON *ver_item = cJSON_GetObjectItem(root, "configVersion"); if (!cJSON_IsNumber(ver_item)) { ESP_LOGE("CONFIG", "Missing or invalid configVersion"); cJSON_Delete(root); return false; } int config_ver = ver_item->valueint; ESP_LOGI("CONFIG", "Parsing config version: %d", config_ver); // 2. 获取gpioConfig数组 cJSON *gpio_array = cJSON_GetObjectItem(root, "gpioConfig"); if (!cJSON_IsArray(gpio_array)) { ESP_LOGE("CONFIG", "gpioConfig is not an array"); cJSON_Delete(root); return false; } int array_size = cJSON_GetArraySize(gpio_array); ESP_LOGI("CONFIG", "Found %d GPIO config items", array_size); gpio_config_item_t configs[array_size]; // C99可变长度数组,或动态分配 int valid_config_count = 0; // 3. 遍历数组,解析每一项 cJSON *item = NULL; cJSON_ArrayForEach(item, gpio_array) { if (!cJSON_IsObject(item)) continue; gpio_config_item_t cfg = {0}; cJSON *pin_item = cJSON_GetObjectItem(item, "pin"); cJSON *mode_item = cJSON_GetObjectItem(item, "mode"); if (!cJSON_IsNumber(pin_item) || !cJSON_IsString(mode_item)) { ESP_LOGW("CONFIG", "Skipping invalid GPIO config item"); continue; } cfg.pin = pin_item->valueint; strncpy(cfg.mode, mode_item->valuestring, sizeof(cfg.mode)-1); // 根据模式解析其他字段 if (strcmp(cfg.mode, "output") == 0) { cJSON *level_item = cJSON_GetObjectItem(item, "level"); if (cJSON_IsNumber(level_item)) { cfg.level = level_item->valueint ? 1 : 0; } } else if (strcmp(cfg.mode, "input") == 0) { cJSON *pull_item = cJSON_GetObjectItem(item, "pull"); if (cJSON_IsString(pull_item)) { strncpy(cfg.pull, pull_item->valuestring, sizeof(cfg.pull)-1); } } configs[valid_config_count++] = cfg; ESP_LOGI("CONFIG", "Parsed: PIN%d as %s", cfg.pin, cfg.mode); } cJSON_Delete(root); // 解析完成,释放JSON树 // 4. 应用配置到硬件(这里只是示例,实际应用需加错误处理) for (int i = 0; i < valid_config_count; i++) { gpio_config_t io_conf = {0}; io_conf.pin_bit_mask = (1ULL << configs[i].pin); if (strcmp(configs[i].mode, "output") == 0) { io_conf.mode = GPIO_MODE_OUTPUT; gpio_config(&io_conf); gpio_set_level(configs[i].pin, configs[i].level); } else if (strcmp(configs[i].mode, "input") == 0) { io_conf.mode = GPIO_MODE_INPUT; if (strcmp(configs[i].pull, "up") == 0) { io_conf.pull_up_en = GPIO_PULLUP_ENABLE; } else if (strcmp(configs[i].pull, "down") == 0) { io_conf.pull_down_en = GPIO_PULLDOWN_ENABLE; } gpio_config(&io_conf); } } ESP_LOGI("CONFIG", "Successfully applied %d GPIO configurations", valid_config_count); return (valid_config_count > 0); }这段代码展示了如何安全、健壮地解析一个复杂的JSON配置。它包含了版本检查、类型验证、错误跳过,最终将解析结果转化为具体的硬件操作。
2.2 设计可扩展的配置结构
上面的例子是平铺的配置。对于更复杂的设备,配置可能是分层的。例如,配置一个PWM通道:
{ "channel": 0, "frequency": 5000, "dutyCycle": 0.5, "gpio": 15 }解析这类嵌套对象时,思路是一致的:逐层获取对象,并检查每一层的类型。关键在于设计好对应的C语言数据结构,并做好默认值处理(JSON中可能缺失某些可选字段)。
3. 场景三:设备状态快照——封装WiFi扫描结果与系统信息
当设备启动或收到诊断请求时,我们常常需要收集一份完整的系统状态快照,并上报给服务器。这份快照通常包含动态信息(如扫描到的WiFi网络)和静态信息(如芯片ID、固件版本)。
3.1 构建包含数组的复杂状态对象
下面这个函数模拟了收集状态并生成JSON报告的过程:
#include "esp_system.h" #include "esp_wifi.h" #include "esp_mac.h" char* create_system_status_report() { cJSON *root = cJSON_CreateObject(); cJSON_AddStringToObject(root, "reportType", "fullStatus"); cJSON_AddNumberToObject(root, "reportTime", (double)time(NULL)); // 1. 系统信息(静态或半静态) cJSON *sys_info = cJSON_CreateObject(); cJSON_AddItemToObject(root, "system", sys_info); cJSON_AddStringToObject(sys_info, "chipModel", CONFIG_IDF_TARGET); cJSON_AddNumberToObject(sys_info, "freeHeap", esp_get_free_heap_size()); cJSON_AddNumberToObject(sys_info, "minFreeHeap", esp_get_minimum_free_heap_size()); uint8_t mac[6]; esp_read_mac(mac, ESP_MAC_WIFI_STA); char mac_str[18]; snprintf(mac_str, sizeof(mac_str), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); cJSON_AddStringToObject(sys_info, "macAddress", mac_str); // 2. WiFi扫描结果(动态数组) cJSON *wifi_scan_array = cJSON_CreateArray(); cJSON_AddItemToObject(root, "wifiScanResults", wifi_scan_array); // 模拟扫描到的网络 const char* ssid_list[] = {"Home_Network", "Office_Guest", "Neighbor_5G"}; int rssi_list[] = {-45, -62, -70}; uint8_t auth_list[] = {WIFI_AUTH_WPA2_PSK, WIFI_AUTH_WPA_WPA2_PSK, WIFI_AUTH_OPEN}; for (int i = 0; i < 3; i++) { cJSON *ap = cJSON_CreateObject(); cJSON_AddStringToObject(ap, "ssid", ssid_list[i]); cJSON_AddNumberToObject(ap, "rssi", rssi_list[i]); // 将认证模式枚举值转换为可读字符串 const char* auth_str = "UNKNOWN"; switch(auth_list[i]) { case WIFI_AUTH_OPEN: auth_str = "OPEN"; break; case WIFI_AUTH_WPA_PSK: auth_str = "WPA_PSK"; break; case WIFI_AUTH_WPA2_PSK: auth_str = "WPA2_PSK"; break; case WIFI_AUTH_WPA_WPA2_PSK: auth_str = "WPA_WPA2_PSK"; break; } cJSON_AddStringToObject(ap, "authMode", auth_str); cJSON_AddItemToArray(wifi_scan_array, ap); } // 3. 任务运行状态(另一个数组示例) cJSON *task_array = cJSON_CreateArray(); cJSON_AddItemToObject(root, "taskStatus", task_array); // 这里可以调用FreeRTOS API获取任务列表,此处用模拟数据 const char* task_names[] = {"mqtt_task", "sensor_task", "http_server"}; int stack_high_watermark[] = {512, 768, 1024}; for (int i = 0; i < 3; i++) { cJSON *task = cJSON_CreateObject(); cJSON_AddStringToObject(task, "name", task_names[i]); cJSON_AddNumberToObject(task, "stackHighWaterMark", stack_high_watermark[i]); cJSON_AddStringToObject(task, "state", "Running"); // 简化状态 cJSON_AddItemToArray(task_array, task); } char *json_str = cJSON_Print(root); cJSON_Delete(root); return json_str; }这个状态报告非常全面,既包含了设备身份信息,也包含了实时环境(WiFi网络)和内部运行状况(任务状态)。服务器收到后,可以对设备进行远程诊断和监控。
3.2 使用表格对比不同状态报告策略
在实际项目中,我们可能需要在“信息详尽度”和“报文大小/上报频率”之间做权衡。下表对比了几种常见的策略:
| 报告类型 | 包含内容 | 生成频率 | 适用场景 | 优缺点 |
|---|---|---|---|---|
| 完整状态报告 | 系统信息、WiFi扫描、任务状态、传感器数据等 | 低(如每日一次或启动时) | 设备诊断、首次注册、深度排查 | 优点:信息全面,利于问题定位。 缺点:数据量大,生成耗时。 |
| 增量状态报告 | 仅包含自上次报告以来变化的数据 | 中高(如每小时或事件触发) | 日常监控、状态跟踪 | 优点:数据量小,效率高。 缺点:逻辑复杂,需维护状态对比。 |
| 最小心跳报告 | 仅设备ID、时间戳、基础健康状态(如内存) | 高(如每分钟) | 连接保活、存活检测 | 优点:开销极小,实时性好。 缺点:信息量少,无法用于诊断。 |
在代码中,我们可以根据不同的触发条件(如定时器、外部指令)选择生成不同类型的报告JSON。
4. 场景四:云端指令响应——解析与控制命令
物联网设备不仅是数据上报端,也是命令执行端。云端下发的控制指令,例如开关继电器、调整采样率、重启设备等,通常也通过JSON传递。一个健壮的指令处理流程必须包含验证、执行与反馈。
4.1 实现一个安全的指令分发器
假设我们定义了几种指令格式:
- 开关控制:
{"cmd": "setGpio", "args": {"pin": 2, "value": 1}} - 配置更新:
{"cmd": "updateConfig", "args": {"samplingInterval": 5000}} - 设备重启:
{"cmd": "reboot", "args": {"delay": 10}}
下面是一个指令处理器的框架:
typedef enum { CMD_UNKNOWN = 0, CMD_SET_GPIO, CMD_UPDATE_CONFIG, CMD_REBOOT } command_type_t; typedef struct { command_type_t type; cJSON *args; // 指向原始JSON中args对象的指针,不单独释放 char *raw_cmd_str; // 原始命令字符串,用于生成响应 } parsed_command_t; bool parse_and_validate_command(const char *json_str, parsed_command_t *out_cmd) { memset(out_cmd, 0, sizeof(parsed_command_t)); cJSON *root = cJSON_Parse(json_str); if (!root) return false; // 必须包含cmd字段 cJSON *cmd_item = cJSON_GetObjectItem(root, "cmd"); if (!cJSON_IsString(cmd_item)) { cJSON_Delete(root); return false; } const char *cmd_str = cmd_item->valuestring; out_cmd->raw_cmd_str = cJSON_PrintUnformatted(root); // 保存原始命令用于响应 // 识别命令类型 if (strcmp(cmd_str, "setGpio") == 0) { out_cmd->type = CMD_SET_GPIO; } else if (strcmp(cmd_str, "updateConfig") == 0) { out_cmd->type = CMD_UPDATE_CONFIG; } else if (strcmp(cmd_str, "reboot") == 0) { out_cmd->type = CMD_REBOOT; } else { out_cmd->type = CMD_UNKNOWN; // 不立即返回false,允许生成“未知命令”的响应 } // 获取args对象(某些命令可能没有args) out_cmd->args = cJSON_GetObjectItem(root, "args"); // 注意:这里我们没有cJSON_Delete(root),因为args指向root的一部分。 // 我们需要在最终处理完命令后,通过释放raw_cmd_str间接释放root。 // 更好的做法是深拷贝args,这里为简化而采用此方式,需注意生命周期管理。 return true; } void execute_command(const parsed_command_t *cmd) { char response_buffer[256]; cJSON *resp_root = cJSON_CreateObject(); cJSON_AddStringToObject(resp_root, "originalCmd", cmd->raw_cmd_str); bool success = false; const char *result_msg = NULL; switch(cmd->type) { case CMD_SET_GPIO: { if (cmd->args && cJSON_IsObject(cmd->args)) { cJSON *pin_item = cJSON_GetObjectItem(cmd->args, "pin"); cJSON *value_item = cJSON_GetObjectItem(cmd->args, "value"); if (cJSON_IsNumber(pin_item) && cJSON_IsNumber(value_item)) { int pin = pin_item->valueint; int value = value_item->valueint ? 1 : 0; // 执行GPIO设置(此处省略硬件操作) gpio_set_level(pin, value); success = true; result_msg = "GPIO set successfully"; } else { result_msg = "Invalid arguments for setGpio"; } } else { result_msg = "Missing arguments for setGpio"; } break; } case CMD_UPDATE_CONFIG: { // ... 解析并更新配置 ... result_msg = "Config update not implemented"; break; } case CMD_REBOOT: { result_msg = "Reboot command accepted"; success = true; // 可以设置一个定时器,延迟重启 break; } case CMD_UNKNOWN: { result_msg = "Unknown command type"; break; } default: { result_msg = "Internal command error"; break; } } cJSON_AddBoolToObject(resp_root, "success", success); cJSON_AddStringToObject(resp_root, "message", result_msg); cJSON_AddNumberToObject(resp_root, "timestamp", (double)time(NULL)); cJSON_PrintPreallocated(resp_root, response_buffer, sizeof(response_buffer), true); cJSON_Delete(resp_root); // 将响应发送回云端 ESP_LOGI("CMD", "Execution Response: %s", response_buffer); // mqtt_publish("device/response", response_buffer); // 清理资源 cJSON_free(cmd->raw_cmd_str); // 这会释放最初parse出来的整个cJSON树 }这个框架的核心思想是:解析与执行分离。parse_and_validate_command函数负责校验格式和提取关键信息,execute_command函数负责具体的业务逻辑和生成响应。响应本身也是一个JSON,包含了执行结果、时间戳和原始命令,形成了完整的闭环。
4.2 指令处理的错误边界与安全
在处理云端指令时,必须格外小心:
- 参数范围校验:即使JSON解析成功,也要检查
pin号是否有效,value是否为0或1。 - 资源访问锁:如果命令会访问共享资源(如配置结构体),需要考虑使用互斥锁。
- 异步响应:某些耗时命令(如重启)可能需要先返回“已接收”,再异步执行。
5. 场景五:数据持久化与读取——将配置保存到文件系统
对于需要断电保存的配置,我们可以将JSON对象序列化成字符串后,写入ESP32的SPIFFS或LittleFS文件系统。反之,启动时再从文件中读取并解析。
5.1 将JSON配置保存至文件
#include "esp_spiffs.h" #include <stdio.h> bool save_config_to_file(const char *filepath, cJSON *config) { if (!config) return false; // 将cJSON对象转换为字符串 char *json_str = cJSON_PrintUnformatted(config); if (!json_str) return false; FILE *f = fopen(filepath, "w"); if (f == NULL) { ESP_LOGE("FS", "Failed to open file for writing: %s", filepath); cJSON_free(json_str); return false; } fprintf(f, "%s", json_str); fclose(f); cJSON_free(json_str); ESP_LOGI("FS", "Configuration saved to %s", filepath); return true; } // 示例:保存一个网络配置 void save_wifi_config_example() { cJSON *config = cJSON_CreateObject(); cJSON *wifi = cJSON_CreateObject(); cJSON_AddItemToObject(config, "wifi", wifi); cJSON_AddStringToObject(wifi, "ssid", "MyHomeWiFi"); // 注意:实际项目中密码应加密存储,这里仅为示例 cJSON_AddStringToObject(wifi, "password", "secure_password"); cJSON_AddBoolToObject(wifi, "dhcp", true); cJSON *static_ip = cJSON_CreateObject(); cJSON_AddItemToObject(wifi, "staticIp", static_ip); // 如果dhcp为false则使用 cJSON_AddStringToObject(static_ip, "ip", "192.168.1.100"); cJSON_AddStringToObject(static_ip, "gateway", "192.168.1.1"); cJSON_AddStringToObject(static_ip, "netmask", "255.255.255.0"); save_config_to_file("/spiffs/wifi_config.json", config); cJSON_Delete(config); }5.2 从文件读取并解析JSON配置
读取和解析是保存的逆过程,但需要更严谨的错误处理。
cJSON* load_config_from_file(const char *filepath) { FILE *f = fopen(filepath, "r"); if (f == NULL) { ESP_LOGW("FS", "Config file not found: %s", filepath); return NULL; } // 获取文件大小 fseek(f, 0, SEEK_END); long fsize = ftell(f); fseek(f, 0, SEEK_SET); if (fsize <= 0 || fsize > 4096) { // 限制配置文件大小 fclose(f); ESP_LOGE("FS", "Invalid config file size: %ld", fsize); return NULL; } // 分配内存并读取文件内容 char *buffer = (char*)malloc(fsize + 1); if (!buffer) { fclose(f); ESP_LOGE("FS", "Memory allocation failed"); return NULL; } size_t read_len = fread(buffer, 1, fsize, f); fclose(f); if (read_len != fsize) { ESP_LOGE("FS", "File read error"); free(buffer); return NULL; } buffer[fsize] = '\0'; // 确保字符串终止 // 解析JSON cJSON *config = cJSON_Parse(buffer); free(buffer); if (!config) { const char *error_ptr = cJSON_GetErrorPtr(); ESP_LOGE("FS", "JSON parse error in config file before: %s", error_ptr ? error_ptr : "unknown"); return NULL; } ESP_LOGI("FS", "Configuration loaded from %s", filepath); return config; // 调用者需要负责cJSON_Delete } // 使用示例:加载并应用WiFi配置 bool load_and_apply_wifi_config() { cJSON *config = load_config_from_file("/spiffs/wifi_config.json"); if (!config) { return false; // 加载失败,可能使用默认配置 } cJSON *wifi = cJSON_GetObjectItem(config, "wifi"); if (!cJSON_IsObject(wifi)) { cJSON_Delete(config); return false; } cJSON *ssid_item = cJSON_GetObjectItem(wifi, "ssid"); cJSON *pass_item = cJSON_GetObjectItem(wifi, "password"); cJSON *dhcp_item = cJSON_GetObjectItem(wifi, "dhcp"); if (cJSON_IsString(ssid_item) && cJSON_IsString(pass_item)) { ESP_LOGI("WIFI", "Config loaded: SSID=%s, DHCP=%s", ssid_item->valuestring, (cJSON_IsTrue(dhcp_item) ? "Yes" : "No")); // 这里可以调用esp_wifi_set_config等函数应用配置 } cJSON_Delete(config); return true; }5.3 文件系统JSON操作的注意事项
- 原子性操作:对于重要配置,写入时可以先写到一个临时文件(如
config.json.tmp),写入完成并fsync后,再重命名覆盖原文件。这可以防止在写入过程中断电导致配置文件损坏。 - 版本管理:可以在JSON根节点中加入一个
"configVersion"字段。当未来配置结构升级时,代码可以根据版本号决定如何解析或迁移旧的配置文件。 - 大小限制:解析来自文件的JSON时,务必检查文件大小,防止恶意或损坏的文件耗尽内存。
这几个场景基本覆盖了ESP32物联网开发中cJSON的绝大部分应用。从简单的数据封装到复杂的指令交互,再到本地存储,JSON作为数据交换的粘合剂,其价值在于提供了一种标准、灵活且开发者友好的方式。我自己的经验是,在项目初期就设计好关键的数据JSON结构,并编写好对应的构建与解析函数,会为后续的联调、功能扩展省下大量时间。尤其是在和云端后端工程师对接时,一份定义清晰的JSON样例,比好几页文档都管用。