// 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) } }