① 文件系统与SPIFFS

② Json与ArduinoJSON

③ 系统参数持久化系统开发

1 文件系统

从系统角度来看,文件系统是对文件存储器空间进行组织、分配、负责文件存储并对存入的文件进行保护和检索的一个系统。

它的目的是使用户和应用程序能够方便地管理存储器上的文件和目录,比如常见的对文件的增、删、改、查。

每当我们操作一个文件的时候,实际都会涉及到存储器、管理系统、被管理文件,还有就是实施文件管理所需的数据结构。

存储器,我们也叫存储介质,也就是电子设备用来存储数据信息的器件。

1.1 存储介质介绍

存储介质,也就是用来存储信息的介质,从远古的石壁刻画,到古代的笔墨纸砚记录,再到电气时代的磁存储、光存储、半导体芯片存储。

光盘存储已近逐渐地退出大众的视野,我们这里主要介绍磁存储和半导体存储。磁存储的代表就是机械硬盘,半导体存储已近被非常广泛地应用了,比如我们常用的U盘、SD卡、TF卡、固态硬盘等。

  • 机械硬盘因为它容量大、价格低,在个人电脑中广泛使用,
  • 固态存储速度快,功耗低,无噪音,同等容量的价格也比较的平民,在各种便携式存储设备中广泛使用。

固态存储,也就是不依赖机械传动,直接使用半导体芯片为介质的一种存储。大的分类可以分为两种: RAM和ROM。

Flash其实也算是ROM的一种变种,现在广泛使用的 ROM 大部分都是基于Flash设计的。大部分固态硬盘的存储介质也是Flash。

(1)RAMROM

RAM(随机访问存储器):

是一种临时存储器,用于保存正在运行时所需的数据和程序,可读可写,允许在需要时快速读取或写入数据,但它也是易失性存储器,设备断电时里面的数据会丢失。

ROM(只读存储器) :

用于存储固定的、不经常变化的数据和程序,它是只读的,通常在制造时被写入,并且其内容在正常操作期间不可更改。但它存在一些变种,如可擦写可编程只读存储器(EPROM)和闪存(Flash),它们允许有限次的可编程操作。

Flash存储器

它是一种非易失性存储技术,广泛用于各种电子设备、USB、固态硬盘等产品。它分为NOR Flash 和NAND Flash 两种

(2) NOR Flash

它采用 NOR(不或非)门结构,与NAND Flash相比,NOR Flash具有一些特定的特性和优势:

  • 读取速度快
  • 随机访问能力
  • 适用于代码存储
  • 写入擦除速度较慢
  • 低功耗

(3) NAND Flash

NAND Flash采用NAND门结构,与NOR Flash相比,它具有一些独特的特点和优势。

  • 高密度存储
  • 相对低成本
  • 擦写熟读快
  • 适用于大容量数据存储
  • 顺序访问

(4)基于Flash的应用

在嵌入式设备,一般是直接使用Flash存储器来存储程序和代码,按总线类型,可以分为串行和并行总线接口,在嵌入式设备中使用的比较多的是SPI串行总线。

1.2 文件系统

文件是操作系统管理用户数据的基本单位,是存储在外部存储设备上的一组相关信息的集合。文件的核心特性包括:

逻辑性和抽象性:文件为用户提供了一种逻辑视图,与底层物理存储细节无关。用户可以通过文件名和路径轻松访问数据,而无需关心数据在磁盘中的存储方式。

多样性:文件可以包含各种类型的数据,包括文本、图像、音频和二进制数据等。

持久性:文件在设备断电或系统重启后仍然存在,确保数据的长期保存。

文件属性:

每个文件具有一组属性,用于描述文件的状态和访问权限,包括文件名、类型、大小、创建时间、最后修改时间、权限和所有者等。

文件操作:

操作系统提供一系列文件操作,包括创建、删除、读、写、重命名、复制和移动文件。这些操作由系统调用实现,如open()、read()和write()等。

文件是现代计算机系统的重要组成部分,为数据存储和管理提供了基本框架。

1.3 SPIFFS

在ESP32S3开发中,支持多种存储介质,包括TF卡、FLASH。其中由于ESP32S3一般会具有较大的SPI flash用于存储代码。但事实上我们很少将全部空间用于存储代码。对于片外的Flash 它是可读可写的,由于它的容量高达16MB,这意味着我们可以在其中存储大量的配置文件、图像、音频文件,甚至是一些小型的视频文件,可以尝试很多有趣的项目和应用.

SPIFFS 是一个用于 SPI NOR Flash 设备的嵌入式文件系统,支持磨损均衡(嵌入式设备使用的大多数存储芯片都支持每个扇区有限的擦除集,如果没有均衡,则嵌入式设备的寿命可能会受到影响)、文件系统一致性检查等功能。该文件系统只需要少量的RAM就可以运行。

SPIFFS 是一个用于 SPI NOR flash 设备的嵌入式文件系统。 目前,SPIFFS 尚不支持目录,但可以生成扁平结构。如果 SPIFFS 挂载在 /spiffs 下,在 /spiffs/tmp/myfile.txt 路径下创建一个文件则会在 SPIFFS 中生成一个名为 /tmp/myfile.txt 的文件,而不是在 /spiffs/tmp 下生成名为 myfile.txt 的文件。

1.3.1 如何做ESP32 Arduino框架下使用SPIFFS

(1) ESP32 FLASH 分区

分区表(Partition Table)是一种数据结构,用于在嵌入式系统中管理存储设备的逻辑分区。它定义了存储设备上不同逻辑分区的起始位置、大小和属性。

分区表通常被用于磁盘驱动器、固态硬盘、闪存等存储设备,以便将存储空间划分为多个逻辑区域。每个逻辑分区可以被格式化为不同的文件系统,并用于存储不同类型的数据。

在分区表中,每个分区都有一些关键信息,例如:

起始地址(Start Address):分区在存储设备上的起始位置。

大小(Size):分区的大小,即分区所占据的存储空间。

类型(Type):分区的类型或标识符,用于表示分区的用途或文件系统类型。

属性(Attributes):分区的属性,如是否可引导、只读等。

分区表通常存储在存储设备的特定位置,例如硬盘的主引导记录(MBR)或GUID分区表(GPT)中。操作系统或引导加载程序可以读取分区表信息,并根据需要访问或操作不同的分区。

使用分区表可以提供更灵活的存储空间管理,使嵌入式系统能够支持多个不同类型的文件系统和数据存储需求。

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x600000,
app1,     app,  ota_1,   0x610000,0x600000,
spiffs,   data, spiffs,  0xC10000,0x3E0000,
coredump, data, coredump,0xFF0000,0x10000,

nvs: 用于存储非易失性数据 (Non-Volatile Storage),例如配置参数。

otadata: 用于存储OTA (Over-the-Air) 更新相关的数据。

app0: 应用程序的第一个固件分区,用于存储主要的应用程序代码。

app1: 应用程序的第二个固件分区,常用于进行固件升级时的备份。

spiffs: SPI Flash 文件系统,用于存储应用程序的文件数据。

coredump: 存储核心转储文件,用于记录程序在发生异常或崩溃时的状态信息。

常见分区有:

① 应用程序分区(Type=app)

factory:出厂固件分区,系统首次启动时加载

ota_0/ota_1:OTA 升级分区,用于存放新版本固件

test:测试固件分区,用于验证新功能

② 数据分区(Type=data)

nvs:非易失性存储,保存配置参数(如 WiFi 密码)

phy:WiFi/BLE 物理层校准数据

spiffs/fatfs:文件系统分区,存储用户文件

nvs_keys:NVS 加密密钥存储区

③ 特殊分区

efuse:存储一次性可编程参数(如芯片 ID、安全密钥)

otadata:OTA 数据分区,记录当前活动分区

我们在配置分区是一般只选择app和data两种类型。

(2)操作步骤

① 建立文件(后缀.csv)

如下(示例):在ESP32项目目录下建立文件(后缀.csv)

② 引入.csv文件

在Platformio.ini配置中引入.csv文件

[env:adafruit_feather_esp32s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
board_build.partitions = config_flash.csv
board_build.arduino.memory_type = qio_opi
board_upload.flash_size = 16MB
build_flags = -DBOARD_HAS_PSRAM
    -DARDUINO_USB_CDC_ON_BOOT=1
    -DCORE_DEBUG_LEVEL=1
monitor_speed = 115200

1.3.2 SPIFFS的API接口

以下是在arduino平台上SPIFFS常见API的作用、形参、返回值及使用介绍:

常见API接口

(1)SPIFFS.begin()

  • 作用 :挂载SPIFFS文件系统,开始使用SPIFFS。
  • 形参bool formatOnFail = true, const char *basePath = "/spiffs", uint8_t maxOpenFiles =5, bool formatIfMountFailed = false 。其中:
    • formatOnFail :指定如果挂载失败是否进行格式化,默认为true
    • basePath :指定文件系统的挂载点,默认为"spiffs"
    • maxOpenFiles :指定允许同时打开的文件最大数量,默认为5。
    • formatIfMountFailed :指定如果挂载失败是否进行格式化,默认为false

* 返回值 :挂载成功返回true,否则返回false

(2)SPIFFS.end()

* 作用 :卸载SPIFFS文件系统,停止使用SPIFFS。

* 形参 :无。

* 返回值 :无 。

(3)SPIFFS.format()

* 作用 :格式化SPIFFS文件系统。格式化后会清除文件系统中的所有数据,但在某些情况下,如文件系统损坏或需要重新组织存储空间时,可能需要进行格式化。

* 形参 :无。

* 返回值 :格式化成功返回true,否则返回false

(4)SPIFFS.open()

* 作用 :打开一个文件。

* 形参const char *filename, const char *mode = "r"。其中:

* filename :指定要打开的文件名。

* mode :指定打开模式,默认为只读模式"r",其他常见模式包括只写模式"w"、读写模式"r+"等。

* 返回值 :返回一个File对象,用于后续文件读写操作。

(5)SPIFFS.exists()

* 作用 :判断一个文件是否存在。

* 形参const char *path,指定要判断的文件路径。

* 返回值 :文件存在返回true,否则返回false

(6)SPIFFS.remove()

* 作用 :删除一个文件。

* 形参const char *path,指定要删除的文件路径。

* 返回值 :删除成功返回true,否则返回false

(7)SPIFFS.rename()

* 作用 :重命名文件或目录。

* 形参const char *oldPath, const char *newPath。其中:

* oldPath :指定要重命名的文件或目录的原始路径。

* newPath :指定重命名后的文件或目录的路径。

* 返回值 :重命名成功返回true,否则返回false

(8)SPIFFS.mkdir()

* 作用 :创建目录。

* 形参const char *path,指定要创建的目录路径。

* 返回值 :创建成功返回true,否则返回false

(9)SPIFFS.rmdir()

* 作用 :删除目录。

* 形参const char *path,指定要删除的目录路径。

* 返回值 :删除成功返回true,否则返回false

(10)SPIFFSFileSize()

* 作用 :获取文件的大小。

* 形参const char *path,指定文件路径。

* 返回值 :返回文件的大小,单位为字节。

(11)SPIFFS.totalBytes()

* 作用 :获取文件系统的总字节数。

* 形参 :无。

* 返回值 :返回文件系统的总字节数。

(12)SPIFFS.usedBytes()

* 作用 :获取文件系统已使用的字节数。

* 形参 :无。

* 返回值 :返回文件系统已使用的字节数。

使用步骤

(1)包含头文件 :在代码中包含FS.h头文件,即#include "FS.h"

(2)初始化SPIFFS :在setup()函数中调用SPIFFS.begin()来挂载SPIFFS文件系统,如果挂载失败,可以尝试格式化文件系统。例如:

  • if (!SPIFFS.begin()) {
  • SPIFFS.format();
  • SPIFFS.begin();
  • }

(3)文件操作 :使用上述的文件操作API进行文件的创建、读写、删除等操作。例如,打开一个文件进行写入操作:

* File file = SPIFFS.open("/test.txt", "w");

* if (file) {

* file.println("Hello, SPIFFS!");

* file.close();

* }

(4)上传文件 :如果需要将文件上传到SPIFFS,可以将文件放在Arduino项目的data文件夹中,然后通过ESP8266 Sketch Data UploadESP32 Sketch Data Upload功能进行上传。

注意事项

  • SPIFFS不支持目录,所以不能创建子目录,只能在根目录下操作文件。
  • SPIFFS的文件名区分大小写,且文件名长度不能超过255个字符。
  • 在使用文件操作API时,要确保文件已正确打开,否则可能会导致程序崩溃或数据丢失。
#include "FS.h"
#include "SPIFFS.h"

// 列出指定目录下的文件和子目录
void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\r\n", dirname);

    // 打开目录
    File root = fs.open(dirname);
    if(!root){
        Serial.println("- failed to open directory");
        return;
    }
    // 检查是否为目录
    if(!root.isDirectory()){
        Serial.println(" - not a directory");
        return;
    }

    // 打开目录的下一个文件
    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){    // 如果是目录,则打印目录名并递归列出子目录
            Serial.print("  DIR : ");
            Serial.println(file.name());
            if(levels){
                listDir(fs, file.path(), levels -1);
            }
        } else {            // 如果是文件,则打印文件名和大小
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("\tSIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }
}

// 读取并显示文件内容
void readFile(fs::FS &fs, const char * path){
    Serial.printf("Reading file: %s\r\n", path);    // 打印正在读取的文件

    File file = fs.open(path);   // 打开文件
    if(!file || file.isDirectory()){
        Serial.println("- failed to open file for reading");
        return;
    }

     // 读取并打印文件内容
    Serial.println("- read from file:");
    while(file.available()){
        Serial.write(file.read());    
    }
    file.close();
}

// 写入内容到文件,如果文件不存在则创建
void writeFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Writing file: %s\r\n", path);
    
    //打印正在写的文件
    File file = fs.open(path, FILE_WRITE);
    // 打开文件用于写入
    if(!file){
        Serial.println("- failed to open file for writing");
        return;
    }
    if(file.print(message)){
        Serial.println("- file written");
    } else {
        Serial.println("- write failed");
    }
    file.close();
}

// 向文件追加内容
void appendFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Appending to file: %s\r\n", path);

    File file = fs.open(path, FILE_APPEND);
    if(!file){
        Serial.println("- failed to open file for appending");
        return;
    }
    if(file.print(message)){
        Serial.println("- message appended");
    } else {
        Serial.println("- append failed");
    }
    file.close();
}

void renameFile(fs::FS &fs, const char * path1, const char * path2){
    Serial.printf("Renaming file %s to %s\r\n", path1, path2);
    if (fs.rename(path1, path2)) {
        Serial.println("- file renamed");
    } else {
        Serial.println("- rename failed");
    }
}

void deleteFile(fs::FS &fs, const char * path){
    Serial.printf("Deleting file: %s\r\n", path);
    if(fs.remove(path)){
        Serial.println("- file deleted");
    } else {
        Serial.println("- delete failed");
    }
}

//函数显示读取文件内容所需的时间
void testFileIO(fs::FS &fs, const char * path){
    Serial.printf("Testing file I/O with %s\r\n", path);

    static uint8_t buf[512];
    size_t len = 0;
    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("- failed to open file for writing");
        return;
    }

    size_t i;
    Serial.print("- writing" );
    uint32_t start = millis();
    for(i=0; i<2048; i++){
        if ((i & 0x001F) == 0x001F){
          Serial.print(".");
        }
        file.write(buf, 512);
    }
    Serial.println("");
    uint32_t end = millis() - start;
    Serial.printf(" - %u bytes written in %u ms\r\n", 2048 * 512, end);
    file.close();

    file = fs.open(path);
    start = millis();
    end = start;
    i = 0;
    if(file && !file.isDirectory()){
        len = file.size();
        size_t flen = len;
        start = millis();
        Serial.print("- reading" );
        while(len){
            size_t toRead = len;
            if(toRead > 512){
                toRead = 512;
            }
            file.read(buf, toRead);
            if ((i++ & 0x001F) == 0x001F){
              Serial.print(".");
            }
            len -= toRead;
        }
        Serial.println("");
        end = millis() - start;
        Serial.printf("- %u bytes read in %u ms\r\n", flen, end);
        file.close();
    } else {
        Serial.println("- failed to open file for reading");
    }
}
 
// 创建一个新的目录
void createDir(fs::FS &fs, const char * path){
  // 打印正在创建的目录
  Serial.printf("Creating Dir: %s\n", path);
  if(fs.mkdir(path)){
  // 尝试创建目录,如果成功则打印成功信息,否则打印失败信息
      Serial.println("Dir created");
  } else {
      Serial.println("mkdir failed");
  }
}

// 删除一个目录及其所有内容
void removeDir(fs::FS &fs, const char * path){
  Serial.printf("Removing Dir: %s\n", path);
  if(fs.rmdir(path)){
  // 尝试删除目录,如果成功则打印成功信息,否则打印失败信息
      Serial.println("Dir removed");
  } else {
      Serial.println("rmdir failed");
  }
}

void setup(){
    Serial.begin(115200);
    sys_delay_ms(3000);
    if(!SPIFFS.begin()){    
        Serial.println("SPIFFS Mount Failed");
        return;
    }

  listDir(SPIFFS, "/", 0);  
  readFile(SPIFFS, "/system.json");
  readFile(SPIFFS, "/wifi.html");
  
  
}

void loop(){

}

2 ArduinoJSON

2.1 什么是JSON?

JSON: JavaScript Object NotationJavaScript 对象标记法)。

JSON 是一种存储交换数据的语法格式。易于阅读和编写,同时也易于机器解析和生成。

JSON 是通过 JavaScript 对象标记法书写的文本。

(1) JSON用于数据交换

最开始,JSON起源于当数据在浏览器与服务器之间的数据通信。当二者进行交换时,这些数据只能是文本。JSON 属于文本,并且我们能够把任何 JavaScript 对象转换为 JSON,然后将 JSON 发送到服务器。我们也能把从服务器接收到的任何 JSON 转换为 JavaScript 对象。以这样的方式,我们能够把数据作为 JavaScript 对象来处理,无需复杂的解析和转译。

JSON后来也越来越多的用于设备与设备之间的数据交换业务。各种语言平台上都有对应的JSON解析库。

(2)存储数据

在存储数据时,数据必须是某种具体的格式,并且无论您选择在何处存储它,文本永远是合法格式之一。

JSON 让 JavaScript 对象存储为文本成为可能。

2.2 JSON 语法规则

JSON 语法衍生于 JavaScript 对象标记法语法:

  • 数据在名称/值对中
  • 数据由逗号分隔
  • 花括号容纳对象
  • 方括号容纳数组
  • 对象 :对象是一个无序的键 / 值对集合。键和值之间用冒号分隔,键 / 值对之间用逗号分隔,整个对象用大括号 {} 包裹。例如:{"name": "John", "age": 30}
  • 数组 :数组是一个有序的值集合,值之间用逗号分隔,整个数组用方括号 [] 包裹。例如:["apple", "banana", "orange"]

(1)JSON 数据- 名称和值

JSON 数据写为名称/值对。名称/值由字段名称构成,后跟冒号和值

"name":"Bill Gates"

在 JSON 中,键必须是字符串,由双引号包围

(2) JSON

  • 字符串 :字符串是双引号包裹的文本序列,可以包含 Unicode 字符和转义字符。例如:"Hello, World!"
  • 数字 :数字可以是整数或浮点数,支持科学计数法。例如:123-45.671.23e4
  • 布尔值 :布尔值可以是 truefalse
  • 空值 :空值用 null 表示。

(3)优点

  1. 可读性高 :JSON 的语法简洁明了,易于人类阅读和理解。
  2. 轻量级 :JSON 与 XML 相比,数据冗余更少,体积更小,传输效率更高。
  3. 语言无关 :JSON 支持多种编程语言,可以跨平台使用。
  4. 易于解析和生成 :JSON 可以轻松地在编程语言中解析和生成。

(4)示例

简单的 JSON 示例

{
  "name": "John",
  "age": 30,
  "hobbies": ["reading", "traveling", "gaming"]
}

嵌套的 JSON 示例

{
    "name": "John",
    "age": 30,
    "address": 
    {
        "street": "123 Main St",
        "city": "New York",
        "state": "NY"
    }
}

推荐AIMC LAB在线校验工具:https://aimc.skyate.com/aimc_static/aimc_tools/json_checker.html

2.3 ArduinoJSON库

ArduinoJSON是一个专为Arduino和嵌入式C++平台设计的轻量级JSON库,以其简单易用和高效性能著称。它支持JSON的序列化反序列化,能够在有限的内存环境中高效地解析和生成JSON数据,非常适合微控制器项目。

JSON序列化

JSON序列化就是把数据结构(如C++中的对象、数组等)转换成JSON格式的字符串。

  • 原理:按照JSON的语法规则,将数据结构中的元素逐一转换为对应的字符串形式。例如,将一个包含多个键值对的对象转换为{键1:值1,键2:值2,...}的格式,将数组转换为[元素1,元素2,...]的格式。
  • 作用:方便数据的存储和传输,因为字符串格式易于被各种存储介质(如文件、数据库)和传输协议(如HTTP)接收和处理。

JSON反序列化

JSON反序列化则是把JSON格式的字符串转换回原来的数据结构。

  • 原理:解析JSON字符串,识别出其中的对象、数组、键值对等元素,然后根据这些元素重建原来的数据结构。
  • 作用:在接收到JSON格式的数据(如从文件读取或从网络接收)后,将其转换为程序可以直接操作的数据结构,便于后续的数据处理。

暂时无法在飞书文档外展示此内容

2.4 ArduinoJSON使用

(1)创建JSON对象

在使用ArduinoJSON库时,需先创建一个 JsonDocument 对象用于存储JSON数据,它有两种类型:

  • StaticJsonDocument :在栈上分配固定大小的内存,适用于存储较小的JSON数据。需在创建时指定内存大小,例如:StaticJsonDocument<200> doc; 会在栈上分配200字节的内存空间。
  • DynamicJsonDocument :在堆上动态分配内存,适用于存储较大的JSON数据。同样需要指定初始内存大小,例如:DynamicJsonDocument doc(200);

(2)序列化

  • 添加数据到JsonDocument :通过操作 JsonDocument 对象,使用 [] 运算符或 add 方法等向其中添加数据。例如:doc["sensor"] = "gps"; 向文档中添加一个键为 “sensor”、值为 “gps” 的键值对;JsonArray data = doc.createNestedArray("data"); data.add(48.756080); data.add(2.302038); 创建一个嵌套数组并添加数据。
  • 序列化为JSON字符串 :使用 serializeJson 函数将 JsonDocument 对象序列化为紧凑的JSON字符串,或使用 serializeJsonPretty 函数序列化为格式化的、可读性更好的JSON字符串,并输出到指定的流(如 Serial)或存储到字符串变量中。

(3)反序列化

  • 准备JSON字符串 :确保要解析的JSON字符串是有效的,并且其格式和内容与预期的结构一致。
  • 反序列化JSON字符串 :调用 deserializeJson 函数,将JSON字符串解析到已创建的 JsonDocument 对象中。例如:DeserializationError error = deserializeJson(doc, json); 如果解析成功,doc 中将包含解析后的JSON数据。
  • 检查解析错误 :通过检查 deserializeJson 函数的返回值来判断解析是否成功。如果返回值不为 DeserializationError::Ok,则表示解析失败,可根据错误信息进行相应的处理。

(4)数据处理

  • 获取数据 :使用 JsonDocument 对象的 [] 运算符或成员函数来获取存储的JSON数据。例如:const char* sensor = doc["sensor"]; 获取键为 “sensor” 对应的字符串值;int value = doc["value"]; 获取键为 “value” 对应的整数值。
  • 遍历数据 :如果需要处理嵌套的JSON数据或数组,可以使用迭代器或其他遍历方法。例如,对于一个包含多个键值对的JSON对象,可以使用 for(auto kv : doc) 循环来遍历其中的每个键值对。

(5)清理与释放存储空间

在完成JSON数据的处理后,对于动态分配的 DynamicJsonDocument 对象,如果不再需要使用,可以使用 clear 方法清理其内部的存储空间,以便后续重新使用该对象来存储新的JSON数据。对于静态分配的 StaticJsonDocument 对象,其存储空间在栈上,函数结束时会自动释放。

#include <Arduino.h>
#include <ArduinoJson.h>

void setup() {
  Serial.begin(115200);

  // JSON序列化示例
  JsonDocument doc_serialize; // 创建静态JSON文档
  doc_serialize["temperature"] = 25.5;   // 添加温度数据
  doc_serialize["humidity"] = 50.0;      // 添加湿度数据
  doc_serialize["status"] = "normal";    // 添加状态数据

  String json_str;
  serializeJson(doc_serialize, json_str); // 序列化为JSON字符串
  Serial.println("序列化后的JSON字符串:");
  Serial.println(json_str);

  // JSON反序列化示例
  JsonDocument doc_deserialize; // 创建动态JSON文档
  DeserializationError error = deserializeJson(doc_deserialize, json_str); // 反序列化JSON字符串
  if (error) {
    Serial.print("JSON反序列化错误: ");
    Serial.println(error.c_str());
    return;
  }

  float temperature = doc_deserialize["temperature"]; // 获取温度数据
  float humidity = doc_deserialize["humidity"];       // 获取湿度数据
  String status = doc_deserialize["status"];          // 获取状态数据

  Serial.println("\n反序列化后的数据:");
  Serial.print("温度: ");
  Serial.println(temperature);
  Serial.print("湿度: ");
  Serial.println(humidity);
  Serial.print("状态: ");
  Serial.println(status);
}

void loop() {
}

3 系统配置程序开发

3.1 程序需求分析

暂时无法在飞书文档外展示此内容

要实现这样一个系统配置程序的开发,还要考虑系统的参数如何存储。本项目提供一个思路,以建立起一个两个层级的配置文件。

参数类A参数1键参数1值
参数2键参数2值
参数3键参数3值
参数类B参数1键参数1值
参数2键参数2值
参数3键参数3值

例如wifi的SSID和密码应该属于NetWorkConfig类,定义如下:

{
  "Network": {
    "SSID": "aust",
    "PASS": "12345678"
  }
}

另外我们还需要思考一下系统配置文件里应该包含哪些变量类型:

整型保存一些整型数据,如用户数量
浮点保存一些浮点数据,如设置电机目标转速
字符串保存诸如wifi名称,密码之类的字符串
布尔二值量,例如某些电磁阀是否打开

3.2 SysCfg模块编程思路

本模块主要实现了系统配置信息的管理功能,包括配置信息的加载、保存、清除、组与项的创建及检查、不同数据类型配置项的设置与获取等操作。通过使用Arduino平台下的SPIFFS文件系统和ArduinoJSON库,实现了配置信息的持久化存储和灵活管理,为设备的配置管理提供了便捷的方式。

3.2.1 核心功能与实现逻辑

(1)配置信息的存储与加载

存储 :在ESP32的SPIFFS文件系统中,配置信息以JSON格式保存在指定路径的文件(/system.json)中。当需要保存配置信息时,通过调用SysCfg_Save函数,先移除旧文件(若存在),然后打开文件进行写操作,将SystemConfigRoot文档中的JSON数据序列化为格式化的字符串写入文件。

加载 :调用SysCfg_Load函数时,先尝试打开配置文件,若文件不存在,则创建文件并写入默认的空JSON对象;若文件存在,则读取文件内容并反序列化到SystemConfigRoot文档中,以便程序后续访问和操作配置信息。

(2)配置组与项的管理

创建组 :通过SysCfg_CreateGroup函数,在SystemConfigRoot文档中添加一个新的JSON对象作为配置组,并设置一个_check键值对用于标识组的有效性。

检查组 :SysCfg_CheckGroup函数用于检查指定的配置组是否存在且有效,即检查组对应的JSON对象是否存在且_check值为true。

创建项 :SysCfg_CreateItem函数在指定的配置组中创建一个新的配置项。每个配置项也是一个JSON对象,包含type(数据类型)、_check(有效性标识)和value(值)三个键值对。

检查项 :SysCfg_CheckItem函数检查指定的配置项是否存在且数据类型与预期一致,确保配置项的有效性和正确性。

(3)配置项的设置与获取

设置值 :提供了针对不同数据类型(整数、字符、字符串、浮点数、布尔值)的SysCfg_SetItem系列函数,用于向指定的配置项设置值。在设置之前,会先检查配置组和项是否存在,若不存在则自动创建。设置值时,直接修改SystemConfigRoot文档中对应配置项的value值,并调用SysCfg_UpdateInter函数标记配置信息已更新(需后续手动调用SysCfg_Save保存)。

获取值 :对应的SysCfg_GetItem系列函数用于从配置项中获取值。根据不同的数据类型,将SystemConfigRoot文档中对应配置项的value值转换为相应的类型后返回给调用者。在获取值之前,也会先检查配置组和项的有效性。

(4)配置信息的清除

SysCfg_ClearAll函数用于清除所有配置信息。它先清除SystemConfigRoot文档中的内容,然后移除SPIFFS上的配置文件,并创建一个空的配置文件以确保后续操作的一致性。

3.2.2 接口函数功能

(1)接口函数列表

  • 配置文件操作接口
  • void SysCfg_Load(void);
  • int SysCfg_Save(void);
  • int SysCfg_ClearAll();
  • 配置值设置接口
  • int SysCfg_SetItemInt(const char *group, const char *item, int data);
  • int SysCfg_SetItemChar(const char *group, const char *item, void *data);
  • int SysCfg_SetItemString(const char *group, const char *item, String data);
  • int SysCfg_SetItemFloat(const char *group, const char *item, float data);
  • int SysCfg_SetItemBool(const char *group, const char *item, bool data);
  • 配置项创建接口
  • int SysCfg_CreateGroup(const char *group);
  • int SysCfg_CreateItem(const char *group, const char *item, int type);
  • int SysCfg_CheckGroup(const char *group);
  • int SysCfg_CheckItem(const char *group, const char *item, int type);
  • 配置值获取接口
  • int SysCfg_GetItemInt(const char *group, const char *item, void *data);
  • int SysCfg_GetItemChar(const char *group, const char *item, void *data, int len);
  • String SysCfg_GetItemString(const char *group, const char *item, void *err);
  • int SysCfg_GetItemFloat(const char *group, const char *item, void *data);
  • int SysCfg_GetItemBool(const char *group, const char *item, void *data);

(2)以下是对所有接口函数的功能、形参、返回值的解释说明:

SysCfg_Load
  • 功能 :加载系统配置文件。
  • 形参 :无。
  • 返回值 :无。
SysCfg_Save
  • 功能 :保存系统配置到文件。
  • 形参 :无。
  • 返回值 :保存成功返回 1,失败返回 0。
SysCfg_ClearAll
  • 功能 :清除所有系统配置,并创建一个空的配置文件。
  • 形参 :无。
  • 返回值 :清除和创建成功返回 true,失败返回 false。
SysCfg_CreateGroup
  • 功能 :创建一个新的配置组。
  • 形参 :const char *group,要创建的组的名称。
  • 返回值 :创建成功返回 0。
SysCfg_CheckGroup
  • 功能 :检查指定的配置组是否存在且有效。
  • 形参 :const char *group,要检查的组的名称。
  • 返回值 :存在且有效返回 true,否则返回 false。
SysCfg_CreateItem
  • 功能 :在指定组中创建一个新的配置项。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;int type,配置项的类型。
  • 返回值 :创建成功返回 true,失败返回 false。
SysCfg_CheckItem
  • 功能 :检查指定的配置项是否存在且类型匹配。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;int type,期望的配置项类型。
  • 返回值 :存在且类型匹配返回 true,否则返回 false。
SysCfg_SetItemInt
  • 功能 :设置指定配置项的整数值。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;int data,要设置的整数值。
  • 返回值 :设置成功返回 true,失败返回 false。
SysCfg_SetItemChar
  • 功能 :设置指定配置项的字符值。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;void *data,要设置的字符值的指针。
  • 返回值 :设置成功返回 true,失败返回 false。
SysCfg_SetItemString
  • 功能 :设置指定配置项的字符串值。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;String data,要设置的字符串值。
  • 返回值 :设置成功返回 true,失败返回 false。
SysCfg_SetItemFloat
  • 功能 :设置指定配置项的浮点数值。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;float data,要设置的浮点数值。
  • 返回值 :设置成功返回 true,失败返回 false。
SysCfg_SetItemBool
  • 功能 :设置指定配置项的布尔值。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;bool data,要设置的布尔值。
  • 返回值 :设置成功返回 true,失败返回 false。
SysCfg_GetItemInt
  • 功能 :获取指定配置项的整数值。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;void *data,用于存储获取到的整数值的指针。
  • 返回值 :获取成功返回 true,失败返回 false。
SysCfg_GetItemChar
  • 功能 :获取指定配置项的字符值。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;void *data,用于存储获取到的字符值的指针;int len,数据缓冲区的长度。
  • 返回值 :获取成功且缓冲区长度足够返回获取到的字符数,缓冲区长度不足或获取失败返回 false。
SysCfg_GetItemString
  • 功能 :获取指定配置项的字符串值。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;void *err,用于存储错误代码的指针。
  • 返回值 :获取成功返回对应的字符串值,失败返回空字符串,并通过 err 参数返回错误代码。
SysCfg_GetItemFloat
  • 功能 :获取指定配置项的浮点数值。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;void *data,用于存储获取到的浮点数值的指针。
  • 返回值 :获取成功返回 true,失败返回 false。
SysCfg_GetItemBool
  • 功能 :获取指定配置项的布尔值。
  • 形参 :const char *group,配置组的名称;const char *item,配置项的名称;void *data,用于存储获取到的布尔值的指针。
  • 返回值 :获取成功返回 true,失败返回 false。
#ifndef __SYSTEM_CONFIG_H__
#define __SYSTEM_CONFIG_H__

#include <Arduino.h>
#include <SPIFFS.h>

typedef struct _System_status_list{
    int wifi;// 0:AP模式,1:STA模式,未连接,2:STA模式,已连接
}Syss_list;

#define DeviceName  "AIMC-SC"
// Const Params
#define SYSCFG_SavePath "/system.json"

#define SYSCFG_GroupWlanSetting        "WlanConfig"
#define SYSCFG_GroupWlanSetting_SSID   "WlanSSID"
#define SYSCFG_GroupWlanSetting_PASS   "WlanPass"
#define SYSCFG_GroupWlanSetting_HOST   "WlanHost"

#define SYSCFG_GroupSystemSetting          "System"
#define SYSCFG_GroupSystemSetting_Device   "Name"

#define SYSCFG_GroupFirmware           "Firmware"
#define SYSCFG_GroupFirmware_version1  "version1"
#define SYSCFG_GroupFirmware_version2  "version2"
#define SYSCFG_GroupFirmware_version3  "version3"

extern uint8_t firmware_v1;
extern uint8_t firmware_v2;
extern uint8_t firmware_v3;

#define SYSCFG_ConfigTypeNone      0
#define SYSCFG_ConfigTypeInt       1
#define SYSCFG_ConfigTypeString    2
#define SYSCFG_ConfigTypeFloat     3
#define SYSCFG_ConfigTypeBool      4

// API List

void SysCfg_Load(void);
int SysCfg_Save(void);
int SysCfg_ClearAll();

int SysCfg_SetItemInt(const char *group, const char *item, int data);
int SysCfg_SetItemChar(const char *group, const char *item, void *data);
int SysCfg_SetItemString(const char *group, const char *item, String data);
int SysCfg_SetItemFloat(const char *group, const char *item, float data);
int SysCfg_SetItemBool(const char *group, const char *item, bool data);

int SysCfg_CreateGroup(const char *group);
int SysCfg_CreateItem(const char *group, const char *item, int type);
int SysCfg_CheckGroup(const char *group);
int SysCfg_CheckItem(const char *group, const char *item, int type);

int SysCfg_GetItemInt(const char *group, const char *item, void *data);
int SysCfg_GetItemChar(const char *group, const char *item, void *data, int len);
String SysCfg_GetItemString(const char *group, const char *item, void *err);
int SysCfg_GetItemFloat(const char *group, const char *item, void *data);
int SysCfg_GetItemBool(const char *group, const char *item, void *data);

#endif
#include <Arduino.h>
#include <SPIFFS.h>
#include <ArduinoJson.h>
#include "SysCfg_api.h"

struct SpiRamAllocator : ArduinoJson::Allocator {
    void* allocate(size_t size) override {
        return heap_caps_malloc(size, MALLOC_CAP_SPIRAM);
    }

    void deallocate(void* pointer) override {
        heap_caps_free(pointer);
    }

    void* reallocate(void* ptr, size_t new_size) override {
        return heap_caps_realloc(ptr, new_size, MALLOC_CAP_SPIRAM);
    }
};
SpiRamAllocator allocator;
JsonDocument SystemConfigRoot(&allocator);
void SysCfg_Load(void)
{
    // 加载设备信息
    File file = SPIFFS.open(SYSCFG_SavePath, FILE_READ);
    if (!file)
    {    
        file.close();
        // logt_warn(LogTopic_FS,"Load System Settings Failed.");
        // System_Printf("Load System Settings Failed.\r\n");
        //创建系统配置文件
        file = SPIFFS.open(SYSCFG_SavePath, FILE_WRITE);
        if (!file)
        {
        //   logt_debug(LogTopic_FS,"SPIFFS创建文件失败");
        }
        String json_str;
        serializeJson(SystemConfigRoot, json_str); // 序列化为JSON字符串
        Serial.println("序列化后的JSON字符串:");
        Serial.println(json_str);
        serializeJson(SystemConfigRoot,file);
        file.close();
    }
    else
    {
        String data2 = file.readString();
        file.close();
        deserializeJson(SystemConfigRoot, data2);
        // serializeJsonPretty(SystemConfigRoot,Serial);
        // logt_info(LogTopic_FS,"系统配置文件已加载");
        // System_Printf("\r\n");
    }
}

int SysCfg_UpdateInter(void)
{  
    //取消每次更新都保存的操作,需独立执行保存
    // SPIFFS.remove(SYSCFG_SavePath);
    // File file = SPIFFS.open(SYSCFG_SavePath, FILE_WRITE);
    // if (!file)
    // {
    //   return 0;
    // }
    // serializeJsonPretty(SystemConfigRoot,file);
    // file.close();
    return 1;
}

int SysCfg_Save(void)
{  
    SPIFFS.remove(SYSCFG_SavePath);
    File file = SPIFFS.open(SYSCFG_SavePath, FILE_WRITE);
    if (!file)
    {
        return 0;
    }
    String json_str;
    serializeJson(SystemConfigRoot, json_str); // 序列化为JSON字符串
    Serial.println("序列化后的JSON字符串:");
    Serial.println(json_str);
    serializeJsonPretty(SystemConfigRoot,file);
    file.close();
    return 1;
}

int SysCfg_ClearAll()
{
    SystemConfigRoot.clear();
    SPIFFS.remove(SYSCFG_SavePath);
    File file = SPIFFS.open(SYSCFG_SavePath, FILE_WRITE);
    if (!file)
    {
        return 0;
    }
    file.print("{}");
    file.close();
    return true;
}

int SysCfg_CreateGroup(const char *group)
{
  SystemConfigRoot[group]["_check"] = true;
  return 0;
}

int SysCfg_CheckGroup(const char *group)
{
    // serializeJsonPretty(SystemConfigRoot,Serial);
    if (SystemConfigRoot[group].is<JsonObject>())
    {
        if(SystemConfigRoot[group]["_check"] == true)
        return true;
        else
        return false;
    }
    else
        return false;    
}

int SysCfg_CreateItem(const char *group, const char *item, int type)
{
    if (!SysCfg_CheckGroup(group))
    {
        //组不存在,创建组
        SysCfg_CreateGroup(group);
    }
    SystemConfigRoot[group][item]["type"] = type;
    SystemConfigRoot[group][item]["_check"] = true;
    return true;  
}

int SysCfg_CheckItem(const char *group, const char *item, int type)
{
    if (SysCfg_CheckGroup(group))
    {
        if (SystemConfigRoot[group][item].is<JsonObject>())
        {
        if(SystemConfigRoot[group][item]["_check"])
        {
            if (type!=SYSCFG_ConfigTypeNone)
            {
            if (SystemConfigRoot[group][item]["type"] == type)
                return true;
            else
                return false;        
            }      
            return true;
        }
        else
            return false;
        }
        else
        return false;    
    }
    else
        return false;
}
int SysCfg_SetItemInt(const char *group, const char *item, int data)
{
    if (!SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeNone))
    {
        //组或元素不存在,创建元素
        SysCfg_CreateItem(group,item,SYSCFG_ConfigTypeInt);
    }
    if (SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeInt))
    {
        SystemConfigRoot[group][item]["value"] = (int)data;
        SysCfg_UpdateInter();
        return true; 
    }
    else
        return false;
}

int SysCfg_SetItemChar(const char *group, const char *item, void *data)
{
    if (!SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeNone))
    {
        //组或元素不存在,创建元素
        SysCfg_CreateItem(group,item,SYSCFG_ConfigTypeString);
    }
    if (SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeString))
    {
        SystemConfigRoot[group][item]["value"] = (const char *)data;
        SysCfg_UpdateInter();
        return true; 
    }
    else
        return false;
}

int SysCfg_SetItemString(const char *group, const char *item, String data)
{
    if (!SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeNone))
    {
        //组或元素不存在,创建元素
        SysCfg_CreateItem(group,item,SYSCFG_ConfigTypeString);
    }
    if (SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeString))
    {
        SystemConfigRoot[group][item]["value"] = data;
        SysCfg_UpdateInter();
        return true; 
    }
    else
        return false;
}

int SysCfg_SetItemFloat(const char *group, const char *item, float data)
{
    if (!SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeNone))
    {
        //组或元素不存在,创建元素
        SysCfg_CreateItem(group,item,SYSCFG_ConfigTypeFloat);
    }
    if (SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeFloat))
    {
        SystemConfigRoot[group][item]["value"] = data;
        SysCfg_UpdateInter();
        return true; 
    }
    else
        return false;
}
int SysCfg_SetItemBool(const char *group, const char *item, bool data)
{
    if (!SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeNone))
    {
        //组或元素不存在,创建元素
        SysCfg_CreateItem(group,item,SYSCFG_ConfigTypeBool);
    }
    if (SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeBool))
    {
        SystemConfigRoot[group][item]["value"] = data;
        
        SysCfg_UpdateInter();
        return true; 
    }
    else
        return false;
}
int SysCfg_GetItemInt(const char *group, const char *item, void *data)
{
    if (SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeInt))
    {
        *(int *)data = SystemConfigRoot[group][item]["value"];
        return true;
    }
    else
        return false;
}
int SysCfg_GetItemChar(const char *group, const char *item, void *data, int len)
{
    if (SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeString))
    {
        if(len > strlen(SystemConfigRoot[group][item]["value"]))
        {
        strcpy((char *)data,SystemConfigRoot[group][item]["value"]);
        return strlen(SystemConfigRoot[group][item]["value"]);
        }
        else
        return false;
    }
    else
        return false;
}
String SysCfg_GetItemString(const char *group, const char *item, void *err)
{
    if (SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeString))
    {
        String data = SystemConfigRoot[group][item]["value"];
        *(int *)err = 0;
        return data;
    }
    else
        *(int *)err = -1;
        return String("");
}
int SysCfg_GetItemFloat(const char *group, const char *item, void *data)
{
    if (SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeFloat))
    {
        *(float *)data = SystemConfigRoot[group][item]["value"];
        return true;
    }
    else
        return false;
}
int SysCfg_GetItemBool(const char *group, const char *item, void *data)
{
    if (SysCfg_CheckItem(group,item,SYSCFG_ConfigTypeBool))
    {
        *(bool *)data = SystemConfigRoot[group][item]["value"];
        return true;
    }
    else
        return false;
}
#include <Arduino.h>
#include <ArduinoJson.h>
#include <SPIFFS.h>
#include <SysCfg_api.h>

void setup() {
    u32_t err;
    Serial.begin(115200);
    sys_delay_ms(4000);
    if (!SPIFFS.begin())
    {
        // 初始化失败时处理
      Serial.println("SPIFFS-An error occurred while mounting SPIFFS");
      // 格式化SPIFFS分区
      if (SPIFFS.format())
      {
        // 格式化成功
        Serial.println("SPIFFS partition formatted successfully");
        // 重启
        ESP.restart();
      }
      else
      {
        Serial.println("SPIFFS partition format failed");
      }
      return;
    }
    SysCfg_Load();
    SysCfg_SetItemString(SYSCFG_GroupFirmware, "TEST", "TEST STRING");
    SysCfg_Save();
    Serial.printf("Get Set Value: %s\r\n",SysCfg_GetItemString(SYSCFG_GroupFirmware, "TEST", &err));
    
}

void loop() {

}
Avatar photo

作者 skyate

发表回复