让小智AI支持运行时扩展(二):配置驱动架构设计

让小智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_initializersmcp_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来控制风扇起停。三、为什么选择JSON1. 可读性好即使没有编程经验的用户也能快速理解{ 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工具的作用和注册。