你可以先对照例程加强理解:👉应用例程
1.前提条件
请确保已经具备了以下条件
- 已经参考 移植到MCU 章节完成了移植
- 移植 emMCP 后的 MCU 工程已经编译成功
- 唤醒模组能够正常接收到数据
2. 创建 emMCP 事件回调函数
▫️事件列表
emMCP 的事件以枚举的形式定义在 emMCP.h 文件中,有以下几种事件:
| 枚举序号 | 事件 | 意义 |
|---|---|---|
| 0 | emMCP_EVENT_NONE | 无事件 |
| 1 | emMCP_EVENT_CMD_OK | 命令执行成功 |
| 2 | emMCP_EVENT_CMD_ERROR | 命令执行失败 |
| 3 | emMCP_EVENT_AI_START | 模组启动 |
| 4 | emMCP_EVENT_AI_NETCFG | 模组网络配置 |
| 5 | emMCP_EVENT_AI_NETERR | 模组网络错误 |
| 6 | emMCP_EVENT_AI_WIFI_CONNNECT | 模组连接上 Wi-Fi |
| 7 | emMCP_EVENT_AI_WIFI_DISCONNECT | 模组断开 Wi-Fi |
| 8 | emMCP_EVENT_AI_WAKE | 模组唤醒 |
| 9 | emMCP_EVENT_AI_SLEEP | 模组休眠 |
| 10 | emMCP_EVENT_AI_OTAUPDATE | 模组开始 OTA |
| 11 | emMCP_EVENT_AI_OTAOK | 模组 OTA 成功 |
| 12 | emMCP_EVENT_AI_OTAERR | 模组 OTA 失败 |
| 13 | emMCP_EVENT_AI_MCP_CMD | 模组收到 MCP 命令 |
| 14 | emMCP_EVENT_AI_MCP_Text | 模组收到字幕信息 |
▫️事件回调函数
emMCP 事件回调函数采用弱定义的方式定义在 emMCP.c 文件中,支持重新定义该函数,实现自定义的事件处理逻辑。原型如下:
__emMCPWeak void emMCP_EventCallback(emMCP_event_t event, mcp_server_tool_type_t type, void *param)
{
char *param_str = (char *)param;
emMCP_log_debug("emMCP_EventCallback: event:%d,type:%d,param:%s", event, type, param_str);
}参数说明
event:事件类型,参考事件列表type:参数的类型,参考mcp_server_tool_type_t枚举,目前只有字符串类型param:事件参数,通常固定为字符串类型
只需要在工程当中重新定义该函数即可,例如:
示例:
void emMCP_EventCallback(emMCP_event_t event, mcp_server_tool_type_t type, void *param)
{
switch (event) {
case emMCP_EVENT_CMD_OK: {
log_info("emMCP_EVENT_CMD_OK");
} break;
case emMCP_EVENT_CMD_ERROR: {
log_error("emMCP_EVENT_CMD_ERROR");
} break;
case emMCP_EVENT_AI_START: {
log_info("emMCP_EVENT_AI_START");
} break;
case emMCP_EVENT_AI_NETCFG: {
log_info("emMCP_EVENT_AI_NETCFG");
} break;
case emMCP_EVENT_AI_NETERR: {
log_info("emMCP_EVENT_AI_NETERR");
} break;
case emMCP_EVENT_AI_WIFI_CONNNECT: {
log_info("emMCP_EVENT_AI_WIFI_CONNNECT");
} break;
case emMCP_EVENT_AI_WIFI_DISCONNECT: {
log_info("emMCP_EVENT_AI_WIFI_DISCONNECT");
} break;
case emMCP_EVENT_AI_WAKE: {
log_info("emMCP_EVENT_AI_WAKE");
} break;
case emMCP_EVENT_AI_SLEEP: {
log_info("emMCP_EVENT_AI_SLEEP");
} break;
case emMCP_EVENT_AI_MCP_CMD: {
log_info("emMCP_EVENT_AI_MCP_CMD:%s", (char *)param);
} break;
case emMCP_EVENT_AI_MCP_Text:
log_info("emMCP_EVENT_AI_MCP_Text:%s", (char *)param);
break;
default:
break;
}
}3. 创建 emMCP 工具
▫️工具创建流程
▫️步骤 1:创建 emMCP_tool_t 变量
创建 工具 要用到 emMCP_tool_t 结构体,这个结构体的定义如下:
/**
* @brief MCP 服务器工具结构体
*
*/
typedef struct emMCP_tool
{
char *name; // 工具名称
char *description; // 工具描述
void (*setRequestHandler)(void *); // 工具回调函数
void (*checkRequestHandler)(void *); // 工具回调函数
inputSchema_t inputSchema; // 输入参数
struct emMCP_tool *next; // 下一个工具
} emMCP_tool_t;
/**
* @brief 输入参数结构体
*
*/
typedef struct
{
properties_t properties[MCP_SERVER_TOOL_PROPERTIES_NUM]; // 属性
methods_t methods[MCP_SERVER_TOOL_METHODS_NUM]; // 方法
} inputSchema_t;/**
* @brief 属性结构体
* 用于描述工具的属性,告诉 AI 平台,该工具提供了哪些可以读取的属性,比如:查询当前音量
*/
typedef struct
{
char *name; // 属性名称
char *description; // 属性描述
mcp_server_tool_type_t type; // 属性类型
} properties_t;/**
* @brief 方法结构体
*
*/
typedef struct
{
char *name; // 方法名称
char *description; // 参数描述
parameters_t parameters[MCP_SERVER_TOOL_METHODS_PARAMETERS_NUM]; // 方法参数
} methods_t;/**
* @brief 参数结构体
* 用于 方法的输入参数,告诉 AI 平台, 需要下发什么数据,才能实现控制,比如:设置音量时需要下发的关键字和值的范围
*/
typedef struct
{
char *name; // 参数名称
char *description; // 参数描述
mcp_server_tool_type_t type; // 参数类型
} parameters_t;结构体说明:
name:工具名称,是工具的唯一标识,不能重复。description:工具描述,用简单的语言说明这个工具能实现什么功能(方便 AI 理解)。setRequestHandler:控制回调函数,当 AI 发送控制指令(比如“打开灯”)时,会调用这个函数。checkRequestHandler:查询回调函数,当 AI 发送查询指令(比如“灯是不是开着的”)时,会调用这个函数。inputSchema:输入参数描述,告诉 AI 这个工具支持哪些查询和控制操作:properties:可查询的属性(AI 能获取这些属性的当前值):name:属性名称(比如“enabled”表示灯的开关状态)。description:属性描述(说明这个属性代表什么,比如“当前灯光状态”)。type:属性类型(比如布尔型MCP_SERVER_TOOL_TYPE_BOOLEAN、数值型MCP_SERVER_TOOL_TYPE_NUMBER)。
methods:可调用的控制方法(AI 能通过这些方法控制设备):name:方法名称(比如“SetEnabled”表示设置灯的开关)。description:方法描述(说明这个方法的作用,比如“设置是否打开灯光”)。parameters:方法需要的参数:name:参数名称(比如“enabled”表示开关参数)。description:参数描述(说明参数的具体含义和范围,比如 “true表示打开灯光,false表示关闭灯光”)。type:参数类型(和属性类型类似)。
- 创建示例(LED 工具):
需要在主循环之前定义工具.
emMCP_t emMCP;// 创建 emMCP 对象
emMCP_tool_t led;//创建工具
int main(void)
{
// 其他初始化代码
// 初始化MCP
emMCP_Init(&emMCP);
led.name = "LED灯";//工具名称,保持唯一性
led.description = "用来控制LED灯的亮灭";//工具的功能描述
led.inputSchema.properties[0].name = "enable";//属性指令,AI 通过这个指令发送命令
led.inputSchema.properties[0].description = "是否打开LED灯,true表示打开,false表示关闭,查询时为null"; //指令描述,AI 通过这个描述理解指令
led.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_BOOLEAN;//指令类型,AI 通过这个类型发送相对应的数据
while (1)
{
emMCP_TickHandle(10);
}
}▫️步骤 2:创建工具的回调函数
工具的回调函数是工具的核心,当 AI 下发MCP 指令时,会调用工具的回调函数,根据指令内容,执行相应的操作。例如:
//控制回调函数
static void emMCP_SetLEDHandler(void *arg)
{
}
//查询回调函数
static void emMCP_GetLEDHandler(void *arg)
{
}
emMCP_t emMCP;// 创建 emMCP 对象
emMCP_tool_t led;//创建工具
int main(void)
{
// 其他初始化代码
// 初始化MCP
emMCP_Init(&emMCP);
led.name = "LED灯";//工具名称,保持唯一性
led.description = "用来控制LED灯的亮灭";//工具的功能描述
led.inputSchema.properties[0].name = "enable";//属性指令,AI 通过这个指令发送命令
led.inputSchema.properties[0].description = "是否打开LED灯,true表示打开,false表示关闭,查询时为null"; //指令描述,AI 通过这个描述理解指令
led.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_BOOLEAN;//指令类型,AI 通过这个类型发送相对应的数据
led.setRequestHandler = emMCP_SetLEDHandler;//设置控制回调
led.checkRequestHandler = emMCP_GetLEDHandler;//设置查询回调
while (1)
{
emMCP_TickHandle(10);
}
}在上方示例中,有两个回调函数,分别是:
emMCP_SetLEDHandler是控制回调函数,当 AI 发送控制指令时,如打开LED灯或关闭LED灯,会调用这个函数emMCP_GetLEDHandler是查询回调函数,当 AI 发送查询指令时,如查询LED灯是否打开,会调用这个函数
▫️步骤 3:添加工具到 emMCP
创建完成之后,需要将工具添加到 emMCP 中,把它存储起来,方便管理。添加工具的函数是 emMCP_AddToolToToolList(emMCP_tool_t *tool),只需要把创建好的工具传入即可。例如:
int main(void)
{
// 其他初始化代码
// 初始化MCP
emMCP_Init(&emMCP);
led.name = "LED灯";//工具名称,保持唯一性
led.description = "用来控制LED灯的亮灭";//工具的功能描述
led.inputSchema.properties[0].name = "enable";//属性指令,AI 通过这个指令发送命令
led.inputSchema.properties[0].description = "是否打开LED灯,true表示打开,false表示关闭,查询时为null"; //指令描述,AI 通过这个描述理解指令
led.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_BOOLEAN;//指令类型,AI 通过这个类型发送相对应的数据
led.setRequestHandler = emMCP_SetLEDHandler;//设置控制回调
led.checkRequestHandler = emMCP_GetLEDHandler;//设置查询回调
// 添加工具到工具列表
emMCP_AddToolToToolList(&led);
while (1)
{
emMCP_TickHandle(10);
}
}每创建一个工具,都需要调用这个函数,将工具添加到工具列表中。但是它还不会马上向小安AI注册,需要确定完成所有工具的创建后,再看下面的步骤。
▫️步骤 4:注册工具到小安AI
一定要确保所有工具都创建完成,并且添加到工具列表中,然后调用 emMCP_RegistrationTools 函数,将工具注册到小安AI。例如:
int main(void)
{
// 其他初始化代码
// 初始化MCP
emMCP_Init(&emMCP);
led.name = "LED灯";//工具名称,保持唯一性
led.description = "用来控制LED灯的亮灭";//工具的功能描述
led.inputSchema.properties[0].name = "enable";//属性指令,AI 通过这个指令发送命令
led.inputSchema.properties[0].description = "控制LED灯,打开:true,关闭为:false,查询为null"; //指令描述,AI 通过这个描述理解指令
led.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_BOOLEAN;//指令类型,AI 通过这个类型发送相对应的数据
led.setRequestHandler = emMCP_SetLEDHandler;//设置控制回调
led.checkRequestHandler = emMCP_GetLEDHandler;//设置查询回调
emMCP_AddToolToToolList(&led); // 添加工具到工具列表
emMCP_RegistrationTools(); // 注册工具到小安AI
while (1)
{
//执行状态机
emMCP_TickHandle(10);
}
}4. 处理 MCP 指令
工具中所设置的回调函数会在 MCP 指令到达时被调用,并返回一个cJSON 的指针,只需要在回调函数中实现对应的功能,实现完成之后,需要主动向小安AI发送控制结果或查询结果。流程如下:
▫️指令参数
在回调函数中,利用 cJSON 获取指令参数,例如:
//控制回调函数
static void emMCP_SetLEDHandler(void *arg)
{
// 接收到的数据
cJSON *param = (cJSON *)arg;
// 控制LED灯
cJSON *enable = cJSON_GetObjectItem(param, "enable");//获取led命令
if (enable != NULL) {
if (enable->valueint == 1) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
} else if (enable->valueint == 0) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
}
emMCP_ResponseValue(emMCP_CTRL_OK); //返回控制成功
}else {
emMCP_ResponseValue(emMCP_CTRL_ERROR); //返回控制失败
}
}
//查询回调函数
static void emMCP_GetLEDHandler(void *arg)
{
}
emMCP_t emMCP;// 创建 emMCP 对象
emMCP_tool_t led;//创建工具
int main(void)
{
// 其他初始化代码
// 初始化MCP
emMCP_Init(&emMCP);
led.name = "LED灯";//工具名称,保持唯一性
led.description = "用来控制LED灯的亮灭";//工具的功能描述
led.inputSchema.properties[0].name = "enable";//属性指令,AI 通过这个指令发送命令
led.inputSchema.properties[0].description = "是否打开LED灯,true表示打开,false表示关闭,查询时为null"; //指令描述,AI 通过这个描述理解指令
led.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_BOOLEAN;//指令类型,AI 通过这个类型发送相对应的数据
led.setRequestHandler = emMCP_SetLEDHandler;//设置控制回调
led.checkRequestHandler = emMCP_GetLEDHandler;//设置查询回调
emMCP_AddToolToToolList(&led); // 添加工具到工具列表
emMCP_RegistrationTools(); // 注册工具到小安AI
while (1)
{
emMCP_TickHandle(10);
}
}5.实现多参数控制
如果一个工具当中有多个需要控制的功能,比如:RGB 灯,基础功能会有以下几个:
- 开关:控制 RGB 灯的开关
- 红色:控制 RGB 灯的红色
- 绿色:控制 RGB 灯的绿色
- 蓝色:控制 RGB 灯的蓝色
如果每一个功能都创建一个工具的话,那么就会有很多内存的浪费。emMCP 支持一个工具定义多个功能,只需要在创建工具的时候,将功能在描述中定义即可。以下面的RGB灯为例。
▫️RGB彩灯示例
一个工具实现多个功能,关键是创建多个属性,并且定义好描述。如下方代码所示:
RGB彩灯示例
emMCP_tool_t rgb;//创建工具
//主函数
int main(void)
{
// 其他初始化代码
// 初始化MCP
emMCP_Init(&emMCP);
rgb.name = "RGB彩灯";//工具名称,保持唯一性
rgb.description = "用来控制RGB彩灯的亮灭";//工具的功能描述
rgb.inputSchema.properties[0].name = "enable";//属性指令,AI 通过这个指令发送命令
rgb.inputSchema.properties[0].description = "是否打开RGB彩灯,true表示打开,false表示关闭,查询时为null"; //指令描述,AI 通过这个描述理解指令
rgb.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_BOOLEAN;//指令类型,AI 通过这个类型发送相对应的数据
rgb.inputSchema.properties[1].name = "red";//属性指令,AI 通过这个指令发送命令
rgb.inputSchema.properties[1].description = "RGB彩灯的红色,范围0-255,查询时为null"; //指令描述,AI 通过这个描述理解指令
rgb.inputSchema.properties[1].type = MCP_SERVER_TOOL_TYPE_NUMBLE;//指令类型,AI 通过这个类型发送相对应的数据
rgb.inputSchema.properties[2].name = "green";//属性指令,AI 通过这个指令发送命令
rgb.inputSchema.properties[2].description = "RGB彩灯的绿色,范围0-255,查询时为null"; //指令描述,AI 通过这个描述理解指令
rgb.inputSchema.properties[2].type = MCP_SERVER_TOOL_TYPE_NUMBLE;//指令类型,AI 通过这个类型发送相对应的数据
rgb.inputSchema.properties[3].name = "blue";//属性指令,AI 通过这个指令发送命令
rgb.inputSchema.properties[3].description = "RGB彩灯的蓝色,范围0-255,查询时为null"; //指令描述,AI 通过这个描述理解指令
rgb.inputSchema.properties[3].type = MCP_SERVER_TOOL_TYPE_NUMBLE;//指令类型,AI 通过这个类型发送相对应的数据
rgb.setRequestHandler = emMCP_SetRGBHandler;//设置控制回调
emMCP_AddToolToToolList(&rgb); // 添加工具到工具列表
emMCP_RegistrationTools(); // 注册工具到小安AI
while (1)
{
emMCP_TickHandle(10);
}
}void emMCP_SetRGBHandler(void *arg) {
cJSON *param = (cJSON *)arg;
cJSON *enable = cJSON_GetObjectItem(param, "enable");
cJSON *ch_red = cJSON_GetObjectItem(param, "red");
cJSON *ch_green = cJSON_GetObjectItem(param, "green");
cJSON *ch_blue = cJSON_GetObjectItem(param, "blue");
if (enable != NULL)
{
if (cJSON_IsTrue(enable))
{
log_info("RGB enable is true");
} else
{
log_info("RGB enable is false");
}
}
if (ch_red != NULL)
{
log_info("RGB red is %d", ch_red->valueint);
}
if (ch_green != NULL)
{
log_info("RGB green is %d", ch_green->valueint);
}
if (ch_blue != NULL)
{
log_info("RGB blue is %d", ch_blue->valueint);
}
// 返回控制结果
char *result = malloc(128);
sprintf(result, " {\"%s\":%s,\"%s\":%d,\"%s\":%d,\"%s\":%d}}", enable->string,
enable->valueint ? "true" : "false", ch_red->string, ch_red->valueint,
ch_green->string, ch_green->valueint, ch_blue->string,
ch_blue->valueint);
emMCP_ResponseValue(result);
free(result);
}🔹实现原理
在 emMCP_tool_t 结构体中,有一个 inputSchema 属性,用来描述工具的输入参数。在 inputSchema 中,properties 属性是一个结构体数组,它允许定义多个属性,通过定义多个 properties ,就可以实现一个工具控制多个功能。
typedef struct
{
properties_t properties[MCP_SERVER_TOOL_PROPERTIES_NUM]; // 属性结构体数组
methods_t methods[MCP_SERVER_TOOL_METHODS_NUM]; // 方法结构体数组
} inputSchema_t;
rgb.inputSchema.properties[0].name = "enable";
rgb.inputSchema.properties[0].description = "是否打开RGB彩灯,true表示打开,false表示关闭,查询时为null";
rgb.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_BOOLEAN;
rgb.inputSchema.properties[1].name = "red";
rgb.inputSchema.properties[1].description = "RGB彩灯的红色,范围0-255,查询时为null";
rgb.inputSchema.properties[1].type = MCP_SERVER_TOOL_TYPE_NUMBLE;
rgb.inputSchema.properties[2].name = "green";
rgb.inputSchema.properties[2].description = "RGB彩灯的绿色,范围0-255,查询时为null";
rgb.inputSchema.properties[2].type = MCP_SERVER_TOOL_TYPE_NUMBLE;
rgb.inputSchema.properties[3].name = "blue";
rgb.inputSchema.properties[3].description = "RGB彩灯的蓝色,范围0-255,查询时为null";
rgb.inputSchema.properties[3].type = MCP_SERVER_TOOL_TYPE_NUMBLE;🔹多参数的返回值
小安 AI 发送多参数的指令时,有两种方式返回控制结果:
JSON格式:可以根据原路返回JSON字符串,例如:
cvoid emMCP_SetRGBHandler(void *arg) { cJSON *param = (cJSON *)arg; // 执行所有控制 // 原路返回控制结果 char *result = cJSON_PrintUnformatted(param); emMCP_ResponseValue(result); free(result); }字符串格式:把所有的控制结果拼接成一个字符串,例如:
cvoid emMCP_SetRGBHandler(void *arg) { cJSON *param = (cJSON *)arg; // 执行所有控制 char *result = malloc(128); memset(result, 0, 128); sprintf(result, "enable:%d,red:%d,green:%d,blue:%d", 1,255,255,255); emMCP_ResponseValue(result); free(result); }
6. 关于内存
创建工具、定义多属性等操作都会占用内存,请根据需求定义以下宏定义,以节省内存,在 emMCP.h 中设置以下宏的数值:
#define MCP_SERVER_TOOL_NUMBLE_MAX 2 // 最大工具数量
#define MCP_SERVER_TOOL_PROPERTIES_NUM 5 // 最大属性数量
#define MCP_SERVER_TOOL_METHODS_NUM 5 // 最大方法数量
#define MCP_SERVER_TOOL_METHODS_PARAMETERS_NUM 5 // 最大方法参数数量7. 其他功能
▫️控制唤醒
调用 emMCP_SetAiWakeUp(uint8_t WakeUp_Time) 函数(参数是设置唤醒的时间 单位 s),可以唤醒小安 AI,例如:
emMCP_SetAiWakeUp(20);//唤醒20s设置成功会触发:emMCP_EVENT_AI_WAKE 事件,请参考:事件列表
▫️设置音量
调用 emMCP_SetAiVolume(uint8_t volume) 函数(参数是音量大小,范围0-100),可以设置小安 AI 的音量,例如:
emMCP_SetAiVolume(50);//设置音量50设置成功会触发:emMCP_EVENT_CMD_OK 事件,请参考:事件列表
▫️设置通讯波特率
调用 emMCP_SetBaudrate(uint16_t baudrate) 函数(参数是波特率大小,范围300-2000000),可以设置小安 AI 的通讯波特率,例如:
emMCP_SetBaudrate(115200);//设置波特率115200重要: 设置波特率之后,会立即生效,务必把 MCU 的波特率也同步设置。
设置成功会触发:emMCP_EVENT_CMD_OK 事件,请参考:事件列表

