让小智AI支持运行时扩展(三):具体功能实现
由于配置文件是我们自己编写,所以其中的内容完全可以由我们自己定义和设计。本篇演示我们定义的GPIO动态初始化与MCP工具注册。
在上一篇,我们介绍了读取配置的函数 LoadDynamicConfiguration(),其中调用的函数ParseExternalInitializers() 用来读取并初始化GPIO状态,ParseMcpTools() 注册相应的Mcp工具。
动态 GPIO 初始化
在配置文件中,GPIO初始化部分示例如下:
{
"gpio_initializers": [
{
"pin": 5,
"mode": "output",
"level": 1
}
]
}
ESP-IDF 官方推荐的 GPIO 初始化方式,是使用配置结构体
gpio_config_t,常用字段包括:
pin_bit_mask 用于指定哪些GPIO可以工作;
mode 相应的GPIO是输入(GPIO_MODE_INPUT) 还是输出(GPIO_MODE_OUTPUT);
还有两个和硬件相关的参数:
pull_up_en 是否内部上拉,即默认该引脚是否为1(高电平);
pull_down_en 是否内部下拉,即默认该引脚是否为0(低电平);
这两个参数不能同时为 ENABLE。
具体函数代码如下:
void SdExtensionManager::ParseExternalInitializers(cJSON *initializers)
{
// gpio_initializers: array of objects with keys:
// "pin" (int), "mode" ("input"|"output"), optional "pull" ("up"|"down"|"none"),
// optional "level" (0|1) for output pins.
if (!cJSON_IsArray(initializers))
return;
int size = cJSON_GetArraySize(initializers);
for (int i = 0; i < size; i++) {
cJSON *item = cJSON_GetArrayItem(initializers, i);
cJSON *pin_obj = cJSON_GetObjectItem(item, kJsonPin);
cJSON *mode_obj = cJSON_GetObjectItem(item, kJsonMode);
if (!pin_obj || !mode_obj) {
ESP_LOGW(TAG, "gpio_initializers[%d]: missing required fields, skipped.", i);
continue;
}
int pin = pin_obj->valueint;
if (!IsGpioSafe(pin))
continue;
std::string mode = mode_obj->valuestring;
gpio_config_t io_conf = {};
io_conf.pin_bit_mask = (1ULL << pin);
if (mode == kGpioModeOutput) {
io_conf.mode = GPIO_MODE_OUTPUT;
gpio_config(&io_conf);
cJSON *level_obj = cJSON_GetObjectItem(item, kJsonLevel);
int level = level_obj ? level_obj->valueint : 0;
gpio_set_level((gpio_num_t)pin, level);
ESP_LOGI(TAG, "Initialized safe GPIO %d as OUTPUT, level=%d", pin, level);
}
else if (mode == kGpioModeInput) {
io_conf.mode = GPIO_MODE_INPUT;
cJSON *pull_obj = cJSON_GetObjectItem(item, kJsonPull);
std::string pull = pull_obj ? pull_obj->valuestring : "none";
if (pull == kJsonPullUp)
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
else if (pull == kJsonPullDown)
io_conf.pull_down_en = GPIO_PULLDOWN_ENABLE;
gpio_config(&io_conf);
ESP_LOGI(TAG, "Initialized safe GPIO %d as INPUT, pull=%s", pin, pull.c_str());
}
else {
ESP_LOGW(TAG, "GPIO %d: unknown mode '%s', skipped.", pin, mode.c_str());
}
}
}
动态MCP 工具注册
在配置文件中,注册MCP工具的示例如下:
{
"mcp_tools": [
{
"name": "sd.relay_power.control",
"description": "控制主电源继电器,1为闭合(通电),0为断开(断电)",
"pin": 5
}]
}
小知识:什么是 MCP Tool?
答:就是让大模型来执行的工具。
在小智项目里,注册MCP工具非常简单。使用的语句为:
McpServer::GetInstance().AddTool(...)
该函数声明如下:
void AddTool(
const std::string& name,
const std::string& description,
const PropertyList& properties,
std::function<ReturnValue(const PropertyList&)> callback);
其中每个参数都需要深刻理解。
name:工具名称,是注册工具的唯一索引,在同一个设备上不能重复;
description:工具描述,是让大模型理解工具的提示词;
properties:参数定义,在实际执行时,大模型会根据这个设定下发相应的函数参数;
callback:这是真正的执行逻辑,其中的内容不需要让大模型知道,是由设备接收到指令后执行的函数。在这里,小智使用了现代C++非常灵活的 using 和 Lambda 捕捉。
通过 using ReturnValue = std::variant<bool, int, std::string, cJSON*, ImageContent*>; 让函数可以返回多种类型的变量;通过lambda捕捉可以在执行函数内部获取当前的某些特性。
具体代码:
void SdExtensionManager::ParseMcpTools(cJSON *tools)
{
// mcp_tools: array of objects with keys:
// "name" (string, must start with "sd." and be printable ASCII, <=32 chars),
// "description" (string), "pin" (int)
if (!cJSON_IsArray(tools))
return;
int size = cJSON_GetArraySize(tools);
size = std::min(size, (int)kMaxMcpToolQuantity); // 强制限制工具数量,防止滥用
for (int i = 0; i < size; i++) {
cJSON *item = cJSON_GetArrayItem(tools, i);
cJSON *name_obj = cJSON_GetObjectItem(item, kJsonName);
cJSON *desc_obj = cJSON_GetObjectItem(item, kJsonDescription);
cJSON *pin_obj = cJSON_GetObjectItem(item, kJsonPin);
if (!name_obj || !desc_obj || !pin_obj) {
ESP_LOGW(TAG, "mcp_tools[%d]: missing required fields (name/description/pin), skipped.", i);
continue;
}
const char *raw_name = name_obj->valuestring;
const char *raw_desc = desc_obj->valuestring;
int pin = pin_obj->valueint;
// 严格校验 tool_name:必须以 "sd." 开头,且只含可读 ASCII,且长度不超过 32
if (strncmp(raw_name, kMcpToolPrefix, kToolPrefixLength) != 0) {
ESP_LOGW(TAG, "mcp_tools[%d]: name '%s' must start with 'sd.', rejected.", i, raw_name);
continue;
}
if (strlen(raw_name) > kMaxMcpToolNameLength) {
ESP_LOGW(TAG, "mcp_tools[%d]: name too long, rejected.", i);
continue;
}
if (!IsCleanAscii(raw_name)) {
ESP_LOGW(TAG, "mcp_tools[%d]: name contains non-printable ASCII, rejected.", i);
continue;
}
if (!IsGpioSafe(pin))
continue;
gpio_config_t io_conf = {};
io_conf.pin_bit_mask = (1ULL << pin);
io_conf.mode = GPIO_MODE_OUTPUT;
gpio_config(&io_conf);
// ------------------------------------------------------------------
// McpTool 内部用 const char* 裸指针保存 name 和 description,
// 这里通过 PersistToPsram 将动态字符串存入外部内存,确保指针在程序
// 运行期间始终有效。
// ------------------------------------------------------------------
std::string persistent_name = std::string(raw_name);
std::string persistent_desc = std::string(raw_desc);
// 构造符合 McpServer 要求的 PropertyList(对应原注释里的 params_schema)
// 工具接受一个必填整数参数 "action":1 = 高电平(开启),0 = 低电平(关闭)
PropertyList properties({Property(kParamAction, kPropertyTypeInteger, /*min=*/0, /*max=*/1)});
// 注册到 McpServer,callback 捕获 pin 值,运行时精准控制对应引脚
McpServer::GetInstance().AddTool(
persistent_name,
persistent_desc,
properties,
[pin](const PropertyList &args) -> ReturnValue {
int action = 0;
try {
action = args[kParamAction].value<int>();
}
catch (const std::exception &e) {
ESP_LOGE("SdMcpAction", "Failed to read 'action' parameter: %s", e.what());
return std::string("error: ") + e.what();
}
gpio_set_level((gpio_num_t)pin, action);
ESP_LOGI("SdMcpAction", "MCP Tool executed: set GPIO %d to %d", pin, action);
return true;
});
ESP_LOGI(TAG, "Successfully registered SD dynamic MCP tool: %s for GPIO %d",
persistent_name, pin);
}
}
总结
本篇我们完成了整个运行时扩展体系中最重要的一环:
• 利用 gpio_config_t 实现 GPIO 动态初始化;
• 利用 GPIO 白名单机制 建立安全边界;
• 利用 McpServer::AddTool() 动态注册 MCP 工具;
• 利用 Lambda 捕获 将工具与具体 GPIO 绑定;
• 让大模型能够通过 MCP 直接控制外部硬件。
而这一切,并不需要重新编译固件,只需要更换一张 TF 卡中的配置文件。这对我们快速验证某些外设的功能提供了非常便利的方法。
对于开发某些外围设备的朋友,尤其是做毕业设计的同学来说,这是一个值得尝试的思路。
更多推荐


所有评论(0)