first version

This commit is contained in:
Daniel Esteban 2026-05-06 17:36:12 +02:00
parent 02313ec8d6
commit d848cbc279
29 changed files with 42144 additions and 77 deletions

131
README.md
View file

@ -1,2 +1,131 @@
# kuzu
# 光 Hikari
A custom WS2812 LED badge powered by a [Seeed XIAO (BLE/RP2040/ESP32C3/...)](https://wiki.seeedstudio.com/SeeedStudio_XIAO_Series_Introduction/) and written in [TinyGo](https://tinygo.org).
The PCB hosts 20 RGB LEDs arranged in a **4 rows × 5 columns** grid behind a frosted 3D-printed diffuser. A single button cycles through lighting effects.
![Video 3](assets/videogif3.gif)
---
## Hardware
| Component | Note |
|-----------|-----|
| Hikari PCB | in hardware/pcb folder |
| 3D printed files | in hardware/STL folder |
| Flathead screw 2.1x10 | 4x |
| 20 × WS2812 LEDs (4 rows × 5 cols) | D6 |
| Tactile button | D3 |
| Qwiic / StemmaQT connector | D4 & D5 (I²C) |
| Passive buffer | D7 |
---
## Effects
Each press of the button advances to the next effect. The sequence cycles endlessly.
| # | Name | Description |
|---|------|-------------|
| 1 | **Rainbow** | All 20 LEDs show the same colour; the hue cycles slowly through the full spectrum (~20 s per revolution) |
| 2 | **Row sweep** | One row lights up at a time and sweeps downward; the hue drifts continuously |
| 3 | **Column sweep** | One column lights up at a time and sweeps rightward |
| 4 | **Stars** | 8 LEDs at random positions fade in with a random colour then fade out; when a star dies a new one appears elsewhere |
| 5 | **Breathing** | All LEDs pulse with a triangular brightness envelope (~5 s per breath); the hue shifts slowly between cycles |
| 6 | **Rainbow rows** | Each row shows a different hue (90° apart on the colour wheel); all hues advance together |
| 7 | **Wave** | A bright column chases across the grid with a two-column fading trail |
---
## Flash
```bash
tinygo flash -target=xiao-esp32c3 -stack-size=8KB .
```
Requires TinyGo ≥ 0.41 and [`tinygo.org/x/drivers`](https://github.com/tinygo-org/drivers) v0.35.
---
## Build
### Step 1 — Bare PCB
The back side of the custom PCB. Footprints for the 20 WS2812 LEDs are arranged in the 4 × 5 grid.
![Step 1 bare PCB front](assets/step1.jpg)
---
### Step 2 — Qwiic / StemmaQT connector
We'll start with the SMD Qwiic / StemmaQT connector since it's the trickiest to solder. It is not really needed, but a cool way to extend the board with sensors. Use the help of some flux if needed.
![Step 2 qwiic stemmaqt](assets/step2.jpg)
---
### Step 3 — Soldering the button
The tactile switch is soldered to its footprint on top of the board. Easier than the stemmaQT.
![Step 3 soldering button](assets/step3.jpg)
---
### Step 4 — PCB fully populated
All 20 LEDs and the button in place. The XIAO BLE/RP2040/ESP32C3/... plugs in on the left edge. It's the easier one to do, and the last one to be done since it goes over one LED.
![Step 4 PCB fully populated](assets/step4.jpg)
**NOTE**: Make sure all the LEDs are correctly soldered since once we solder the board there's no way to fix the LED behind it.
---
### Step 5 — PCB seated in the frame
The board drops into the back shell. The silkscreen on the rear shows the Go gopher pattern.
![Step 5 PCB in frame](assets/step6.jpg)
---
### Step 6 — 3D-printed back cover
The black back shell printed in PLA. The XIAO slides into the slot on the side for USB access. Use 4 screws on the holes.
![Step 6 back cover](assets/step5.jpg)
---
### Step 7 — Frosted diffuser panel
A white PLA diffuser snaps onto the front of the frame to spread the light evenly across the grid. You could print different models of the diffuser!
![Step 7 diffuser panel](assets/step7.jpg)
---
### Step 8 — Finished
Fully assembled and running. The diffuser softens each LED point into a smooth glow.
![Step 8 finished, lit](assets/step8.jpg)
---
## Showroom
![Video 1](assets/videogif1.gif)
![Video 2](assets/videogif2.gif)
## License
[MIT](LICENSE)

BIN
assets/step1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
assets/step2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/step3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/step4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
assets/step5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/step6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
assets/step7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
assets/step8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
assets/video1.mp4 Normal file

Binary file not shown.

BIN
assets/video2.mp4 Normal file

Binary file not shown.

BIN
assets/video3.mp4 Normal file

Binary file not shown.

BIN
assets/videogif1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 MiB

BIN
assets/videogif2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 MiB

BIN
assets/videogif3.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

BIN
hardware/STL/hikari.SLDPRT Executable file

Binary file not shown.

BIN
hardware/STL/hikari.stl Normal file

Binary file not shown.

8222
hardware/pcb/hikari/.step Normal file

File diff suppressed because it is too large Load diff

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because it is too large Load diff

25538
hardware/pcb/hikari/hikari.stl Normal file

File diff suppressed because it is too large Load diff

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

108
main.go
View file

@ -1,12 +1,3 @@
// 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 (
@ -17,8 +8,6 @@ import (
"tinygo.org/x/drivers/ws2812"
)
// ─── constants ────────────────────────────────────────────────────────────────
const (
numLEDs = 20
numRows = 4
@ -26,26 +15,32 @@ const (
numEffects = 7
frameDelay = 20 * time.Millisecond
debounce = 200 * time.Millisecond
numStars = 8
starStep = 7
)
// Pin assignments for XIAO ESP32C3
const (
pinLED = machine.D6 // WS2812 data line
pinBtn = machine.D3 // Button (pulled up, LOW when pressed)
pinLED = machine.D6
pinBtn = machine.D3
)
// ─── globals ──────────────────────────────────────────────────────────────────
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
}
var (
leds [numLEDs]color.RGBA
strip ws2812.Device
starList [numStars]star
starsOK bool
leds [numLEDs]color.RGBA
strip ws2812.Device
// xorshift32 PRNG seeded with a compile-time constant
rngState uint32 = 0xCAFE1234
rngState uint32 = 0xDEADBEEF
)
// ─── colour helpers ───────────────────────────────────────────────────────────
// wheel maps 0-255 → smooth full-hue spectrum (R→G→B→R)
func wheel(pos uint8) color.RGBA {
switch {
@ -87,9 +82,7 @@ func rand() uint32 {
return rngState
}
// ─── effect 1 Rainbow ───────────────────────────────────────────────────────
// All LEDs show the same hue; the hue cycles slowly through the full spectrum.
// effectRainbow cycles through all the colors slowly
func effectRainbow(step uint32) {
c := wheel(uint8(step >> 2)) // full cycle ≈ 20 s
for i := range leds {
@ -97,9 +90,7 @@ func effectRainbow(step uint32) {
}
}
// ─── effect 2 Row sweep ─────────────────────────────────────────────────────
// One row at a time is lit; the active row sweeps downward while the hue drifts.
// effectRowSweep lit a row at a time, the color/hue changes
func effectRowSweep(step uint32) {
clear()
row := int(step/40) % numRows
@ -109,9 +100,7 @@ func effectRowSweep(step uint32) {
}
}
// ─── effect 3 Column sweep ──────────────────────────────────────────────────
// One column at a time is lit; the active column sweeps rightward.
// effectColSweep lit a column at a time, the color/hue changes
func effectColSweep(step uint32) {
clear()
col := int(step/40) % numCols
@ -121,38 +110,20 @@ func effectColSweep(step uint32) {
}
}
// ─── 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
)
// initStars resets the data for the stars effect
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
br: uint8(i * (255 / numStars)),
dir: 1,
}
}
starsOK = true
}
// effectStarts lit random LEDs and dimm them slowly
func effectStars(_ uint32) {
if !starsOK {
initStars()
@ -181,30 +152,24 @@ func effectStars(_ uint32) {
}
}
// ─── effect 5 Breathing ─────────────────────────────────────────────────────
// All LEDs pulse in unison with a triangular brightness envelope; the hue
// drifts imperceptibly slowly.
// effectBreathing shows classic breathing effect
func effectBreathing(step uint32) {
t := uint8(step) // 0-255, period = 256 frames ≈ 5 s
t := uint8(step)
var br uint8
if t < 128 {
br = t * 2 // 0 → 254
br = t * 2
} else {
br = (255 - t) * 2 // 254 → 0
br = (255 - t) * 2
}
hue := uint8(step >> 7) // hue changes once per breath cycle
hue := uint8(step >> 7)
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.
// effectRainbowRows shows a different color in each row and change its hue slowly
func effectRainbowRows(step uint32) {
base := uint8(step >> 2) // same drift speed as effect 1
base := uint8(step >> 2)
for row := 0; row < numRows; row++ {
c := wheel(base + uint8(row*64))
for col := 0; col < numCols; col++ {
@ -213,15 +178,12 @@ func effectRainbowRows(step uint32) {
}
}
// ─── effect 7 Column wave ───────────────────────────────────────────────────
// A bright column chases across the grid with a fading two-column tail.
// effectWave runs a cool wave effect
func effectWave(step uint32) {
clear()
head := int(step/8) % numCols // advances one column every 8 frames ≈ 160 ms
head := int(step/8) % numCols
hue := uint8(step >> 1)
// Brightness per distance behind the head
bri := [numCols]uint8{255, 110, 35, 0, 0}
for col := 0; col < numCols; col++ {
@ -236,14 +198,10 @@ func effectWave(step uint32) {
}
}
// ─── 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 (
@ -254,16 +212,14 @@ func main() {
)
for {
// ── Button: detect falling edge with debounce ─────────────────────
btnDown := !pinBtn.Get() // LOW = pressed (active-low)
btnDown := !pinBtn.Get()
if btnDown && !prevBtn && time.Since(lastPress) > debounce {
effect = (effect + 1) % numEffects
lastPress = time.Now()
starsOK = false // force stars to reinitialise next time
starsOK = false
}
prevBtn = btnDown
// ── Render current effect ─────────────────────────────────────────
switch effect {
case 0:
effectRainbow(step)