本帖最后由 云天 于 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数据线(用于供电和程序上传)
注:掌控板板载资源包括: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请求。掌控板解析请求、执行函数、返回结果。整个过程无缝衔接。
这样一来,我们实现了“语音 → 云端理解 → 调用硬件”的完整闭环。
【 掌控板端代码解析】
掌控板端程序是整个项目的核心。我们使用Arduino IDE开发,依赖以下库:
WebSocketMCP:用于实现MCP服务器
Adafruit_NeoPixel:控制WS2812灯带
U8g2:驱动OLED显示屏
ArduinoJson:解析和处理JSON数据
1. MCP服务器初始化与工具注册
在 `setup()` 中,我们初始化WiFi、OLED、传感器,然后启动MCP客户端(这里实际上是服务器模式,但库命名为client,我们沿用其API):
- mcpClient.begin(MCP_ENDPOINT, onMcpConnectionChange);
复制代码
`MCP_ENDPOINT` 是你在小智AI平台申请到的专属接入点地址,包含了你的用户和智能体信息。连接成功后,会调用 `registerMcpTools()` 注册所有工具。
每个工具注册时需提供名称、描述、输入参数JSON schema和一个回调函数。以LED控制工具为例:
- mcpClient.registerTool(
-
- "led_blink",
-
- "控制 WS2812 灯带 (on/off/blink/flow)",
-
- "{"properties":{"state":{"title":"LED状态","type":"string","enum":["on","off","blink","flow"]}},"required":["state"],"title":"ledControlArguments","type":"object"}",
-
- [](const String& args) -> WebSocketMCP::ToolResponse {
-
- // 解析参数,执行控制逻辑,返回结果
-
- }
-
- );
复制代码 类似的,我们注册了 `light_sensor`、`sound_sensor` 和 `ir_camera` 工具。
2. 非阻塞LED状态机
为了在LED闪烁或流水灯时不影响MCP通信和OLED刷新,我们设计了状态机。在 `loop()` 中定期调用 `handleLedStateMachine()`,根据当前模式(`LED_MODE_ON`、`LED_MODE_BLINK`、`LED_MODE_FLOW`)逐步更新灯带,避免使用 `delay()`。
- void handleLedStateMachine() {
- // 仅在MCP控制激活且未超时时执行
- switch (currentLedMode) {
- case LED_MODE_BLINK:
- // 每200ms翻转一次,计数5次后结束
- break;
- case LED_MODE_FLOW:
- // 每300ms点亮下一个灯珠,循环3次
- break;
- // ...
- }
- }
复制代码
3. OLED实时信息显示
OLED分四行显示:WiFi状态、IP地址、MCP连接状态、最新MCP消息(支持滚动)。当收到工具调用结果时,`onMcpOutput` 会被触发(实际在库中未用,我们改为在工具回调中直接更新显示),但这里我们简化设计:每次工具执行后,将返回的摘要信息赋值给 `mcpMessage`,并开启滚动标志。
4. 红外定位摄像头读取
SEN0158红外摄像头通过I2C接口连接。我们编写了 `initIRCamera()` 和 `readIRCamera()` 函数,完全参照官方示例代码。在 `ir_camera` 工具的回调中,调用 `readIRCamera()` 读取四个点的坐标,打包成JSON返回:
- {
- "success": true,
- "points": [
- {"x": 123, "y": 456},
- {"x": 1023, "y": 1023},
- // ...
- ]
- }
复制代码 无效点坐标为 `(1023, 1023)`,可根据需要过滤。
红外源(使用红外发射二极管串联 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)、执行器(电机、舵机),甚至让多个掌控板同时在线,打造一个全屋智能语音控制系统。期待你的创意!
【附:完整代码】
- /*
- 掌控板 MCP 客户端 + WS2812 灯带 + OLED 显示屏+IO扩展展+红外定位摄像头模块
- 功能:
- - 连接 WiFi
- - 连接 WebSocket MCP 服务器
- - 通过 WS2812 灯带指示连接状态(红色快闪:WiFi未连;绿色慢闪:WiFi已连但MCP未连;蓝色常亮:全部连接)
- - OLED 显示当前 WiFi 状态、IP 地址、MCP 状态
- - OLED 第四行滚动显示最新的 MCP 消息
- - 通过串口发送命令给 MCP 服务器
- - 注册 MCP 工具:
- * LED 控制(led_blink)
- * 系统信息(system-info)
- * 光线传感器(light_sensor)——直接读取 P4 (GPIO36)
- * 声音传感器(sound_sensor)——直接读取 P10 (GPIO26)
- *红外定位摄像头模块(Positioning IR Camera)-i2c(SCL 22,SDA 23)
- - 解决 MCP 工具与状态指示灯的冲突(MCP 工具控制期间暂停状态指示,超时后恢复)
- */
-
- #include <Arduino.h>
- #include <WiFi.h>
- #include <WebSocketMCP.h> // 请确保已安装此库
- #include <Adafruit_NeoPixel.h> // WS2812 库
- #include <U8g2lib.h> // OLED 库
- #include <Wire.h>
-
- /********** 配置项 ***********/
- // WiFi 设置
- const char* WIFI_SSID = "**********";
- const char* WIFI_PASS = "**********";
-
- // WebSocket MCP 服务器地址(请替换为你的有效 token)
- const char* MCP_ENDPOINT = "wss://api.xiaozhi.me/mcp/?token=你自己的token";
-
- // 调试串口
- #define DEBUG_SERIAL Serial
- #define DEBUG_BAUD_RATE 115200
-
- /********** 硬件引脚定义 ***********/
- // WS2812 灯带(掌控板上通常接在 GPIO17,三个灯珠)
- #define WS2812_PIN 17
- #define NUM_PIXELS 3
- Adafruit_NeoPixel pixels(NUM_PIXELS, WS2812_PIN, NEO_GRB + NEO_KHZ800);
-
- // OLED 显示屏(掌控板固定 I2C 引脚:SCL=22, SDA=23)
- #define OLED_SCL 22
- #define OLED_SDA 23
- // 使用硬件 I2C,驱动为 SH1106(若实际为 SSD1306 可修改构造函数)
- U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL, OLED_SDA);
-
- // 传感器引脚定义
- #define LIGHT_SENSOR_PIN 39 // 光线传感器接 P4 (GPIO36, ADC1_CH3)
- #define SOUND_SENSOR_PIN 36 // 声音传感器接 P10 (GPIO26, ADC2_CH0)
- // 新增:数字激光接近传感器接 P2 (GPIO35)
- #define PROXIMITY_SENSOR_PIN 35
- // 红外定位摄像头模块 (SEN0158) 相关定义
- #define IRCAM_I2C_ADDR 0x58 // I2C 设备地址 (0xB0 右移一位)
- #define IRCAM_DATA_LEN 16 // 一次读取的数据长度
- byte irCamData[IRCAM_DATA_LEN]; // 存储原始数据的缓冲区
-
- // 存储最多4个点的坐标,无效点坐标通常为 1023,1023
- int irX[4] = {1023, 1023, 1023, 1023};
- int irY[4] = {1023, 1023, 1023, 1023};
- /********** 全局变量 ***********/
- WebSocketMCP mcpClient; // MCP 客户端对象
- // LED 模式枚举
- enum LedMode {
- LED_MODE_IDLE, // 空闲,由系统状态指示灯控制
- LED_MODE_ON, // 常亮(白色)
- LED_MODE_OFF, // 熄灭
- LED_MODE_BLINK, // 闪烁(白色,5次)
- LED_MODE_FLOW // 流水灯(蓝色,单次)
- };
- // LED 状态机变量
- LedMode currentLedMode = LED_MODE_IDLE; // 当前模式
- int flowIndex = 0; // 流水灯当前索引
- unsigned long lastLedStep = 0; // 上次步骤时间
- bool blinkState = false; // 闪烁当前亮灭状态
- int blinkCount = 0; // 闪烁已进行次数
- const int blinkTotal = 5; // 闪烁总次数
- const int flowDelay = 300; // 流水灯步进间隔(毫秒)
- const int blinkDelay = 200; // 闪烁间隔(毫秒)
- // 连接状态
- bool wifiConnected = false;
- bool mcpConnected = false;
-
- // MCP 工具主动控制 LED 的标志(用于解决冲突)
- bool mcpLedActive = false; // 当前是否由 MCP 工具控制 LED
- unsigned long mcpLedTimeout = 0; // 控制模式的超时时间(毫秒)
-
- // OLED 上次显示的状态(用于避免频繁刷新)
- String lastWifiStatus = "";
- String lastMCPStatus = "";
- String lastIP = "";
-
- // MCP 消息滚动相关
- String mcpMessage = ""; // 最新 MCP 消息
- int scrollOffset = 0; // 滚动偏移(像素)
- unsigned long lastScrollTime = 0; // 上次滚动时间
- const int scrollInterval = 300; // 滚动间隔(毫秒)
- bool isScrolling = false; // 是否正在滚动
- int messageWidth = 0; // 消息总像素宽度
-
- /********** 函数声明 ***********/
- void setupWifi();
- void onMcpOutput(const String &message);
- void onMcpError(const String &error);
- void onMcpConnectionChange(bool connected);
- void registerMcpTools();
- void processSerialCommands();
- void updateOLED(); // 更新 OLED 显示
- void handleLEDs(); // 处理 WS2812 状态指示(考虑 MCP 覆盖)
- void printHelp();
- void printStatus();
-
- /********** setup() ***********/
- void setup() {
- DEBUG_SERIAL.begin(DEBUG_BAUD_RATE);
- DEBUG_SERIAL.println("\n\n======== 掌控板 MCP 客户端启动 ========");
-
- // 初始化 WS2812 灯带
- pixels.begin();
- pixels.clear();
- pixels.show();
-
- // 初始化 OLED 显示屏
- Wire.begin(OLED_SDA, OLED_SCL); // 启动 I2C 总线
- u8g2.begin();
- u8g2.enableUTF8Print(); // 启用 UTF-8 打印(如需显示中文)
- u8g2.setFont(u8g2_font_wqy16_t_gb2312b); // 使用中文字体(需库支持,若没有可换英文字体)
- u8g2.clearBuffer();
- u8g2.setCursor(0, 15);
- u8g2.print("系统初始化...");
- u8g2.sendBuffer();
- initIRCamera(); // <--- 新增:初始化红外摄像头
- // 连接 WiFi
- setupWifi();
-
- // 初始化 MCP 客户端
- if (mcpClient.begin(MCP_ENDPOINT, onMcpConnectionChange)) {
- DEBUG_SERIAL.println("[MCP] 客户端初始化成功,正在连接服务器...");
- } else {
- DEBUG_SERIAL.println("[MCP] 客户端初始化失败!");
- }
-
- // 显示帮助信息
- DEBUG_SERIAL.println("\n使用说明:");
- DEBUG_SERIAL.println("- 通过串口输入命令并回车发送给 MCP 服务器");
- DEBUG_SERIAL.println("- 输入 help 查看可用命令");
- DEBUG_SERIAL.println();
-
- // 短暂显示启动信息后,进入主循环
- delay(1000);
- }
-
- /********** loop() ***********/
- void loop() {
- mcpClient.loop();
- processSerialCommands();
-
- // 处理 LED 状态机(非阻塞效果)
- handleLedStateMachine();
-
- // 更新 WS2812 状态指示灯(仅在非 MCP 控制时生效)
- handleLEDs();
-
- // 滚动更新:如果正在滚动且达到时间间隔,增加偏移并刷新显示
- if (isScrolling && (millis() - lastScrollTime > scrollInterval)) {
- lastScrollTime = millis();
- scrollOffset += 2; // 每次滚动2像素,可根据需要调整速度
- if (scrollOffset > messageWidth) {
- scrollOffset = 0; // 循环滚动
- }
- // 强制刷新 OLED(因为状态未变,但 isScrolling 为 true,updateOLED 会重绘)
- updateOLED();
- }
-
- // 更新 OLED 显示(当状态变化或需要滚动时,updateOLED 内部判断)
- updateOLED();
-
- // 小延时,避免过度占用 CPU
- delay(10);
- }
- /**
- * 初始化红外定位摄像头
- * 参考 SEN0158 Wiki 示例代码
- */
- void initIRCamera() {
- DEBUG_SERIAL.println("[IR Camera] 正在初始化...");
- Wire.beginTransmission(IRCAM_I2C_ADDR);
- Wire.write(0x30); Wire.write(0x01); delay(10);
- Wire.endTransmission();
-
- Wire.beginTransmission(IRCAM_I2C_ADDR);
- Wire.write(0x30); Wire.write(0x08); delay(10);
- Wire.endTransmission();
-
- Wire.beginTransmission(IRCAM_I2C_ADDR);
- Wire.write(0x06); Wire.write(0x90); delay(10);
- Wire.endTransmission();
-
- Wire.beginTransmission(IRCAM_I2C_ADDR);
- Wire.write(0x08); Wire.write(0xC0); delay(10);
- Wire.endTransmission();
-
- Wire.beginTransmission(IRCAM_I2C_ADDR);
- Wire.write(0x1A); Wire.write(0x40); delay(10);
- Wire.endTransmission();
-
- Wire.beginTransmission(IRCAM_I2C_ADDR);
- Wire.write(0x33); Wire.write(0x33); delay(10);
- Wire.endTransmission();
-
- delay(100);
- DEBUG_SERIAL.println("[IR Camera] 初始化完成");
- }
- /**
- * 读取红外摄像头数据并解析出最多4个点的坐标
- * 返回 true 表示读取成功(即使没有检测到点),false 表示 I2C 通信失败
- */
- bool readIRCamera() {
- // 请求读取数据
- Wire.beginTransmission(IRCAM_I2C_ADDR);
- Wire.write(0x36);
- if (Wire.endTransmission() != 0) {
- return false; // I2C 通信错误
- }
-
- // 请求 16 字节数据
- uint8_t bytesReceived = Wire.requestFrom(IRCAM_I2C_ADDR, IRCAM_DATA_LEN);
- if (bytesReceived < IRCAM_DATA_LEN) {
- return false; // 数据长度不足
- }
-
- // 读取原始数据到缓冲区
- for (int i = 0; i < IRCAM_DATA_LEN; i++) {
- irCamData[i] = Wire.read();
- }
-
- // --- 解析坐标 (完全按照示例代码的逻辑) ---
- int s;
- // 点 0
- irX[0] = irCamData[1];
- irY[0] = irCamData[2];
- s = irCamData[3];
- irX[0] += (s & 0x30) << 4;
- irY[0] += (s & 0xC0) << 2;
-
- // 点 1
- irX[1] = irCamData[4];
- irY[1] = irCamData[5];
- s = irCamData[6];
- irX[1] += (s & 0x30) << 4;
- irY[1] += (s & 0xC0) << 2;
-
- // 点 2
- irX[2] = irCamData[7];
- irY[2] = irCamData[8];
- s = irCamData[9];
- irX[2] += (s & 0x30) << 4;
- irY[2] += (s & 0xC0) << 2;
-
- // 点 3
- irX[3] = irCamData[10];
- irY[3] = irCamData[11];
- s = irCamData[12];
- irX[3] += (s & 0x30) << 4;
- irY[3] += (s & 0xC0) << 2;
-
- return true;
- }
- /********** WiFi 连接 ***********/
- void setupWifi() {
- DEBUG_SERIAL.print("[WiFi] 正在连接 ");
- DEBUG_SERIAL.println(WIFI_SSID);
-
- WiFi.mode(WIFI_STA);
- WiFi.begin(WIFI_SSID, WIFI_PASS);
-
- int attempts = 0;
- while (WiFi.status() != WL_CONNECTED && attempts < 40) { // 最多等待 20 秒
- delay(500);
- DEBUG_SERIAL.print(".");
- attempts++;
- }
-
- if (WiFi.status() == WL_CONNECTED) {
- wifiConnected = true;
- DEBUG_SERIAL.println("\n[WiFi] 连接成功!");
- DEBUG_SERIAL.print("[WiFi] IP 地址: ");
- DEBUG_SERIAL.println(WiFi.localIP());
- } else {
- wifiConnected = false;
- DEBUG_SERIAL.println("\n[WiFi] 连接失败!将继续尝试...");
- }
- }
-
- /********** MCP 回调函数 ***********/
- void onMcpOutput(const String &message) {
- DEBUG_SERIAL.print("[MCP 输出] ");
- DEBUG_SERIAL.println(message);
- // 更新消息并重置滚动
- mcpMessage = message;
- scrollOffset = 0;
- lastScrollTime = millis();
- // 计算消息宽度
- messageWidth = u8g2.getUTF8Width(mcpMessage.c_str());
- isScrolling = (messageWidth > 128); // 如果宽度超过屏幕宽度则滚动
- // 强制刷新显示
- updateOLED();
- }
-
- void onMcpError(const String &error) {
- DEBUG_SERIAL.print("[MCP 错误] ");
- DEBUG_SERIAL.println(error);
- }
-
- void onMcpConnectionChange(bool connected) {
- mcpConnected = connected;
- if (connected) {
- DEBUG_SERIAL.println("[MCP] 已连接到服务器");
- registerMcpTools(); // 连接成功后注册工具
- } else {
- DEBUG_SERIAL.println("[MCP] 与服务器断开连接");
- }
- }
-
- /********** 注册 MCP 工具(注意检查此函数的闭合括号)***********/
- void registerMcpTools() {
- DEBUG_SERIAL.println("[MCP] 正在注册工具...");
-
- // 1. LED 控制工具(解决与状态指示的冲突)
- // 1. LED 控制工具(已增加 "flow" 流水灯模式)
- mcpClient.registerTool(
- "led_blink",
- "控制 WS2812 灯带 (on/off/blink/flow)",
- "{"properties":{"state":{"title":"LED状态","type":"string","enum":["on","off","blink","flow"]}},"required":["state"],"title":"ledControlArguments","type":"object"}",
- [](const String& args) -> WebSocketMCP::ToolResponse {
- DynamicJsonDocument doc(256);
- DeserializationError error = deserializeJson(doc, args);
- if (error) {
- return WebSocketMCP::ToolResponse("{"success":false,"error":"无效的参数格式"}", true);
- }
-
- String state = doc["state"].as<String>();
- DEBUG_SERIAL.println("[工具] LED 控制: " + state);
-
- // 激活 MCP 控制模式,设置超时(30秒)
- mcpLedActive = true;
- mcpLedTimeout = millis() + 30000;
-
- // 根据命令设置状态机模式
- if (state == "on") {
- currentLedMode = LED_MODE_ON;
- pixels.fill(pixels.Color(255, 255, 255));
- pixels.show();
- } else if (state == "off") {
- currentLedMode = LED_MODE_OFF;
- pixels.clear();
- pixels.show();
- } else if (state == "blink") {
- currentLedMode = LED_MODE_BLINK;
- blinkState = false;
- blinkCount = 0;
- lastLedStep = millis();
- // 立即熄灭(准备开始闪烁)
- pixels.clear();
- pixels.show();
- } else if (state == "flow") {
- currentLedMode = LED_MODE_FLOW;
- flowIndex = 0;
- lastLedStep = millis();
- // 立即开始第一步(点亮第一个灯)
- pixels.clear();
- pixels.setPixelColor(0, pixels.Color(0, 0, 255));
- pixels.show();
- }
-
- String resultJson = "{"success":true,"state":"" + state + ""}";
- return WebSocketMCP::ToolResponse(resultJson);
- }
- );
- DEBUG_SERIAL.println("[MCP] LED 控制工具已注册");
-
- // 2. 系统信息工具
- mcpClient.registerTool(
- "system-info",
- "获取掌控板系统信息",
- "{"properties":{},"title":"systemInfoArguments","type":"object"}",
- [](const String& args) -> WebSocketMCP::ToolResponse {
- String chipModel = ESP.getChipModel();
- uint32_t chipId = ESP.getEfuseMac() & 0xFFFFFFFF;
- uint32_t freeHeap = ESP.getFreeHeap() / 1024;
- String ip = WiFi.localIP().toString();
-
- String resultJson = "{"success":true,"model":"" + chipModel +
- "","chipId":"" + String(chipId, HEX) +
- "","freeHeap":" + String(freeHeap) +
- ","wifiStatus":"" + (WiFi.status() == WL_CONNECTED ? "connected" : "disconnected") +
- "","ipAddress":"" + ip + ""}";
- return WebSocketMCP::ToolResponse(resultJson);
- }
- );
- DEBUG_SERIAL.println("[MCP] 系统信息工具已注册");
-
- // 3. 光线传感器工具(直接读取引脚)
- mcpClient.registerTool(
- "light_sensor",
- "读取光线传感器原始值(P4, GPIO39)",
- "{"properties":{},"title":"lightSensorArguments","type":"object"}",
- [](const String& args) -> WebSocketMCP::ToolResponse {
- int value = analogRead(LIGHT_SENSOR_PIN);
- String resultJson = "{"success":true,"value":" + String(value) + "}";
- // 在 OLED 上显示结果摘要
- mcpMessage = "光线传感器: " + String(value) ;
- scrollOffset = 0;
- lastScrollTime = millis();
- messageWidth = u8g2.getUTF8Width(mcpMessage.c_str());
- isScrolling = (messageWidth > 128);
- updateOLED(); // 立即刷新显示
- return WebSocketMCP::ToolResponse(resultJson);
- }
- );
- DEBUG_SERIAL.println("[MCP] 光线传感器工具已注册");
-
- // 4. 声音传感器工具(直接读取引脚)
- mcpClient.registerTool(
- "sound_sensor",
- "读取声音传感器原始值(P10, GPIO36)",
- "{"properties":{},"title":"soundSensorArguments","type":"object"}",
- [](const String& args) -> WebSocketMCP::ToolResponse {
- int value = analogRead(SOUND_SENSOR_PIN);
- String resultJson = "{"success":true,"value":" + String(value) + "}";
- // 在 OLED 上显示结果摘要
- mcpMessage = "获取的声音传感器的原始值: " + String(value) ;
- scrollOffset = 0;
- lastScrollTime = millis();
- messageWidth = u8g2.getUTF8Width(mcpMessage.c_str());
- isScrolling = (messageWidth > 128);
- updateOLED(); // 立即刷新显示
- return WebSocketMCP::ToolResponse(resultJson);
- }
- );
-
- DEBUG_SERIAL.println("[MCP] 声音传感器工具已注册");
- // 5. 红外定位摄像头工具
- mcpClient.registerTool(
- "ir_camera",
- "读取红外定位摄像头数据 (最多4个点, 无效点坐标为1023)",
- "{"properties":{},"title":"irCameraArguments","type":"object"}",
- [](const String& args) -> WebSocketMCP::ToolResponse {
- DynamicJsonDocument doc(1024); // 稍大的缓冲区用于存放4个点
-
- if (readIRCamera()) {
- // 构建包含所有点的 JSON 数组
- JsonArray points = doc.createNestedArray("points");
- for (int i = 0; i < 4; i++) {
- JsonObject point = points.createNestedObject();
- point["x"] = irX[i];
- point["y"] = irY[i];
- }
- doc["success"] = true;
- } else {
- doc["success"] = false;
- doc["error"] = "I2C 读取失败";
- }
-
- String resultJson;
- serializeJson(doc, resultJson);
- return WebSocketMCP::ToolResponse(resultJson, !doc["success"].as<bool>());
- }
- );
- DEBUG_SERIAL.println("[MCP] 红外定位摄像头工具已注册");
-
- DEBUG_SERIAL.println("[MCP] 工具注册完成,共 " + String(mcpClient.getToolCount()) + " 个工具");
- } // <--- 确保这里有一个闭合大括号,与开头的 void registerMcpTools() { 匹配
-
- /********** 处理串口命令 ***********/
- #define MAX_INPUT_LENGTH 1024
- char inputBuffer[MAX_INPUT_LENGTH];
- int inputBufferIndex = 0;
-
- void processSerialCommands() {
- while (DEBUG_SERIAL.available() > 0) {
- char inChar = (char)DEBUG_SERIAL.read();
-
- if (inChar == '\n' || inChar == '\r') {
- if (inputBufferIndex > 0) {
- inputBuffer[inputBufferIndex] = '\0';
- String command = String(inputBuffer);
- command.trim();
-
- if (command.length() > 0) {
- if (command == "help") {
- printHelp();
- } else if (command == "status") {
- printStatus();
- } else if (command == "reconnect") {
- DEBUG_SERIAL.println("正在重新连接 MCP 服务器...");
- mcpClient.disconnect();
- } else if (command == "tools") {
- DEBUG_SERIAL.println("已注册工具数量: " + String(mcpClient.getToolCount()));
- } else {
- if (mcpClient.isConnected()) {
- mcpClient.sendMessage(command);
- DEBUG_SERIAL.println("[发送] " + command);
- } else {
- DEBUG_SERIAL.println("未连接到 MCP 服务器,无法发送命令");
- }
- }
- }
- inputBufferIndex = 0;
- }
- } else if (inChar == '\b' || inChar == 127) {
- if (inputBufferIndex > 0) {
- inputBufferIndex--;
- DEBUG_SERIAL.print("\b \b");
- }
- } else if (inputBufferIndex < MAX_INPUT_LENGTH - 1) {
- inputBuffer[inputBufferIndex++] = inChar;
- DEBUG_SERIAL.print(inChar);
- }
- }
- }
-
- void printHelp() {
- DEBUG_SERIAL.println("可用命令:");
- DEBUG_SERIAL.println(" help - 显示此帮助");
- DEBUG_SERIAL.println(" status - 显示当前连接状态");
- DEBUG_SERIAL.println(" reconnect- 重新连接 MCP 服务器");
- DEBUG_SERIAL.println(" tools - 查看已注册工具");
- DEBUG_SERIAL.println(" 其他内容将直接发送到 MCP 服务器");
- }
-
- void printStatus() {
- DEBUG_SERIAL.println("当前状态:");
- DEBUG_SERIAL.print(" WiFi: ");
- DEBUG_SERIAL.println(wifiConnected ? "已连接" : "未连接");
- if (wifiConnected) {
- DEBUG_SERIAL.print(" IP 地址: ");
- DEBUG_SERIAL.println(WiFi.localIP());
- DEBUG_SERIAL.print(" 信号强度: ");
- DEBUG_SERIAL.println(WiFi.RSSI());
- }
- DEBUG_SERIAL.print(" MCP 服务器: ");
- DEBUG_SERIAL.println(mcpConnected ? "已连接" : "未连接");
- }
-
- /********** OLED 显示更新(支持滚动消息)***********/
- void updateOLED() {
- static String lastWifiStatus = "";
- static String lastMCPStatus = "";
- static String lastIP = "";
- static String lastMessage = "";
-
- String currentWifiStatus = wifiConnected ? "已连接" : "未连接";
- String currentMCPStatus = mcpConnected ? "已连接" : "未连接";
- String currentIP = wifiConnected ? WiFi.localIP().toString() : "0.0.0.0";
-
- bool stateChanged = (currentWifiStatus != lastWifiStatus) ||
- (currentMCPStatus != lastMCPStatus) ||
- (currentIP != lastIP) ||
- (mcpMessage != lastMessage); // 消息内容变化
-
- // 如果正在滚动或状态变化,则重绘
- if (stateChanged || isScrolling) {
- u8g2.clearBuffer();
-
- // 绘制前三行
- u8g2.setCursor(0, 15);
- u8g2.print("WiFi: " + currentWifiStatus);
- u8g2.setCursor(0, 30);
- u8g2.print("IP: " + currentIP);
- u8g2.setCursor(0, 45);
- u8g2.print("MCP: " + currentMCPStatus);
-
- // 绘制第四行(消息滚动)
- if (mcpMessage.length() > 0) {
- // 设置裁剪窗口,限定在第四行区域(y从53到63,根据字体高度调整)
- u8g2.setClipWindow(0, 45, 127, 63);
- u8g2.setCursor(0 - scrollOffset, 60);
- u8g2.print(mcpMessage);
- u8g2.setClipWindow(0, 0, 127, 63); // 恢复全屏裁剪
- }
-
- u8g2.sendBuffer();
-
- lastWifiStatus = currentWifiStatus;
- lastMCPStatus = currentMCPStatus;
- lastIP = currentIP;
- lastMessage = mcpMessage;
- }
- }
- /**
- * LED 状态机处理函数
- * 在 loop 中定期调用,根据当前模式执行非阻塞操作
- */
- void handleLedStateMachine() {
- // 仅当 MCP 控制模式激活且未超时时,才处理状态机
- if (!mcpLedActive || millis() >= mcpLedTimeout) {
- // 如果超时,恢复空闲模式,交给 handleLEDs 处理
- if (currentLedMode != LED_MODE_IDLE) {
- currentLedMode = LED_MODE_IDLE;
- // 强制熄灭,让 handleLEDs 接管
- pixels.clear();
- pixels.show();
- }
- return;
- }
-
- unsigned long now = millis();
-
- switch (currentLedMode) {
- case LED_MODE_ON:
- case LED_MODE_OFF:
- // 常亮/常灭模式无需额外处理,已在工具回调中设置
- break;
-
- case LED_MODE_BLINK:
- if (now - lastLedStep >= blinkDelay) {
- lastLedStep = now;
- if (!blinkState) {
- // 点亮
- pixels.fill(pixels.Color(255, 255, 255));
- blinkState = true;
- } else {
- // 熄灭
- pixels.clear();
- blinkState = false;
- blinkCount++;
- }
- pixels.show();
-
- // 如果闪烁次数完成,退出 MCP 控制模式(可选:让超时自然结束,或立即返回空闲)
- if (blinkCount >= blinkTotal) {
- // 闪烁完成,可以立即结束 MCP 控制,恢复状态指示
- mcpLedActive = false;
- currentLedMode = LED_MODE_IDLE;
- // 确保灯灭,让 handleLEDs 接管
- pixels.clear();
- pixels.show();
- }
- }
- break;
-
- case LED_MODE_FLOW:
- if (now - lastLedStep >= flowDelay) {
- lastLedStep = now;
- // 移动到下一个灯珠
- flowIndex++;
- if (flowIndex < NUM_PIXELS) {
- pixels.clear();
- pixels.setPixelColor(flowIndex, pixels.Color(0, 0, 255));
- pixels.show();
- } else {
- // 流水结束,退出 MCP 控制模式
- mcpLedActive = false;
- currentLedMode = LED_MODE_IDLE;
- pixels.clear();
- pixels.show();
- }
- }
- break;
-
- case LED_MODE_IDLE:
- default:
- break;
- }
- }
- /********** WS2812 灯带状态指示(考虑 MCP 工具覆盖)***********/
- void handleLEDs() {
- // 如果处于 MCP 工具控制模式且未超时,则跳过状态指示,交由状态机处理
- if (mcpLedActive && millis() < mcpLedTimeout) {
- return; // 状态机已在别处运行
- }
-
- // 确保 MCP 控制标志被清除(超时后)
- mcpLedActive = false;
- currentLedMode = LED_MODE_IDLE; // 确保模式复位
- static unsigned long lastUpdate = 0;
- static bool ledState = false;
- uint32_t targetColor = 0;
- unsigned long interval = 0;
-
- if (!wifiConnected) {
- targetColor = pixels.Color(255, 0, 0); // 红色
- interval = 100; // 快速闪烁
- } else if (!mcpConnected) {
- targetColor = pixels.Color(0, 255, 0); // 绿色
- interval = 500; // 慢速闪烁
- } else {
- targetColor = pixels.Color(0, 0, 255); // 蓝色
- interval = 0; // 常亮
- }
-
- if (interval == 0) {
- // 常亮
- pixels.fill(targetColor);
- pixels.show();
- } else {
- // 非阻塞闪烁
- if (millis() - lastUpdate > interval) {
- lastUpdate = millis();
- ledState = !ledState;
- if (ledState) {
- pixels.fill(targetColor);
- } else {
- pixels.clear();
- }
- pixels.show();
- }
- }
- }
复制代码
【参考资料】
行空板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
如果你有任何问题或改进建议,欢迎在评论区留言交流!
|