hikari/main.go
Daniel Esteban 02313ec8d6 first commit
2026-04-30 11:16:53 +02:00

288 lines
8.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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