ESP32 变身 WiFi 智能灯控制器
在浏览器里控制你的彩灯,手机电脑都能用 从零搭建一个完整的嵌入式 Web 服务器
目录
- 项目概述
- 两种联网模式
- [AP 模式:ESP32 自己开热点](#AP 模式:ESP32 自己开热点 "#3-ap-%E6%A8%A1%E5%BC%8Fesp32-%E8%87%AA%E5%B7%B1%E5%BC%80%E7%83%AD%E7%82%B9")
- [STA 模式:ESP32 连路由器](#STA 模式:ESP32 连路由器 "#4-sta-%E6%A8%A1%E5%BC%8Fesp32-%E8%BF%9E%E8%B7%AF%E7%94%B1%E5%99%A8")
- [搭建 HTTP 服务器](#搭建 HTTP 服务器 "#5-%E6%90%AD%E5%BB%BA-http-%E6%9C%8D%E5%8A%A1%E5%99%A8")
- [编写 Web 控制页面](#编写 Web 控制页面 "#6-%E7%BC%96%E5%86%99-web-%E6%8E%A7%E5%88%B6%E9%A1%B5%E9%9D%A2")
- 完整的项目代码
- 接线与硬件
- 常见问题
1. 项目概述
1.1 最终效果
arduino
你的手机 / 电脑
│
│ WiFi 连接
│
▼
ESP32-S3 (HTTP 服务器)
│
│ GPIO 48
│
▼
WS2812 RGB 彩灯
你在浏览器里点颜色 → ESP32 收到 HTTP 请求 → 灯立马变色
1.2 技术栈
| 组件 | 用途 |
|---|---|
| ESP-IDF (v5.4.4) | 开发框架 |
esp_http_server |
内置 HTTP 服务器 |
led_strip |
WS2812 驱动 |
esp_wifi |
WiFi 功能 |
| HTML + CSS + JS | 前端控制页面 |
| REST API | 前后端通信 |
1.3 涉及的文件
css
esp32-wifi-light/
├── CMakeLists.txt
├── main/
│ ├── CMakeLists.txt
│ ├── idf_component.yml ← led_strip 依赖
│ └── main.c ← 全部代码
└── build/ ← 编译产物
2. 两种联网模式
ESP32 当 WiFi 设备有两种工作模式:
2.1 AP 模式(Access Point,热点模式)
arduino
ESP32 自己开一个 WiFi 热点
电脑/手机 连这个热点
访问 http://192.168.4.1
✅ 不需要路由器
✅ 不需要联网
✅ 任何时候都能控制
⚠️ 手机连了 ESP32 的热点就不能上网了
2.2 STA 模式(Station,客户端模式)
ESP32 连你家路由器
电脑/手机 也连同一个路由器
访问 ESP32 的 IP 地址
✅ 手机可以同时上网
✅ 可以远程控制(配合内网穿透)
✅ 适合固定安装场景
⚠️ 需要路由器
⚠️ 需要知道 ESP32 的 IP 地址
2.3 选哪个?
| 场景 | 推荐模式 |
|---|---|
| 调试阶段,电脑连板子 | AP 模式(省心) |
| 做产品放家里 | STA 模式 |
| 拿去展示/演示 | AP 模式 |
初学者建议先用 AP 模式,跑通之后再切 STA。
3. AP 模式:ESP32 自己开热点
3.1 原理
arduino
ESP32 创建 WiFi 网络
┌──────────┐
│ ESP32 │ SSID: "ESP32_LED"
│ ● │ PASS: "12345678"
│ 192.168. │ IP: 192.168.4.1 (固定)
│ 4.1 │
└──────────┘
│
┌────┴────┐
│ 电脑连接 │
│ 热点即可 │
└─────────┘
3.2 代码实现
c
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include <string.h>
#define AP_SSID "ESP32_LED"
#define AP_PASS "12345678"
void wifi_init_ap(void)
{
// 1. 初始化网络接口
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_ap(); // 创建 AP 网络接口
// 2. 初始化 WiFi 驱动
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// 3. 配置热点参数
wifi_config_t wifi_config = {
.ap = {
.ssid = AP_SSID,
.ssid_len = strlen(AP_SSID), // 热点名长度
.password = AP_PASS, // 密码
.max_connection = 4, // 最多 4 个设备同时连
.authmode = WIFI_AUTH_WPA_WPA2_PSK, // 加密方式
},
};
// 4. 设置为 AP 模式并启动
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
printf("✅ 热点已开启: %s\n", AP_SSID);
printf(" 密码: %s\n", AP_PASS);
printf(" 控制页面: http://192.168.4.1\n");
}
3.3 主要函数说明
scss
esp_netif_init() ← 网络接口系统初始化
esp_event_loop_create_default() ← 创建事件循环(WiFi事件都在这里处理)
esp_netif_create_default_wifi_ap() ← 创建 AP 模式的网络接口
WIFI_INIT_CONFIG_DEFAULT() ← WiFi 配置宏,给个默认配置
esp_wifi_init(&cfg) ← 用配置初始化 WiFi 驱动
esp_wifi_set_mode(WIFI_MODE_AP) ← 设为 AP 模式
esp_wifi_set_config(WIFI_IF_AP, &wifi_config) ← 应用热点配置
esp_wifi_start() ← 启动 WiFi
3.4 踩坑:NVS 必须初始化
c
// WiFi 依赖 NVS(Non-Volatile Storage,非易失存储)
// 没有这步初始化,esp_wifi_init 会报错
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
这段代码的作用: NVS 就像 ESP32 的"小硬盘",WiFi 配置(如 MAC 地址)存在里面。第一次用可能空间不够或版本不对,就擦除重建。
4. STA 模式:ESP32 连路由器
4.1 原理
arduino
你的路由器 (192.168.1.1)
┌──────────────────┐
│ WiFi │
└──┬───────────┬───┘
│ │
▼ ▼
┌──────┐ ┌──────┐
│ 电脑 │ │ ESP32│
│ 手机 │ │ │
└──────┘ └──────┘
电脑访问 http://192.168.1.xxx 控制灯
4.2 代码实现
c
#include "freertos/event_groups.h" // 事件组:等待 WiFi 连接结果
#define WIFI_SSID "你家WiFi名字"
#define WIFI_PASS "你家WiFi密码"
#define WIFI_CONNECTED_BIT BIT0 // 连接成功标志
#define WIFI_FAIL_BIT BIT1 // 连接失败标志
static EventGroupHandle_t s_wifi_event_group;
static int s_retry_num = 0; // 重试次数
// WiFi 事件回调函数(重要!)
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
// WiFi 已启动 → 开始连接
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
// 连接断开 → 自动重连,最多 5 次
if (s_retry_num < 5) {
esp_wifi_connect();
s_retry_num++;
} else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
// 拿到 IP 了 → 连接成功!
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
printf("✅ 连接成功!IP: " IPSTR "\n", IP2STR(&event->ip_info.ip));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
};
}
// STA 模式初始化
void wifi_init_sta(void)
{
// 创建事件组(用来等待连接结果)
s_wifi_event_group = xEventGroupCreate();
// 网络初始化
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta(); // STA 接口
// WiFi 初始化
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// 注册事件回调
esp_event_handler_instance_t instance_any_id, instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(
WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL, &instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(
IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_event_handler, NULL, &instance_got_ip));
// 配置 WiFi
wifi_config_t wifi_config = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASS,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
printf("正在连接 %s ...\n", WIFI_SSID);
// 等连接结果
EventBits_t bits = xEventGroupWaitBits(
s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE, pdFALSE,
portMAX_DELAY // 无限等待
);
if (bits & WIFI_FAIL_BIT) {
printf("❌ 连接失败!请检查密码或信号\n");
}
}
4.3 事件回调机制说明(新手必看)
ESP32 的 WiFi 是事件驱动 的,不会在 esp_wifi_start() 那里阻塞等待。
arduino
你写的代码: ESP32 实际执行:
esp_wifi_start() ──────→ "我启动WiFi了,不等人!"
│
│ 后台自动连
│
WIFI_EVENT_STA_DISCONNECTED ──→ "没连上,再试试..."
│
IP_EVENT_STA_GOT_IP ────────→ "连上了!IP是xxx"
你的事件回调函数里处理这个
所以需要:
- 事件回调函数------收到事件时执行
- 事件组------让主程序能"等待"连接完成
4.4 STA 模式踩坑指南
如果连不上:
arduino
1. 确认 WiFi 名字和密码有没有写错
#define WIFI_SSID "HONOR 100" ← 注意大小写和空格
#define WIFI_PASS "1234567890"
2. ESP32-S3 只支持 2.4GHz
不支持 5GHz WiFi!
如果路由器是 5G 频段 → 换 2.4G 或改双频合一
3. 信号太弱
ESP32 的 WiFi 信号不强
离路由器近一点试试
4. 手机热点
大部分手机热点有 "AP 隔离"
连了 ESP32 的设备之间互相 ping 不通
→ 关掉热点的"设备隔离"开关
→ 或用 AP 模式绕过这个问题
5. 搭建 HTTP 服务器
5.1 什么是 HTTP 服务器?
javascript
你在浏览器输入 http://192.168.4.1/
浏览器发起 HTTP GET 请求 ───────→ ESP32
ESP32 收到请求
返回 HTML 页面 ←─────────┐
浏览器收到 HTML ←──────────────┘
然后你点了一个"红色"按钮:
浏览器发起 HTTP GET /set?r=255&g=0&b=0 ─→ ESP32
ESP32 解析参数
设置灯为红色
返回 JSON {"r":255,"g":0,"b":0}
浏览器收到 JSON,更新页面上的颜色预览 ←─┘
5.2 ESP-IDF 的 HTTP 服务器
esp_http_server 是 ESP-IDF 内置的 HTTP 服务器,不需要任何外部库。
c
#include "esp_http_server.h"
5.3 创建服务器
c
void start_http_server(void)
{
httpd_handle_t server = NULL;
// 服务器默认配置
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = 8; // 最多 8 个路由
config.lru_purge_enable = true; // 内存不够时自动清理旧连接
// 启动
if (httpd_start(&server, &config) == ESP_OK) {
// 注册路由(下面讲)
}
}
5.4 注册路由(URI Handler)
路由 = URL 路径 → 处理函数 的映射
c
// 根路径:返回控制页面
httpd_uri_t root_uri = {
.uri = "/", // 浏览器访问 /
.method = HTTP_GET, // GET 请求
.handler = root_get_handler, // 处理函数
};
httpd_register_uri_handler(server, &root_uri);
// 控制路径:设置颜色
httpd_uri_t set_uri = {
.uri = "/set", // 浏览器访问 /set?r=255&g=0&b=0
.method = HTTP_GET,
.handler = set_color_handler,
};
httpd_register_uri_handler(server, &set_uri);
5.5 编写处理函数
根路径处理:返回整个 HTML 页面
c
static esp_err_t root_get_handler(httpd_req_t *req)
{
// 设置响应头:告诉浏览器这是 HTML
httpd_resp_set_type(req, "text/html; charset=utf-8");
// 发送 HTML 内容
httpd_resp_sendstr(req, "<html>...</html>");
return ESP_OK;
}
颜色控制处理:解析 URL 参数,设置灯
c
static esp_err_t set_color_handler(httpd_req_t *req)
{
// 1. 从 URL 获取查询字符串
char buf[64];
if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) != ESP_OK) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "参数缺失");
return ESP_OK;
}
// 此时 buf = "r=255&g=0&b=0"
// 2. 解析每个参数
char r_str[8] = "0", g_str[8] = "0", b_str[8] = "0";
httpd_query_key_value(buf, "r", r_str, sizeof(r_str)); // "255"
httpd_query_key_value(buf, "g", g_str, sizeof(g_str)); // "0"
httpd_query_key_value(buf, "b", b_str, sizeof(b_str)); // "0"
// 3. 转成整数
int r = atoi(r_str);
int g = atoi(g_str);
int b = atoi(b_str);
// 4. 限制范围(防止恶意请求)
if (r < 0) { r = 0; }
if (r > 255) { r = 255; }
if (g < 0) { g = 0; }
if (g > 255) { g = 255; }
if (b < 0) { b = 0; }
if (b > 255) { b = 255; }
// 5. 设置 LED
set_color(r, g, b);
// 6. 返回 JSON 给浏览器确认
char resp[64];
snprintf(resp, sizeof(resp), "{\"r\":%d,\"g\":%d,\"b\":%d}", r, g, b);
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, resp);
return ESP_OK;
}
5.6 完整的处理函数结构
javascript
handler(req) {
1. 解析请求(URL/参数/正文)
2. 执行业务逻辑(设置灯/读传感器)
3. 返回响应(HTML/JSON/纯文本)
4. return ESP_OK
}
6. 编写 Web 控制页面
6.1 页面嵌入方式
最省事的方式:把 HTML 直接写在 C 代码里
c
#define PAGE "<!DOCTYPE html><html>..."
优点:不需要额外文件 缺点:代码可读性差,编辑器没有语法高亮
小贴士: 先在浏览器里调试好 HTML, 确认没问题了再压缩到一行嵌入到 C 代码里。
6.2 控制页面功能
完整页面包含:
less
┌────────────────────────┐
│ ESP32 RGB 彩灯 │
│ │
│ ┌────────────────┐ │
│ │ 颜色预览圆盘 │ │ ← 实时显示当前颜色
│ └────────────────┘ │
│ │
│ [红][橙][黄][绿] │ ← 预设颜色按钮
│ [青][蓝][紫][白] │
│ │
│ 拾色器: [■] │ ← 系统颜色选择器
│ │
│ R ████████░░ 200 │
│ G ████████░░ 100 │ ← RGB 滑条微调
│ B ████████░░ 50 │
│ │
│ [❌ 关灯] │ ← 关灯按钮
└────────────────────────┘
6.3 前端代码(HTML + CSS + JS)
HTML 结构:
html
<h1>ESP32 RGB 彩灯</h1>
<div class="preview" id="preview"></div> <!-- 颜色预览 -->
<div class="presets">
<button onclick="setc(255,0,0)" style="background:red">红</button>
<button onclick="setc(255,128,0)" style="background:orange">橙</button>
<button onclick="setc(255,255,0)" style="background:gold">黄</button>
<!-- ...更多颜色 -->
</div>
<div class="picker">
<input type="color" onchange="fromPicker()"> <!-- 系统拾色器 -->
</div>
<div class="sliders">
R: <input type="range" min="0" max="255" oninput="fromSliders()">
G: <input type="range" min="0" max="255" oninput="fromSliders()">
B: <input type="range" min="0" max="255" oninput="fromSliders()">
</div>
<button onclick="setc(0,0,0)">关灯</button>
JavaScript 核心函数:
javascript
// 设置颜色(核心函数)
async function setc(r, g, b) {
// 向 ESP32 发送 HTTP 请求
await fetch(`/set?r=${r}&g=${g}&b=${b}`);
// 更新页面预览
document.getElementById('preview')
.style.background = `rgb(${r},${g},${b})`;
// 同步更新滑条数值
document.getElementById('rSlider').value = r;
document.getElementById('gSlider').value = g;
document.getElementById('bSlider').value = b;
}
// 拾色器取色
function fromPicker() {
const p = document.getElementById('colorPicker').value;
// p = "#ff0000" 格式,转成 RGB
const r = parseInt(p.slice(1, 3), 16);
const g = parseInt(p.slice(3, 5), 16);
const b = parseInt(p.slice(5, 7), 16);
setc(r, g, b);
}
// 滑条微调
function fromSliders() {
const r = document.getElementById('rSlider').value;
const g = document.getElementById('gSlider').value;
const b = document.getElementById('bSlider').value;
setc(r, g, b);
}
6.4 HTTP 请求详解
浏览器向 ESP32 发送请求的完整过程:
scss
你点击"红色"按钮
│
▼
setc(255, 0, 0) 被调用
│
▼
fetch("/set?r=255&g=0&b=0") ← 浏览器发出 GET 请求
│
▼
ESP32 收到请求
→ set_color_handler() 被调用
→ 解析出 r=255, g=0, b=0
→ led_strip_set_pixel(0, 255, 0, 0)
→ led_strip_refresh() ← 灯真的变色了!
→ 返回 JSON: {"r":255,"g":0,"b":0}
│
▼
浏览器收到响应
→ await 完成
→ 更新预览圆的颜色
→ 更新滑条位置
关键词: async/await 让 JavaScript 可以"等一下"ESP32 回应再继续。
6.5 添加开关功能(进阶)
在页面上加一个「电源开关」:
c
// ESP32 端:记录开关状态
static int power_on = 0;
static int cur_r = 0, cur_g = 0, cur_b = 0;
void set_color(uint8_t r, uint8_t g, uint8_t b)
{
cur_r = r; cur_g = g; cur_b = b;
if (power_on) {
led_strip_set_pixel(led_strip, 0, r, g, b);
led_strip_refresh(led_strip);
}
}
void set_power(int on)
{
power_on = on;
if (on) {
led_strip_set_pixel(led_strip, 0, cur_r, cur_g, cur_b);
led_strip_refresh(led_strip);
} else {
led_strip_clear(led_strip);
}
}
前端行为:
- 开关关 → 灯灭,所有颜色按钮变灰不可点
- 开关开 → 灯亮,显示上次保存的颜色,控件恢复
- 关灯状态下点颜色 → 自动开灯
7. 完整的项目代码
7.1 项目结构
css
esp32-wifi-light/
├── CMakeLists.txt
└── main/
├── CMakeLists.txt
├── idf_component.yml
└── main.c
7.2 CMakeLists.txt(根目录)
cmake
cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(wifi_light)
7.3 main/CMakeLists.txt
cmake
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES led_strip esp_wifi esp_event esp_netif nvs_flash lwip esp_http_server)
7.4 main/idf_component.yml
yaml
dependencies:
espressif/led_strip: "^2.0"
7.5 main/main.c
c
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_http_server.h"
#include "nvs_flash.h"
#include "led_strip.h"
// ======================== 配置(改这里!)=====================
#define LED_GPIO GPIO_NUM_48
// AP 热点模式配置(ESP32 自己开热点)
#define AP_SSID "ESP32_LED"
#define AP_PASS "12345678"
// STA 客户端模式配置(ESP32 连路由器)
// 把下面两个注释去掉,AP 模式注释掉,就可以切换
// #define WIFI_SSID "你的WiFi名字"
// #define WIFI_PASS "你的WiFi密码"
// ======================== 全局变量 ========================
static led_strip_handle_t led_strip;
// 灯光状态
static int power_on = 0;
static int cur_r = 0, cur_g = 0, cur_b = 0;
// ======================== LED 控制 ========================
static void set_color(uint8_t r, uint8_t g, uint8_t b)
{
cur_r = r; cur_g = g; cur_b = b;
if (power_on) {
led_strip_set_pixel(led_strip, 0, r, g, b);
led_strip_refresh(led_strip);
}
}
static void set_power(int on)
{
power_on = on;
if (on) {
led_strip_set_pixel(led_strip, 0, cur_r, cur_g, cur_b);
led_strip_refresh(led_strip);
} else {
led_strip_clear(led_strip);
}
}
// ======================== WiFi AP 模式 ========================
void wifi_init_ap(void)
{
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_ap();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
wifi_config_t wifi_config = {
.ap = {
.ssid = AP_SSID,
.ssid_len = strlen(AP_SSID),
.password = AP_PASS,
.max_connection = 4,
.authmode = WIFI_AUTH_WPA_WPA2_PSK,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
printf("\n🎯 热点: %s\n", AP_SSID);
printf("🔑 密码: %s\n", AP_PASS);
printf("🌐 控制页面: http://192.168.4.1\n");
}
// ======================== HTTP 服务器 ========================
/*
* Web 控制页面(内嵌 HTML)
* 包含:颜色预览、预设按钮、拾色器、RGB滑条、开关
* 注意:压缩在一行是因为 C 语言字符串不能跨行
*/
#define PAGE "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>ESP32 彩灯控制</title><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:Arial,sans-serif;background:#1a1a2e;color:#fff;display:flex;flex-direction:column;align-items:center;padding:20px;min-height:100vh}h1{font-size:24px;margin:10px 0}.preview{width:120px;height:120px;border-radius:50%;margin:15px auto;border:3px solid #555;transition:background .2s,opacity .3s;box-shadow:0 0 30px rgba(255,255,255,.15)}.preview.off{opacity:.15;box-shadow:none}.switch-wrap{display:flex;align-items:center;gap:12px;margin:10px 0}.switch{position:relative;width:56px;height:28px;background:#444;border-radius:14px;cursor:pointer;transition:background .25s}.switch.on{background:#4caf50}.switch::after{content:'';position:absolute;top:3px;left:3px;width:22px;height:22px;background:#fff;border-radius:50%;transition:transform .25s}.switch.on::after{transform:translateX(28px)}.switch-label{font-size:16px;font-weight:bold}.presets{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;max-width:320px;width:100%;margin:12px 0}.preset{height:50px;border:none;border-radius:8px;cursor:pointer;font-size:13px;color:#fff;font-weight:bold;text-shadow:0 1px 2px rgba(0,0,0,.5);transition:transform .1s,opacity .2s}.preset:active{transform:scale(.92)}.preset.disabled{opacity:.25;pointer-events:none}.picker label{font-size:14px;color:#aaa}.picker.disabled{opacity:.25;pointer-events:none}.sliders{width:100%;max-width:300px;display:flex;flex-direction:column;gap:8px;transition:opacity .2s}.slider-row{display:flex;align-items:center;gap:10px}.slider-row label{width:20px}.slider-row input{flex:1}.slider-row .val{width:32px;text-align:right;font-size:13px}.sliders.disabled{opacity:.25;pointer-events:none}.off-btn{width:100%;max-width:300px;padding:12px;margin-top:10px;border:none;border-radius:8px;background:#e74c3c;color:#fff;font-size:16px;cursor:pointer}.off-btn:active{background:#c0392b}input[type=range]{accent-color:#fff}</style></head><body><h1>ESP32 RGB 彩灯</h1><div class=\"switch-wrap\"><div class=\"switch\" id=powerSwitch onclick=togglePower()></div><span class=switch-label id=powerLabel>💡 关</span></div><div class=\"preview off\" id=preview></div><div class=presets><button class=preset disabled style=background:red onclick=setc(255,0,0)>红</button><button class=preset disabled style=background:orange onclick=setc(255,128,0)>橙</button><button class=preset disabled style=background:gold onclick=setc(255,255,0)>黄</button><button class=preset disabled style=background:lime onclick=setc(0,255,0)>绿</button><button class=preset disabled style=background:cyan onclick=setc(0,255,255)>青</button><button class=preset disabled style=background:dodgerblue onclick=setc(0,128,255)>蓝</button><button class=preset disabled style=background:blueviolet onclick=setc(138,43,226)>紫</button><button class=preset disabled style=background:white onclick=setc(255,255,255)>白</button></div><div class=picker><label>🎨 拾色器</label><input type=color id=colorPicker onchange=fromPicker() style=\"width:80px;height:80px;border:none;cursor:pointer\"></div><div class=sliders disabled><div class=slider-row><label>R</label><input type=range min=0 max=255 id=rSlider oninput=fromSliders()><span class=val id=rVal>0</span></div><div class=slider-row><label>G</label><input type=range min=0 max=255 id=gSlider oninput=fromSliders()><span class=val id=gVal>0</span></div><div class=slider-row><label>B</label><input type=range min=0 max=255 id=bSlider oninput=fromSliders()><span class=val id=bVal>0</span></div></div><button class=off-btn onclick=setc(0,0,0)>❌ 关灯</button><script>async function setc(r,g,b){await fetch(`/set?r=${r}&g=${g}&b=${b}`);const onoff=(r||g||b)?1:0;const prv=document.getElementById('preview');const sw=document.getElementById('powerSwitch');const pl=document.getElementById('powerLabel');prv.style.background=`rgb(${r},${g},${b})`;prv.classList.toggle('off',!onoff);sw.classList.toggle('on',onoff);pl.textContent=onoff?'💡 开':'💡 关';document.getElementById('rSlider').value=r;document.getElementById('gSlider').value=g;document.getElementById('bSlider').value=b;document.getElementById('rVal').textContent=r;document.getElementById('gVal').textContent=g;document.getElementById('bVal').textContent=b;document.querySelectorAll('.preset,.picker,.sliders').forEach(e=>e.classList.toggle('disabled',!onoff))}function fromPicker(){const p=document.getElementById('colorPicker').value;setc(parseInt(p.slice(1,3),16),parseInt(p.slice(3,5),16),parseInt(p.slice(5,7),16))}function fromSliders(){setc(+document.getElementById('rSlider').value,+document.getElementById('gSlider').value,+document.getElementById('bSlider').value)}async function togglePower(){const r=await fetch('/power');const d=await r.json();if(d.on){setc(d.r,d.g,d.b)}else{document.getElementById('preview').style.background='none';document.getElementById('powerSwitch').classList.remove('on');document.getElementById('powerLabel').textContent='💡 关';document.querySelectorAll('.preset,.picker,.sliders').forEach(e=>e.classList.add('disabled'))}}document.querySelector('input[type=color]').value='#000000'</script></body></html>"
static esp_err_t root_get_handler(httpd_req_t *req)
{
httpd_resp_set_type(req, "text/html; charset=utf-8");
httpd_resp_sendstr(req, PAGE);
return ESP_OK;
}
static esp_err_t set_color_handler(httpd_req_t *req)
{
char buf[64];
if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) != ESP_OK) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "参数缺失");
return ESP_OK;
}
char r_str[8] = "0", g_str[8] = "0", b_str[8] = "0";
httpd_query_key_value(buf, "r", r_str, sizeof(r_str));
httpd_query_key_value(buf, "g", g_str, sizeof(g_str));
httpd_query_key_value(buf, "b", b_str, sizeof(b_str));
int r = atoi(r_str), g = atoi(g_str), b = atoi(b_str);
if (r < 0) { r = 0; }
if (r > 255) { r = 255; }
if (g < 0) { g = 0; }
if (g > 255) { g = 255; }
if (b < 0) { b = 0; }
if (b > 255) { b = 255; }
if (!power_on && (r || g || b)) {
set_power(1);
}
if (r == 0 && g == 0 && b == 0) {
set_power(0);
} else {
set_color(r, g, b);
}
char resp[64];
snprintf(resp, sizeof(resp), "{\"r\":%d,\"g\":%d,\"b\":%d}", r, g, b);
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, resp);
return ESP_OK;
}
static esp_err_t power_handler(httpd_req_t *req)
{
set_power(!power_on);
char resp[64];
snprintf(resp, sizeof(resp), "{\"on\":%d,\"r\":%d,\"g\":%d,\"b\":%d}",
power_on, cur_r, cur_g, cur_b);
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, resp);
return ESP_OK;
}
static void start_http_server(void)
{
httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = 8;
config.lru_purge_enable = true;
if (httpd_start(&server, &config) == ESP_OK) {
httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = root_get_handler };
httpd_register_uri_handler(server, &root);
httpd_uri_t set = { .uri = "/set", .method = HTTP_GET, .handler = set_color_handler };
httpd_register_uri_handler(server, &set);
httpd_uri_t power = { .uri = "/power", .method = HTTP_GET, .handler = power_handler };
httpd_register_uri_handler(server, &power);
printf("✅ HTTP 服务器已启动\n");
}
}
// ======================== 主函数 ========================
void app_main(void)
{
printf("\n======== ESP32 WiFi 智能灯控制器 ========\n");
// 1. 初始化 LED
led_strip_config_t strip_config = {
.strip_gpio_num = LED_GPIO,
.max_leds = 1,
};
led_strip_rmt_config_t rmt_config = {
.resolution_hz = 10 * 1000 * 1000,
.flags.with_dma = false,
};
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
led_strip_clear(led_strip);
// 2. 初始化 NVS(WiFi 必须)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// 3. 开 WiFi
#ifdef WIFI_SSID
// STA 模式(连路由器)
// 取消上面 #define WIFI_SSID/WIFI_PASS 的注释来启用
printf("连接到 %s ...\n", WIFI_SSID);
wifi_init_sta(); // 需要自己补 STA 的代码
#else
// AP 模式(自己开热点)
wifi_init_ap();
#endif
// 4. 启动 HTTP 服务器
start_http_server();
printf("\n========================================\n");
printf(" 浏览器打开 http://192.168.4.1\n");
printf("========================================\n");
// 开机绿色 → 表示就绪
set_power(1);
set_color(0, 255, 0); // 绿色
while (1) {
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
8. 接线与硬件
8.1 你的 ESP32-S3-N8R2 板载硬件
ini
板子布局(正面)
┌────────────────────────────┐
│ ESP32-S3-N8R2 │
│ ┌────────────────────┐ │
│ │ 芯片 │ │
│ └────────────────────┘ │
│ ┌──────┐ 🔴PWR🟢TX🟡RX │
│ │ WS │ │ ← 中间右侧三个指示灯
│ │ 2812 │ │ (PWR=电源, TX=发, RX=收)
│ │ 彩灯 │ │
│ └──────┘ │
│ USB-C① USB-C② │ ← 烧录用左下角(标USB)口
└────────────────────────────┘
| 硬件 | 说明 |
|---|---|
| WS2812 彩灯 | 板载可编程 RGB LED,接 GPIO 48 |
| PWR 红灯 | 电源指示灯,通电常亮,不可控 |
| TX 绿灯 | 串口发送时闪烁,不可控 |
| RX 黄灯 | 串口接收时闪烁,不可控 |
| USB-C ① (左下,背标 USB) | 烧录、串口监视、供电 用这个口 ✅ |
| USB-C ② (右下,背标 COM) | 串口通信,较少使用 |
8.2 最小系统
你不需要任何外部元件,插上 USB-C ② 就能用:
ESP32-S3-N8R2 板载 WS2812 彩灯
GPIO 48 ─────────── DIN(内部已连接)
3.3V ─────────── VCC(内部已连接)
GND ─────────── GND(内部已连接)
只需插 USB 线,即可写代码控制彩灯
不需要任何外部接线!
前面章节的代码可以直接运行,无需修改任何硬件。
8.3 外接灯带
markdown
ESP32-S3 外部 5V WS2812 灯带
GPIO 任意 ────── DIN(数据线)
GND ────── GND(共地)
外部 5V 电源 ──── VCC(5V)
GND(与 ESP32 共地)
⚠️ 不要用 ESP32 的 3.3V 给灯带供电!
灯带需要 5V,ESP32 的 3.3V 带不动
单颗 WS2812 约 60mA,100 颗 = 6A!
8.3 供电建议
| 场景 | 供电方式 |
|---|---|
| 单颗板载 LED | USB 线插电脑/充电头 |
| 外接 10 颗以内灯带 | USB + 外部 5V |
| 外接 10 颗以上灯带 | 必须独立 5V 电源 |
8.4 保护电阻
WS2812 数据线上建议串一个 330Ω 电阻:
css
ESP32 GPIO ──── [330Ω] ──── WS2812 DIN
防止上电瞬间的电流冲击烧坏灯珠。 (板载 LED 一般已经焊好了,不需要加。)
9. 常见问题
9.1 网页打不开
arduino
1. 确认电脑连的是 ESP32 的 WiFi
→ 检查 WiFi 列表,连的是不是 "ESP32_LED"
2. 确认 ESP32 已经开机启动
→ 串口看有没有打印 "HTTP 服务器已启动"
3. 确认 IP 地址对不对
→ AP 模式固定是 192.168.4.1
→ 浏览器输入 http://192.168.4.1(不要输错)
4. 关闭手机流量
→ iOS 连了 ESP32 的热点但开着流量,可能走流量
→ 关掉蜂窝数据
9.2 页面能打开但点颜色没反应
arduino
原因:浏览器控制台(F12)看网络请求有没有 404
1. 检查 ESP32 串口输出
→ 点颜色时串口会打印 HTTP 请求日志
→ 如果有日志但不生效 → 代码问题
→ 如果没日志 → 网络没通
2. 检查前端 URL 拼写
→ /set?r=255&g=0&b=0
→ 注意是 "set" 不是 "Set"(大小写敏感?路由区分?)
9.3 手机连了热点就断网了
css
AP 模式就是这样------ESP32 不是路由器
它没有互联网连接
解决方案:
a) 接受它:控制灯的时候短暂离线
b) 换 STA 模式:ESP32 也连路由器,手机也连路由器
这样手机既能上网又能控制灯
9.4 编译报错找不到组件
css
1. 检查 main/CMakeLists.txt 的 REQUIRES 列表
2. 检查 main/idf_component.yml 文件格式
3. 运行 idf.py fullclean 清缓存后重试
9.5 烧录后一直重启
arduino
可能是 PSRAM 配置不对
N8R2 需要在 menuconfig 里配好:
Component config → ESP PSRAM
→ Initialize SPI RAM during startup ✅
→ Octal Mode PSRAM ✅(非 Quad)
或者关掉 PSRAM 先测试纯 GPIO 功能
进阶方向
学会了 WiFi 控制彩灯,接下来可以:
- 接入 MQTT:通过 MQTT 服务器控制,可以实现 App/语音控制
- 添加 OTA 升级:不用插 USB 也能升级固件
- 接入 Home Assistant:智能家居统一控制
- 加 SU-03T 语音模块:喊一声"开灯"就亮
- 做定时器/闹钟:每天 7 点渐亮模拟日出
这三个文档到此完成!如果你跟着做到了这里,你已经从零到一搭建了一个完整的 WiFi 智能灯控制器 🎉