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 模型)主要基于以下几点考量:

  1. 性价比与易用性 :Google Cloud为新用户提供了300美元的免费额度,并且Gemini API的定价,尤其是 gemini-1.5-flash 模型,在处理这类轻量级、高并发的文本情感分析任务时,成本非常低,甚至对于个人项目来说,在免费额度内几乎可以忽略不计。相比之下,一些其他模型的API调用成本可能更高。
  2. 上下文长度与响应速度 gemini-1.5-flash 模型在保持不错理解能力的同时,拥有极快的响应速度,这对于需要实时反馈的灯光系统至关重要。它也能处理足够长的上下文,方便我们设计更复杂的提示词(Prompt)。
  3. 与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能力的第一步,也是最容易卡住的地方。

  1. 创建Google Cloud项目

    • 访问 Google Cloud Console
    • 如果你没有项目,点击左上角项目下拉框,选择“新建项目”。给它起个名字,比如 gemini-moodlights
    • 地区选择 :在创建项目时,可能会让你选择地区。这个选择对于Gemini API本身影响不大,因为API是全局服务。但对于你后续可能用到的其他服务(如Cloud Run部署Python服务),选择离你用户群体近的地区(如 asia-east1 (台湾)或 us-central1 )有助于降低延迟。对于纯本地开发,可以任意选择。
  2. 启用Gemini API并创建密钥

    • 在Cloud Console中,使用顶部的搜索栏,搜索“Gemini API”。
    • 进入Gemini API页面,点击“启用”。
    • API启用后,在左侧菜单栏,依次进入“API和服务” -> “凭据”。
    • 点击“创建凭据” -> “API密钥”。系统会生成一个密钥字符串, 立即复制并妥善保存 。这个密钥一旦关闭对话框就无法再次查看完整内容,如果丢失需要重新生成。
  3. 设置环境变量(安全最佳实践) : 千万不要把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+。我们创建一个干净的虚拟环境并安装依赖。

  1. 创建项目目录并初始化虚拟环境

    mkdir gemini-moodlights-server
    cd gemini-moodlights-server
    python -m venv venv  # 创建虚拟环境
    # 激活虚拟环境
    # Windows (PowerShell):
    .\venv\Scripts\Activate.ps1
    # Linux/macOS:
    source venv/bin/activate
    
  2. 安装依赖库 : 创建一个 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.txt
    
    • fastapi uvicorn 是我们的Web框架和服务器。
    • google-generativeai 是Google官方提供的Gemini SDK,比直接用 requests 封装更简单。
    • pydantic 用于数据验证和设置,FastAPI深度集成它。

3.3 ESP32开发环境搭建

ESP32的编程有多种方式,这里我们选择最易上手的 Arduino IDE 方案。

  1. 安装Arduino IDE :从官网下载并安装。
  2. 添加ESP32开发板支持
    • 打开Arduino IDE,进入“文件” -> “首选项”。
    • 在“附加开发板管理器网址”中,添加以下URL: https://espressif.github.io/arduino-esp32/package_esp32_index.json
    • 然后进入“工具” -> “开发板” -> “开发板管理器”,搜索“esp32”,找到由 Espressif Systems 提供的版本并安装。
  3. 安装必要的库
    • 我们需要两个库:用于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,代码中相应修改即可。
  • 灯带 :注意数据流向, 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 接口:

  1. 点击“Try it out”。
  2. 在请求体(Request body)中填入 {"text": "我今天完成了项目,感觉很轻松"}
  3. 点击“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 代码烧录与配置

  1. 修改配置 :在代码开头的“配置区”,务必修改 ssid password serverUrl serverUrl 中的IP地址必须是你运行Python服务的电脑在局域网中的IP。
  2. 选择开发板与端口 :在Arduino IDE中,“工具” -> “开发板”选择你的ESP32型号(如 ESP32 Dev Module ),“端口”选择对应的串口。
  3. 编译与上传 :点击“上传”按钮。首次上传可能需要长按ESP32板上的 BOOT 按钮进入下载模式。上传成功后,ESP32会自动重启。

打开串口监视器(波特率设置为115200),你将看到ESP32连接Wi-Fi和轮询服务器的日志。如果一切顺利,LED灯带会从启动时的白色,变为根据默认文本“Hello World”分析出的颜色。

6. 系统联调与功能扩展

基础功能已经跑通,但一个完整的项目还需要考虑稳定性、交互方式和更多可能性。

6.1 联调测试与问题排查

将Python服务和ESP32固件都运行起来后,进行端到端测试:

  1. 测试流程

    • 确保电脑和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灯光是否相应变化。
  2. 常见问题与解决方案

问题现象 可能原因 排查步骤与解决方案
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工具手动发送请求,交互性不强。我们可以轻松扩展:

  1. 添加一个简单的Web界面 :使用FastAPI的静态文件服务,创建一个 index.html 页面,包含一个文本框和一个按钮。用户直接在浏览器里输入文本,点击按钮,通过JavaScript调用后端的 /mood-to-light 接口,并将指令转发给ESP32(或由后端直接转发)。
  2. 集成语音输入 :利用浏览器的Web Speech API或第三方语音识别服务,让用户可以直接说话来控制灯光。
  3. 创建定时任务或情绪日记 :让灯光在特定时间根据日程变化,或者连接一个日记应用,每晚根据日记内容生成一段总结性的灯光秀。

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的抽象理解与物理世界的直观反馈连接起来。剩下的,就取决于你的想象力和创造力了。从今天开始,让你的房间灯光,真正听懂你的心情。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐