项目背景
本项目旨在构建一个基于WebRTC的AI互动智能照明控制系统,通过网页端的小智AI卡通少女形象与用户进行实时语音对话,并借助MCP(Model Context Protocol)服务实现对FireBeetle 2 ESP32-C5开发板连接LED灯带的智能控制。该系统融合了人工智能、物联网、实时音视频通信等前沿技术,为用户提供一种全新的、拟人化的智能家居交互体验。
技术实现
1. 小智WebRTC AI互动系统 小智WebRTC是一个开源的AI实时音视频互动项目,专注于打造具有情感温度的AI伴侣。该系统具备以下核心能力:
基于WebRTC的实时音视频通信,提供超低延迟的互动体验
Live2D动态卡通形象呈现,增强用户的亲和感和沉浸感
融合视觉多模态理解、智能问答与MCP控制能力
支持网页端直接访问,无需安装额外软件
2. MCP(Model Context Protocol)服务 MCP是一种新兴的标准化协议,专门用于AI模型与物联网设备之间的高效通信。通过MCP服务,可以实现:
标准化的设备能力描述和发现机制
自然语言到设备控制命令的转换
实时的设备状态监控和数据获取
低延迟、高可靠性的设备控制
3. FireBeetle 2 ESP32-C5开发板 FireBeetle 2 ESP32-C5是DFRobot推出的高性能物联网开发板,具备:
ESP32-C5芯片:240MHz RISC-V 32位单核处理器
双频Wi-Fi 6(2.4GHz和5GHz)和蓝牙5.0连接
19个可编程GPIO引脚,支持多种外设接口
丰富的PWM控制器,特别适合LED灯带控制
支持MicroPython开发,降低开发门槛
【配置XiaoZhi WebRTC】
一个基于 **WebRTC** 的 AI 实时音视频互动项目,致力于打造你的专属、贴心且充满温度的情感伴侣。
---
## ✨ 功能特性
- **XiaoZhi 核心能力**:融合视觉多模态理解、智能问答与 MCP 控制,带来更强大的交互与处理能力。
- **实时音视频沟通**:超低延迟与高清体验,让交流顺畅自然。
- **Live2D 动态呈现**:拟真互动与沉浸式表现,提升亲和力与互动感。
---
## 🎯 在线体验
[https://xiaozhi.dairoot.cn](https://xiaozhi.dairoot.cn)
> 💡 **提示**: 由于部署在海外服务器,访问会稍微卡顿(仅体验)
https://github.com/user-attachments/assets/d985aacc-b07d-4874-a10a-c2139bd6c4bf
---
## 🚀 快速开始
```bash
# 克隆项目
git clone https://github.com/dairoot/xiaozhi-webrtc.git
cd xiaozhi-webrtc
```
#### 方法一:使用 uv(推荐)
```bash
# 安装 uv
#powershell
#临时用(一次性)
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple uv
#或者给当前用户永久换源
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install uv
pip install uv
# 安装项目依赖
uv sync
# 运行项目
uv run main.py
```
#### 方法二:使用 Docker
```bash
# 使用 Docker Compose 运行
docker compose up
```
#### 方法三:传统 pip 安装
```bash
# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Linux/Mac
# 或 venv\Scripts\activate # Windows
#(如出现如下错误——source : 无法将“source”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径
正确,然后再试一次。
所在位置 行:1 字符: 1
+ source venv/bin/activate
+ ~~~~~~
+ CategoryInfo : ObjectNotFound: (source:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
)
解决办法:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\venv\Scripts\Activate.ps1
# 安装依赖
pip install -e .
# 运行项目
python main.py
```
---
## ⚙️ 部署要求
### 端口要求
WebRTC 需要以下端口用于实时音视频通信:
| 端口 | 协议 | 用途 |
|------|------|------|
| 3478 | UDP | STUN 服务 |
| 49152–65535 | UDP | WebRTC 媒体流端口(默认) |
**注意:** 确保防火墙允许这些端口的通信,特别是在生产环境中部署时。
### HTTPS 要求
**线上环境必须使用 HTTPS**:WebRTC 需要访问摄像头和麦克风,现代浏览器出于安全考虑只允许在 HTTPS 环境下使用这些功能。
## 📖 使用说明
1. **启动服务**: 运行项目后,服务将在 `http://localhost:51000` 或者 `https://yourdomain.com` 启动
2. **访问页面**: 在浏览器中打开上述地址
3. **授权权限**: 允许浏览器访问摄像头和麦克风
4. **开始通信**: 点击开始按钮建立 WebRTC 连接
**注意**: 生产环境必须使用 HTTPS,否则 WebRTC 功能将无法正常工作。
4.在xiaozhi控制台注册,并获取mcp接入点
【FireBeetle 2 esp32c5程序】
#include
#include
#include
#include
#define PIN 3
#define NUMPIXELS 5
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
// WiFi配置
const char* ssid = "************";
const char* password = "**************";
// MCP服务器配置
const char* mcpEndpoint = "你的mcp接入点";
// 创建MCP客户端实例
WebSocketMCP mcpClient;
// 效果控制变量
int currentEffect = 0;
unsigned long previousMillis = 0;
int effectPosition = 0;
String currentColor = "0,255,0"; // 默认绿色
// 连接状态回调函数
void onConnectionStatus(bool connected) {
if (connected) {
Serial.println("[MCP] 已连接到服务器");
registerMcpTools(); // 连接成功后注册工具
} else {
Serial.println("[MCP] 与服务器断开连接");
}
}
void setup() {
Serial.begin(115200);
pixels.begin();
pixels.show(); // 初始化时关闭所有灯
// 连接WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("WiFi已连接");
// 初始化MCP客户端
mcpClient.begin(mcpEndpoint, onConnectionStatus);
}
void loop() {
mcpClient.loop(); // 处理MCP客户端事件
// 根据当前效果执行动画
switch(currentEffect) {
case 0: // 无效果 - 静态显示
break;
case 1: // 流水灯效果
runningLights();
break;
case 2: // 呼吸灯效果
breathingEffect();
break;
case 3: // 彩虹效果
rainbowCycle();
break;
case 4: // 警车灯效果
policeLights();
break;
case 5: // 颜色渐变效果
colorFade();
break;
}
delay(10);
}
// 注册MCP工具
void registerMcpTools() {
// 工具1: 控制单个灯
mcpClient.registerTool(
"neopixel_single",
"控制单个LED灯,参数:灯号(0-4),红色(0-255),绿色(0-255),蓝色(0-255),例如:设置第2个灯为红色",
"{\"type\":\"object\",\"properties\":{\"pixel\":{\"type\":\"number\",\"minimum\":0,\"maximum\":4},\"red\":{\"type\":\"number\",\"minimum\":0,\"maximum\":255},\"green\":{\"type\":\"number\",\"minimum\":0,\"maximum\":255},\"blue\":{\"type\":\"number\",\"minimum\":0,\"maximum\":255}},\"required\":[\"pixel\",\"red\",\"green\",\"blue\"]}",
[](const String& args) {
DynamicJsonDocument doc(256);
deserializeJson(doc, args);
int pixel = doc["pixel"];
int red = doc["red"];
int green = doc["green"];
int blue = doc["blue"];
if (pixel >= 0 && pixel < NUMPIXELS) {
pixels.setPixelColor(pixel, pixels.Color(red, green, blue));
pixels.show();
currentEffect = 0; // 切换到静态模式
String response = "设置灯" + String(pixel) + "为颜色(R:" + String(red) + ",G:" + String(green) + ",B:" + String(blue) + ")";
return WebSocketMCP::ToolResponse("{\"success\":true,\"message\":\"" + response + "\"}");
} else {
return WebSocketMCP::ToolResponse("{\"success\":false,\"message\":\"灯号无效,请输入0-4\"}");
}
}
);
// 工具2: 控制所有灯
mcpClient.registerTool(
"neopixel_all",
"控制所有LED灯,参数:红色(0-255),绿色(0-255),蓝色(0-255),例如:设置所有灯为蓝色",
"{\"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(256);
deserializeJson(doc, args);
int red = doc["red"];
int green = doc["green"];
int blue = doc["blue"];
for(int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(red, green, blue));
}
pixels.show();
currentEffect = 0; // 切换到静态模式
currentColor = String(red) + "," + String(green) + "," + String(blue);
String response = "设置所有灯为颜色(R:" + String(red) + ",G:" + String(green) + ",B:" + String(blue) + ")";
return WebSocketMCP::ToolResponse("{\"success\":true,\"message\":\"" + response + "\"}");
}
);
// 工具3: 开关控制
mcpClient.registerTool(
"neopixel_switch",
"开关所有LED灯,参数:状态(开灯、关灯)",
"{\"type\":\"object\",\"properties\":{\"state\":{\"type\":\"string\",\"enum\":[\"开灯\",\"关灯\"]}},\"required\":[\"state\"]}",
[](const String& args) {
DynamicJsonDocument doc(256);
deserializeJson(doc, args);
String state = doc["state"];
if (state == "开灯") {
// 使用当前颜色或默认绿色开灯
int red, green, blue;
sscanf(currentColor.c_str(), "%d,%d,%d", &red, &green, &blue);
for(int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(red, green, blue));
}
pixels.show();
currentEffect = 0;
return WebSocketMCP::ToolResponse("{\"success\":true,\"message\":\"已开灯\"}");
} else if (state == "关灯") {
pixels.clear();
pixels.show();
currentEffect = 0;
return WebSocketMCP::ToolResponse("{\"success\":true,\"message\":\"已关灯\"}");
}
return WebSocketMCP::ToolResponse("{\"success\":false,\"message\":\"参数错误\"}");
}
);
// 工具4: 灯光效果
mcpClient.registerTool(
"neopixel_effect",
"设置LED灯光效果,参数:效果类型(流水灯、呼吸灯、彩虹灯、警车灯、颜色渐变、关闭效果)",
"{\"type\":\"object\",\"properties\":{\"effect\":{\"type\":\"string\",\"enum\":[\"流水灯\",\"呼吸灯\",\"彩虹灯\",\"警车灯\",\"颜色渐变\",\"关闭效果\"]}},\"required\":[\"effect\"]}",
[](const String& args) {
DynamicJsonDocument doc(256);
deserializeJson(doc, args);
String effect = doc["effect"];
if (effect == "流水灯") {
currentEffect = 1;
effectPosition = 0;
} else if (effect == "呼吸灯") {
currentEffect = 2;
} else if (effect == "彩虹灯") {
currentEffect = 3;
effectPosition = 0;
} else if (effect == "警车灯") {
currentEffect = 4;
effectPosition = 0;
} else if (effect == "颜色渐变") {
currentEffect = 5;
effectPosition = 0;
} else if (effect == "关闭效果") {
currentEffect = 0;
}
return WebSocketMCP::ToolResponse("{\"success\":true,\"message\":\"已切换到效果:" + effect + "\"}");
}
);
Serial.println("[MCP] NeoPixel控制工具已注册");
}
// ========== 灯光效果函数 ==========
// 流水灯效果
void runningLights() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= 150) {
previousMillis = currentMillis;
// 清除所有灯
for(int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
}
// 设置当前灯和下一个灯
int current = effectPosition % NUMPIXELS;
int next = (effectPosition + 1) % NUMPIXELS;
pixels.setPixelColor(current, pixels.Color(0, 150, 0)); // 主灯绿色
pixels.setPixelColor(next, pixels.Color(0, 50, 0)); // 下一个灯暗绿色
pixels.show();
effectPosition = (effectPosition + 1) % NUMPIXELS;
}
}
// 呼吸灯效果
void breathingEffect() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= 20) {
previousMillis = currentMillis;
// 使用正弦波计算亮度
float breath = (exp(sin(millis()/2000.0*PI)) - 0.36787944)*108.0;
uint32_t color = pixels.Color(0, breath, 0); // 绿色呼吸
for(int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, color);
}
pixels.show();
}
}
// 彩虹效果
void rainbowCycle() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= 100) {
previousMillis = currentMillis;
for(int i = 0; i < NUMPIXELS; i++) {
// 计算每个灯的颜色
int hue = (effectPosition * 256 / NUMPIXELS) + (i * 256 / NUMPIXELS);
uint32_t color = wheel(hue & 255);
pixels.setPixelColor(i, color);
}
pixels.show();
effectPosition = (effectPosition + 1) % 256;
}
}
// 警车灯效果
void policeLights() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= 300) {
previousMillis = currentMillis;
if (effectPosition == 0) {
// 红色闪烁
for(int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(255, 0, 0));
}
effectPosition = 1;
} else {
// 蓝色闪烁
for(int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 255));
}
effectPosition = 0;
}
pixels.show();
}
}
// 颜色渐变效果
void colorFade() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= 100) {
previousMillis = currentMillis;
// 在多个颜色间渐变
uint32_t colors[] = {
pixels.Color(255, 0, 0), // 红
pixels.Color(255, 255, 0), // 黄
pixels.Color(0, 255, 0), // 绿
pixels.Color(0, 255, 255), // 青
pixels.Color(0, 0, 255), // 蓝
pixels.Color(255, 0, 255) // 紫
};
int numColors = 6;
int currentColor = (effectPosition / 50) % numColors;
int nextColor = (currentColor + 1) % numColors;
float blendRatio = (effectPosition % 50) / 50.0;
uint32_t blended = colorBlend(colors[currentColor], colors[nextColor], blendRatio);
for(int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, blended);
}
pixels.show();
effectPosition = (effectPosition + 1) % (50 * numColors);
}
}
// ========== 辅助函数 ==========
// 生成彩虹色轮
uint32_t wheel(byte WheelPos) {
WheelPos = 255 - WheelPos;
if(WheelPos < 85) {
return pixels.Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
if(WheelPos < 170) {
WheelPos -= 85;
return pixels.Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
WheelPos -= 170;
return pixels.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
// 颜色混合函数
uint32_t colorBlend(uint32_t color1, uint32_t color2, float ratio) {
int r1 = (color1 >> 16) & 0xFF;
int g1 = (color1 >> 8) & 0xFF;
int b1 = color1 & 0xFF;
int r2 = (color2 >> 16) & 0xFF;
int g2 = (color2 >> 8) & 0xFF;
int b2 = color2 & 0xFF;
int r = r1 + (r2 - r1) * ratio;
int g = g1 + (g2 - g1) * ratio;
int b = b1 + (b2 - b1) * ratio;
return pixels.Color(r, g, b);
}
项目创新点
1. 拟人化AI交互 通过小智AI卡通少女形象,为用户提供具有情感温度的交互体验。用户可以通过自然语言与AI对话,实现LED灯带的控制,如"小智,把灯光调成温暖的黄色"、"让灯带闪烁出彩虹效果"等。
2. 多模态交互融合 结合语音识别、自然语言处理、视觉呈现等多种技术手段,实现用户与AI之间的多维度交互。AI不仅能理解用户的语音指令,还能通过表情、动作等方式给予反馈。
3. 低延迟实时控制 利用WebRTC技术实现网页端与AI的实时音视频通信,通过MCP服务将AI的决策快速转化为设备控制指令,确保LED灯带能够实时响应用户的指令。
4. 标准化物联网架构 采用MCP协议作为AI与物联网设备之间的通信桥梁,实现设备能力的标准化描述和调用,为未来的功能扩展和设备接入奠定基础。
评论