news 2026/6/8 6:54:37

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
让小智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工具的作用和注册。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/8 6:52:34

告别乱码!用PCtoLCD和Img2lcd搞定ESP32 OLED汉字与图片显示(附完整代码)

ESP32 OLED显示优化实战&#xff1a;从汉字取模到图片渲染的全流程解析在物联网设备开发中&#xff0c;OLED显示屏因其低功耗、高对比度和快速响应特性&#xff0c;成为许多嵌入式项目的首选显示方案。然而&#xff0c;当开发者尝试在12864像素的OLED屏幕上显示中文或自定义图像…

作者头像 李华
网站建设 2026/6/8 6:52:09

网络测试工具箱v8.5----最牛逼的网络工具箱

网络测试工具箱v8.5&#xff1a;电脑网络问题一站式解决神器_windows电脑使用 核心功能 网络诊断&#xff1a;快速检测网络连通性&#xff0c;精准定位故障点。 速度测试&#xff1a;准确测试网速&#xff0c;支持多节点对比&#xff0c;告别网络卡顿。 数据包 抓包&#xff1a…

作者头像 李华
网站建设 2026/6/8 6:52:05

别只当退货做!用MIGO的122移动类型,为你的供应商质量评估攒数据

解锁MIGO 122移动类型&#xff1a;从退货操作到供应商质量管理的数字化转型在传统供应链管理中&#xff0c;采购退货往往被视为流程终点——问题商品退回&#xff0c;流程结束。但当我们戴上数据思维的眼镜重新审视SAP中的每一次MIGO操作&#xff0c;会发现移动类型122&#xf…

作者头像 李华