让小智AI支持运行时扩展(二):配置驱动架构设计
在上一篇,我们介绍了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工具的作用和注册。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)