基于Google Gemini与ESP32的智能情绪灯光系统开发实战
1. 项目概述:当AI大模型遇见物理世界
“Gemini Moodlights”这个项目名,听起来就充满了极客的浪漫。它不是一个简单的网页应用,也不是一个孤立的脚本,而是一个将前沿的AI大语言模型(Google Gemini)与物理世界的嵌入式硬件(ESP32)连接起来的创意项目。简单来说,就是让一块小小的ESP32开发板,能够根据你输入的文字或对话的情绪,实时地控制一组LED灯带,变换出与之匹配的颜色和灯光效果。比如,你对它说“我今天很开心”,灯光可能就会变成温暖明亮的橙色;如果你说“我需要冷静一下”,灯光或许会缓缓过渡到宁静的蓝色。
这背后的核心,是打通了从云端智能到本地执行的通路。我们不再仅仅满足于在屏幕上看到AI生成的文字回复,而是让AI的“理解”能够以最直观的光影形式,在现实空间中具象化。这听起来很酷,但实现起来并不复杂,它巧妙地结合了几个成熟的技术栈:Google Cloud的Gemini API提供了强大的自然语言理解和情感分析能力;Python作为粘合剂,负责调用API、处理逻辑;而ESP32则作为物理世界的执行者,通过Wi-Fi接收指令,精准控制LED。
这个项目非常适合对物联网、AI应用和创意编程感兴趣的开发者。无论你是想为你的工作室、书房增添一个智能氛围灯,还是想深入学习如何将云服务与嵌入式设备结合,它都是一个绝佳的练手项目。整个过程涉及API调用、网络通信、硬件编程,但每一步都有清晰的路径,不需要你从头发明轮子。接下来,我会带你从零开始,拆解每一个环节,分享我在搭建过程中踩过的坑和总结的技巧,让你也能轻松复现这个“有情绪”的智能灯光系统。
2. 核心架构与方案选型
在动手写第一行代码之前,我们需要把整个系统的蓝图规划清楚。一个健壮的“Gemini Moodlights”系统,其核心工作流可以概括为: 用户输入文本 -> Python服务调用Gemini API分析情绪 -> 将情绪映射为灯光参数 -> 通过网络发送给ESP32 -> ESP32解析并控制LED 。围绕这个流程,我们需要做出几个关键的技术选型。
2.1 云端大脑:为什么选择Google Gemini API?
当前可用的AI模型API很多,比如OpenAI的GPT系列、Anthropic的Claude,以及国内的一些大模型。我选择Gemini API(特别是 gemini-1.5-flash 模型)主要基于以下几点考量:
- 性价比与易用性 :Google Cloud为新用户提供了300美元的免费额度,并且Gemini API的定价,尤其是
gemini-1.5-flash模型,在处理这类轻量级、高并发的文本情感分析任务时,成本非常低,甚至对于个人项目来说,在免费额度内几乎可以忽略不计。相比之下,一些其他模型的API调用成本可能更高。 - 上下文长度与响应速度 :
gemini-1.5-flash模型在保持不错理解能力的同时,拥有极快的响应速度,这对于需要实时反馈的灯光系统至关重要。它也能处理足够长的上下文,方便我们设计更复杂的提示词(Prompt)。 - 与Google生态的整合 :如果你未来想扩展功能,比如接入Google Assistant、或者使用Cloud Functions(云函数)来构建无服务器架构,那么从Gemini开始会是一个更平滑的路径。
注意 :在申请Google Cloud API密钥时,务必注意启用正确的API(Gemini API),并合理设置密钥的使用限制(如每日调用限额、IP限制等),即使是在测试阶段,这也是一个良好的安全习惯,能避免意外超支或被滥用。
2.2 粘合层:Python服务的角色与框架选择
Python在这里扮演着“中间件”或“服务器”的角色。它的任务很明确:
- 接收用户输入的文本(可以通过命令行、简单的Web界面、甚至Telegram机器人)。
- 构造提示词,调用Gemini API。
- 解析Gemini返回的JSON结果,提取或计算出对应的灯光参数(如RGB颜色值、亮度、动态模式)。
- 将这些参数通过Wi-Fi发送给ESP32。
关于Python框架,我们不需要重量级的Django或Flask。一个轻量级的方案就足够了:
- 方案A(简单脚本) :直接使用
requests库调用Gemini API,然后用socket或HTTP库与ESP32通信。这是最直接、依赖最少的方式,适合快速验证。 - 方案B(微框架) :使用
FastAPI或Flask创建一个简单的HTTP服务端点。这样,我们可以通过发送HTTP POST请求来触发灯光变化,未来也更容易与移动端App或其他服务集成。我推荐这个方案,因为它结构更清晰,扩展性更好。
我将选择 FastAPI ,因为它异步性能好,自动生成交互式API文档(Swagger UI),对于调试和后续开发非常方便。
2.3 硬件执行端:ESP32与LED选型
ESP32是项目的“手和脚”。选择它是因为:
- 双核处理器与丰富外设 :性能足以处理网络通信和复杂的LED控制协议(如WS2812B需要的精确时序)。
- 内置Wi-Fi与蓝牙 :无需额外模块即可连接网络,这是我们与Python服务通信的基础。
- 庞大的社区与库支持 :Arduino框架或ESP-IDF下都有成熟的Wi-Fi、HTTP Client和NeoPixel(WS2812B驱动)库,极大降低了开发难度。
对于LED, WS2812B(常被称为NeoPixel)智能RGB灯带 是首选。每个LED都可以独立编程控制颜色和亮度,非常适合实现动态、渐变的效果。你需要根据你想要的灯光长度来购买相应数量的灯带。记得,ESP32的GPIO引脚驱动能力有限,当灯带较长(如超过30个LED)时,需要外接5V电源,并将电源地(GND)与ESP32的地相连。
整体架构图 (文字描述): 用户通过客户端(浏览器/App/命令行)发送文本到运行在电脑或云服务器上的Python FastAPI服务。FastAPI服务接收到文本后,调用Google Gemini API进行分析。Gemini返回结构化的情绪或颜色描述,Python服务将其转换为具体的RGB值和模式代码,然后通过HTTP请求发送给ESP32。ESP32上运行着一个Wi-Fi客户端程序,持续监听这个HTTP端点,收到新指令后,立即调用FastLED或NeoPixel库来驱动WS2812B灯带,改变灯光。
3. 环境搭建与核心配置
工欲善其事,必先利其器。这一部分,我们来搞定所有前置的软硬件环境。我会尽量给出清晰的步骤和避坑指南。
3.1 Google Cloud与Gemini API密钥获取
这是接入AI能力的第一步,也是最容易卡住的地方。
-
创建Google Cloud项目 :
- 访问 Google Cloud Console 。
- 如果你没有项目,点击左上角项目下拉框,选择“新建项目”。给它起个名字,比如
gemini-moodlights。 - 地区选择 :在创建项目时,可能会让你选择地区。这个选择对于Gemini API本身影响不大,因为API是全局服务。但对于你后续可能用到的其他服务(如Cloud Run部署Python服务),选择离你用户群体近的地区(如
asia-east1(台湾)或us-central1)有助于降低延迟。对于纯本地开发,可以任意选择。
-
启用Gemini API并创建密钥 :
- 在Cloud Console中,使用顶部的搜索栏,搜索“Gemini API”。
- 进入Gemini API页面,点击“启用”。
- API启用后,在左侧菜单栏,依次进入“API和服务” -> “凭据”。
- 点击“创建凭据” -> “API密钥”。系统会生成一个密钥字符串, 立即复制并妥善保存 。这个密钥一旦关闭对话框就无法再次查看完整内容,如果丢失需要重新生成。
-
设置环境变量(安全最佳实践) : 千万不要把API密钥硬编码在代码里!我们使用环境变量来管理。
# 在Linux/macOS的终端或Windows的PowerShell中 export GEMINI_API_KEY="你的_API_密钥_字符串"在Python代码中,这样读取:
import os api_key = os.environ.get("GEMINI_API_KEY") if not api_key: raise ValueError("请设置 GEMINI_API_KEY 环境变量")
3.2 Python服务端环境配置
假设你已经安装了Python 3.8+。我们创建一个干净的虚拟环境并安装依赖。
-
创建项目目录并初始化虚拟环境 :
mkdir gemini-moodlights-server cd gemini-moodlights-server python -m venv venv # 创建虚拟环境 # 激活虚拟环境 # Windows (PowerShell): .\venv\Scripts\Activate.ps1 # Linux/macOS: source venv/bin/activate -
安装依赖库 : 创建一个
requirements.txt文件,内容如下:fastapi==0.104.1 uvicorn[standard]==0.24.0 google-generativeai==0.3.0 pydantic==2.5.0 requests==2.31.0 python-dotenv==1.0.0 # 可选,用于从.env文件加载环境变量然后安装:
pip install -r requirements.txtfastapi和uvicorn是我们的Web框架和服务器。google-generativeai是Google官方提供的Gemini SDK,比直接用requests封装更简单。pydantic用于数据验证和设置,FastAPI深度集成它。
3.3 ESP32开发环境搭建
ESP32的编程有多种方式,这里我们选择最易上手的 Arduino IDE 方案。
- 安装Arduino IDE :从官网下载并安装。
- 添加ESP32开发板支持 :
- 打开Arduino IDE,进入“文件” -> “首选项”。
- 在“附加开发板管理器网址”中,添加以下URL:
https://espressif.github.io/arduino-esp32/package_esp32_index.json - 然后进入“工具” -> “开发板” -> “开发板管理器”,搜索“esp32”,找到由
Espressif Systems提供的版本并安装。
- 安装必要的库 :
- 我们需要两个库:用于Wi-Fi连接的库(通常已内置)和用于驱动WS2812B的库。
- 进入“工具” -> “管理库”,搜索“FastLED”,找到并安装。FastLED库对WS2812B等LED的控制性能优化得非常好。
3.4 硬件连接
这是一个简单的连接示意图,假设你使用一根有5个LED的WS2812B灯条:
- ESP32开发板 (以常见的ESP32 DevKit C为例):
- 5V引脚 -> 连接灯带的
VCC(5V输入)。 注意 :如果灯带较长,请务必使用外部5V电源适配器为灯带供电,否则ESP32的USB供电可能不足,导致灯光闪烁或不稳定。 - GND引脚 -> 连接灯带的
GND。 至关重要 :必须将ESP32的GND、外部电源的GND(如果使用)以及灯带的GND连接在一起,即“共地”。 - GPIO引脚 (例如GPIO16) -> 连接灯带的
DIN(数据输入)。你可以选择其他空闲的GPIO,代码中相应修改即可。
- 5V引脚 -> 连接灯带的
- 灯带 :注意数据流向,
DIN接ESP32,DOUT可以接下一段灯带的DIN。
实操心得 :在第一次上电测试前,务必再三检查电源和地线的连接。接反电压会瞬间烧毁LED或ESP32。建议先使用USB为ESP32供电,并只连接少数几个LED进行测试,成功后再接入长灯带和外接电源。
4. Python服务端核心代码实现
现在,我们来构建项目的大脑——Python服务。我们将创建一个FastAPI应用,它提供一个API端点,接收文本,调用Gemini,并返回灯光控制指令。
4.1 项目结构与主程序
创建以下文件结构:
gemini-moodlights-server/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI应用主文件
│ ├── gemini_client.py # 封装Gemini调用
│ └── models.py # Pydantic数据模型
├── requirements.txt
└── .env # 存储环境变量(记得加入.gitignore)
首先,在 .env 文件中设置你的密钥:
GEMINI_API_KEY=你的_实际_API_密钥
4.2 数据模型定义 ( models.py )
我们定义客户端发送请求和服务器返回响应的数据结构。
from pydantic import BaseModel
from typing import Optional, List
class MoodRequest(BaseModel):
text: str # 用户输入的文本
# 可选:可以添加其他参数,如灯光亮度预设、模式偏好等
# brightness: float = 0.8
class LightCommand(BaseModel):
"""发送给ESP32的灯光指令"""
# 基础指令:固定颜色模式
mode: str = "solid" # 模式:solid(固定), gradient(渐变), pulse(呼吸), rainbow(彩虹)
rgb: Optional[List[int]] = None # RGB颜色值,如 [255, 100, 0]
brightness: float = 0.7 # 亮度 0.0 - 1.0
# 动态模式参数
speed: Optional[int] = None # 效果速度
# 未来扩展:可以支持更复杂的指令序列
4.3 Gemini客户端封装 ( gemini_client.py )
这里封装与Gemini API的交互。核心是设计一个有效的 提示词(Prompt) ,让Gemini能稳定地返回我们需要的结构化信息。
import os
import google.generativeai as genai
from typing import Dict, Any
import json
# 配置API密钥
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
raise ValueError("GEMINI_API_KEY环境变量未设置")
genai.configure(api_key=api_key)
# 选择模型,gemini-1.5-flash 性价比高,响应快
model = genai.GenerativeModel('gemini-1.5-flash')
def analyze_mood_and_get_color(text: str) -> Dict[str, Any]:
"""
调用Gemini分析文本情绪,并映射为灯光参数。
返回一个字典,包含颜色、模式等信息。
"""
# 精心设计的系统提示词(System Instruction)是关键
prompt = f"""
你是一个智能灯光系统的情绪分析引擎。用户会输入一段文字,你需要做两件事:
1. 分析文字中蕴含的主要情绪(如:快乐、悲伤、平静、兴奋、愤怒、专注、浪漫等)。
2. 根据分析出的情绪,推荐一个适合的RGB灯光颜色和动态模式。
请严格按照以下JSON格式回复,不要有任何额外的解释、标记或文本:
{{
"primary_emotion": "情绪关键词",
"color_rgb": [R, G, B], // 每个值在0-255之间
"suggested_mode": "solid|gradient|pulse|rainbow", // 推荐模式
"brightness_suggestion": 0.0到1.0之间的浮点数 // 亮度建议
}}
示例输入:“今天阳光真好,心情愉悦。”
示例输出:{{"primary_emotion": "happy", "color_rgb": [255, 200, 50], "suggested_mode": "solid", "brightness_suggestion": 0.9}}
用户输入:{text}
"""
try:
response = model.generate_content(prompt)
# Gemini的响应内容在 response.text 中
response_text = response.text.strip()
# 处理可能出现的markdown代码块包裹
if response_text.startswith('```json'):
response_text = response_text[7:] # 移除 ```json
if response_text.endswith('```'):
response_text = response_text[:-3]
result = json.loads(response_text)
return result
except json.JSONDecodeError as e:
print(f"Gemini返回了非JSON内容: {response_text}")
# 提供一个优雅的降级方案
return {
"primary_emotion": "neutral",
"color_rgb": [128, 128, 128], # 中性灰
"suggested_mode": "solid",
"brightness_suggestion": 0.5
}
except Exception as e:
print(f"调用Gemini API时出错: {e}")
raise
注意事项 :提示词工程是这类项目的灵魂。你需要明确告诉AI你想要的输出格式(JSON),并给出清晰的示例。即使这样,AI偶尔也可能返回格式不完整或意外的内容,因此代码中的
try-except和降级处理(返回默认中性颜色)至关重要,能保证系统的基本可用性。
4.4 FastAPI主应用 ( main.py )
这里创建Web服务器,提供API端点。
from fastapi import FastAPI, HTTPException
from app.models import MoodRequest, LightCommand
from app.gemini_client import analyze_mood_and_get_color
import uvicorn
app = FastAPI(title="Gemini Moodlights API", description="将文本情绪转化为灯光指令")
@app.post("/mood-to-light", response_model=LightCommand)
async def convert_mood_to_light(request: MoodRequest):
"""
核心端点:接收文本,调用Gemini分析,返回灯光指令。
"""
try:
# 1. 调用Gemini分析
analysis_result = analyze_mood_and_get_color(request.text)
# 2. 将分析结果映射为灯光指令
light_command = LightCommand(
mode=analysis_result.get("suggested_mode", "solid"),
rgb=analysis_result.get("color_rgb", [255, 255, 255]),
brightness=analysis_result.get("brightness_suggestion", 0.7)
)
# 可以根据 primary_emotion 做更复杂的映射,比如“兴奋”对应彩虹模式
if analysis_result.get("primary_emotion") in ["excited", "energetic"]:
light_command.mode = "rainbow"
light_command.speed = 100
return light_command
except Exception as e:
# 记录详细日志到服务器控制台
print(f"处理请求时发生错误: {e}")
# 向客户端返回一个通用的错误信息,避免泄露内部细节
raise HTTPException(status_code=500, detail="内部服务器错误,情绪分析失败")
@app.get("/health")
async def health_check():
"""健康检查端点,用于测试服务是否运行"""
return {"status": "healthy"}
if __name__ == "__main__":
# 使用uvicorn运行,host='0.0.0.0'允许同一网络下的其他设备访问
uvicorn.run(app, host="0.0.0.0", port=8000)
4.5 运行与测试服务
在项目根目录下,激活虚拟环境后运行:
python -m app.main
如果看到类似 Uvicorn running on http://0.0.0.0:8000 的信息,说明服务启动成功。
打开浏览器,访问 http://localhost:8000/docs ,你会看到自动生成的Swagger UI界面。在这里,你可以方便地测试 /mood-to-light 接口:
- 点击“Try it out”。
- 在请求体(Request body)中填入
{"text": "我今天完成了项目,感觉很轻松"}。 - 点击“Execute”。 如果一切正常,你会在“Responses”部分看到一个JSON格式的灯光指令,例如:
{
"mode": "solid",
"rgb": [100, 200, 255],
"brightness": 0.8,
"speed": null
}
这表示你的Python服务端已经成功工作,能够将文本“我今天完成了项目,感觉很轻松”解析为一种偏蓝绿色的、亮度较高的固定光。
5. ESP32端固件开发
服务端已经能产生指令了,现在我们需要让ESP32能够接收并执行这些指令。我们将编写一个Arduino程序,让ESP32连接Wi-Fi,定期轮询(或通过WebSocket监听)我们的Python服务,获取最新的灯光指令。
5.1 固件代码详解
创建一个新的Arduino项目,命名为 esp32_moodlights_client.ino 。
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <FastLED.h>
// ==================== 配置区 ====================
// 1. Wi-Fi 设置
const char* ssid = "你的Wi-Fi名称";
const char* password = "你的Wi-Fi密码";
// 2. 服务器地址 (运行Python服务的电脑IP地址)
// 在电脑命令行输入 `ipconfig` (Windows) 或 `ifconfig` (macOS/Linux) 查看本地IP
const char* serverUrl = "http://192.168.1.100:8000/mood-to-light"; // 替换为你的实际IP和端口
// 3. LED 设置
#define LED_PIN 16 // 连接灯带数据线的GPIO引脚
#define NUM_LEDS 30 // 你的LED数量
#define BRIGHTNESS 100 // 全局亮度初始值 (0-255)
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB // WS2812B灯珠通常是GRB顺序,如果颜色不对请调整
CRGB leds[NUM_LEDS];
// 4. 全局变量
String currentText = "Hello World"; // 默认显示的文本
unsigned long lastPollTime = 0;
const long pollInterval = 5000; // 轮询间隔,5秒一次(可根据需要调整)
// 灯光指令结构体,用于解析服务器返回的JSON
struct LightCommand {
String mode;
int r;
int g;
int b;
float brightness;
int speed;
};
LightCommand currentCommand = {"solid", 255, 255, 255, 0.7, 100}; // 默认命令:白色,70%亮度
// ==================== 函数声明 ====================
void connectToWiFi();
LightCommand fetchLightCommand(String text);
void applyLightCommand(const LightCommand& cmd);
void solidMode(const CRGB& color);
void rainbowMode(int speed);
void pulseMode(const CRGB& color, int speed);
void gradientMode(const CRGB& startColor, const CRGB& endColor);
// ==================== 主程序 ====================
void setup() {
Serial.begin(115200);
delay(1000);
// 初始化LED
FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
FastLED.setBrightness(BRIGHTNESS);
fill_solid(leds, NUM_LEDS, CRGB::White); // 启动时显示白色
FastLED.show();
Serial.println("LED初始化完成。");
// 连接Wi-Fi
connectToWiFi();
}
void loop() {
// 检查Wi-Fi连接
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Wi-Fi断开,尝试重连...");
connectToWiFi();
}
// 定时轮询服务器
unsigned long currentMillis = millis();
if (currentMillis - lastPollTime >= pollInterval) {
lastPollTime = currentMillis;
Serial.println("正在获取灯光指令...");
LightCommand newCommand = fetchLightCommand(currentText); // 这里可以改成从其他方式获取文本
// 如果获取到新指令,则应用它
if (newCommand.mode != "") {
currentCommand = newCommand;
applyLightCommand(currentCommand);
}
}
// 根据当前模式,执行动态效果(对于solid模式,只需设置一次颜色,无需在loop中重复)
// 动态模式如rainbow, pulse需要持续更新
if (currentCommand.mode == "rainbow") {
rainbowMode(currentCommand.speed);
} else if (currentCommand.mode == "pulse") {
pulseMode(CRGB(currentCommand.r, currentCommand.g, currentCommand.b), currentCommand.speed);
}
// solid和gradient模式在applyLightCommand中一次性设置完成
FastLED.show();
delay(10); // 给系统一点喘息时间
}
// ==================== 函数定义 ====================
void connectToWiFi() {
Serial.printf("正在连接Wi-Fi: %s", ssid);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) { // 最多尝试20次
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWi-Fi连接成功!");
Serial.print("IP地址: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\nWi-Fi连接失败!");
// 可以在这里添加失败处理,比如进入配置模式
}
}
LightCommand fetchLightCommand(String text) {
LightCommand cmd = {"", 0, 0, 0, 0.7, 100}; // 默认空命令
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
WiFiClient client;
// 准备请求
http.begin(client, serverUrl);
http.addHeader("Content-Type", "application/json");
// 构造JSON请求体
String jsonPayload = "{\"text\":\"" + text + "\"}";
// 发送POST请求
int httpResponseCode = http.POST(jsonPayload);
if (httpResponseCode == 200) {
String response = http.getString();
Serial.println("服务器响应: " + response);
// 使用ArduinoJson解析响应
DynamicJsonDocument doc(1024); // 根据响应大小调整缓冲区
DeserializationError error = deserializeJson(doc, response);
if (!error) {
cmd.mode = doc["mode"].as<String>();
cmd.r = doc["rgb"][0];
cmd.g = doc["rgb"][1];
cmd.b = doc["rgb"][2];
cmd.brightness = doc["brightness"];
// speed字段可能为null,需要安全获取
if (!doc["speed"].isNull()) {
cmd.speed = doc["speed"];
}
Serial.println("指令解析成功。");
} else {
Serial.print("JSON解析失败: ");
Serial.println(error.c_str());
}
} else {
Serial.printf("HTTP请求失败,错误码: %d\n", httpResponseCode);
Serial.println(http.getString()); // 打印错误信息
}
http.end();
} else {
Serial.println("Wi-Fi未连接,无法获取指令。");
}
return cmd;
}
void applyLightCommand(const LightCommand& cmd) {
// 计算实际亮度值 (0-255)
uint8_t actualBrightness = (uint8_t)(cmd.brightness * 255);
FastLED.setBrightness(actualBrightness);
CRGB targetColor = CRGB(cmd.r, cmd.g, cmd.b);
if (cmd.mode == "solid") {
solidMode(targetColor);
} else if (cmd.mode == "rainbow") {
// rainbow模式会在loop中持续运行
// 这里可以初始化一些状态变量
} else if (cmd.mode == "pulse") {
// pulse模式会在loop中持续运行
// 这里可以初始化一些状态变量
} else if (cmd.mode == "gradient") {
// 简化版:从当前颜色渐变到目标颜色
// 实际可以更复杂,比如定义起止颜色
gradientMode(leds[0], targetColor); // 示例:从第一个LED的颜色渐变
} else {
// 未知模式,降级为solid
solidMode(targetColor);
}
FastLED.show();
}
// 具体的灯光效果函数
void solidMode(const CRGB& color) {
fill_solid(leds, NUM_LEDS, color);
}
void rainbowMode(int speed) {
// speed值越小,彩虹变化越快
static uint8_t hue = 0;
fill_rainbow(leds, NUM_LEDS, hue);
hue += (speed / 10); // 根据speed调整变化速率
}
void pulseMode(const CRGB& color, int speed) {
// 简单的呼吸灯效果
static uint8_t brightness = 0;
static int8_t direction = 5; // 亮度变化步长
brightness += direction;
if (brightness >= 255 || brightness <= 0) {
direction = -direction;
}
// 根据基础颜色和动态亮度计算当前颜色
CRGB currentColor = color;
currentColor.nscale8_video(brightness); // 使用视频缩放,效果更平滑
fill_solid(leds, NUM_LEDS, currentColor);
}
void gradientMode(const CRGB& startColor, const CRGB& endColor) {
// 简单的线性渐变填充
for (int i = 0; i < NUM_LEDS; i++) {
// 计算每个LED的混合比例
float ratio = (float)i / (float)(NUM_LEDS - 1);
leds[i] = blend(startColor, endColor, ratio * 255);
}
}
5.2 代码烧录与配置
- 修改配置 :在代码开头的“配置区”,务必修改
ssid、password和serverUrl。serverUrl中的IP地址必须是你运行Python服务的电脑在局域网中的IP。 - 选择开发板与端口 :在Arduino IDE中,“工具” -> “开发板”选择你的ESP32型号(如
ESP32 Dev Module),“端口”选择对应的串口。 - 编译与上传 :点击“上传”按钮。首次上传可能需要长按ESP32板上的
BOOT按钮进入下载模式。上传成功后,ESP32会自动重启。
打开串口监视器(波特率设置为115200),你将看到ESP32连接Wi-Fi和轮询服务器的日志。如果一切顺利,LED灯带会从启动时的白色,变为根据默认文本“Hello World”分析出的颜色。
6. 系统联调与功能扩展
基础功能已经跑通,但一个完整的项目还需要考虑稳定性、交互方式和更多可能性。
6.1 联调测试与问题排查
将Python服务和ESP32固件都运行起来后,进行端到端测试:
-
测试流程 :
- 确保电脑和ESP32在同一个局域网。
- 运行Python服务 (
python -m app.main)。 - 给ESP32上电,观察串口日志,确认它连接Wi-Fi成功并开始轮询。
- 使用Swagger UI (
http://localhost:8000/docs) 或curl命令发送一个新的文本请求。
curl -X POST "http://localhost:8000/mood-to-light" -H "Content-Type: application/json" -d "{\"text\":\"我有点焦虑\"}"- 观察ESP32串口日志,看是否收到了新的指令,以及LED灯光是否相应变化。
-
常见问题与解决方案 :
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| ESP32无法连接Wi-Fi | SSID/密码错误,信号弱 | 检查代码中的SSID/密码;将ESP32靠近路由器;查看串口日志。 |
| ESP32轮询失败,HTTP错误码 | 服务器地址/端口错误,防火墙阻止 | 在电脑上 ping ESP32_IP 和ESP32上 ping 电脑_IP 测试连通性;检查电脑防火墙是否允许8000端口入站(Windows Defender防火墙或macOS/Linux的ufw/iptables)。 |
| 灯光颜色与预期不符 | RGB顺序错误,亮度映射问题 | WS2812B常见为GRB顺序,调整代码中 COLOR_ORDER 为 GRB ;检查Gemini返回的RGB值是否在0-255。 |
| 灯光闪烁或不稳定 | 电源功率不足 | 为长灯带单独接5V/2A以上的电源,并确保共地。 |
| Gemini API返回400/403错误 | API密钥无效或未启用,项目未配置 | 检查环境变量 GEMINI_API_KEY 是否正确;在Google Cloud Console确认Gemini API已启用,且API密钥有权限。 |
| Python服务启动报错 | 端口被占用,依赖未安装 | 换用其他端口(如8080);确认在虚拟环境中并安装了所有 requirements.txt 中的包。 |
6.2 交互方式扩展
目前的系统需要通过API工具手动发送请求,交互性不强。我们可以轻松扩展:
- 添加一个简单的Web界面 :使用FastAPI的静态文件服务,创建一个
index.html页面,包含一个文本框和一个按钮。用户直接在浏览器里输入文本,点击按钮,通过JavaScript调用后端的/mood-to-light接口,并将指令转发给ESP32(或由后端直接转发)。 - 集成语音输入 :利用浏览器的Web Speech API或第三方语音识别服务,让用户可以直接说话来控制灯光。
- 创建定时任务或情绪日记 :让灯光在特定时间根据日程变化,或者连接一个日记应用,每晚根据日记内容生成一段总结性的灯光秀。
6.3 部署优化
- Python服务部署 :可以将FastAPI服务部署到云服务器(如Google Cloud Run, Heroku, VPS),这样ESP32就可以通过公网地址访问,不再受限于本地网络。部署时注意设置好环境变量和安全配置。
- ESP32优化 :
- 改用WebSocket :轮询效率低且有延迟。可以修改代码,让ESP32作为WebSocket客户端,与服务端建立长连接。当服务端收到新文本时,主动将指令推送给ESP32,实现真正的实时控制。
- OTA(空中升级) :实现OTA功能,以后更新固件无需再插线烧录。
- 低功耗模式 :如果使用电池供电,可以设计仅在检测到有新指令时才唤醒Wi-Fi模块。
6.4 创意灯光效果深化
当前的灯光效果比较基础。利用FastLED库强大的功能,可以创造出令人惊叹的效果:
- 情绪序列 :不是只映射一种颜色,而是让Gemini生成一个代表情绪变化的颜色序列(如从“沮丧”的深蓝,渐变到“希望”的浅黄),然后让灯带动态播放这个序列。
- 音乐可视化 :让ESP32通过麦克风模块(如INMP441)采集环境声音,将音频的频谱或节奏数据发送给Python服务,Python服务可以调用Gemini来“解读”这段音乐的“情绪色彩”,再控制灯光。这实现了从听觉到视觉的情绪转换。
- 多区域控制 :如果你有多条灯带(比如在房间的不同位置),可以让Gemini根据文本描述,为不同区域生成不同的灯光方案,营造更立体的氛围。
这个项目的魅力在于,它提供了一个坚实的框架,将AI的抽象理解与物理世界的直观反馈连接起来。剩下的,就取决于你的想象力和创造力了。从今天开始,让你的房间灯光,真正听懂你的心情。
更多推荐



所有评论(0)