ESP32-C3 + 小智 MCP:用自然语言控制你的彩灯
对小智说“天黑了”,彩灯自动亮起;说“我想睡觉了”,它安静熄灭。13根磁吸软灯丝,多种颜色自由搭配,两节干电池就能点亮。
项目简介
本项目使用 DFRobot Beetle ESP32-C3 开发板,结合 小智 MCP(Model Context Protocol) 服务,通过自然语言对话控制 Gravity 数字继电器模块,进而控制自制的磁吸式柔性软灯丝彩灯的亮灭。
彩灯采用独立供电方案——两节1.5V干电池串联(3V),通过继电器控制灯架总电源的通断。ESP32-C3只负责“下命令”,继电器负责“执行开关”,两者电源完全隔离,接线清晰、安全可靠。
你不需要说“打开引脚5”,只需对小智说“开灯”、“关灯”、“天黑了”、“我想睡觉了”,它就能理解你的意图并执行操作。
- 核心硬件:ESP32-C3、Gravity 数字继电器模块、自制磁吸彩灯(13根柔性软灯丝)
- 核心软件:Arduino IDE、
xiaozhi-mcp库、小智 AI 平台 - 控制方式:自然语言语音对话
- 支持指令:开灯、关灯、亮灯、天黑了、天亮了、我想睡觉了、我要起床了等
硬件准备
| 名称 | 数量 | 备注 |
|---|---|---|
| DFRobot Beetle ESP32-C3 | 1 | 或任意 ESP32-C3 开发板 |
| Gravity 数字继电器模块 | 1 | DFR0473(3V/10A),兼容3.3V主控 |
| DFRobot 3V 260mm 柔性软灯丝 | 13根 | 多种颜色自由搭配 |
| 钕磁铁小球 | 26颗 | 每根灯丝两端各一颗 |
| 铁圈(灯架) | 2个 | 上下各一个,作为正负极公共触点 |
| 5号干电池 | 2节 | 1.5V × 2 = 3V |
| 电池盒(2节装) | 1 | 带引线,方便接线 |
| Gravity-3Pin 连接线 | 1 | 继电器模块随附 |
| USB 数据线 | 1 | 供电+烧录 |

Gravity 数字继电器模块
本项目选用 Gravity 数字继电器模块(SKU: DFR0473) ,这是 DFRobot 专为低压主控器设计的新款模块,与 ESP32-C3 的 3.3V 电平完美匹配。
关键特性
Gravity 数字继电器模块专为低压主控器设计,可以稳定工作在 3V 供电场景下。
模块具有宽电压供电特性,在 2.8~5V 供电下,对于 1.8V 的设备信号也可以做到良好的识别与支持。这意味着 ESP32-C3 的 3.3V GPIO 信号可以被继电器模块稳定识别,无需额外的电平转换电路。
接口说明
模块采用 Gravity-3Pin 接口,即插即用,无需焊接。接线端设有常开触点接口 (NO) 和常闭触点接口 (NC) ,方便扩展使用。板载一个红色 LED 工作状态指示灯,当常开触点闭合时 LED 点亮,断开时 LED 熄灭,可以直观判断继电器的工作状态。
| 标号 | 名称 | 功能描述 |
|---|---|---|
| 1 | - | 电源输入端负极 |
| 2 | + | 电源输入端正极 |
| 3 | D | 控制信号输入端 |
| 4 | NC | 常闭触点:平时闭合,有信号断开 |
| 5 | NO | 常开触点:平时断开,有信号闭合 |
| 6 | N/A | 空脚 |
| 7 | COM | 公共触点/端口 |
继电器的作用
继电器是一种典型的弱电控制强电的应用。由于主控器(单片机)无法直接控制交流电、大电流、大电压等设备,必须通过中间层(继电器)来实施。本项目使用继电器控制彩灯的总电源回路,实现小电流信号控制大电流负载。
自制磁吸彩灯

设计灵感
传统灯带焊接后颜色固定,想换颜色就得重新焊接。本项目采用磁吸式模块化设计,每根灯丝都是一个独立的发光单元,通过钕磁铁吸附在铁圈上,想换颜色随手取下即可,想加数量直接吸附上去。
柔性软灯丝规格
本项目使用的发光元件是 DFRobot 3V 260mm 柔性软灯丝。它是一条直径仅 2mm、长度 260mm 的柔性 LED 灯丝,可以像面条一样随意弯曲、折叠,发出 360° 全角度的光。
| 参数项 | 规格 |
|---|---|
| 灯丝长度 | 260mm(10.24英寸) |
| 灯丝直径 | 2mm |
| 供电电压 | 3V(直流) |
| 工作电流 | 80mA |
| 发光角度 | 360° |
| 极性标识 | 小圆孔端为正极,无孔端为负极 |
极性识别非常重要:拿到灯丝后,请仔细观察两端。带小圆孔的一端是正极,另一端无孔的是负极。接反会导致灯丝不亮,但通常不会烧毁,调换即可。
磁吸灯架结构
灯架由上下两个铁圈组成:
- 上铁圈:连接电源正极,作为公共阳极
- 下铁圈:连接电源负极,作为公共阴极
- 两个铁圈上下平行固定,间距与灯丝长度匹配
单根灯丝的制作方式
取一根柔性软灯丝,在两端分别焊接一个钕磁铁小球,然后吸附到上下铁圈之间。
磁力自动吸附形成机械固定 + 电气连接:上磁铁球接触上铁圈(正极),下磁铁球接触下铁圈(负极),回路导通,灯丝点亮。想换颜色或增减灯丝,直接取下或吸附新的即可,不需要焊接或插拔端子。
钕磁铁的电接触性能
钕磁铁本身具有导电性,两个磁铁之间的接触电阻小于 0.01Ω。以单根灯丝80mA的工作电流为例,即使13根灯丝全部并联,总电流约为 1040mA,接触电阻带来的压降仍远低于灯丝的驱动电压需求,不会影响正常发光。磁力吸附的接触压力稳定,正常使用中不会出现虚接或闪烁。
多种颜色的组合
13根灯丝可以自由选择多种颜色(红、橙、黄、绿、青、蓝、紫、暖白等),通过磁吸结构按需搭配。不需要考虑串联压降匹配问题——每根灯丝独立并联在上下铁圈之间,颜色差异不会相互影响。你可以按色谱渐变排列,也可以按冷暖分区排布,随心情调整。
扩展性
整个灯架使用低压直流供电(3V),从原理上说,只要增加铁圈的长度或数量,就可以吸附更多灯丝。13根只是一个起点,后续可以扩展到20根、30根甚至更多,每种颜色对应一根灯丝,形成渐变或分区效果。
供电方案:干电池独立供电
为什么用干电池?
柔性软灯丝内部是多颗LED芯片并联结构,并联LED的伏安特性非常陡峭,对电压波动敏感。本项目采用两节1.5V干电池串联的方案,恰好提供3V稳定电压,与灯丝的额定工作电压完全匹配,接线简单、安全隔离。
更重要的是,干电池为灯架提供独立电源,与ESP32-C3的供电完全隔离。这样即使灯架上的灯丝数量增加,也不会影响ESP32-C3的稳定运行,避免因负载过大导致开发板重启或WiFi掉线。
为什么通过继电器供电?
如果不用继电器,直接把干电池接到灯架上,灯就会一直亮着。加上继电器后,电池的正极经过继电器的COM → NO触点再到灯架:
- GPIO5 输出高电平 → 继电器吸合 → COM与NO导通 → 电池正极接通灯架 → 灯亮
- GPIO5 输出低电平 → 继电器断开 → COM与NO断开 → 电池正极被切断 → 灯灭
这样,两节干电池提供的3V电源被继电器“开关化”,而开关的控制权交给了ESP32-C3,最终交给了小智的自然语言对话。
供电能力
单根灯丝工作电流约 80mA,13根灯丝并联总电流约 1040mA(1A) 。两节普通5号干电池串联的理论容量约为2000mAh,在1A放电电流下可持续工作约1.5~2小时。如果需要更长续航,可以:
- 使用碱性电池(容量更大,可支撑2~3小时)
- 使用3节5号电池盒 + 假电池(占位筒)保持3V电压
- 使用3.7V锂电池 + 降压(但会失去“纯电池供电”的简洁性)
小技巧:13根灯丝全部点亮时电流接近1A,如果发现电池发热明显或灯光偏暗,可以适当减少同时点亮的灯丝数量(比如只用5~8根),续航会显著提升。
接线说明
继电器与ESP32-C3
| ESP32-C3 | Gravity 继电器模块 |
|---|---|
| GPIO5 | D(控制信号输入端) |
| 5V | +(电源正极) |
| GND | -(电源负极) |
灯架供电回路
两节干电池串联(3V)正极 → 继电器COM
继电器NO → 上铁圈(公共阳极)
干电池负极 → 下铁圈(公共阴极)
继电器串联在灯架的总电源回路中。小智通过MCP控制GPIO5电平,继电器吸合或断开,整个灯架的正极供电被接通或切断,所有吸附在铁圈上的灯丝同时亮灭。
重要:本项目的 Gravity 继电器模块为高电平触发——GPIO5输出高电平时继电器吸合,输出低电平时继电器断开。代码中的 RELAY_ON 应设为 HIGH,RELAY_OFF 应设为 LOW。
由于磁吸灯丝是纯并联结构,继电器只需控制一路总电源即可控制全部灯丝。

软件准备
1. 安装Arduino IDE与ESP32支持
- 下载并安装 Arduino IDE(建议2.x版本)
- 在 文件 → 首选项 → 附加开发板管理器地址 中添加:
https://espressif.github.io/arduino-esp32/package_esp32_index.json - 打开 工具 → 开发板 → 开发板管理器,搜索
esp32,安装 esp32 by Espressif Systems(2.0.17或更高版本) - 开发板选择 DFRobot Beetle ESP32-C3
2. 安装依赖库
在 Arduino IDE 中打开 工具 → 管理库,搜索并安装以下库:
| 库名 | 作者 | 版本要求 |
|---|---|---|
xiaozhi-mcp |
toddpan | 最新版 |
WebSockets |
Markus Sattler | 2.3.6+ |
ArduinoJson |
Benoit Blanchon | 6.21+ |
3. 获取小智 MCP 接入点 Token
- 登录 https://xiaozhi.me
- 进入 控制台 → 智能体,找到你的设备
- 点击 配置角色 → MCP接入点,复制 Token
- 将 Token 填入代码中的
MCP_ENDPOINT变量

完整代码
将以下代码复制到 Arduino IDE 中,修改 WiFi 名称、密码和 MCP Token 后上传。
#include <Arduino.h>
#include <WiFi.h>
#include <WebSocketMCP.h>
#include <ArduinoJson.h>
// ===================== 配置区 =====================
const char* WIFI_SSID = "你的WiFi名称";
const char* WIFI_PASS = "你的WiFi密码";
const char* MCP_ENDPOINT = "wss://api.xiaozhi.me/mcp/?token=你的Token";
#define RELAY_PIN 5
#define RELAY_ON HIGH // Gravity 继电器模块为高电平触发
#define RELAY_OFF LOW
// =================================================
WebSocketMCP mcpClient;
bool wifiConnected = false;
bool mcpConnected = false;
// 前置声明
void registerMcpTools();
// ---------- 继电器控制 ----------
void setRelay(bool on) {
digitalWrite(RELAY_PIN, on ? RELAY_ON : RELAY_OFF);
Serial.printf("[RELAY] Light %s\n", on ? "ON" : "OFF");
}
// ---------- 连接状态回调 ----------
void onMcpConnectionChange(bool connected) {
mcpConnected = connected;
if (connected) {
Serial.println("[MCP] Connected. Registering tools...");
registerMcpTools();
} else {
Serial.println("[MCP] Disconnected.");
}
}
// ---------- 注册MCP工具 ----------
void registerMcpTools() {
Serial.println("[MCP] Registering tools...");
// 工具1:直接开关灯
mcpClient.registerTool(
"light_control",
"直接控制彩灯的开关。当用户说“开灯”、“关灯”、“亮灯”、“把灯打开”时调用此工具。",
"{\"type\":\"object\",\"properties\":{"
"\"action\":{\"type\":\"string\",\"description\":\"操作类型,取值为 on(开灯)或 off(关灯)\","
"\"enum\":[\"on\",\"off\"]}},"
"\"required\":[\"action\"]}",
[](const String& args) {
Serial.println("[Tool] light_control args: " + args);
DynamicJsonDocument doc(256);
DeserializationError err = deserializeJson(doc, args);
if (err) {
return WebSocketMCP::ToolResponse("{\"success\":false,\"error\":\"参数格式错误\"}", true);
}
String action = doc["action"].as<String>();
action.toLowerCase();
if (action == "on" || action == "开" || action == "打开") {
setRelay(true);
return WebSocketMCP::ToolResponse("{\"success\":true,\"msg\":\"灯已打开\"}");
}
if (action == "off" || action == "关" || action == "关闭") {
setRelay(false);
return WebSocketMCP::ToolResponse("{\"success\":true,\"msg\":\"灯已关闭\"}");
}
return WebSocketMCP::ToolResponse("{\"success\":false,\"error\":\"action 只能为 on 或 off\"}", true);
}
);
// 工具2:场景语义控制
mcpClient.registerTool(
"scene_control",
"根据场景语义控制灯。当用户说“天黑了”、“天亮了”、“我想睡觉了”、“我要起床了”等场景描述时调用此工具。",
"{\"type\":\"object\",\"properties\":{"
"\"scene\":{\"type\":\"string\","
"\"description\":\"场景标识。天黑/晚上用 dark;天亮/白天用 dawn;睡觉/休息用 sleep;起床/醒来用 wake\","
"\"enum\":[\"dark\",\"dawn\",\"sleep\",\"wake\"]}},"
"\"required\":[\"scene\"]}",
[](const String& args) {
Serial.println("[Tool] scene_control args: " + args);
DynamicJsonDocument doc(256);
DeserializationError err = deserializeJson(doc, args);
if (err) {
return WebSocketMCP::ToolResponse("{\"success\":false,\"error\":\"参数格式错误\"}", true);
}
String scene = doc["scene"].as<String>();
scene.toLowerCase();
if (scene == "dark" || scene == "night" || scene == "天黑" || scene == "晚上") {
setRelay(true);
return WebSocketMCP::ToolResponse("{\"success\":true,\"msg\":\"天黑了,已为你开灯\"}");
}
if (scene == "dawn" || scene == "day" || scene == "天亮" || scene == "白天") {
setRelay(false);
return WebSocketMCP::ToolResponse("{\"success\":true,\"msg\":\"天亮了,已为你关灯\"}");
}
if (scene == "sleep" || scene == "睡觉" || scene == "休息") {
setRelay(false);
return WebSocketMCP::ToolResponse("{\"success\":true,\"msg\":\"晚安,灯已关闭\"}");
}
if (scene == "wake" || scene == "起床" || scene == "醒来") {
setRelay(true);
return WebSocketMCP::ToolResponse("{\"success\":true,\"msg\":\"早上好,灯已打开\"}");
}
return WebSocketMCP::ToolResponse("{\"success\":false,\"error\":\"暂不支持该场景\"}", true);
}
);
Serial.println("[MCP] Tools registered: light_control, scene_control");
}
// ---------- WiFi连接 ----------
void setupWifi() {
Serial.printf("[WiFi] Connecting to %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
wifiConnected = true;
Serial.println("\n[WiFi] Connected. IP: " + WiFi.localIP().toString());
} else {
wifiConnected = false;
Serial.println("\n[WiFi] Connect failed.");
}
}
// ================= 初始化 =================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n[ESP32-C3 Relay MCP] Starting...");
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, RELAY_OFF);
setupWifi();
if (mcpClient.begin(MCP_ENDPOINT, onMcpConnectionChange)) {
Serial.println("[MCP] Client initialized, connecting...");
} else {
Serial.println("[MCP] Initialization failed!");
}
}
void loop() {
mcpClient.loop();
delay(10);
}
配置小智角色提示词
为了让小智准确理解你的意图,需要在小智控制台配置角色提示词。进入 控制台 → 智能体 → 配置角色,在角色介绍中填入以下内容:
# 角色身份
你是我的智能家居语音助手,名字叫“小智”。你性格亲切、回应简洁,核心任务是帮我控制家里的彩灯。
# 设备与工具信息
你拥有两个MCP工具可以控制彩灯,设备是连接在GPIO 5上的Gravity数字继电器模块。
1. 工具 `light_control`:直接控制灯的开关。
- 参数 `action`,取值 `on`(开灯)或 `off`(关灯)。
- 触发场景:我说“开灯”、“关灯”、“亮灯”、“把灯打开”、“把灯关掉”时调用。
2. 工具 `scene_control`:根据场景语义控制灯。
- 参数 `scene`,取值 `dark`(天黑)、`dawn`(天亮)、`sleep`(睡觉)、`wake`(起床)。
- 触发场景:
- 我说“天黑了”、“晚上了” → 调用 `scene_control`,`scene=dark`
- 我说“天亮了”、“白天了” → 调用 `scene_control`,`scene=dawn`
- 我说“我想睡觉了”、“该休息了” → 调用 `scene_control`,`scene=sleep`
- 我说“我起床了”、“醒了” → 调用 `scene_control`,`scene=wake`
# 行为准则
- 当我的指令涉及上述场景时,必须调用对应的MCP工具来执行,不要仅做口头回复。
- 工具调用成功后,用简短、自然的话确认,例如“灯已经打开啦”、“晚安,灯已关闭”。
- 如果指令模糊无法确定,可以向我确认,例如“你是想开灯还是关灯呢?”

技术原理
MCP工具调用流程
- 设备启动:ESP32-C3连接WiFi,然后通过WebSocket连接到小智MCP服务器
- 工具注册:连接成功后,向MCP服务器注册
light_control和scene_control两个工具 - 语音识别:你对小智说话,小智将语音转为文字
- 意图理解:小智的大模型根据角色提示词,判断应该调用哪个工具、传入什么参数
- 工具调用:MCP服务器将调用请求转发给ESP32-C3,触发对应的Lambda回调函数
- 执行动作:回调函数解析参数,控制GPIO5电平,继电器吸合或断开
- 返回结果:回调函数返回执行结果,小智用自然语言回复你
为什么选择 Gravity 继电器模块?
Gravity 数字继电器模块的宽电压供电特性(2.8~5.5V) 和高电平触发信号(2.8V以上) 与 ESP32-C3 的 3.3V 电平完美匹配。传统的5V继电器模块在3.3V信号下可能无法可靠吸合,而 Gravity 模块专门为低电压主控器设计,无需额外的电平转换电路,接线更简洁、运行更稳定。同时其10A的最大切换电流和300W的最大切换功率为本项目的灯架供电提供了充足的余量。
为什么用干电池独立供电?
电源隔离是本项目的一个重要设计考虑。ESP32-C3 需要稳定的3.3V供电来维持WiFi和WebSocket连接,如果让它同时为13根灯丝供电(总电流1A),开发板上的LDO可能会过热或电压跌落,导致WiFi掉线甚至重启。
采用两节干电池单独为灯架供电后:
- ESP32-C3 的电源负载很轻,WiFi连接更稳定
- 灯架上的电流不会回流影响主控
- 两套电源通过继电器隔离,互不干扰
而且两节干电池串联恰好提供3V,与柔性软灯丝的额定工作电压完美匹配,无需额外的降压模块或限流电阻,接线最简洁。
为什么用磁吸结构?
柔性软灯丝的直径仅2mm,重量极轻,钕磁铁的吸力足以稳固支撑。磁吸接触的接触电阻小于0.01Ω,在80mA的工作电流下压降可忽略不计。这种结构让灯丝可以自由弯曲成任意形状后再吸附,不需要考虑焊接位置和导线走向,极大地提高了DIY的自由度。
测试与效果
上传代码后,打开串口监视器,等待以下日志出现:
[WiFi] Connected. IP: 192.168.x.x
[MCP] Client initialized, connecting...
[MCP] Connected. Registering tools...
[MCP] Tools registered: light_control, scene_control
然后就可以通过小智语音对话控制彩灯了:
- 对小智说“开灯” → 彩灯亮,小智回复“灯已经打开啦”
- 说“关灯” → 彩灯灭
- 说“天黑了” → 彩灯自动亮
- 说“我想睡觉了” → 彩灯自动关闭
- 说“我起床了” → 彩灯自动打开


演示视频
https://www.bilibili.com/video/BV1tkhX6VEgf/?spm_id_from=333.1387.upload.video_card.click
注意事项
1. 继电器触发电平
Gravity 数字继电器模块为高电平触发,GPIO5输出高电平时继电器吸合,低电平时断开。代码中 RELAY_ON 应为 HIGH,RELAY_OFF 应为 LOW。
2. 灯丝极性
柔性软灯丝的小圆孔端为正极,无孔端为负极,焊接钕磁铁前务必确认极性。
3. 电池正负极
两节干电池串联时,注意电池盒的正负极标识,电池正极经过继电器再到灯架上铁圈,电池负极直接到灯架下铁圈。接线反了灯不会亮,但不会烧毁任何元件。
4. 电源隔离
ESP32-C3 与灯架使用两套独立电源:ESP32-C3 由USB或5V适配器供电,灯架由两节干电池供电。两者仅在继电器的 COM/NO 触点处“交汇”,控制回路与负载回路完全隔离,安全可靠。
5. 钕磁铁安全
钕磁铁吸力强,操作时注意不要夹伤手指,远离心脏起搏器等医疗设备。
6. WiFi频段
ESP32-C3仅支持2.4GHz WiFi,请确保连接的是2.4GHz频段。
7. 继电器安全警告
请勿在无指导的情况下接触继电器模块的强电部分。电压超过36V可能对人体造成危险。8岁以下儿童请不要使用此模块。
常见问题
Q:编译报错 'ToolResponse' does not name a type
A:确认使用 WebSocketMCP::ToolResponse,而不是单独的 ToolResponse。同时确保 xiaozhi-mcp 库为最新版。
Q:继电器不动作
A:Gravity 模块为高电平触发,确认GPIO5输出高电平时继电器是否吸合。观察模块上的红色LED指示灯,吸合时应点亮。
Q:灯全部点亮时电池发热
A:13根灯丝同时点亮的总电流接近1A,普通干电池在大电流放电下会发热。可以适当减少同时点亮的灯丝数量,或更换为碱性电池。
Q:灯亮度不一致
A:不同颜色的LED灯丝正向压降略有不同,在3V供电下表现出的亮度会有差异。这是柔性软灯丝的正常特性,不影响使用。
Q:MCP连接失败
A:检查Token是否完整、WiFi是否正常。串口日志会显示 [MCP] Disconnected,可尝试重新上电。
Q:小智不调用工具,只口头回复
A:检查角色提示词中是否明确写了工具名称和触发场景。可以强调“必须调用对应的MCP工具”。
总结
这个项目展示了如何用 ESP32-C3、Gravity 数字继电器模块和小智 MCP 实现自然语言控制自制磁吸彩灯。核心在于:
- 手工制作:柔性软灯丝 + 钕磁铁 + 铁圈,打造模块化磁吸灯架
- 电源隔离:两节干电池独立为灯架供电,ESP32-C3 与负载电源完全隔离
- 硬件匹配:Gravity 继电器模块的 3.3V 兼容性和宽电压特性,与 ESP32-C3 完美配合
- MCP 工具注册:在 Arduino 代码中注册工具,用
WebSocketMCP::ToolResponse返回结果 - 角色提示词:在小智控制台引导大模型将自然语言映射到工具调用
你可以在此基础上扩展更多设备,比如风扇、插座、窗帘,只需增加对应的工具和继电器即可。让 AI 真正成为你的智能家居管家。
参考链接:
Beetle ESP32-C3 (RISC-V 芯片)
3V 260mm 柔性软灯丝
Gravity: 数字继电器模块





云天老师的作品越来越精细了