Skip to content

你可以先对照例程加强理解:👉应用例程

1.前提条件

请确保已经具备了以下条件

  • 已经参考 移植到MCU 章节完成了移植
  • 移植 emMCP 后的 MCU 工程已经编译成功
  • 唤醒模组能够正常接收到数据

2. 创建 emMCP 事件回调函数

▫️事件列表

emMCP 的事件以枚举的形式定义在 emMCP.h 文件中,有以下几种事件:

枚举序号事件意义
0emMCP_EVENT_NONE无事件
1emMCP_EVENT_CMD_OK命令执行成功
2emMCP_EVENT_CMD_ERROR命令执行失败
3emMCP_EVENT_AI_START模组启动
4emMCP_EVENT_AI_NETCFG模组网络配置
5emMCP_EVENT_AI_NETERR模组网络错误
6emMCP_EVENT_AI_WIFI_CONNNECT模组连接上 Wi-Fi
7emMCP_EVENT_AI_WIFI_DISCONNECT模组断开 Wi-Fi
8emMCP_EVENT_AI_WAKE模组唤醒
9emMCP_EVENT_AI_SLEEP模组休眠
10emMCP_EVENT_AI_OTAUPDATE模组开始 OTA
11emMCP_EVENT_AI_OTAOK模组 OTA 成功
12emMCP_EVENT_AI_OTAERR模组 OTA 失败
13emMCP_EVENT_AI_MCP_CMD模组收到 MCP 命令
14emMCP_EVENT_AI_MCP_Text模组收到字幕信息

▫️事件回调函数

emMCP 事件回调函数采用弱定义的方式定义在 emMCP.c 文件中,支持重新定义该函数,实现自定义的事件处理逻辑。原型如下:

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:事件参数,通常固定为字符串类型

只需要在工程当中重新定义该函数即可,例如:

示例:
c
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 结构体,这个结构体的定义如下:

c

/**
 * @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;
c

/**
 * @brief 输入参数结构体
 *
 */
typedef struct
{
	properties_t properties[MCP_SERVER_TOOL_PROPERTIES_NUM]; // 属性
	methods_t methods[MCP_SERVER_TOOL_METHODS_NUM];			 // 方法
} inputSchema_t;
c
/**
 * @brief 属性结构体
 *  用于描述工具的属性,告诉 AI 平台,该工具提供了哪些可以读取的属性,比如:查询当前音量
 */
typedef struct
{
	char *name;					 // 属性名称
	char *description;			 // 属性描述
	mcp_server_tool_type_t type; // 属性类型
} properties_t;
c
/**
 * @brief 方法结构体
 *
 */
typedef struct
{
	char *name;														 // 方法名称
	char *description;												 // 参数描述
	parameters_t parameters[MCP_SERVER_TOOL_METHODS_PARAMETERS_NUM]; // 方法参数
} methods_t;
c
/**
 * @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 工具):

需要在主循环之前定义工具.

c
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 指令时,会调用工具的回调函数,根据指令内容,执行相应的操作。例如:

c
//控制回调函数
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),只需要把创建好的工具传入即可。例如:

c
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。例如:

c
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 获取指令参数,例如:

c
//控制回调函数
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彩灯示例
c
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);
  }
}
c
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 ,就可以实现一个工具控制多个功能。

c

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字符串,例如:

    c
    void emMCP_SetRGBHandler(void *arg) {
      cJSON *param = (cJSON *)arg;
      // 执行所有控制
    
      // 原路返回控制结果
      char *result = cJSON_PrintUnformatted(param);
      emMCP_ResponseValue(result);
      free(result);
    }
  • 字符串格式:把所有的控制结果拼接成一个字符串,例如:

    c
    void 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 中设置以下宏的数值:

c
#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,例如:

c
emMCP_SetAiWakeUp(20);//唤醒20s

设置成功会触发:emMCP_EVENT_AI_WAKE 事件,请参考:事件列表

▫️设置音量

调用 emMCP_SetAiVolume(uint8_t volume) 函数(参数是音量大小,范围0-100),可以设置小安 AI 的音量,例如:

c
emMCP_SetAiVolume(50);//设置音量50

设置成功会触发:emMCP_EVENT_CMD_OK 事件,请参考:事件列表

▫️设置通讯波特率

调用 emMCP_SetBaudrate(uint16_t baudrate) 函数(参数是波特率大小,范围300-2000000),可以设置小安 AI 的通讯波特率,例如:

c
emMCP_SetBaudrate(115200);//设置波特率115200

重要: 设置波特率之后,会立即生效,务必把 MCU 的波特率也同步设置。

设置成功会触发:emMCP_EVENT_CMD_OK 事件,请参考:事件列表

Released under the MIT License.