Skip to the content
2 layers32.5 × 20.6 mmWaveLight, the printed circuit board in 3D

gesture activated night light

Size
32.5 × 20.6 mm
Copper layers
2
Thickness
1.6 mm
Parts
29
Nets
32
Pads
131
Tracks, vias
290, 53
Pours
2
Routed
Yes
KiCad
KiCad 10.0 (file 20260206)
Created
11 May 2026
Updated
25 Oct 2026

Overview

WaveLight is an open-source printed circuit board by lukmatuska, from the lukmatuska/WaveLight repository on GitHub. It is a 2-layer board of 32.5 × 20.6 mm with 29 parts on 32 nets, routed with 290 tracks and 53 vias.

It is built around the ATTINY416-MN. Its parts include 7 connectors, 6 capacitors, 5 resistors, 3 ICs and 1 transistor. It belongs with the AVR (ATmega, ATtiny) boards of this library.

Main components

1

README

README.md

WaveLight

A gesture-controlled night light. Wave your hand over it to turn it on or off, hold your hand over it to set the flame size, and double-wave to pick a color. The light is an animated candle flame on WS2812 LEDs. It runs on a Li-ion battery and sleeps when nobody is around.

  • MCU: ATtiny416 (4 kB flash, 256 B RAM), internal 16 MHz oscillator
  • Sensor: ams TMF8801 time-of-flight distance sensor
  • LEDs: 4× WS2812B-2020 (count is configurable)
  • Firmware: C, MPLAB XC8 (free license), in FW/
  • Hardware: KiCad project in the repository root

Using it

GestureWhat it does
Wave (hand over the sensor and away again, under 0.6 s)Light on / off. Reacts ~0.5 s after the hand leaves, because it waits to see if a second wave follows
Hold + move (hand stays over the sensor, then moves)Flame size: closer = smaller, dimmer embers; farther = bigger, brighter flame. Saved when the hand leaves
Wave, then hold right awayTurns on, then sets the flame size
Double waveColor mode: the light dips briefly. Bring your hand back within 2 s and the color follows its distance. Take the hand away, and after 1 s the light dips again and the color is saved

Distances: hands are detected up to 60 cm. Size and color are set between about 6 cm (closest) and 32 cm (farthest).

Colors, from close to far in color mode:

Hand distance~6 cm~32 cm
Colorredorangeyellowgreencyanbluemagentaredwarm white (2700 K)cool white (6500 K)

So the strip goes once around the color wheel back to red, then into warm white and cools down to 6500 K. The hotter parts of the flame shift a little further along the strip, which gives the flame its lighter tips.

Status signals

LEDsMeaning
Brief dip to dark and backColor mode started / ended
Magenta for 1 s at power-onSensor calibrated and calibration stored (first boot after flashing)
Red blinking continuouslySensor not responding. Retries every ~8 s (watchdog reset)
2 red blinks when turning onBattery low (< 3.5 V), turns on anyway
3 red blinks, stays offBattery critical (< 3.3 V). Charge above 3.5 V to use again

The battery signals only appear if the battery check is enabled (see Configuration).

Things that might surprise you

  • Static objects are learned. Anything that stays still for 10 s becomes background. A hand only counts if it is at least 8 cm closer than the background, so you can wave between the lamp and e.g. a shelf. After you place or remove something, give it ~10 s to adapt.
  • Placed objects never change the settings. A hold only adjusts the flame once the hand has moved 3 cm, and objects don't move.
  • Pulling your hand away doesn't change the setting. On release the value from ~200 ms earlier is kept.
  • Idle: after 15 s without a hand, whether the light is on or off, the sensor slows down (one shorter measurement every 200 ms) to save power. The first wave still works, but a very quick flick can be missed. Idle measurements are shorter, so a hand near the 60 cm limit may only be noticed once it comes a bit closer.

Hardware

Pinout

ATtiny416 pinFunction
PB3WS2812 data (LED strip connector J2; also gate of Q1 for the alternative power LED on J3)
PB1 / PB0I²C SDA / SCL to the TMF8801 (internal pull-ups, no external resistors fitted)
PC0TMF8801 INT (open drain, internal pull-up) - wakes the MCU
PC1TMF8801 EN
PB4 / PB2TMF8801 GPIO0 / GPIO1 (unused, driven low by the sensor)
PA1Battery voltage measurement (divider middle)
PC3Battery divider low side - pulled low only while measuring
PA0UPDI programming

Board

  • Power: Li-ion battery (J6) → power switch (SW1) → TCR2L LDO → VDD for the MCU and sensor. The LED strip runs directly from the battery voltage.
  • Charging: USB-C (J7) or the jack tip → charger IC (U3, charge current set by R3). The charger's battery output is on the switched side of SW1, so the battery only charges while the switch is on.
  • 3.5 mm jack (J1): tip = charger input, ring = UPDI, sleeve = GND. It is used for programming.
  • J4 / J5: sensor signal headers (VDD, GPIO0/1, SDA, SCL, INT, EN, GND).
  • JP1: solder jumper EN → GND (holds the sensor off when closed).
  • Battery divider: R2 1 MΩ (battery → PA1) and R4 470 kΩ (PA1 → PC3).

Known hardware limitations

  • No I²C pull-ups. The internal ones are used and the bus runs at 50 kHz. Add 4.7 kΩ to VDD on SDA/SCL in the next revision.
  • The LEDs run straight from the battery. The WS2812B-2020 is specified for 3.3-5.3 V. Below ~3.6 V the green and blue channels may lose current (dimmer, redder light), and below 3.3 V the LEDs are out of spec.
  • The sensor dominates standby current (~3 mA), see Power analysis. A 300 mAh battery lasts about 3.5 days in standby.
  • Sensor range is ~64 cm with the firmware built into the sensor (see Sensor range).

Firmware

Files

FileContents
main.cConfiguration, startup, main loop, gesture → action mapping, sleep, EEPROM settings
gesture.hWave / double wave / hold / tracking detection, background learning
light.hFlame animation, colors, fades, gamma correction, auto-off
tmf8801.hTMF8801 driver: power-up, factory calibration, measurement, results
battery.hBattery voltage check (ADC)
twi.hI²C master driver
ws2812b.hCycle-counted WS2812 driver (16 MHz)
PATCHING.mdHow to upload the TMF8801 RAM patch after switching to an ATtiny1616

Building and flashing

  1. Open FW/ in VS Code with the MPLAB extensions (project file FW/.vscode/Wavelight-firmware.mplab.json, XC8 v3.00, ATtiny_DFP 3.4.278).
  2. Build. The optimization level in the project doesn't matter: main.c forces -O2 without inlining with a #pragma. At -O0 the firmware doesn't fit. The free XC8 license allows -O2 but not -Os.
  3. Flash over UPDI (the project uses an MPLAB Snap).
  4. Set the fuse OSCCFG to 16 MHz (FREQSEL_16MHZ) on every new chip. The factory default is 20 MHz, and then all timing (LED data, I²C, gestures) is off by 25%.
  5. First boot after flashing: keep nothing within 40 cm of the sensor, ideally in the dark. The sensor calibrates itself and stores the result (1 s magenta flash).

Flashing erases the EEPROM, which holds the sensor calibration and the saved size and color, unless the EESAVE fuse is set.

Flash usage is about 3.65 kB, or 3.9 kB with the battery check, out of 4 kB.

Configuration

All settings are #defines.

main.c

DefineDefaultMeaning
DEBUG_DISTANCE01 = show the raw distance on the LEDs instead of the night light (see Debug mode)
BATTERY_CHECK01 = battery divider is fitted, enable the battery check
LED_COUNT4Number of WS2812 LEDs (~5 bytes RAM each, up to ~40)
TICK_MS10Main loop period
ADJUST_NEAR_MM60Hand distance for the smallest flame / first color. +1 step per mm up to 255 steps (~32 cm)
TONE_MODE_WAIT_MS2000Time to bring the hand back after a double wave
TONE_MODE_END_MS1000Color mode ends this long after the hand left
BATTERY_PERIOD_MS60000Battery check interval while running
SENSOR_ACTIVE_MS33Sensor measurement period while a hand is around
SENSOR_ACTIVE_KITER900Sensor iterations (thousands) per measurement while active
SENSOR_IDLE_MS200Sensor measurement period while idle (max 255). Longer = less power, more missed flicks
SENSOR_IDLE_KITER600Iterations while idle. Fewer = less power per measurement, maybe less range
IDLE_AFTER_MS15000No hand this long → sensor idle (light on or off)

gesture.h

DefineDefaultMeaning
GESTURE_NEAR_MM600Maximum hand distance
GESTURE_DROPOUT2Missed readings in a row before a hand counts as gone (bridges glitches near the range limit)
GESTURE_BG_MARGIN_MM80A hand must be this much closer than the background
GESTURE_BG_TOL_MM30Readings within this count as "not moving"
GESTURE_BG_LEARN_MS10000Static this long → becomes background
GESTURE_WAVE_MIN_MS50Shorter presence = noise
GESTURE_WAVE_MAX_MS600Longer presence = hold. Raise it if your waves are slow
GESTURE_DOUBLE_GAP_MS500Maximum pause between the two waves of a double wave
GESTURE_HOLD_MOVE_MM30A hold only adjusts after the hand moved this much
GESTURE_HISTORY6Readings (~200 ms) rolled back when the hand leaves

light.h

DefineDefaultMeaning
LIGHT_FADE_ON_MS / LIGHT_FADE_OFF_MS800 / 1500Fade times
LIGHT_AUTO_OFF_MIN0Turn off after this many minutes, 0 = never
LIGHT_DEFAULT_BRIGHTNESS255Flame size before anything is saved
LIGHT_MIN_BRIGHTNESS24Smallest flame size
LIGHT_DEFAULT_TONE0Color before anything is saved (0 = red-orange flame). ~10 gives a more orange flame
LIGHT_WB_G / LIGHT_WB_B176 / 240White balance: WS2812 green and blue are much stronger than red. Tune until the 4000 K white looks neutral
FLAME_SPREAD110How much the flame flickers above its base heat
FLAME_CHANGE40How often the flicker changes (chance per tick, of 256)
FLAME_GUST2How often the whole flame dips (chance per tick, of 256)
FLAME_SPEED2Smoothing; higher = calmer flame
FLAME_HUE_SHIFT24How much hotter parts shift along the color strip
LIGHT_WHEEL_END192Where the color wheel ends and the white strip starts

battery.h

DefineDefaultMeaning
BATTERY_LOW_MV3500Warn when turning on
BATTERY_CRITICAL_MV3300Refuse to turn on / turn off. Cleared above BATTERY_LOW_MV
BATTERY_R_TOP / BATTERY_R_BOTTOM1000 / 470Divider resistors in kΩ

How it works

Sensor. The TMF8801 runs the measurement app built into its ROM. No firmware is uploaded, because the ~11 kB patch doesn't fit in 4 kB. On the first boot the sensor runs its factory calibration (crosstalk). The 14-byte result is stored in EEPROM and sent to the sensor on every start. It measures with 900k iterations; each measurement takes ~27 ms.

Gestures. Every sensor result goes into gesture_reading(), which decides whether a hand is present. gesture_update() runs every 10 ms and turns presence over time into events: wave, double wave, hold, hold end, track, track end. While color mode is active, wave and hold detection are off and the hand position is reported directly ("tracking"). When color mode ends, a hand that is still there is ignored until it leaves, so color mode never turns into a size change.

Light. The output level fades linearly in perceptual brightness and is gamma-corrected, so fades look even. Each LED has a "heat" value that drifts towards random targets. Neighboring LEDs are coupled, so the flicker travels along the strip, and occasionally the whole flame dips (a gust). Heat sets both the brightness and a small color shift. The size setting moves the base heat up and down.

Power. While a hand is around, the sensor measures every 33 ms with 900k iterations. After IDLE_AFTER_MS with no hand, no gesture pending and no color mode, whether the light is on or off, the sensor switches to SENSOR_IDLE_MS / SENSOR_IDLE_KITER (200 ms, 600k) and goes to standby between measurements. A hand switches it back.

What the MCU does then depends on the light:

  • Light on (or fading): the MCU stays awake and runs the flame every 10 ms.
  • Light off: the MCU goes to power-down and is woken by the sensor's INT pin, which goes low on every result (the TMF8801 has no distance-threshold interrupt). After each wake, the firmware catches up the missed 10 ms ticks so all timing stays correct, then sleeps again unless a hand was seen.

Unused pins have their digital inputs disabled, and the battery divider only draws current during a measurement. A watchdog (~8 s, also running in sleep) resets the device if the sensor ever stops waking it.

For the resulting currents and battery life, see Power analysis.

Settings. The flame size and color are saved to EEPROM when a hold or color mode ends, and only if they changed. EEPROM layout: page 0 = calibration + marker, page 1 = size, color + marker.

Debug mode

Set DEBUG_DISTANCE 1 to show what the sensor sees, at 10 cm resolution:

  • Color = 40 cm band: green 0-40, yellow 40-80, red 80-120, magenta 120-160, white over 160 cm
  • Number of lit LEDs = 10 cm steps within the band (1-4). For example, 3 yellow = 60-70 cm.
  • First LED blue: the sensor reports no object
  • All cyan: no new reading for 0.5 s

The display assumes 4 LEDs.

Sensor range

With the ROM firmware, the TMF8801 here reports "no object" beyond ~64 cm, even for a white wall in the dark with calibration. The datasheet only promises its 2.5 m range with the latest ams RAM patch (~11 kB, uploaded on every boot) and a tuned oscillator. An ATtiny1616 (16 kB) in the same package fits the patch. The steps are in FW/PATCHING.md.


Power analysis

Estimates for 4× WS2812B-2020 and a 300 mAh Li-ion battery. They are calculated from datasheet values and a simulation of the flame code. Nothing has been measured yet.

Assumptions

  • Usable capacity 270 mAh: about 90% of 300 mAh, from 4.2 V down to the 3.3 V cut-off.
  • Battery current = load current. The LDO is linear, and the LEDs are constant-current drivers. This holds while the battery is high enough for the green/blue LED channels (~3.6 V); below that the LEDs draw less and get dimmer.
  • Room temperature, typical (not maximum) values.
  • Default firmware settings: sensor every 33 ms with 900k iterations while a hand is around, every 200 ms with 600k iterations while idle; MCU at 16 MHz.
  • The iteration count really changes the measurement time. The datasheet contradicts itself on the byte order, and the firmware follows the register description. Verify by measuring the standby current with SENSOR_IDLE_KITER 600 vs 900. It should drop by about a third; if it doesn't, the byte order is wrong.

Per component

ComponentStateCurrentSource
TMF8801active: every 33 ms, 900k (hand around, or < 15 s after)~27 mADS000648: 27 mA average at 30 Hz, 900k iterations
TMF8801idle: every 200 ms, 600k~3.0 mADS000648: 17.7 mA at 30 Hz with 600k → ~0.59 mC per measurement; + 85 µA standby in between
ATtiny416awake, 16 MHz, ~3 V (light on)~3 mAdatasheet fig. 36-1
ATtiny416light off, idle: power-down + ~6 ms awake per wake-up (I²C, catch-up ticks)~0.1 mA3% of 3 mA; power-down itself ~0.2 µA (fig. 36-9)
WS2812B-2020 ×4dark≤ 4 µAV6 datasheet: quiescent ≤ 1 µA each
WS2812B-2020 ×4flame3-71 mA average, see below12 mA per channel at full, flame simulated
LDO, charger, watchdog, divider, pull-upsalwaysa few µAnegligible

LED current of the flame

These figures come from simulating the actual light.h algorithm (random generator, heat model, colors, white balance) for 10 minutes. Each LED channel draws 12 mA × PWM / 255.

ColorSize 255Size 160Size 80Size 24 (min)
Red (default)31.5 mA15.6 mA6.9 mA3.1 mA
Orange44.4 mA21.3 mA9.1 mA4.0 mA
Yellow28.6 mA17.3 mA9.1 mA4.6 mA
Blue30.5 mA14.9 mA6.6 mA3.0 mA
Warm white 2700 K41.2 mA21.1 mA9.5 mA4.3 mA
Cool white 6500 K71.4 mA37.6 mA17.4 mA8.1 mA
  • Peak: about 108 mA for the LEDs (cool white, size 255, during flicker peaks), so ~140 mA in total. This is under 0.5 C for a 300 mAh cell.
  • Theoretical maximum: 4 × 36 mA = 144 mA; the flame never gets there.

Total current and run time (270 mAh usable)

After the first 15 s without a hand, the light runs with the sensor idle. The "on" rows assume that; the active state only applies briefly after each interaction.

ScenarioSensorMCULEDsTotalRun time
Standby (light off, idle)3.00.103.1 mA~87 h (3.6 days)
Hand around / < 15 s after (light off)273030 mA(short)
On: red, size 243.033.19.1 mA30 h
On: red, size 803.036.912.9 mA21 h
On: red, size 1603.0315.621.6 mA12.5 h
On: red, size 255 (default)3.0331.537.5 mA7.2 h
On: warm white, size 2553.0341.247.2 mA5.7 h
On: cool white, size 2553.0371.477.4 mA3.5 h

Example day: the light is on for 30 min (red, size 160) and the rest is standby. That's 11 mAh + 73 mAh = ~84 mAh per day, so a charge lasts about 3 days.

Before the sensor idled while on and used fewer iterations (100 ms / 900k idle, 33 ms whenever on), standby was 9.3 mA (~29 h) and red at size 160 ran 5.9 h.

Findings

  1. Standby is still mostly the sensor (3.0 of 3.1 mA). Each measurement costs a fixed charge (~0.59 mC at 600k), so the average current scales with measurement interval and iteration count.
  2. While the light is on, the MCU is now a big share. At small sizes the busy 16 MHz loop (3 mA) costs as much as the sensor or the LEDs.
  3. The LEDs cost nothing when dark (WS2812B-2020 V6: ≤ 1 µA). Older WS2812B revisions and clones draw ~0.5-1 mA each, so check the part revision if standby measures higher than expected.

Ways to extend run time further (not implemented)

ChangeEffectCost
Idle sleep instead of busy-waiting between 10 ms ticks while on (MCU ~1 mA instead of 3 mA)red, size 24: 30 → ~38 hsmall code change
Fewer idle iterations (e.g. 400k)standby roughly -30%idle range shrinks further (untested)
Power switch off when not in use~0 mAthe battery doesn't charge while switched off (see Board)

References

Parts

29
QtyValueFootprintReferences
40.1uCapacitor_SMD:C_0402_1005Metric_Pad0.74x0.62mm_HandSolderC3, C4, C5, C6
2100nCapacitor_SMD:C_0402_1005Metric_Pad0.74x0.62mm_HandSolderC1, C2
2Conn_01x08_PinConnector_PinHeader_1.27mm:PinHeader_1x08_P1.27mm_VerticalJ4, J5
2LOGOLOGOG***, G***
1
110kResistor_SMD:R_0603_1608Metric_Pad0.98x0.95mm_HandSolderR1
11MResistor_SMD:R_0603_1608Metric_Pad0.98x0.95mm_HandSolderR2
1470KResistor_SMD:R_0603_1608Metric_Pad0.98x0.95mm_HandSolderR4
156kResistor_SMD:R_0603_1608Metric_Pad0.98x0.95mm_HandSolderR5
1AO3400A or similarPackage_TO_SOT_SMD:SOT-23Q1
1ATtiny416-MPackage_DFN_QFN:VQFN-20-1EP_3x3mm_P0.4mm_EP1.7x1.7mmU1
1AudioJack4_SwitchT1Mylib:ASJ-190-AJ1
1BattConnector_PinHeader_1.27mm:PinHeader_1x02_P1.27mm_VerticalJ6
1CH_PROGResistor_SMD:R_0603_1608Metric_Pad0.98x0.95mm_HandSolderR3
1Ledstrip_connConnector_PinHeader_1.00mm:PinHeader_1x03_P1.00mm_VerticalJ2
1POWER_SW_SPDTConnector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_VerticalSW1
1PowerLED_connConnector_PinHeader_1.00mm:PinHeader_1x02_P1.00mm_VerticalJ3
1SolderJumper_2_OpenJumper:SolderJumper-2_P1.3mm_Open_RoundedPad1.0x1.5mmJP1
1TCR2Lx1Package_TO_SOT_SMD:SOT-23-5TCR2Lx1
1TP chargerPackage_TO_SOT_SMD:SOT-23-5_HandSolderingU3
1USB_C_Receptacle_USB2.0_14PConnector_USB:USB_C_Receptacle_GCT_USB4110J7
1~Mylib:TMF8801U2

The 28 most common of 29 parts.

Similar boards

  • 4 layers13.6 × 41.4 mmLightsaber necklace, the printed circuit board in 3D

    Lightsaber necklace

    winedarksea/solar-necklace-attiny

    Solar Necklace ATTiny code

    39 parts1MIT
  • 2 layers112.7 × 114.2 mmChunk pannel, the printed circuit board in 3D

    Chunk pannel

    paul-jo-dreyer/Atom

    Micro swarm robots

    108 parts
  • 4 layers20.7 × 25.9 mmSkin optoelectronics module, the printed circuit board in 3D

    Skin optoelectronics module

    Roboy/roboy-skin-electronics

    Parent repo which contains all the boards and schematics related to the light skin project.

    80 parts
  • 2 layers15 × 9 mmLoPiTS, the printed circuit board in 3D

    LoPiTS

    bask185/Train-Science-DIY

    16 parts3
  • 2 layers4.7 × 52.9 mmSingle key keyboard, the printed circuit board in 3D

    Single key keyboard

    AeroX2/aerox2.github.io

    One repository and npm installation builds the workshop at jamesridey.dev and the professional site at cv.jamesridey.dev.

    26 parts
  • 2 layers52.3 × 10.5 mmStackLink, the printed circuit board in 3D

    StackLink

    gusmanb/logicanalyzer

    24 channel, 100Msps logic analyzer hardware and software

    29 parts5.1kGPL-3.0
  • 2 layers56 × 20 mmHallEffectXY Endstop, the printed circuit board in 3D

    HallEffectXY Endstop

    VoronDesign/Voron-Hardware

    Cool Hardware designed by the Voron Design group

    29 parts1.1kGPL-3.0

Questions

How big is the WaveLight PCB?

It measures 32.5 × 20.6 mm and has 2 copper layers, 1.6 mm thick.

What parts does the WaveLight use?

29 parts, built around the ATTINY416-MN. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the WaveLight KiCad files?

From the lukmatuska/WaveLight repository on GitHub, at the commit this page was made from: the board is WaveLight.kicad_pcb and its schematic WaveLight.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the WaveLight design in my own project?

Its repository states no licence, so its author keeps all rights: ask lukmatuska before reusing the design.