在上一篇,我们介绍了TF卡的挂载工作。ESP32设备已经能够访问TF卡中的配置文件:
/sdcard/ex_mcp.cfg
可以看到文件名称非常短小,使用的是早年间DOS系统下的 8.3 文件名称规范。这是由于ESP IDF架构为了减少对内存资源的消耗,默认是不开启长文件名支持的。

对于实际项目来说,其实更重要的问题是:
如何让设备在不修改固件、不重新编译、不重新烧录的情况下获得新的能力?
本文将介绍我们为小智AI设计的一套“配置驱动扩展架构”,实现通过TF卡配置文件动态扩展设备能力。

一、总体架构设计

整个运行时扩展系统的结构如下:

TF Card
   │
   └── ex_mcp.cfg
            │
            ▼
   SdExtensionManager
            │
            ├── ParseExternalInitializers()
            │
            └── ParseMcpTools()

工作流程如下:

设备启动
    │
    ▼
挂载TF卡
    │
    ▼
读取配置文件
    │
    ▼
解析JSON
    │
    ▼
GPIO初始化
    │
    ▼
MCP Tool注册

整个过程由 SdExtensionManager::LoadDynamicConfiguration() 负责完成。该函数的职责比较明晰,只做三件事:

1. 读取配置文件

2. 解析JSON结构

3. 分发给不同解析模块

二、配置文件结构设计

本项目采用JSON作为配置格式。示例配置文件如下:

{
    "board_name": "lonrock-esp32s3-audio",
    "l_code": "982002702",

    "gpio_initializers": [
        {
            "pin": 8,
            "mode": "output",
            "level": 1
        }
    ],

    "mcp_tools": [
        {
            "name": "sd.fan_switch",
            "description": "控制散热风扇",
            "pin": 7
        }
    ]
}

整体结构分为三部分:

合法性校验
gpio_initializers
mcp_tools


合法性校验

board_name 表明了当前设备的类型,l_code是我司为设备确定的唯一序列号。这些字段用来确认TF卡上的配置文件确实是为对应的设备所准备。
如果TF卡是为同一类型的设备准备,那么代码里就可以不校验l_code。
合法性校验还可以用其他方式,例如校验设备的MAC地址。

gpio_initializers

用于设备启动后的GPIO初始化。例如:

{
    "pin": 8,
    "mode": "output",
    "level": 1
}

对应:GPIO8,设置为输出模式,上电后置高电平

mcp_tools

用于动态注册MCP工具。例如:

{
    "name":"sd.fan_switch",
    "description":"控制散热风扇",
    "pin":7
}

启动后系统会自动生成对应的MCP 工具。
该工具的名称是sd.fan_switch,通过描述告诉大模型这个工具用来控制散热风扇的起停,我们在程序中会使用GPIO7来控制风扇起停。
 

三、为什么选择JSON

1. 可读性好,即使没有编程经验的用户也能快速理解:

{
    "pin": 8,
    "mode": "output"
}

2. 支持嵌套结构,例如:
 

{
    "gpio_initializers":[...],
    "mcp_tools":[...]
}

3. 小智代码已经有解析实例

小智代码中已经有非常成熟的使用cJSON解析JSON数据的功能模块,在解析服务器数据时尤其稳定,因此使用cJSON库来解析TF卡上的JSON文件非常方便。

四、JSON解析中的注意事项

虽然JSON解析本身并不复杂,但有几个细节必须注意。

检查节点是否存在

不要假设配置一定正确。必须对节点进行检查,否则可能导致运行异常。
代码示例:

cJSON* pin = cJSON_GetObjectItem(item, "pin");

if(pin == nullptr) {
    return;
}

检查数据类型

下面两种写法并不相同:

{
    "pin":5
}

{
    "pin":"5"
}

前者是数字。后者是字符串。因此解析前必须检查:

cJSON_IsNumber(pin)

避免错误配置导致系统异常。

释放JSON资源

这是C++/C的初学者最容易忽略的问题。如果不释放资源,系统内存被无谓占用,导致其他功能可以分配的内存减少,在ESP32这种资源有限的单片机上面,是非常大的损失。

释放资源只需要一条语句,把整个JSON的根节点释放即可:

cJSON* root =   cJSON_Parse(buffer);  // 获取根节点
if(root == nullptr) {
   return;
}
// 处理JSON内容

cJSON_Delete(root);  // 释放根节点

五、完整函数代码

// 头部需要引入cJSON库的头文件
#include "cJSON.h"

// JSON字段常量以及必要的常量
namespace
{
// String constants centralized here to ensure they reside in .rodata (flash)
// and to avoid per-translation-unit pointer objects which would consume RAM.
static const char MOUNT_POINT[] = "/sdcard";
static const char CONFIG_PATH[] = "/sdcard/ex_mcp.cfg";

static const char kJsonBoardName[] = "board_name";
static const char kJsonLCode[] = "l_code";

static const char kJsonGpioInitializers[] = "gpio_initializers";
static const char kJsonPin[] = "pin";
static const char kJsonMode[] = "mode";
static const char kJsonLevel[] = "level";
static const char kJsonPull[] = "pull";
static const char kJsonPullUp[] = "up";
static const char kJsonPullDown[] = "down";
static const char kGpioModeInput[] = "input";
static const char kGpioModeOutput[] = "output";

static const char kJsonMcpTools[] = "mcp_tools";
static const char kJsonName[] = "name";
static const char kJsonDescription[] = "description";
static const char kMcpToolPrefix[] = "sd.";
static const char kParamAction[] = "action";

static const int8_t kToolPrefixLength = 3; // "sd." 长度
static const int8_t kMaxMcpToolNameLength = 32;
static const int8_t kMaxMcpToolQuantity = 10; // 限制 MCP Tool 数量,防止滥用
static const int16_t kMaxFileSize = 8 * 1024; // 限制配置文件大小,防止内存耗尽
}

void SdExtensionManager::LoadDynamicConfiguration()
{
    if (!is_sdcard_found_)
        return;

    FILE *f = fopen(CONFIG_PATH, "r");
    if (f == NULL) {
        ESP_LOGW(TAG, "Configuration file %s not found. Skipping dynamic setup.", CONFIG_PATH);
        return;
    }

    fseek(f, 0, SEEK_END);
    long fsize = ftell(f);
    if (fsize < 0 || fsize > kMaxFileSize) {
        ESP_LOGE(TAG, "Configuration file size %ld is invalid or exceeds limit (%d bytes).", fsize, kMaxFileSize);
        fclose(f);
        return;
    }
    fseek(f, 0, SEEK_SET);

    char *json_buf = (char *)malloc(fsize + 1);
    fread(json_buf, 1, fsize, f);
    json_buf[fsize] = '\0';
    fclose(f);

    cJSON *root = cJSON_Parse(json_buf);
    free(json_buf);

    if (root == NULL) {
        ESP_LOGE(TAG, "JSON parse error before: [%s]", cJSON_GetErrorPtr());
        return;
    }

    // 安全校验:设备名称与 LCID 必须匹配
    cJSON *board_name_obj = cJSON_GetObjectItem(root, kJsonBoardName);
    cJSON *lcode_obj = cJSON_GetObjectItem(root, kJsonLCode);

    if (!board_name_obj || !lcode_obj ||
        GetBoardName() != board_name_obj->valuestring ||
        GetLCode() != lcode_obj->valuestring) {
        ESP_LOGE(TAG,
                 "Device validation failed! Target: BoardName=%s, LCODE=%s. File rejected.",
                 board_name_obj ? board_name_obj->valuestring : "Unknown",
                 lcode_obj ? lcode_obj->valuestring : "Unknown");
        cJSON_Delete(root);
        return;
    }
    ESP_LOGI(TAG, "Device validation passed. Processing rules...");

    ParseExternalInitializers(cJSON_GetObjectItem(root, kJsonGpioInitializers));
    ParseMcpTools(cJSON_GetObjectItem(root, kJsonMcpTools));

    cJSON_Delete(root);
}

下篇介绍

下一篇我们将详细介绍如何通过配置文件动态初始化GPIO,以及MCP工具的作用和注册。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐