13浏览
查看: 13|回复: 0

[M10项目] 行空板k10小智与掌控板MCP:语音控制硬件,追踪红外点!

[复制链接]
本帖最后由 云天 于 2026-3-1 15:21 编辑

用行空板K10当你的AI助手,用掌控板作为智能执行终端,通过MCP协议实现语音控制LED、读取传感器、追踪红外点,打造一个可扩展的智能硬件控制系统。

【项目引言】

       行空板K10和掌控板是创客学习和作品中的两大明星设备。一个拥有强大的屏幕和AI算力,可以运行完整的AI语音助手;另一个小巧灵活,拥有丰富的板载传感器和扩展接口。如果能将两者结合起来,让行空板K10成为你的“大脑”,用语音指挥掌控板完成各种任务,那该多酷!

       本文将带你实现这样一个系统:行空板K10刷入小智AI固件,作为语音交互终端;掌控板运行MCP(模型上下文协议)服务器,注册各种硬件控制工具。当你对行空板说出指令时,小智AI通过WiFi调用掌控板上的MCP工具,实现对LED灯、传感器、甚至红外摄像头的实时控制。项目还扩展了红外定位摄像头模块,展示了系统的强大扩展能力。

【硬件清单】

       行空板K10 × 1(已刷小智AI固件)
       掌控板 × 1
       IO扩展板 × 1(用于连接掌控板,方便插接外部传感器)
       红外定位摄像头模块(DFRobot SEN0158)× 1
       杜邦线若干
       USB数据线(用于供电和程序上传)
行空板k10小智与掌控板MCP:语音控制硬件,追踪红外点!图3

       注:掌控板板载资源包括:WS2812 RGB灯带(3颗)、光线传感器(P4)、声音传感器(P10)、0.96英寸OLED显示屏(128×64,I2C接口)。这些直接可用,无需额外连接。

【实现原理】
       整个系统基于 “小智AI平台” 和 “MCP协议” 实现。
       行空板K10:刷入小智AI固件后,它成为一台反文旁虫立的语音助手。在手机上用小智AI App扫码绑定,即可通过WiFi接入云端。当我们对它说话时,语音会被识别并发送到小智AI云端进行处理。
       掌控板:运行我们编写的Arduino程序,作为 “MCP服务器”。程序向小智AI平台注册了多个“工具”(例如“打开LED”、“读取光线传感器”、“获取红外点”)。每个工具对应一个函数,可以被远程调用。
       MCP协议:行空板K10与小智AI云端通信,云端根据用户意图,选择合适的工具,通过WebSocket向掌控板发送JSON-RPC请求。掌控板解析请求、执行函数、返回结果。整个过程无缝衔接。
       这样一来,我们实现了“语音 → 云端理解 → 调用硬件”的完整闭环。
行空板k10小智与掌控板MCP:语音控制硬件,追踪红外点!图1

【 掌控板端代码解析】

       掌控板端程序是整个项目的核心。我们使用Arduino IDE开发,依赖以下库:
       WebSocketMCP:用于实现MCP服务器
       Adafruit_NeoPixel:控制WS2812灯带
       U8g2:驱动OLED显示屏
      ArduinoJson:解析和处理JSON数据

       1. MCP服务器初始化与工具注册

       在 `setup()` 中,我们初始化WiFi、OLED、传感器,然后启动MCP客户端(这里实际上是服务器模式,但库命名为client,我们沿用其API):
  1. mcpClient.begin(MCP_ENDPOINT, onMcpConnectionChange);
复制代码

       `MCP_ENDPOINT` 是你在小智AI平台申请到的专属接入点地址,包含了你的用户和智能体信息。连接成功后,会调用 `registerMcpTools()` 注册所有工具。

       每个工具注册时需提供名称、描述、输入参数JSON schema和一个回调函数。以LED控制工具为例:
  1. mcpClient.registerTool(
  2.   "led_blink",
  3.   "控制 WS2812 灯带 (on/off/blink/flow)",
  4.   "{"properties":{"state":{"title":"LED状态","type":"string","enum":["on","off","blink","flow"]}},"required":["state"],"title":"ledControlArguments","type":"object"}",
  5.   [](const String& args) -> WebSocketMCP::ToolResponse {
  6.     // 解析参数,执行控制逻辑,返回结果
  7.   }
  8. );
复制代码
      类似的,我们注册了 `light_sensor`、`sound_sensor` 和 `ir_camera` 工具。

       2. 非阻塞LED状态机

       为了在LED闪烁或流水灯时不影响MCP通信和OLED刷新,我们设计了状态机。在 `loop()` 中定期调用 `handleLedStateMachine()`,根据当前模式(`LED_MODE_ON`、`LED_MODE_BLINK`、`LED_MODE_FLOW`)逐步更新灯带,避免使用 `delay()`。

  1. void handleLedStateMachine() {
  2. // 仅在MCP控制激活且未超时时执行
  3. switch (currentLedMode) {
  4. case LED_MODE_BLINK:
  5. // 每200ms翻转一次,计数5次后结束
  6. break;
  7. case LED_MODE_FLOW:
  8. // 每300ms点亮下一个灯珠,循环3次
  9. break;
  10. // ...
  11. }
  12. }
复制代码

       3. OLED实时信息显示

        OLED分四行显示:WiFi状态、IP地址、MCP连接状态、最新MCP消息(支持滚动)。当收到工具调用结果时,`onMcpOutput` 会被触发(实际在库中未用,我们改为在工具回调中直接更新显示),但这里我们简化设计:每次工具执行后,将返回的摘要信息赋值给 `mcpMessage`,并开启滚动标志。

       4. 红外定位摄像头读取

       SEN0158红外摄像头通过I2C接口连接。我们编写了 `initIRCamera()` 和 `readIRCamera()` 函数,完全参照官方示例代码。在 `ir_camera` 工具的回调中,调用 `readIRCamera()` 读取四个点的坐标,打包成JSON返回:
  1. {
  2. "success": true,
  3. "points": [
  4. {"x": 123, "y": 456},
  5. {"x": 1023, "y": 1023},
  6. // ...
  7. ]
  8. }
复制代码
      无效点坐标为 `(1023, 1023)`,可根据需要过滤。
行空板k10小智与掌控板MCP:语音控制硬件,追踪红外点!图2


红外源(使用红外发射二极管串联 220Ω 电阻,5V供电)

【行空板K10端配置】

       行空板K10端相对简单,主要是刷入小智AI固件并进行配置。

       1. 刷写固件:从小智AI官网下载适用于行空板K10的固件,用balenaEtcher等工具写入TF卡,插入行空板并启动。
       2. 配网与绑定:启动后,行空板屏幕会显示二维码。用手机上的小智AI App扫描,按照提示连接WiFi并完成绑定。
       3. 创建智能体:在App或网页控制台中,创建一个新的智能体(或修改已有角色)。在“MCP接入点”设置中,填入你掌控板代码中的那个长串 `MCP_ENDPOINT` 地址。
       4. 保存并同步:保存设置,行空板会自动同步最新配置。

       现在,当你对着行空板说“打开LED”时,云端会识别意图,并通过MCP调用掌控板的 `led_blink` 工具,传入参数 `{"state":"on"}`。
【使用演示】

       下面是一些你可以尝试的语音指令,以及掌控板的反应:

语音指令
掌控板行为
OLED显示反馈
“打开LED”灯带亮起白光Tool: led_blink state=on
“流水灯”灯带依次点亮蓝色光,循环一次Tool: led_blink state=flow
“光线值多少”读取光线传感器,返回数值Light: 356
“检测红外点”读取红外摄像头,返回四个点坐标显示第一个有效点坐标(如有)
“关灯”灯带熄灭Tool: led_blink state=off

       你还可以组合指令,例如:“如果光线值小于100,打开LED”。这需要云端具备逻辑判断能力,小智AI的智能体可以通过“工作流”功能实现,本文暂不展开。

【项目扩展:红外定位摄像头的应用】

       加入红外定位摄像头后,项目的玩法大大增加:

       红外目标追踪:用940nm红外遥控器或自制红外信标,在摄像头前移动,掌控板可实时获取其坐标。你可以将这些坐标发送给行空板,在屏幕上绘制轨迹。
       互动游戏:结合投影或大屏幕,制作一个“红外射击游戏”,玩家用红外枪瞄准屏幕上的目标,摄像头检测到红外点位置,判断是否命中。
       机器人导航:在机器人身上安装红外LED,用多个摄像头实现室内定位。

       由于摄像头支持最多4个点,你甚至可以同时追踪多个目标,实现更复杂的交互。

【演示视频】

【项目总结】

       本文展示了如何将行空板K10与掌控板结合,通过小智AI平台和MCP协议,实现语音控制硬件、读取传感器、显示信息,并扩展红外定位摄像头。这个方案不仅让两块常用创客板各展所长,还为未来的智能硬件项目提供了一个可复用的架构:用AI语音作为人机交互入口,用MCP作为设备控制桥梁,硬件端可以无限扩展。

       你可以在此基础上添加更多传感器(如温湿度、PM2.5)、执行器(电机、舵机),甚至让多个掌控板同时在线,打造一个全屋智能语音控制系统。期待你的创意!

【附:完整代码】
  1. /*
  2.   掌控板 MCP 客户端 + WS2812 灯带 + OLED 显示屏+IO扩展展+红外定位摄像头模块
  3.   功能:
  4.   - 连接 WiFi
  5.   - 连接 WebSocket MCP 服务器
  6.   - 通过 WS2812 灯带指示连接状态(红色快闪:WiFi未连;绿色慢闪:WiFi已连但MCP未连;蓝色常亮:全部连接)
  7.   - OLED 显示当前 WiFi 状态、IP 地址、MCP 状态
  8.   - OLED 第四行滚动显示最新的 MCP 消息
  9.   - 通过串口发送命令给 MCP 服务器
  10.   - 注册 MCP 工具:
  11.       * LED 控制(led_blink)
  12.       * 系统信息(system-info)
  13.       * 光线传感器(light_sensor)——直接读取 P4 (GPIO36)
  14.       * 声音传感器(sound_sensor)——直接读取 P10 (GPIO26)
  15.       *红外定位摄像头模块(Positioning IR Camera)-i2c(SCL 22,SDA 23)
  16.   - 解决 MCP 工具与状态指示灯的冲突(MCP 工具控制期间暂停状态指示,超时后恢复)
  17. */
  18. #include <Arduino.h>
  19. #include <WiFi.h>
  20. #include <WebSocketMCP.h>          // 请确保已安装此库
  21. #include <Adafruit_NeoPixel.h>     // WS2812 库
  22. #include <U8g2lib.h>               // OLED 库
  23. #include <Wire.h>
  24. /********** 配置项 ***********/
  25. // WiFi 设置
  26. const char* WIFI_SSID = "**********";
  27. const char* WIFI_PASS = "**********";
  28. // WebSocket MCP 服务器地址(请替换为你的有效 token)
  29. const char* MCP_ENDPOINT = "wss://api.xiaozhi.me/mcp/?token=你自己的token";
  30. // 调试串口
  31. #define DEBUG_SERIAL Serial
  32. #define DEBUG_BAUD_RATE 115200
  33. /********** 硬件引脚定义 ***********/
  34. // WS2812 灯带(掌控板上通常接在 GPIO17,三个灯珠)
  35. #define WS2812_PIN    17
  36. #define NUM_PIXELS    3
  37. Adafruit_NeoPixel pixels(NUM_PIXELS, WS2812_PIN, NEO_GRB + NEO_KHZ800);
  38. // OLED 显示屏(掌控板固定 I2C 引脚:SCL=22, SDA=23)
  39. #define OLED_SCL      22
  40. #define OLED_SDA      23
  41. // 使用硬件 I2C,驱动为 SH1106(若实际为 SSD1306 可修改构造函数)
  42. U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL, OLED_SDA);
  43. // 传感器引脚定义
  44. #define LIGHT_SENSOR_PIN   39   // 光线传感器接 P4 (GPIO36, ADC1_CH3)
  45. #define SOUND_SENSOR_PIN   36   // 声音传感器接 P10 (GPIO26, ADC2_CH0)
  46. // 新增:数字激光接近传感器接 P2 (GPIO35)
  47. #define PROXIMITY_SENSOR_PIN 35
  48. // 红外定位摄像头模块 (SEN0158) 相关定义
  49. #define IRCAM_I2C_ADDR  0x58      // I2C 设备地址 (0xB0 右移一位)
  50. #define IRCAM_DATA_LEN   16        // 一次读取的数据长度
  51. byte irCamData[IRCAM_DATA_LEN];    // 存储原始数据的缓冲区
  52. // 存储最多4个点的坐标,无效点坐标通常为 1023,1023
  53. int irX[4] = {1023, 1023, 1023, 1023};
  54. int irY[4] = {1023, 1023, 1023, 1023};
  55. /********** 全局变量 ***********/
  56. WebSocketMCP mcpClient;          // MCP 客户端对象
  57. // LED 模式枚举
  58. enum LedMode {
  59.   LED_MODE_IDLE,       // 空闲,由系统状态指示灯控制
  60.   LED_MODE_ON,         // 常亮(白色)
  61.   LED_MODE_OFF,        // 熄灭
  62.   LED_MODE_BLINK,      // 闪烁(白色,5次)
  63.   LED_MODE_FLOW        // 流水灯(蓝色,单次)
  64. };
  65. // LED 状态机变量
  66. LedMode currentLedMode = LED_MODE_IDLE;   // 当前模式
  67. int flowIndex = 0;                         // 流水灯当前索引
  68. unsigned long lastLedStep = 0;              // 上次步骤时间
  69. bool blinkState = false;                     // 闪烁当前亮灭状态
  70. int blinkCount = 0;                           // 闪烁已进行次数
  71. const int blinkTotal = 5;                     // 闪烁总次数
  72. const int flowDelay = 300;                     // 流水灯步进间隔(毫秒)
  73. const int blinkDelay = 200;                     // 闪烁间隔(毫秒)
  74. // 连接状态
  75. bool wifiConnected = false;
  76. bool mcpConnected  = false;
  77. // MCP 工具主动控制 LED 的标志(用于解决冲突)
  78. bool mcpLedActive = false;       // 当前是否由 MCP 工具控制 LED
  79. unsigned long mcpLedTimeout = 0; // 控制模式的超时时间(毫秒)
  80. // OLED 上次显示的状态(用于避免频繁刷新)
  81. String lastWifiStatus = "";
  82. String lastMCPStatus  = "";
  83. String lastIP         = "";
  84. // MCP 消息滚动相关
  85. String mcpMessage = "";                // 最新 MCP 消息
  86. int scrollOffset = 0;                   // 滚动偏移(像素)
  87. unsigned long lastScrollTime = 0;       // 上次滚动时间
  88. const int scrollInterval = 300;          // 滚动间隔(毫秒)
  89. bool isScrolling = false;                // 是否正在滚动
  90. int messageWidth = 0;                    // 消息总像素宽度
  91. /********** 函数声明 ***********/
  92. void setupWifi();
  93. void onMcpOutput(const String &message);
  94. void onMcpError(const String &error);
  95. void onMcpConnectionChange(bool connected);
  96. void registerMcpTools();
  97. void processSerialCommands();
  98. void updateOLED();                // 更新 OLED 显示
  99. void handleLEDs();                // 处理 WS2812 状态指示(考虑 MCP 覆盖)
  100. void printHelp();
  101. void printStatus();
  102. /********** setup() ***********/
  103. void setup() {
  104.   DEBUG_SERIAL.begin(DEBUG_BAUD_RATE);
  105.   DEBUG_SERIAL.println("\n\n======== 掌控板 MCP 客户端启动 ========");
  106.   // 初始化 WS2812 灯带
  107.   pixels.begin();
  108.   pixels.clear();
  109.   pixels.show();
  110.   // 初始化 OLED 显示屏
  111.   Wire.begin(OLED_SDA, OLED_SCL);   // 启动 I2C 总线
  112.   u8g2.begin();
  113.   u8g2.enableUTF8Print();            // 启用 UTF-8 打印(如需显示中文)
  114.   u8g2.setFont(u8g2_font_wqy16_t_gb2312b); // 使用中文字体(需库支持,若没有可换英文字体)
  115.   u8g2.clearBuffer();
  116.   u8g2.setCursor(0, 15);
  117.   u8g2.print("系统初始化...");
  118.   u8g2.sendBuffer();
  119.   initIRCamera();                    // <--- 新增:初始化红外摄像头
  120.   // 连接 WiFi
  121.   setupWifi();
  122.   // 初始化 MCP 客户端
  123.   if (mcpClient.begin(MCP_ENDPOINT, onMcpConnectionChange)) {
  124.     DEBUG_SERIAL.println("[MCP] 客户端初始化成功,正在连接服务器...");
  125.   } else {
  126.     DEBUG_SERIAL.println("[MCP] 客户端初始化失败!");
  127.   }
  128.   // 显示帮助信息
  129.   DEBUG_SERIAL.println("\n使用说明:");
  130.   DEBUG_SERIAL.println("- 通过串口输入命令并回车发送给 MCP 服务器");
  131.   DEBUG_SERIAL.println("- 输入 help 查看可用命令");
  132.   DEBUG_SERIAL.println();
  133.   // 短暂显示启动信息后,进入主循环
  134.   delay(1000);
  135. }
  136. /********** loop() ***********/
  137. void loop() {
  138.   mcpClient.loop();
  139.   processSerialCommands();
  140.   // 处理 LED 状态机(非阻塞效果)
  141.   handleLedStateMachine();
  142.   // 更新 WS2812 状态指示灯(仅在非 MCP 控制时生效)
  143.   handleLEDs();
  144.   // 滚动更新:如果正在滚动且达到时间间隔,增加偏移并刷新显示
  145.   if (isScrolling && (millis() - lastScrollTime > scrollInterval)) {
  146.     lastScrollTime = millis();
  147.     scrollOffset += 2;  // 每次滚动2像素,可根据需要调整速度
  148.     if (scrollOffset > messageWidth) {
  149.       scrollOffset = 0; // 循环滚动
  150.     }
  151.     // 强制刷新 OLED(因为状态未变,但 isScrolling 为 true,updateOLED 会重绘)
  152.     updateOLED();
  153.   }
  154.   // 更新 OLED 显示(当状态变化或需要滚动时,updateOLED 内部判断)
  155.   updateOLED();
  156.   // 小延时,避免过度占用 CPU
  157.   delay(10);
  158. }
  159. /**
  160. * 初始化红外定位摄像头
  161. * 参考 SEN0158 Wiki 示例代码
  162. */
  163. void initIRCamera() {
  164.   DEBUG_SERIAL.println("[IR Camera] 正在初始化...");
  165.   Wire.beginTransmission(IRCAM_I2C_ADDR);
  166.   Wire.write(0x30); Wire.write(0x01); delay(10);
  167.   Wire.endTransmission();
  168.   
  169.   Wire.beginTransmission(IRCAM_I2C_ADDR);
  170.   Wire.write(0x30); Wire.write(0x08); delay(10);
  171.   Wire.endTransmission();
  172.   
  173.   Wire.beginTransmission(IRCAM_I2C_ADDR);
  174.   Wire.write(0x06); Wire.write(0x90); delay(10);
  175.   Wire.endTransmission();
  176.   
  177.   Wire.beginTransmission(IRCAM_I2C_ADDR);
  178.   Wire.write(0x08); Wire.write(0xC0); delay(10);
  179.   Wire.endTransmission();
  180.   
  181.   Wire.beginTransmission(IRCAM_I2C_ADDR);
  182.   Wire.write(0x1A); Wire.write(0x40); delay(10);
  183.   Wire.endTransmission();
  184.   
  185.   Wire.beginTransmission(IRCAM_I2C_ADDR);
  186.   Wire.write(0x33); Wire.write(0x33); delay(10);
  187.   Wire.endTransmission();
  188.   
  189.   delay(100);
  190.   DEBUG_SERIAL.println("[IR Camera] 初始化完成");
  191. }
  192. /**
  193. * 读取红外摄像头数据并解析出最多4个点的坐标
  194. * 返回 true 表示读取成功(即使没有检测到点),false 表示 I2C 通信失败
  195. */
  196. bool readIRCamera() {
  197.   // 请求读取数据
  198.   Wire.beginTransmission(IRCAM_I2C_ADDR);
  199.   Wire.write(0x36);
  200.   if (Wire.endTransmission() != 0) {
  201.     return false; // I2C 通信错误
  202.   }
  203.   // 请求 16 字节数据
  204.   uint8_t bytesReceived = Wire.requestFrom(IRCAM_I2C_ADDR, IRCAM_DATA_LEN);
  205.   if (bytesReceived < IRCAM_DATA_LEN) {
  206.     return false; // 数据长度不足
  207.   }
  208.   // 读取原始数据到缓冲区
  209.   for (int i = 0; i < IRCAM_DATA_LEN; i++) {
  210.     irCamData[i] = Wire.read();
  211.   }
  212.   // --- 解析坐标 (完全按照示例代码的逻辑) ---
  213.   int s;
  214.   // 点 0
  215.   irX[0] = irCamData[1];
  216.   irY[0] = irCamData[2];
  217.   s = irCamData[3];
  218.   irX[0] += (s & 0x30) << 4;
  219.   irY[0] += (s & 0xC0) << 2;
  220.   // 点 1
  221.   irX[1] = irCamData[4];
  222.   irY[1] = irCamData[5];
  223.   s = irCamData[6];
  224.   irX[1] += (s & 0x30) << 4;
  225.   irY[1] += (s & 0xC0) << 2;
  226.   // 点 2
  227.   irX[2] = irCamData[7];
  228.   irY[2] = irCamData[8];
  229.   s = irCamData[9];
  230.   irX[2] += (s & 0x30) << 4;
  231.   irY[2] += (s & 0xC0) << 2;
  232.   // 点 3
  233.   irX[3] = irCamData[10];
  234.   irY[3] = irCamData[11];
  235.   s = irCamData[12];
  236.   irX[3] += (s & 0x30) << 4;
  237.   irY[3] += (s & 0xC0) << 2;
  238.   return true;
  239. }
  240. /********** WiFi 连接 ***********/
  241. void setupWifi() {
  242.   DEBUG_SERIAL.print("[WiFi] 正在连接 ");
  243.   DEBUG_SERIAL.println(WIFI_SSID);
  244.   WiFi.mode(WIFI_STA);
  245.   WiFi.begin(WIFI_SSID, WIFI_PASS);
  246.   int attempts = 0;
  247.   while (WiFi.status() != WL_CONNECTED && attempts < 40) { // 最多等待 20 秒
  248.     delay(500);
  249.     DEBUG_SERIAL.print(".");
  250.     attempts++;
  251.   }
  252.   if (WiFi.status() == WL_CONNECTED) {
  253.     wifiConnected = true;
  254.     DEBUG_SERIAL.println("\n[WiFi] 连接成功!");
  255.     DEBUG_SERIAL.print("[WiFi] IP 地址: ");
  256.     DEBUG_SERIAL.println(WiFi.localIP());
  257.   } else {
  258.     wifiConnected = false;
  259.     DEBUG_SERIAL.println("\n[WiFi] 连接失败!将继续尝试...");
  260.   }
  261. }
  262. /********** MCP 回调函数 ***********/
  263. void onMcpOutput(const String &message) {
  264.   DEBUG_SERIAL.print("[MCP 输出] ");
  265.   DEBUG_SERIAL.println(message);
  266.   // 更新消息并重置滚动
  267.   mcpMessage = message;
  268.   scrollOffset = 0;
  269.   lastScrollTime = millis();
  270.   // 计算消息宽度
  271.   messageWidth = u8g2.getUTF8Width(mcpMessage.c_str());
  272.   isScrolling = (messageWidth > 128);  // 如果宽度超过屏幕宽度则滚动
  273.   // 强制刷新显示
  274.   updateOLED();
  275. }
  276. void onMcpError(const String &error) {
  277.   DEBUG_SERIAL.print("[MCP 错误] ");
  278.   DEBUG_SERIAL.println(error);
  279. }
  280. void onMcpConnectionChange(bool connected) {
  281.   mcpConnected = connected;
  282.   if (connected) {
  283.     DEBUG_SERIAL.println("[MCP] 已连接到服务器");
  284.     registerMcpTools();   // 连接成功后注册工具
  285.   } else {
  286.     DEBUG_SERIAL.println("[MCP] 与服务器断开连接");
  287.   }
  288. }
  289. /********** 注册 MCP 工具(注意检查此函数的闭合括号)***********/
  290. void registerMcpTools() {
  291.   DEBUG_SERIAL.println("[MCP] 正在注册工具...");
  292.   // 1. LED 控制工具(解决与状态指示的冲突)
  293.   // 1. LED 控制工具(已增加 "flow" 流水灯模式)
  294. mcpClient.registerTool(
  295.   "led_blink",
  296.   "控制 WS2812 灯带 (on/off/blink/flow)",
  297.   "{"properties":{"state":{"title":"LED状态","type":"string","enum":["on","off","blink","flow"]}},"required":["state"],"title":"ledControlArguments","type":"object"}",
  298.   [](const String& args) -> WebSocketMCP::ToolResponse {
  299.     DynamicJsonDocument doc(256);
  300.     DeserializationError error = deserializeJson(doc, args);
  301.     if (error) {
  302.       return WebSocketMCP::ToolResponse("{"success":false,"error":"无效的参数格式"}", true);
  303.     }
  304.     String state = doc["state"].as<String>();
  305.     DEBUG_SERIAL.println("[工具] LED 控制: " + state);
  306.     // 激活 MCP 控制模式,设置超时(30秒)
  307.     mcpLedActive = true;
  308.     mcpLedTimeout = millis() + 30000;
  309.     // 根据命令设置状态机模式
  310.     if (state == "on") {
  311.       currentLedMode = LED_MODE_ON;
  312.       pixels.fill(pixels.Color(255, 255, 255));
  313.       pixels.show();
  314.     } else if (state == "off") {
  315.       currentLedMode = LED_MODE_OFF;
  316.       pixels.clear();
  317.       pixels.show();
  318.     } else if (state == "blink") {
  319.       currentLedMode = LED_MODE_BLINK;
  320.       blinkState = false;
  321.       blinkCount = 0;
  322.       lastLedStep = millis();
  323.       // 立即熄灭(准备开始闪烁)
  324.       pixels.clear();
  325.       pixels.show();
  326.     } else if (state == "flow") {
  327.       currentLedMode = LED_MODE_FLOW;
  328.       flowIndex = 0;
  329.       lastLedStep = millis();
  330.       // 立即开始第一步(点亮第一个灯)
  331.       pixels.clear();
  332.       pixels.setPixelColor(0, pixels.Color(0, 0, 255));
  333.       pixels.show();
  334.     }
  335.     String resultJson = "{"success":true,"state":"" + state + ""}";
  336.     return WebSocketMCP::ToolResponse(resultJson);
  337.   }
  338. );
  339.   DEBUG_SERIAL.println("[MCP] LED 控制工具已注册");
  340.   // 2. 系统信息工具
  341.   mcpClient.registerTool(
  342.     "system-info",
  343.     "获取掌控板系统信息",
  344.     "{"properties":{},"title":"systemInfoArguments","type":"object"}",
  345.     [](const String& args) -> WebSocketMCP::ToolResponse {
  346.       String chipModel = ESP.getChipModel();
  347.       uint32_t chipId = ESP.getEfuseMac() & 0xFFFFFFFF;
  348.       uint32_t freeHeap = ESP.getFreeHeap() / 1024;
  349.       String ip = WiFi.localIP().toString();
  350.       String resultJson = "{"success":true,"model":"" + chipModel +
  351.                           "","chipId":"" + String(chipId, HEX) +
  352.                           "","freeHeap":" + String(freeHeap) +
  353.                           ","wifiStatus":"" + (WiFi.status() == WL_CONNECTED ? "connected" : "disconnected") +
  354.                           "","ipAddress":"" + ip + ""}";
  355.       return WebSocketMCP::ToolResponse(resultJson);
  356.     }
  357.   );
  358.   DEBUG_SERIAL.println("[MCP] 系统信息工具已注册");
  359.   // 3. 光线传感器工具(直接读取引脚)
  360.   mcpClient.registerTool(
  361.     "light_sensor",
  362.     "读取光线传感器原始值(P4, GPIO39)",
  363.     "{"properties":{},"title":"lightSensorArguments","type":"object"}",
  364.     [](const String& args) -> WebSocketMCP::ToolResponse {
  365.       int value = analogRead(LIGHT_SENSOR_PIN);
  366.       String resultJson = "{"success":true,"value":" + String(value) + "}";
  367.       // 在 OLED 上显示结果摘要
  368.     mcpMessage = "光线传感器: " + String(value) ;
  369.     scrollOffset = 0;
  370.     lastScrollTime = millis();
  371.     messageWidth = u8g2.getUTF8Width(mcpMessage.c_str());
  372.     isScrolling = (messageWidth > 128);
  373.     updateOLED();  // 立即刷新显示
  374.       return WebSocketMCP::ToolResponse(resultJson);
  375.     }
  376.   );
  377.   DEBUG_SERIAL.println("[MCP] 光线传感器工具已注册");
  378.   // 4. 声音传感器工具(直接读取引脚)
  379.   mcpClient.registerTool(
  380.     "sound_sensor",
  381.     "读取声音传感器原始值(P10, GPIO36)",
  382.     "{"properties":{},"title":"soundSensorArguments","type":"object"}",
  383.     [](const String& args) -> WebSocketMCP::ToolResponse {
  384.       int value = analogRead(SOUND_SENSOR_PIN);
  385.       String resultJson = "{"success":true,"value":" + String(value) + "}";
  386.       // 在 OLED 上显示结果摘要
  387.     mcpMessage = "获取的声音传感器的原始值: " + String(value) ;
  388.     scrollOffset = 0;
  389.     lastScrollTime = millis();
  390.     messageWidth = u8g2.getUTF8Width(mcpMessage.c_str());
  391.     isScrolling = (messageWidth > 128);
  392.     updateOLED();  // 立即刷新显示
  393.       return WebSocketMCP::ToolResponse(resultJson);
  394.     }
  395.   );
  396.   
  397.   DEBUG_SERIAL.println("[MCP] 声音传感器工具已注册");
  398. // 5. 红外定位摄像头工具
  399. mcpClient.registerTool(
  400.   "ir_camera",
  401.   "读取红外定位摄像头数据 (最多4个点, 无效点坐标为1023)",
  402.   "{"properties":{},"title":"irCameraArguments","type":"object"}",
  403.   [](const String& args) -> WebSocketMCP::ToolResponse {
  404.     DynamicJsonDocument doc(1024); // 稍大的缓冲区用于存放4个点
  405.     if (readIRCamera()) {
  406.       // 构建包含所有点的 JSON 数组
  407.       JsonArray points = doc.createNestedArray("points");
  408.       for (int i = 0; i < 4; i++) {
  409.         JsonObject point = points.createNestedObject();
  410.         point["x"] = irX[i];
  411.         point["y"] = irY[i];
  412.       }
  413.       doc["success"] = true;
  414.     } else {
  415.       doc["success"] = false;
  416.       doc["error"] = "I2C 读取失败";
  417.     }
  418.     String resultJson;
  419.     serializeJson(doc, resultJson);
  420.     return WebSocketMCP::ToolResponse(resultJson, !doc["success"].as<bool>());
  421.   }
  422. );
  423. DEBUG_SERIAL.println("[MCP] 红外定位摄像头工具已注册");
  424.   DEBUG_SERIAL.println("[MCP] 工具注册完成,共 " + String(mcpClient.getToolCount()) + " 个工具");
  425. } // <--- 确保这里有一个闭合大括号,与开头的 void registerMcpTools() { 匹配
  426. /********** 处理串口命令 ***********/
  427. #define MAX_INPUT_LENGTH 1024
  428. char inputBuffer[MAX_INPUT_LENGTH];
  429. int inputBufferIndex = 0;
  430. void processSerialCommands() {
  431.   while (DEBUG_SERIAL.available() > 0) {
  432.     char inChar = (char)DEBUG_SERIAL.read();
  433.     if (inChar == '\n' || inChar == '\r') {
  434.       if (inputBufferIndex > 0) {
  435.         inputBuffer[inputBufferIndex] = '\0';
  436.         String command = String(inputBuffer);
  437.         command.trim();
  438.         if (command.length() > 0) {
  439.           if (command == "help") {
  440.             printHelp();
  441.           } else if (command == "status") {
  442.             printStatus();
  443.           } else if (command == "reconnect") {
  444.             DEBUG_SERIAL.println("正在重新连接 MCP 服务器...");
  445.             mcpClient.disconnect();
  446.           } else if (command == "tools") {
  447.             DEBUG_SERIAL.println("已注册工具数量: " + String(mcpClient.getToolCount()));
  448.           } else {
  449.             if (mcpClient.isConnected()) {
  450.               mcpClient.sendMessage(command);
  451.               DEBUG_SERIAL.println("[发送] " + command);
  452.             } else {
  453.               DEBUG_SERIAL.println("未连接到 MCP 服务器,无法发送命令");
  454.             }
  455.           }
  456.         }
  457.         inputBufferIndex = 0;
  458.       }
  459.     } else if (inChar == '\b' || inChar == 127) {
  460.       if (inputBufferIndex > 0) {
  461.         inputBufferIndex--;
  462.         DEBUG_SERIAL.print("\b \b");
  463.       }
  464.     } else if (inputBufferIndex < MAX_INPUT_LENGTH - 1) {
  465.       inputBuffer[inputBufferIndex++] = inChar;
  466.       DEBUG_SERIAL.print(inChar);
  467.     }
  468.   }
  469. }
  470. void printHelp() {
  471.   DEBUG_SERIAL.println("可用命令:");
  472.   DEBUG_SERIAL.println("  help     - 显示此帮助");
  473.   DEBUG_SERIAL.println("  status   - 显示当前连接状态");
  474.   DEBUG_SERIAL.println("  reconnect- 重新连接 MCP 服务器");
  475.   DEBUG_SERIAL.println("  tools    - 查看已注册工具");
  476.   DEBUG_SERIAL.println("  其他内容将直接发送到 MCP 服务器");
  477. }
  478. void printStatus() {
  479.   DEBUG_SERIAL.println("当前状态:");
  480.   DEBUG_SERIAL.print("  WiFi: ");
  481.   DEBUG_SERIAL.println(wifiConnected ? "已连接" : "未连接");
  482.   if (wifiConnected) {
  483.     DEBUG_SERIAL.print("  IP 地址: ");
  484.     DEBUG_SERIAL.println(WiFi.localIP());
  485.     DEBUG_SERIAL.print("  信号强度: ");
  486.     DEBUG_SERIAL.println(WiFi.RSSI());
  487.   }
  488.   DEBUG_SERIAL.print("  MCP 服务器: ");
  489.   DEBUG_SERIAL.println(mcpConnected ? "已连接" : "未连接");
  490. }
  491. /********** OLED 显示更新(支持滚动消息)***********/
  492. void updateOLED() {
  493.   static String lastWifiStatus = "";
  494.   static String lastMCPStatus = "";
  495.   static String lastIP = "";
  496.   static String lastMessage = "";
  497.   String currentWifiStatus = wifiConnected ? "已连接" : "未连接";
  498.   String currentMCPStatus = mcpConnected ? "已连接" : "未连接";
  499.   String currentIP = wifiConnected ? WiFi.localIP().toString() : "0.0.0.0";
  500.   bool stateChanged = (currentWifiStatus != lastWifiStatus) ||
  501.                       (currentMCPStatus != lastMCPStatus) ||
  502.                       (currentIP != lastIP) ||
  503.                       (mcpMessage != lastMessage); // 消息内容变化
  504.   // 如果正在滚动或状态变化,则重绘
  505.   if (stateChanged || isScrolling) {
  506.     u8g2.clearBuffer();
  507.     // 绘制前三行
  508.     u8g2.setCursor(0, 15);
  509.     u8g2.print("WiFi: " + currentWifiStatus);
  510.     u8g2.setCursor(0, 30);
  511.     u8g2.print("IP: " + currentIP);
  512.     u8g2.setCursor(0, 45);
  513.     u8g2.print("MCP: " + currentMCPStatus);
  514.     // 绘制第四行(消息滚动)
  515.     if (mcpMessage.length() > 0) {
  516.       // 设置裁剪窗口,限定在第四行区域(y从53到63,根据字体高度调整)
  517.       u8g2.setClipWindow(0, 45, 127, 63);
  518.       u8g2.setCursor(0 - scrollOffset, 60);
  519.       u8g2.print(mcpMessage);
  520.       u8g2.setClipWindow(0, 0, 127, 63); // 恢复全屏裁剪
  521.     }
  522.     u8g2.sendBuffer();
  523.     lastWifiStatus = currentWifiStatus;
  524.     lastMCPStatus = currentMCPStatus;
  525.     lastIP = currentIP;
  526.     lastMessage = mcpMessage;
  527.   }
  528. }
  529. /**
  530. * LED 状态机处理函数
  531. * 在 loop 中定期调用,根据当前模式执行非阻塞操作
  532. */
  533. void handleLedStateMachine() {
  534.   // 仅当 MCP 控制模式激活且未超时时,才处理状态机
  535.   if (!mcpLedActive || millis() >= mcpLedTimeout) {
  536.     // 如果超时,恢复空闲模式,交给 handleLEDs 处理
  537.     if (currentLedMode != LED_MODE_IDLE) {
  538.       currentLedMode = LED_MODE_IDLE;
  539.       // 强制熄灭,让 handleLEDs 接管
  540.       pixels.clear();
  541.       pixels.show();
  542.     }
  543.     return;
  544.   }
  545.   unsigned long now = millis();
  546.   switch (currentLedMode) {
  547.     case LED_MODE_ON:
  548.     case LED_MODE_OFF:
  549.       // 常亮/常灭模式无需额外处理,已在工具回调中设置
  550.       break;
  551.     case LED_MODE_BLINK:
  552.       if (now - lastLedStep >= blinkDelay) {
  553.         lastLedStep = now;
  554.         if (!blinkState) {
  555.           // 点亮
  556.           pixels.fill(pixels.Color(255, 255, 255));
  557.           blinkState = true;
  558.         } else {
  559.           // 熄灭
  560.           pixels.clear();
  561.           blinkState = false;
  562.           blinkCount++;
  563.         }
  564.         pixels.show();
  565.         // 如果闪烁次数完成,退出 MCP 控制模式(可选:让超时自然结束,或立即返回空闲)
  566.         if (blinkCount >= blinkTotal) {
  567.           // 闪烁完成,可以立即结束 MCP 控制,恢复状态指示
  568.           mcpLedActive = false;
  569.           currentLedMode = LED_MODE_IDLE;
  570.           // 确保灯灭,让 handleLEDs 接管
  571.           pixels.clear();
  572.           pixels.show();
  573.         }
  574.       }
  575.       break;
  576.     case LED_MODE_FLOW:
  577.       if (now - lastLedStep >= flowDelay) {
  578.         lastLedStep = now;
  579.         // 移动到下一个灯珠
  580.         flowIndex++;
  581.         if (flowIndex < NUM_PIXELS) {
  582.           pixels.clear();
  583.           pixels.setPixelColor(flowIndex, pixels.Color(0, 0, 255));
  584.           pixels.show();
  585.         } else {
  586.           // 流水结束,退出 MCP 控制模式
  587.           mcpLedActive = false;
  588.           currentLedMode = LED_MODE_IDLE;
  589.           pixels.clear();
  590.           pixels.show();
  591.         }
  592.       }
  593.       break;
  594.     case LED_MODE_IDLE:
  595.     default:
  596.       break;
  597.   }
  598. }
  599. /********** WS2812 灯带状态指示(考虑 MCP 工具覆盖)***********/
  600. void handleLEDs() {
  601.   // 如果处于 MCP 工具控制模式且未超时,则跳过状态指示,交由状态机处理
  602.   if (mcpLedActive && millis() < mcpLedTimeout) {
  603.     return;  // 状态机已在别处运行
  604.   }
  605.   // 确保 MCP 控制标志被清除(超时后)
  606.   mcpLedActive = false;
  607.   currentLedMode = LED_MODE_IDLE;  // 确保模式复位
  608.   static unsigned long lastUpdate = 0;
  609.   static bool ledState = false;
  610.   uint32_t targetColor = 0;
  611.   unsigned long interval = 0;
  612.   if (!wifiConnected) {
  613.     targetColor = pixels.Color(255, 0, 0); // 红色
  614.     interval = 100;                         // 快速闪烁
  615.   } else if (!mcpConnected) {
  616.     targetColor = pixels.Color(0, 255, 0); // 绿色
  617.     interval = 500;                         // 慢速闪烁
  618.   } else {
  619.     targetColor = pixels.Color(0, 0, 255); // 蓝色
  620.     interval = 0;                            // 常亮
  621.   }
  622.   if (interval == 0) {
  623.     // 常亮
  624.     pixels.fill(targetColor);
  625.     pixels.show();
  626.   } else {
  627.     // 非阻塞闪烁
  628.     if (millis() - lastUpdate > interval) {
  629.       lastUpdate = millis();
  630.       ledState = !ledState;
  631.       if (ledState) {
  632.         pixels.fill(targetColor);
  633.       } else {
  634.         pixels.clear();
  635.       }
  636.       pixels.show();
  637.     }
  638.   }
  639. }
复制代码


【参考资料】

       行空板K10产品页:https://www.dfrobot.com.cn/goods-4014.html
       DFRobot SEN0158红外定位摄像头wiki:https://wiki.dfrobot.com.cn/_SKU_SEN0158_Positioning_IR_Camera
       小智AI官网:https://xiaozhi.me
       掌控板官网:https://www.mpython.cn
       如果你有任何问题或改进建议,欢迎在评论区留言交流!



您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2026 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail