目录

一、系统接线部

1.1 硬件清单

1.2 接线方案表

1.3 连接示意图

1.4 具体接线图

二、安装与使用教程

2.1 烧录根节点

2.2 烧录子节点

2.4 使用方法

2.5 自定义配置

三、代码讲解部分

3.1 项目文件结构

3.2 通信协议

3.3 WS2812 灯光控制

3.4 状态增量更新

3.5 TCP 连接管理

3.6 OLED 双色屏布局

3.7 子节点断线重连

四、项目结果演示

五、常见问题解答(FAQ)


项目概述

        本文详细介绍一套基于 零知派ESP32 的多节点 WS2812 智能灯控系统,采用 WiFi AP + TCP 星型拓扑,支持多盏灯独立控制、网页远程操控、OLED 本地状态显示。无需云服务器、无需手机 APP,局域网内低延迟直连,适合有一定 ESP32 基础的朋友参考实现。


一、系统接线部

1.1 硬件清单

每个节点(根节点和子节点硬件完全一致)所需元件如下:

元件

规格参数

数量

备注

ESP32 开发板

零知派 ESP32 

1 块

根节点和子节点各一块起

WS2812B 灯带

3.3V~5V 供电,8 颗灯珠

1 条

灯珠数量可在代码中修改

SSD1306 OLED

0.96 寸,I2C 接口,128×64 分辨率

1 块

注意区分 I2C 和 SPI 版本

杜邦线

公对公 / 公对母

若干

面包板

SYB-170

1 块

可选,用于快速搭建原型

5V 电源

电流建议 ≥ 1A

1 个

灯珠数量多时需要独立供电

💡 选购提示:本项目使用 4 针 I2C 版本(GND / VCC / SCK / SDA)。注意区分,SPI 版本引脚定义完全不同。


1.2 接线方案表

WS2812B 灯带接线:

WS2812B 引脚

ESP32 引脚

说明

DIN(数据输入)

GPIO5

建议串联 330Ω 限流电阻

VCC

3.3V或5V

灯珠较多时建议独立 5V 供电

GND

GND

独立供电时两块 GND 需共地

SSD1306 OLED 接线(I2C):

OLED 引脚

ESP32 引脚

说明

VCC

3.3V

部分模块支持 5V,看模块规格

GND

GND

SDA

GPIO21

ESP32 默认 I2C SDA

SCL

GPIO22

ESP32 默认 I2C SCL

⚠️ 注意:GPIO5 输出 3.3V 电平,WS2812B 大多数型号可以直接驱动。若出现灯带颜色异常或随机闪烁,在数据线上串一个 330Ω 电阻可以有效改善信号质量。


1.3 连接示意图

1.4 具体接线图

单个节点完整接线(根节点与子节点相同):

💡 供电说明:每颗 WS2812B 满亮白光约 60mA,8 颗约 480mA。多个节点同时工作建议使用独立 5V/2A 以上电源,避免 ESP32 USB 供电不足导致灯带亮度异常或重启。


二、安装与使用教程

2.1 烧录根节点

①开源平台-搜索"TCP"(根节点)-代码下载自动打开

②连接-验证-上传

③串口输出

打开串口监视器(波特率 115200),看到以下输出说明正常:

2.2 烧录子节点

①开源平台-搜索"TCP"(分节点)-代码下载自动打开

②连接-验证-上传

③串口输出

重复此步骤烧录所有分节点

2.4 使用方法

连接网络:

  1.  手机或电脑打开 WiFi 设置

  2.  连接热点:SmartLight-Net

  3.  输入密码:smartlight2026

打开控制台:

  1.  浏览器地址栏输入:http://192.168.10.1

  2.  页面自动加载所有在线节点,每 3 秒刷新一次

控制功能说明:

功能区域

操作

效果

全局控制栏

点击「全部开灯」

所有节点同时亮灯

全局控制栏

点击「全部关灯」

所有节点同时关灯

全局控制栏

拖动亮度滑块

所有节点亮度同步调节(0-255)

全局控制栏

点击色板色块

所有节点切换到对应颜色

全局控制栏

点击拾色器圆圈

弹出颜色选择器,自定义颜色

节点卡片

拨动开关拨杆

单独控制该节点开/关

节点卡片

拖动亮度滑块

单独调节该节点亮度

节点卡片

点击色板

单独切换该节点颜色

2.5 自定义配置

如需修改网络参数,编辑 light_network.h

#define AP_SSID         "SmartLight-Net"   // 热点名称
#define AP_PASSWORD     "smartlight2026"   // 热点密码
#define AP_IP           "192.168.10.1"     // 网关IP
#define TCP_PORT        9527               // TCP端口
#define MAX_NODES       10                 // 最大子节点数
#define HEARTBEAT_MS    3000               // 心跳间隔(毫秒)
#define NODE_TIMEOUT_MS 10000              // 离线判定超时(毫秒)
#define LED_COUNT       8                  // 每条灯带灯珠数量

三、代码讲解部分

3.1 项目文件结构

SmartLight_Gateway/
├── SmartLight_Gateway.ino   主程序入口 + 内嵌网页 HTML/CSS/JS
├── gateway.h / .cpp         AP热点 + TCP服务端 + Web路由
├── light_network.h / .cpp   公共数据结构 + 校验函数
├── ws2812_ctrl.h / .cpp     WS2812 灯带控制封装
└── oled_display.h / .cpp    SSD1306 OLED 显示封装

SmartLight_Node/
├── SmartLight_Node.ino      主程序入口
├── light_node.h             WiFi+TCP客户端 + 指令执行
├── light_network.h / .cpp   公共数据结构(与网关共用)
├── ws2812_ctrl.h / .cpp     WS2812 灯带控制(与网关共用)
└── oled_display.h / .cpp    OLED 显示(与网关共用)

3.2 通信协议

节点之间传输定长结构体,加 XOR 校验,共 11 字节:

// 灯光状态,5字节
struct __attribute__((packed)) LightState {
    bool    power;        // 开关状态
    uint8_t brightness;   // 亮度 0-255
    uint8_t r, g, b;      // RGB 颜色分量
};

// 网络数据包,11字节
struct __attribute__((packed)) NetPacket {
    uint8_t    msgType;   // 消息类型
    uint32_t   nodeId;    // 节点ID(基于MAC地址生成)
    LightState state;     // 灯光状态
    uint8_t    checksum;  // XOR校验字节
};

为什么要加 __attribute__((packed))

不加的话,编译器会在 uint8_t msgTypeuint32_t nodeId 之间自动插入 3 字节对齐填充。两端填充内容不同导致 XOR 校验结果永远不一致,包会被全部丢弃。加了 packed 后禁止填充,结构体固定 11 字节,两端完全一致。

// 编译期断言,确认 packed 生效
static_assert(sizeof(NetPacket) == 11, "NetPacket大小不对!");

消息类型定义:

#define MSG_HEARTBEAT   0x01   // 心跳包(子节点→网关,携带当前状态)
#define MSG_SET_COLOR   0x02   // 设置颜色
#define MSG_SET_POWER   0x03   // 设置开关
#define MSG_SET_BRIGHT  0x04   // 设置亮度
#define MSG_STATUS_REQ  0x05   // 请求状态上报
#define MSG_STATUS_RPT  0x06   // 状态回报(子节点→网关)

3.3 WS2812 灯光控制

亮度控制采用颜色融合方式而非库自带的 setBrightness(),避免全局缩放带来的色偏:

void WS2812Controller::_render() {
    if (!_state.power) {
        _strip.clear();
        _strip.show();
        return;
    }
    // 亮度系数直接乘入 RGB 各分量
    float scale = _state.brightness / 255.0f;
    uint8_t r = (uint8_t)(_state.r * scale);
    uint8_t g = (uint8_t)(_state.g * scale);
    uint8_t b = (uint8_t)(_state.b * scale);

    _strip.fill(_strip.Color(r, g, b));
    _strip.show();
}

3.4 状态增量更新

网页每次只发送改变的那一项(亮度 / 颜色 / 开关),必须基于目标灯当前状态做增量修改,否则改亮度会重置颜色,改颜色会重置亮度:

// 取目标灯的当前状态作为基础
LightState base = isAll ? myState : getBaseState(tid);

if (action == "brightness") {
    base.brightness = val;       // 只改亮度,RGB 保持不变
} else if (action == "color") {
    base.r = r; base.g = g; base.b = b;  // 只改颜色,亮度保持不变
} else if (action == "power") {
    base.power = on;             // 只改开关,其他全部保留
}

3.5 TCP 连接管理

根节点用 WiFiServer 管理所有子节点连接。WiFiClient 是 C++ 对象,不能放在用 memset 初始化的结构体里,必须单独声明:

class Gateway {
    NodeInfo   _nodes[MAX_NODES];    // POD 结构体,可以 memset 清零
    WiFiClient _clients[MAX_NODES];  // C++ 对象,靠默认构造函数初始化
    int        _nodeCount;
};
// _nodes[i] 和 _clients[i] 通过下标一一对应

新节点接入流程:

void Gateway::_acceptClients() {
    WiFiClient client = _tcpServer.accept();
    if (!client) return;

    int idx = _nodeCount++;
    _clients[idx] = client;           // 正确:赋值给独立数组
    _nodes[idx].nodeId   = 0;         // 等待首个心跳包确认真实ID
    _nodes[idx].online   = true;
    _nodes[idx].lastSeen = millis();
}

3.6 OLED 双色屏布局

0.96 寸 SSD1306 是物理双色屏,顶部 Y:0-15 固定黄色,下方 Y:16-63 固定蓝色,这是硬件特性无法修改。布局必须严格按分界线划分:

Y:  0 ┌─────────────────────────────┐
      │ [GW]  ID:1A2B3C4D           │ ← 黄色区,角色标签 + 节点ID
Y: 15 ├─────────────────────────────┤ ← 硬件分界线
      │ Power: ON    Bri: 128       │
      │ R:255  G:128  B: 64         │ ← 蓝色区,灯光状态
      │ [R▓▓▓░] [G▓▓░░] [B▓░░░]   │
Y: 63 │ [████████████░░░░░░░░░░░░]  │ ← 亮度进度条
      └─────────────────────────────┘

根节点显示 [GW],子节点显示 [NODE],通过 myRole 变量区分:

const char* roleStr = (role == ROLE_GATEWAY) ? " GW " : " NODE ";

3.7 子节点断线重连

子节点 loop() 中分两层检测,WiFi 和 TCP 各自独立重连:

void LightNode::loop() {
    // 第一层:WiFi 断线检测
    if (WiFi.status() != WL_CONNECTED) {
        if (millis() - _lastConnectAttempt > 5000) {
            _connectWiFi();
            _lastConnectAttempt = millis();
        }
        return;  // WiFi 未连上,不继续后续逻辑
    }

    // 第二层:TCP 断线检测
    if (!_client.connected()) {
        if (millis() - _lastConnectAttempt > 3000) {
            _connectTCP();
            _lastConnectAttempt = millis();
        }
        return;
    }

    // 正常工作
    _readServer();
    _sendHeartbeatIfNeeded();
}

四、项目结果演示

零知派ESP32——基于WiFi+TCP的多节点智能灯控系统


五、常见问题解答(FAQ)

Q1:子节点上电后灯带没有蓝色闪烁,说明什么?

蓝色闪3下是 TCP 连接成功的提示。没有闪烁说明 TCP 连接失败,可能原因:

  • 根节点未上电或未正常启动,先检查根节点串口输出 

  • 子节点没有连上根节点的 WiFi 热点,串口会打印 WiFi连接失败

  • 代码里的 AP_SSID / AP_PASSWORD 与根节点不一致,检查 light_network.h


Q2:网页打开后看不到子节点,只有 Gateway 一个卡片?

原因可能有两个:

  • 子节点还未上电或连接中,等待子节点蓝色闪烁后再刷新网页

  • 子节点注册靠首个心跳包,心跳间隔 3 秒,稍等片刻或点击「刷新」按钮


Q3:控制灯光时网页提示「操作失败」?

检查以下几点:

  • 手机或电脑是否仍连着 SmartLight-Net 热点,有时手机会自动切换到其他 WiFi

  • 根节点是否还在正常运行,串口看有无报错

  • 浏览器地址栏确认是 http://192.168.10.1,不是 https


Q4:WS2812 灯带颜色显示异常或随机闪烁?

  • 在 GPIO5 数据线上串一个 330Ω 电阻,改善信号质量

  • 检查灯带供电是否充足,8颗全亮白光约需 480mA,USB 供电可能不够

  • 多个节点同时工作时,建议使用独立 5V/2A 电源,并确保所有 GND 共地


Q5:OLED 不亮或花屏?

  • 用 I2C 扫描程序确认 OLED 地址是否为 0x3C,部分模块地址是 0x3D,在 oled_display.h 中修改 #define OLED_ADDR 0x3C

  • 检查 SDA/SCL 接线是否正确,GPIO21=SDA,GPIO22=SCL

  • 确认 OLED 供电是 3.3V 还是 5V,看模块背面的规格标注


Q6:OLED 顶部文字被黄色和蓝色分成两段颜色?

这是 0.96 寸 SSD1306 的物理特性,顶部 16 像素固定黄色、下方固定蓝色,无法软件修改。代码已针对此做了布局适配,黄色区域只放节点ID,蓝色区域放所有状态信息,如果出现分割说明烧录的是旧版代码,重新烧录最新版即可。


Q7:想增加更多子节点怎么办?

直接给新 ESP32 烧录 SmartLight_Node 固件,上电自动入网,无需修改任何代码。默认最多支持 10 个子节点,如需更多修改 light_network.h 中的 #define MAX_NODES 10 并重新烧录根节点固件。


Q8:想换成更多灯珠的灯带怎么改?

修改 light_network.h 中一行:

#define LED_COUNT  8   // 改成你的灯珠数量,如 30、60 等

两个项目(Gateway 和 Node)都要改,然后重新烧录。供电需同步升级,每增加 8 颗灯珠约需增加 480mA 电流余量。


Q9:能不能不用 OLED,去掉这个模块?

可以。删除主程序中 #include "oled_display.h" 以及 setup() 里的 oled.begin()loop() 里的 oled.update(...) 三处调用即可,其余代码不需要修改。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐