由于配置文件是我们自己编写,所以其中的内容完全可以由我们自己定义和设计。本篇演示我们定义的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 卡中的配置文件。这对我们快速验证某些外设的功能提供了非常便利的方法。
对于开发某些外围设备的朋友,尤其是做毕业设计的同学来说,这是一个值得尝试的思路。
 

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐