9.4
【目标任务】
行空板K10 主芯片是ESP32 S3 n16r8,所以它可以变身小智AI,同时支持Arduino IDE后,运用xiaozhi-mcp库,它还可以成为受控端,引脚外接模块也可以用小智AI自然语音控制,可玩性大大增强了。
在本帖子中,我将在不用扩展板的情况下,在K10的两个3Pin PH2.0全功能IO接口(支持数字输入输出、模拟输入和PWM输出)上外接RGB灯环和继电器进行基础训练,为实现更多创意做准备。

本帖子中的接线为:

计划中,我们将用小智AI自然语音来控制K10引脚P0、P1外接灯环的开关、亮度、简单灯效,控制继电器的开启与断开,由它做开关控制其它电器,本帖子中控制了一个小风扇。

材料清单
- 小智AI X1
- 行空板K10 X1
- RGB灯环16灯 X1
- 3V继电器 X1
- 小风扇和电源 X1
1、准备工作参看Arduino IDE K10行空板MCP控制:屏幕显示与RGB灯- Makelog(造物记)
2、安装RGB支持库如下图:

3、编写代码,不断优化与调试。
代码如下:
四个控制工具分别控制断电器开关、RGB灯环的亮度、颜色、简单灯效。
屏幕显示连接过程和命令执行状态。

代码
#include <WiFi.h>
#include <WebSocketMCP.h>
#include "unihiker_k10.h"
#include <Adafruit_NeoPixel.h>
// WiFi配置
const char* ssid = "your ssid";
const char* password = "your password";
// MCP服务器配置
const char* mcpEndpoint = "ws://api.xiaozhi.me/mcp/?token=your token";
WebSocketMCP mcpClient;
UNIHIKER_K10 k10;
uint8_t screen_dir = 2;
// 引脚定义
#define RGB_LED_PIN P0
#define RELAY_PIN P1
// RGB灯环设置
#define NUM_LEDS 16
Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, RGB_LED_PIN, NEO_GRB + NEO_KHZ800);
// 设备状态
bool relayState = false;
uint8_t rgbBrightness = 100;
uint32_t rgbColor = 0xFFFFFF;
// 非阻塞动画控制变量
unsigned long previousMillis = 0;
uint8_t animationMode = 0; // 0: off, 1: rainbow, 2: chase, 3: theater
uint16_t j = 0; // 彩虹效果计数器
// 屏幕状态缓存
bool lastRelayState = false;
uint8_t lastRgbBrightness = 0;
bool lastMcpConnected = false;
IPAddress lastIP;
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW);
strip.begin();
strip.setBrightness(50);
strip.show();
k10.begin();
k10.initScreen(screen_dir);
k10.creatCanvas();
k10.setScreenBackground(0x000000);
// 初始屏幕显示
k10.canvas->canvasText("K10智能控制系统", 10, 10, 0xFFFFFF, k10.canvas->eCNAndENFont24, 200, true);
k10.canvas->canvasText("等待WiFi连接...", 10, 50, 0xFFFFFF, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->updateCanvas();
// 改进的WiFi连接带超时
WiFi.begin(ssid, password);
unsigned long startAttemptTime = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 30000) {
delay(500);
Serial.print(".");
}
if (WiFi.status() != WL_CONNECTED) {
k10.canvas->canvasClear();
k10.canvas->canvasText("WiFi连接失败", 10, 10, 0xFF0000, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->updateCanvas();
while(1) delay(1000); // 停止运行
}
// 初始化状态缓存
lastIP = WiFi.localIP();
lastMcpConnected = false;
// 只更新连接状态部分
k10.canvas->canvasText("WiFi已连接", 10, 50, 0xFFFFFF, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->canvasText("IP: " + lastIP.toString(), 10, 80, 0xFFFFFF, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->updateCanvas();
delay(2000);
mcpClient.begin(mcpEndpoint, [](bool connected) {
if (connected) {
Serial.println("已连接到MCP服务器");
lastMcpConnected = true;
// 只更新连接状态部分
k10.canvas->canvasText("MCP连接成功", 10, 110, 0x00FF00, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->updateCanvas();
registerTools();
} else {
Serial.println("与MCP服务器断开连接");
lastMcpConnected = false;
// 只更新连接状态部分
k10.canvas->canvasText("MCP连接断开", 10, 110, 0xFF0000, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->updateCanvas();
}
});
}
void registerTools() {
mcpClient.registerTool(
"relay_control",
"控制继电器开关",
"{\"type\":\"object\",\"properties\":{\"state\":{\"type\":\"string\",\"enum\":[\"on\",\"off\"]}},\"required\":[\"state\"]}",
[](const String& args) {
DynamicJsonDocument doc(128);
DeserializationError error = deserializeJson(doc, args);
if (error) {
return WebSocketMCP::ToolResponse("{\"success\":false,\"error\":\"JSON解析失败\"}");
}
String state = doc["state"].as<String>();
if (state == "on") {
digitalWrite(RELAY_PIN, HIGH);
relayState = true;
} else if (state == "off") {
digitalWrite(RELAY_PIN, LOW);
relayState = false;
}
// 只更新继电器状态部分
if (relayState != lastRelayState) {
k10.canvas->canvasText("继电器状态: " + String(relayState ? "开启" : "关闭"), 10, 140,
relayState ? 0x00FF00 : 0xFF0000, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->updateCanvas();
lastRelayState = relayState;
}
return WebSocketMCP::ToolResponse("{\"success\":true,\"state\":\"" + state + "\"}");
}
);
mcpClient.registerTool(
"rgb_control",
"控制RGB灯环",
"{\"type\":\"object\",\"properties\":{\"red\":{\"type\":\"number\",\"minimum\":0,\"maximum\":255},\"green\":{\"type\":\"number\",\"minimum\":0,\"maximum\":255},\"blue\":{\"type\":\"number\",\"minimum\":0,\"maximum\":255}},\"required\":[\"red\",\"green\",\"blue\"]}",
[](const String& args) {
DynamicJsonDocument doc(128);
DeserializationError error = deserializeJson(doc, args);
if (error) {
return WebSocketMCP::ToolResponse("{\"success\":false,\"error\":\"JSON解析失败\"}");
}
uint8_t r = doc["red"];
uint8_t g = doc["green"];
uint8_t b = doc["blue"];
rgbColor = (r << 16) | (g << 8) | b;
animationMode = 0; // 停止动画
for(int i = 0; i < strip.numPixels(); i++) {
strip.setPixelColor(i, strip.Color(r, g, b));
}
strip.show();
return WebSocketMCP::ToolResponse("{\"success\":true,\"message\":\"RGB灯环颜色已设置\"}");
}
);
mcpClient.registerTool(
"rgb_mode",
"控制RGB灯环模式",
"{\"type\":\"object\",\"properties\":{\"mode\":{\"type\":\"string\",\"enum\":[\"rainbow\",\"chase\",\"theater\",\"off\"]}},\"required\":[\"mode\"]}",
[](const String& args) {
DynamicJsonDocument doc(128);
DeserializationError error = deserializeJson(doc, args);
if (error) {
return WebSocketMCP::ToolResponse("{\"success\":false,\"error\":\"JSON解析失败\"}");
}
String mode = doc["mode"].as<String>();
if (mode == "rainbow") {
animationMode = 1;
j = 0;
} else if (mode == "chase") {
animationMode = 2;
} else if (mode == "theater") {
animationMode = 3;
} else if (mode == "off") {
animationMode = 0;
for(int i = 0; i < strip.numPixels(); i++) {
strip.setPixelColor(i, 0);
}
strip.show();
}
return WebSocketMCP::ToolResponse("{\"success\":true,\"mode\":\"" + mode + "\"}");
}
);
mcpClient.registerTool(
"rgb_brightness",
"控制RGB灯环亮度",
"{\"type\":\"object\",\"properties\":{\"brightness\":{\"type\":\"number\",\"minimum\":0,\"maximum\":100}},\"required\":[\"brightness\"]}",
[](const String& args) {
DynamicJsonDocument doc(128);
DeserializationError error = deserializeJson(doc, args);
if (error) {
return WebSocketMCP::ToolResponse("{\"success\":false,\"error\":\"JSON解析失败\"}");
}
rgbBrightness = doc["brightness"];
strip.setBrightness(map(rgbBrightness, 0, 100, 0, 255));
strip.show();
// 只更新亮度部分
if (rgbBrightness != lastRgbBrightness) {
k10.canvas->canvasText("RGB亮度: " + String(rgbBrightness) + "%", 10, 170,
0xFFFFFF, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->updateCanvas();
lastRgbBrightness = rgbBrightness;
}
return WebSocketMCP::ToolResponse("{\"success\":true,\"brightness\":" + String(rgbBrightness) + "}");
}
);
Serial.println("所有工具已注册");
// 初始显示设备状态
k10.canvas->canvasText("继电器状态: " + String(relayState ? "开启" : "关闭"), 10, 140,
relayState ? 0x00FF00 : 0xFF0000, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->canvasText("RGB亮度: " + String(rgbBrightness) + "%", 10, 170,
0xFFFFFF, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->updateCanvas();
}
// 非阻塞彩虹效果
void updateRainbow() {
for(int i=0; i< strip.numPixels(); i++) {
strip.setPixelColor(i, Wheel(((i * 256 / strip.numPixels()) + j) & 255));
}
strip.show();
j = (j + 1) % (256 * 5);
}
// 非阻塞追逐效果
void updateChase() {
static int pos = 0;
strip.setPixelColor(pos, strip.Color(255, 0, 0));
if (pos > 0) strip.setPixelColor(pos-1, 0);
strip.show();
pos = (pos + 1) % strip.numPixels();
}
// 非阻塞剧院效果
void updateTheaterChase() {
static int q = 0;
for (int i=0; i < strip.numPixels(); i=i+3) {
strip.setPixelColor(i+q, strip.Color(0, 0, 255));
}
strip.show();
delay(50);
for (int i=0; i < strip.numPixels(); i=i+3) {
strip.setPixelColor(i+q, 0);
}
q = (q + 1) % 3;
}
uint32_t Wheel(byte WheelPos) {
WheelPos = 255 - WheelPos;
if(WheelPos < 85) {
return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
if(WheelPos < 170) {
WheelPos -= 85;
return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
WheelPos -= 170;
return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
void loop() {
mcpClient.loop();
// 非阻塞动画更新
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= 50) {
previousMillis = currentMillis;
switch(animationMode) {
case 1: updateRainbow(); break;
case 2: updateChase(); break;
case 3: updateTheaterChase(); break;
}
}
// 定期检查连接状态,但只在状态变化时更新屏幕
static unsigned long lastStatusCheck = 0;
if (currentMillis - lastStatusCheck >= 1000) {
lastStatusCheck = currentMillis;
bool currentConnected = mcpClient.isConnected();
IPAddress currentIP = WiFi.localIP();
// 只在状态变化时更新屏幕
if (currentConnected != lastMcpConnected) {
k10.canvas->canvasText("MCP连接状态: " + String(currentConnected ? "已连接" : "断开"), 10, 200,
currentConnected ? 0x00FF00 : 0xFF0000, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->updateCanvas();
lastMcpConnected = currentConnected;
}
if (currentIP != lastIP) {
k10.canvas->canvasText("IP: " + currentIP.toString(), 10, 230,
0xFFFFFF, k10.canvas->eCNAndENFont16, 200, true);
k10.canvas->updateCanvas();
lastIP = currentIP;
}
}
}
上传测试通过。
小智后台:

评论