first version
131
README.md
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 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 3 — Soldering the button
|
||||||
|
|
||||||
|
The tactile switch is soldered to its footprint on top of the board. Easier than the stemmaQT.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
**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 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 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 8 — Finished
|
||||||
|
|
||||||
|
Fully assembled and running. The diffuser softens each LED point into a smooth glow.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Showroom
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
|
||||||
BIN
assets/step1.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
assets/step2.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
assets/step3.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/step4.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
assets/step5.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/step6.jpg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
assets/step7.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
assets/step8.jpg
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
assets/video1.mp4
Normal file
BIN
assets/video2.mp4
Normal file
BIN
assets/video3.mp4
Normal file
BIN
assets/videogif1.gif
Normal file
|
After Width: | Height: | Size: 24 MiB |
BIN
assets/videogif2.gif
Normal file
|
After Width: | Height: | Size: 35 MiB |
BIN
assets/videogif3.gif
Normal file
|
After Width: | Height: | Size: 7.6 MiB |
BIN
hardware/STL/hikari.SLDPRT
Executable file
BIN
hardware/STL/hikari.stl
Normal file
8222
hardware/pcb/hikari/.step
Normal file
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
8222
hardware/pcb/hikari/hikari.step
Normal file
25538
hardware/pcb/hikari/hikari.stl
Normal file
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
104
main.go
|
|
@ -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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -17,8 +8,6 @@ import (
|
||||||
"tinygo.org/x/drivers/ws2812"
|
"tinygo.org/x/drivers/ws2812"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ─── constants ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
numLEDs = 20
|
numLEDs = 20
|
||||||
numRows = 4
|
numRows = 4
|
||||||
|
|
@ -26,26 +15,32 @@ const (
|
||||||
numEffects = 7
|
numEffects = 7
|
||||||
frameDelay = 20 * time.Millisecond
|
frameDelay = 20 * time.Millisecond
|
||||||
debounce = 200 * time.Millisecond
|
debounce = 200 * time.Millisecond
|
||||||
|
numStars = 8
|
||||||
|
starStep = 7
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pin assignments for XIAO ESP32C3
|
|
||||||
const (
|
const (
|
||||||
pinLED = machine.D6 // WS2812 data line
|
pinLED = machine.D6
|
||||||
pinBtn = machine.D3 // Button (pulled up, LOW when pressed)
|
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 (
|
var (
|
||||||
|
starList [numStars]star
|
||||||
|
starsOK bool
|
||||||
leds [numLEDs]color.RGBA
|
leds [numLEDs]color.RGBA
|
||||||
strip ws2812.Device
|
strip ws2812.Device
|
||||||
|
|
||||||
// xorshift32 PRNG – seeded with a compile-time constant
|
// 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)
|
// wheel maps 0-255 → smooth full-hue spectrum (R→G→B→R)
|
||||||
func wheel(pos uint8) color.RGBA {
|
func wheel(pos uint8) color.RGBA {
|
||||||
switch {
|
switch {
|
||||||
|
|
@ -87,9 +82,7 @@ func rand() uint32 {
|
||||||
return rngState
|
return rngState
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── effect 1 – Rainbow ───────────────────────────────────────────────────────
|
// effectRainbow cycles through all the colors slowly
|
||||||
// All LEDs show the same hue; the hue cycles slowly through the full spectrum.
|
|
||||||
|
|
||||||
func effectRainbow(step uint32) {
|
func effectRainbow(step uint32) {
|
||||||
c := wheel(uint8(step >> 2)) // full cycle ≈ 20 s
|
c := wheel(uint8(step >> 2)) // full cycle ≈ 20 s
|
||||||
for i := range leds {
|
for i := range leds {
|
||||||
|
|
@ -97,9 +90,7 @@ func effectRainbow(step uint32) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── effect 2 – Row sweep ─────────────────────────────────────────────────────
|
// effectRowSweep lit a row at a time, the color/hue changes
|
||||||
// One row at a time is lit; the active row sweeps downward while the hue drifts.
|
|
||||||
|
|
||||||
func effectRowSweep(step uint32) {
|
func effectRowSweep(step uint32) {
|
||||||
clear()
|
clear()
|
||||||
row := int(step/40) % numRows
|
row := int(step/40) % numRows
|
||||||
|
|
@ -109,9 +100,7 @@ func effectRowSweep(step uint32) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── effect 3 – Column sweep ──────────────────────────────────────────────────
|
// effectColSweep lit a column at a time, the color/hue changes
|
||||||
// One column at a time is lit; the active column sweeps rightward.
|
|
||||||
|
|
||||||
func effectColSweep(step uint32) {
|
func effectColSweep(step uint32) {
|
||||||
clear()
|
clear()
|
||||||
col := int(step/40) % numCols
|
col := int(step/40) % numCols
|
||||||
|
|
@ -121,38 +110,20 @@ func effectColSweep(step uint32) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── effect 4 – Stars ─────────────────────────────────────────────────────────
|
// initStars resets the data for the stars effect
|
||||||
// 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() {
|
func initStars() {
|
||||||
for i := range starList {
|
for i := range starList {
|
||||||
starList[i] = star{
|
starList[i] = star{
|
||||||
pos: uint8(rand() % numLEDs),
|
pos: uint8(rand() % numLEDs),
|
||||||
hue: uint8(rand()),
|
hue: uint8(rand()),
|
||||||
br: uint8(i * (255 / numStars)), // stagger so they don't sync
|
br: uint8(i * (255 / numStars)),
|
||||||
dir: 1,
|
dir: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
starsOK = true
|
starsOK = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// effectStarts lit random LEDs and dimm them slowly
|
||||||
func effectStars(_ uint32) {
|
func effectStars(_ uint32) {
|
||||||
if !starsOK {
|
if !starsOK {
|
||||||
initStars()
|
initStars()
|
||||||
|
|
@ -181,30 +152,24 @@ func effectStars(_ uint32) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── effect 5 – Breathing ─────────────────────────────────────────────────────
|
// effectBreathing shows classic breathing effect
|
||||||
// All LEDs pulse in unison with a triangular brightness envelope; the hue
|
|
||||||
// drifts imperceptibly slowly.
|
|
||||||
|
|
||||||
func effectBreathing(step uint32) {
|
func effectBreathing(step uint32) {
|
||||||
t := uint8(step) // 0-255, period = 256 frames ≈ 5 s
|
t := uint8(step)
|
||||||
var br uint8
|
var br uint8
|
||||||
if t < 128 {
|
if t < 128 {
|
||||||
br = t * 2 // 0 → 254
|
br = t * 2
|
||||||
} else {
|
} 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 {
|
for i := range leds {
|
||||||
leds[i] = dim(wheel(hue), br)
|
leds[i] = dim(wheel(hue), br)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── effect 6 – Rainbow rows ──────────────────────────────────────────────────
|
// effectRainbowRows shows a different color in each row and change its hue slowly
|
||||||
// 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) {
|
func effectRainbowRows(step uint32) {
|
||||||
base := uint8(step >> 2) // same drift speed as effect 1
|
base := uint8(step >> 2)
|
||||||
for row := 0; row < numRows; row++ {
|
for row := 0; row < numRows; row++ {
|
||||||
c := wheel(base + uint8(row*64))
|
c := wheel(base + uint8(row*64))
|
||||||
for col := 0; col < numCols; col++ {
|
for col := 0; col < numCols; col++ {
|
||||||
|
|
@ -213,15 +178,12 @@ func effectRainbowRows(step uint32) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── effect 7 – Column wave ───────────────────────────────────────────────────
|
// effectWave runs a cool wave effect
|
||||||
// A bright column chases across the grid with a fading two-column tail.
|
|
||||||
|
|
||||||
func effectWave(step uint32) {
|
func effectWave(step uint32) {
|
||||||
clear()
|
clear()
|
||||||
head := int(step/8) % numCols // advances one column every 8 frames ≈ 160 ms
|
head := int(step/8) % numCols
|
||||||
hue := uint8(step >> 1)
|
hue := uint8(step >> 1)
|
||||||
|
|
||||||
// Brightness per distance behind the head
|
|
||||||
bri := [numCols]uint8{255, 110, 35, 0, 0}
|
bri := [numCols]uint8{255, 110, 35, 0, 0}
|
||||||
|
|
||||||
for col := 0; col < numCols; col++ {
|
for col := 0; col < numCols; col++ {
|
||||||
|
|
@ -236,14 +198,10 @@ func effectWave(step uint32) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── main ─────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Initialise WS2812
|
|
||||||
pinLED.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
pinLED.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||||
strip = ws2812.New(pinLED)
|
strip = ws2812.New(pinLED)
|
||||||
|
|
||||||
// Initialise button with internal pull-up
|
|
||||||
pinBtn.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
|
pinBtn.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -254,16 +212,14 @@ func main() {
|
||||||
)
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// ── Button: detect falling edge with debounce ─────────────────────
|
btnDown := !pinBtn.Get()
|
||||||
btnDown := !pinBtn.Get() // LOW = pressed (active-low)
|
|
||||||
if btnDown && !prevBtn && time.Since(lastPress) > debounce {
|
if btnDown && !prevBtn && time.Since(lastPress) > debounce {
|
||||||
effect = (effect + 1) % numEffects
|
effect = (effect + 1) % numEffects
|
||||||
lastPress = time.Now()
|
lastPress = time.Now()
|
||||||
starsOK = false // force stars to reinitialise next time
|
starsOK = false
|
||||||
}
|
}
|
||||||
prevBtn = btnDown
|
prevBtn = btnDown
|
||||||
|
|
||||||
// ── Render current effect ─────────────────────────────────────────
|
|
||||||
switch effect {
|
switch effect {
|
||||||
case 0:
|
case 0:
|
||||||
effectRainbow(step)
|
effectRainbow(step)
|
||||||
|
|
|
||||||