WS2812B讓水井三寶會發光:ESP32燈光控制原理、接線與程式實作
水井三寶智慧互動展覽系統|智慧互動層燈光控制專題
在水井三寶智慧互動展覽系統中,聲音負責說故事,燈光則讓觀眾「看見故事正在發生」。這篇把WS2812B燈光控制獨立整理,從工作原理、接線、LED分區,到白馬追光、烏龜呼吸與姻緣花綻放的程式邏輯,完整做成可直接上課與實作的教材。
一、WS2812B是什麼?
WS2812B是一種可定址RGB LED,每顆LED內部整合驅動控制電路。多顆串接時,只需要一條DATA線,就能逐顆指定顏色與亮度。Arduino端使用Adafruit_NeoPixel函式庫,水井三寶目前採 NEO_GRB + NEO_KHZ800,由ESP32 GPIO13控制62顆LED。
二、正式接線方式
三、62顆LED不是一整條,而是四個燈區
| 燈區 | LED編號 | 數量 | 角色 |
|---|---|---|---|
| 底座 | 0–23 | 24 | 待機與環境柔光 |
| 白馬 | 24–33 | 10 | 暖金追光 |
| 烏龜 | 34–45 | 12 | 水藍呼吸 |
| 姻緣花 | 46–61 | 16 | 粉紅漸次綻放 |
四、燈光控制不是裝飾,而是故事的第二語言
| 故事 | 燈效 | 意義 |
|---|---|---|
| 001 白馬 | 暖金追光 | 向前、行動、奔跑感 |
| 002 烏龜 | 藍色呼吸 | 沉穩、守水、守護土地 |
| 003 姻緣花 | 粉紅逐顆點亮 | 花朵慢慢綻放 |
| 004 水井三寶總故事 | 三角色輪替 | 白馬→烏龜→姻緣花依序出場 |
| 005 創作者資訊 | 三區共同暖光 | 溫暖、穩定、不搶作品主體 |
五、為什麼白馬用追光?
白馬的主光點會沿著10顆LED移動,後方保留較暗的尾光,因此不是整區同時亮,而是形成「奔跑」的速度感。
六、為什麼烏龜用呼吸燈?
烏龜燈區使用藍色,亮度從低到高、再由高到低反覆變化,形成穩定呼吸感。這比快速閃爍更適合「守水、沉穩、長久陪伴土地」的角色設定。
七、姻緣花如何「開花」?
姻緣花不是整區一起閃,而是控制目前亮起的LED數量,由1顆慢慢增加到16顆,再逐步縮回。這種漸次點亮的動畫更接近「花開」而不是一般警示燈。
八、燈效如何跟故事狀態機連動?
正式系統由 currentStory 決定燈效,因此聲音與燈光會同步對應故事。燈光也成為「狀態可視化」介面:即使觀眾沒有看手機,也能知道目前正在播放哪個角色。
九、程式設計重點:不要用長時間delay()
水井三寶還要同時處理ToF、按鈕、JQ6500、Edge Web與Wi-Fi,因此燈效大量使用 millis() 控制更新節奏,避免動畫阻塞其他任務。
delay() 做動畫;真正的IoT系統要進一步學會非阻塞式時間控制。燈光再漂亮,也不能讓感測器、Web或故事播放停住。十、完整獨立測試程式
下面程式只保留WS2812B燈光模組,適合先測接線、燈區與動畫。正式系統再把這些函式整合回Layer 2~Layer 4。
#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#define LED_PIN 13
#define LED_COUNT 62
#define BASE_START 0
#define BASE_COUNT 24
#define HORSE_START 24
#define HORSE_COUNT 10
#define TURTLE_START 34
#define TURTLE_COUNT 12
#define FLOWER_START 46
#define FLOWER_COUNT 16
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
enum Story {
STORY_NONE=0, STORY_HORSE=1, STORY_TURTLE=2,
STORY_FLOWER=3, STORY_ALL=4, STORY_CREATOR=5
};
Story currentStory = STORY_NONE;
void setZone(int start, int count, uint32_t color){
for(int i=0;i<count;i++) strip.setPixelColor(start+i,color);
}
void ledIdle(){
static unsigned long last=0;
static int b=4, dir=1;
if(millis()-last<80) return;
last=millis();
b+=dir;
if(b>=14) dir=-1;
if(b<=4) dir=1;
strip.clear();
setZone(BASE_START,BASE_COUNT,strip.Color(b,b/2,0));
strip.show();
}
void ledReady(){
strip.clear();
setZone(HORSE_START,HORSE_COUNT,strip.Color(20,8,0));
setZone(TURTLE_START,TURTLE_COUNT,strip.Color(0,8,18));
setZone(FLOWER_START,FLOWER_COUNT,strip.Color(18,0,7));
strip.show();
}
void ledHorse(){
static unsigned long last=0;
static int pos=0;
if(millis()-last<80) return;
last=millis();
strip.clear();
for(int i=0;i<HORSE_COUNT;i++){
int dist=(i-pos+HORSE_COUNT)%HORSE_COUNT;
if(dist==0)
strip.setPixelColor(HORSE_START+i,strip.Color(100,45,0));
else if(dist<=2)
strip.setPixelColor(HORSE_START+i,strip.Color(30,10,0));
}
pos++;
if(pos>=HORSE_COUNT) pos=0;
strip.show();
}
void ledTurtle(){
static unsigned long last=0;
static int b=10, dir=1;
if(millis()-last<70) return;
last=millis();
b+=dir;
if(b>=80) dir=-1;
if(b<=10) dir=1;
strip.clear();
setZone(TURTLE_START,TURTLE_COUNT,strip.Color(0,b/2,b));
strip.show();
}
void ledFlower(){
static unsigned long last=0;
static int count=1;
static bool growing=true;
if(millis()-last<120) return;
last=millis();
strip.clear();
for(int i=0;i<count;i++)
strip.setPixelColor(FLOWER_START+i,strip.Color(80,5,25));
strip.show();
if(growing){
count++;
if(count>=FLOWER_COUNT){count=FLOWER_COUNT;growing=false;}
}else{
count--;
if(count<=2){count=2;growing=true;}
}
}
void ledAllStory(){
static unsigned long last=0;
static int phase=0;
if(millis()-last<900) return;
last=millis();
phase++;
if(phase>2) phase=0;
strip.clear();
if(phase==0)
setZone(HORSE_START,HORSE_COUNT,strip.Color(70,30,0));
else if(phase==1)
setZone(TURTLE_START,TURTLE_COUNT,strip.Color(0,25,70));
else
setZone(FLOWER_START,FLOWER_COUNT,strip.Color(70,5,25));
strip.show();
}
void ledCreator(){
strip.clear();
uint32_t warm=strip.Color(45,28,10);
setZone(HORSE_START,HORSE_COUNT,warm);
setZone(TURTLE_START,TURTLE_COUNT,warm);
setZone(FLOWER_START,FLOWER_COUNT,warm);
strip.show();
}
void updateStoryLED(){
switch(currentStory){
case STORY_HORSE: ledHorse(); break;
case STORY_TURTLE: ledTurtle(); break;
case STORY_FLOWER: ledFlower(); break;
case STORY_ALL: ledAllStory(); break;
case STORY_CREATOR: ledCreator(); break;
default: ledReady(); break;
}
}
void setup(){
Serial.begin(115200);
strip.begin();
strip.setBrightness(100);
strip.clear();
strip.show();
}
void loop(){
unsigned long section=(millis()/10000UL)%6;
switch(section){
case 0: ledIdle(); break;
case 1: currentStory=STORY_HORSE; updateStoryLED(); break;
case 2: currentStory=STORY_TURTLE; updateStoryLED(); break;
case 3: currentStory=STORY_FLOWER; updateStoryLED(); break;
case 4: currentStory=STORY_ALL; updateStoryLED(); break;
case 5: currentStory=STORY_CREATOR; updateStoryLED(); break;
}
}
十一、從RGB燈條到文化互動語言
WS2812B真正有價值的不是「顏色很多」,而是每顆LED都可以被程式個別控制。當白馬用追光、烏龜用呼吸、姻緣花用綻放來呈現各自故事時,LED就不再只是裝飾,而成為文化敘事的一部分。


沒有留言:
張貼留言