ESP32 变身 WiFi 智能灯控制器

ESP32 变身 WiFi 智能灯控制器

在浏览器里控制你的彩灯,手机电脑都能用 从零搭建一个完整的嵌入式 Web 服务器


目录

  1. 项目概述
  2. 两种联网模式
  3. [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")
  4. [STA 模式:ESP32 连路由器](#STA 模式:ESP32 连路由器 "#4-sta-%E6%A8%A1%E5%BC%8Fesp32-%E8%BF%9E%E8%B7%AF%E7%94%B1%E5%99%A8")
  5. [搭建 HTTP 服务器](#搭建 HTTP 服务器 "#5-%E6%90%AD%E5%BB%BA-http-%E6%9C%8D%E5%8A%A1%E5%99%A8")
  6. [编写 Web 控制页面](#编写 Web 控制页面 "#6-%E7%BC%96%E5%86%99-web-%E6%8E%A7%E5%88%B6%E9%A1%B5%E9%9D%A2")
  7. 完整的项目代码
  8. 接线与硬件
  9. 常见问题

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"
                                你的事件回调函数里处理这个

所以需要:

  1. 事件回调函数------收到事件时执行
  2. 事件组------让主程序能"等待"连接完成

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 控制彩灯,接下来可以:

  1. 接入 MQTT:通过 MQTT 服务器控制,可以实现 App/语音控制
  2. 添加 OTA 升级:不用插 USB 也能升级固件
  3. 接入 Home Assistant:智能家居统一控制
  4. 加 SU-03T 语音模块:喊一声"开灯"就亮
  5. 做定时器/闹钟:每天 7 点渐亮模拟日出

这三个文档到此完成!如果你跟着做到了这里,你已经从零到一搭建了一个完整的 WiFi 智能灯控制器 🎉

相关推荐
泓博4 小时前
Raspberry Camera
物联网
威联通网络存储4 小时前
QNAP 边缘计算底座:车间 IoT 容器化部署方案
人工智能·python·物联网·边缘计算
AIoT科技物语6 小时前
包邮168元!无须编程,AI 驱动,ESP-Claw 物联网 OpenClaw 智能体套件,打通智能家居本地「感知、推理、决策」完整闭环
人工智能·物联网·智能家居
The Shio6 小时前
OptiByte 操练场:面向 IoT/嵌入式的协议可视化调试工具
网络·嵌入式硬件·物联网·c#·.net·业界资讯·iot
GIS数据转换器6 小时前
农业物联网可视化管理系统
人工智能·物联网·3d·无人机·知识图谱·旅游
iNeuOS工业互联网6 小时前
iNeuOS,从单一产品向产品族生态演进,物联网(IOT)、视觉分析(Vision)、大模型智库(AiMind·心智灵慧)
物联网
砍材农夫8 小时前
物联网 Protobuf入门+梳理
物联网
兴通物联科技10 小时前
3C半导体DPM金属雕刻码扫码器技术解析——兴通物联硬件架构与算法优化
大数据·物联网·计算机视觉·硬件架构
上海合宙LuatOS10 小时前
Air8000多网通信-NTP
服务器·arm开发·物联网·网络协议·luatos