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
|
||||
|
||||
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 (
|
||||
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)
|
||||
|
|
|
|||