288 lines
8.2 KiB
Go
288 lines
8.2 KiB
Go
// Hikari – WS2812 LED effects for XIAO ESP32C3
|
||
//
|
||
// Hardware:
|
||
// D6 (GPIO21) – WS2812 data (20 LEDs, 4 rows × 5 columns)
|
||
// D3 (GPIO5) – Pushbutton, active-low (internal pull-up)
|
||
//
|
||
// Flash:
|
||
// tinygo flash -target=xiao-esp32c3 -stack-size=8KB .
|
||
|
||
package main
|
||
|
||
import (
|
||
"image/color"
|
||
"machine"
|
||
"time"
|
||
|
||
"tinygo.org/x/drivers/ws2812"
|
||
)
|
||
|
||
// ─── constants ────────────────────────────────────────────────────────────────
|
||
|
||
const (
|
||
numLEDs = 20
|
||
numRows = 4
|
||
numCols = 5
|
||
numEffects = 7
|
||
frameDelay = 20 * time.Millisecond
|
||
debounce = 200 * time.Millisecond
|
||
)
|
||
|
||
// Pin assignments for XIAO ESP32C3
|
||
const (
|
||
pinLED = machine.D6 // WS2812 data line
|
||
pinBtn = machine.D3 // Button (pulled up, LOW when pressed)
|
||
)
|
||
|
||
// ─── globals ──────────────────────────────────────────────────────────────────
|
||
|
||
var (
|
||
leds [numLEDs]color.RGBA
|
||
strip ws2812.Device
|
||
|
||
// xorshift32 PRNG – seeded with a compile-time constant
|
||
rngState uint32 = 0xCAFE1234
|
||
)
|
||
|
||
// ─── colour helpers ───────────────────────────────────────────────────────────
|
||
|
||
// wheel maps 0-255 → smooth full-hue spectrum (R→G→B→R)
|
||
func wheel(pos uint8) color.RGBA {
|
||
switch {
|
||
case pos < 85:
|
||
return color.RGBA{R: pos * 3, G: 255 - pos*3, B: 0}
|
||
case pos < 170:
|
||
pos -= 85
|
||
return color.RGBA{R: 255 - pos*3, G: 0, B: pos * 3}
|
||
default:
|
||
pos -= 170
|
||
return color.RGBA{R: 0, G: pos * 3, B: 255 - pos*3}
|
||
}
|
||
}
|
||
|
||
// dim scales a colour by brightness (0-255)
|
||
func dim(c color.RGBA, brightness uint8) color.RGBA {
|
||
return color.RGBA{
|
||
R: uint8(uint16(c.R) * uint16(brightness) >> 8),
|
||
G: uint8(uint16(c.G) * uint16(brightness) >> 8),
|
||
B: uint8(uint16(c.B) * uint16(brightness) >> 8),
|
||
}
|
||
}
|
||
|
||
// idx converts grid coordinates to LED index (simple row-major)
|
||
func idx(row, col int) int { return row*numCols + col }
|
||
|
||
// clear sets all LEDs to off
|
||
func clear() {
|
||
for i := range leds {
|
||
leds[i] = color.RGBA{}
|
||
}
|
||
}
|
||
|
||
// rand returns the next pseudo-random uint32
|
||
func rand() uint32 {
|
||
rngState ^= rngState << 13
|
||
rngState ^= rngState >> 17
|
||
rngState ^= rngState << 5
|
||
return rngState
|
||
}
|
||
|
||
// ─── effect 1 – Rainbow ───────────────────────────────────────────────────────
|
||
// All LEDs show the same hue; the hue cycles slowly through the full spectrum.
|
||
|
||
func effectRainbow(step uint32) {
|
||
c := wheel(uint8(step >> 2)) // full cycle ≈ 20 s
|
||
for i := range leds {
|
||
leds[i] = c
|
||
}
|
||
}
|
||
|
||
// ─── effect 2 – Row sweep ─────────────────────────────────────────────────────
|
||
// One row at a time is lit; the active row sweeps downward while the hue drifts.
|
||
|
||
func effectRowSweep(step uint32) {
|
||
clear()
|
||
row := int(step/40) % numRows
|
||
c := wheel(uint8(step >> 1))
|
||
for col := 0; col < numCols; col++ {
|
||
leds[idx(row, col)] = c
|
||
}
|
||
}
|
||
|
||
// ─── effect 3 – Column sweep ──────────────────────────────────────────────────
|
||
// One column at a time is lit; the active column sweeps rightward.
|
||
|
||
func effectColSweep(step uint32) {
|
||
clear()
|
||
col := int(step/40) % numCols
|
||
c := wheel(uint8(step >> 1))
|
||
for row := 0; row < numRows; row++ {
|
||
leds[idx(row, col)] = c
|
||
}
|
||
}
|
||
|
||
// ─── effect 4 – Stars ─────────────────────────────────────────────────────────
|
||
// Random LEDs glow in random colours then fade out, one by one.
|
||
|
||
type star struct {
|
||
pos uint8 // LED index
|
||
hue uint8 // colour on the wheel
|
||
br uint8 // current brightness (0-255)
|
||
dir int8 // +1 rising, -1 falling
|
||
}
|
||
|
||
const (
|
||
numStars = 8
|
||
starStep = 7 // brightness change per frame (~35 frames to full glow)
|
||
)
|
||
|
||
var (
|
||
starList [numStars]star
|
||
starsOK bool
|
||
)
|
||
|
||
func initStars() {
|
||
for i := range starList {
|
||
starList[i] = star{
|
||
pos: uint8(rand() % numLEDs),
|
||
hue: uint8(rand()),
|
||
br: uint8(i * (255 / numStars)), // stagger so they don't sync
|
||
dir: 1,
|
||
}
|
||
}
|
||
starsOK = true
|
||
}
|
||
|
||
func effectStars(_ uint32) {
|
||
if !starsOK {
|
||
initStars()
|
||
}
|
||
clear()
|
||
for i := range starList {
|
||
s := &starList[i]
|
||
|
||
// Advance brightness
|
||
nb := int16(s.br) + int16(s.dir)*starStep
|
||
switch {
|
||
case nb >= 255:
|
||
s.br = 255
|
||
s.dir = -1
|
||
case nb <= 0:
|
||
// Star died – pick a new random one
|
||
s.br = 0
|
||
s.dir = 1
|
||
s.pos = uint8(rand() % numLEDs)
|
||
s.hue = uint8(rand())
|
||
default:
|
||
s.br = uint8(nb)
|
||
}
|
||
|
||
leds[s.pos] = dim(wheel(s.hue), s.br)
|
||
}
|
||
}
|
||
|
||
// ─── effect 5 – Breathing ─────────────────────────────────────────────────────
|
||
// All LEDs pulse in unison with a triangular brightness envelope; the hue
|
||
// drifts imperceptibly slowly.
|
||
|
||
func effectBreathing(step uint32) {
|
||
t := uint8(step) // 0-255, period = 256 frames ≈ 5 s
|
||
var br uint8
|
||
if t < 128 {
|
||
br = t * 2 // 0 → 254
|
||
} else {
|
||
br = (255 - t) * 2 // 254 → 0
|
||
}
|
||
hue := uint8(step >> 7) // hue changes once per breath cycle
|
||
for i := range leds {
|
||
leds[i] = dim(wheel(hue), br)
|
||
}
|
||
}
|
||
|
||
// ─── effect 6 – Rainbow rows ──────────────────────────────────────────────────
|
||
// Each row shows a distinct hue (separated by 64/256 of the wheel); all hues
|
||
// advance together, making the whole grid slowly rotate through colours.
|
||
|
||
func effectRainbowRows(step uint32) {
|
||
base := uint8(step >> 2) // same drift speed as effect 1
|
||
for row := 0; row < numRows; row++ {
|
||
c := wheel(base + uint8(row*64))
|
||
for col := 0; col < numCols; col++ {
|
||
leds[idx(row, col)] = c
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── effect 7 – Column wave ───────────────────────────────────────────────────
|
||
// A bright column chases across the grid with a fading two-column tail.
|
||
|
||
func effectWave(step uint32) {
|
||
clear()
|
||
head := int(step/8) % numCols // advances one column every 8 frames ≈ 160 ms
|
||
hue := uint8(step >> 1)
|
||
|
||
// Brightness per distance behind the head
|
||
bri := [numCols]uint8{255, 110, 35, 0, 0}
|
||
|
||
for col := 0; col < numCols; col++ {
|
||
dist := (head - col + numCols) % numCols
|
||
if dist >= numCols || bri[dist] == 0 {
|
||
continue
|
||
}
|
||
c := dim(wheel(hue+uint8(col*12)), bri[dist])
|
||
for row := 0; row < numRows; row++ {
|
||
leds[idx(row, col)] = c
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── main ─────────────────────────────────────────────────────────────────────
|
||
|
||
func main() {
|
||
// Initialise WS2812
|
||
pinLED.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||
strip = ws2812.New(pinLED)
|
||
|
||
// Initialise button with internal pull-up
|
||
pinBtn.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
|
||
|
||
var (
|
||
effect int
|
||
step uint32
|
||
prevBtn bool
|
||
lastPress time.Time
|
||
)
|
||
|
||
for {
|
||
// ── Button: detect falling edge with debounce ─────────────────────
|
||
btnDown := !pinBtn.Get() // LOW = pressed (active-low)
|
||
if btnDown && !prevBtn && time.Since(lastPress) > debounce {
|
||
effect = (effect + 1) % numEffects
|
||
lastPress = time.Now()
|
||
starsOK = false // force stars to reinitialise next time
|
||
}
|
||
prevBtn = btnDown
|
||
|
||
// ── Render current effect ─────────────────────────────────────────
|
||
switch effect {
|
||
case 0:
|
||
effectRainbow(step)
|
||
case 1:
|
||
effectRowSweep(step)
|
||
case 2:
|
||
effectColSweep(step)
|
||
case 3:
|
||
effectStars(step)
|
||
case 4:
|
||
effectBreathing(step)
|
||
case 5:
|
||
effectRainbowRows(step)
|
||
case 6:
|
||
effectWave(step)
|
||
}
|
||
|
||
strip.WriteColors(leds[:])
|
||
step++
|
||
time.Sleep(frameDelay)
|
||
}
|
||
}
|