LuxLibre › Documentation › LuxLibre firmware
On this page: Features · Why C++17 · Architecture · Building · Flashing · The console · Parameters · Tests · More
One firmware for many flashlights. The same core code runs on an 8-bit ATtiny1616, a RISC-V CH32V003 that costs about $0.13 in quantity, an ESP32-C3, and on a PC, where a simulated flashlight (battery, LED, heat) runs the unit and behaviour tests.
| Port | MCU | Status |
|---|---|---|
| host | PC (Linux, macOS, Windows with WSL) | simulator and tests; every change is tested here |
| attiny1616 | Microchip ATtiny1616 | builds and links (about 11.4 KB of 16 KB flash); not yet tested on hardware |
| ch32v003 | WCH CH32V003 | builds and links (about 11.8 KB of 15.75 KB flash); not yet tested on hardware |
| esp32c3 | Espressif ESP32-C3 (FlashKa v0.5, v0.6 and v0.7) | experimental: written for ESP-IDF 5.x and checked against its API, CI build configured (allowed to fail until it has passed once); not yet compiled with ESP-IDF or tested on hardware |
Contents: features · why C++17 · architecture · building · flashing · the console · parameters · tests · more
UI_STYLE), with the common LuxLibre
gestures (FW-3): 1 click on/off with memory, hold to ramp or
change mode, 2 clicks turbo with a timer, 3 clicks battery check, 4 clicks lockout, 9 clicks
factory reset, click and hold for the laser or momentary turbo; beacon, SOS and strobe
if enabled. Full map: user-interface.md.The firmware had to run smoothly on very small chips, give fine control over the hardware, and still be usable in ten years. The options:
| Language | Runs on 16 KB AVR and RV32E | Same code on a PC simulator | Fine hardware control | Long-term support |
|---|---|---|---|---|
| C | yes | yes | yes | excellent, but no namespaces, no compile-time computation, macros everywhere |
| C++17 (embedded subset) | yes | yes | yes | excellent: ISO standard, GCC and Clang on every MCU family |
| Rust | experimental for 8-bit AVR and RV32E | yes | yes | very good on 32-bit chips (the FlashKa v0.5 firmware is Rust) |
| MicroPython, Arduino | too large or too slow for 16 KB parts | partly | limited | good for prototypes |
LuxLibre uses C++17 with strict limits: no heap allocation, no exceptions, no run-time type
information, no virtual functions (on AVR their tables would live in the 2 KB of RAM), no
standard containers in the core. What it does use: namespaces, constexpr to compute lookup tables at compile time
(levels, NTC tables), strong enums and static_assert checks of board settings. The result
compiles to about 11-12 KB on both small MCUs and runs unchanged on the PC.
flowchart LR
board["boards/NAME/board.h<br/>pins, limits, defaults"] --> cfg["core/lx_board.h<br/>fills in defaults"]
cfg --> core
subgraph core["core/ (portable C++17)"]
light["lx_light<br/>main loop"] --> input["lx_input<br/>gestures"]
light --> ui["lx_ui<br/>states + binding table"]
light --> thermal["lx_thermal"]
light --> battery["lx_battery"]
light --> aux["lx_aux"]
light --> console["lx_console"]
ui --> ramp["lx_ramp<br/>levels"]
light --> storage["lx_storage"]
params["lx_params<br/>+ lx_params_list.h"]
end
core --> hal["hal/lx_hal.h<br/>HAL interface"]
hal --> ports["ports/: host (simulator), attiny1616, ch32v003, esp32c3"]
lx_light runs one tick every
LX_TICK_MS (10 ms): read the button, feed the gesture decoder (lx_input) and the user
interface state machine (lx_ui), update sensors and protection every 100 ms (lx_battery,
lx_thermal), take the lowest of all caps (thermal, low voltage, cell current, USB), smooth
the level, set the output, run the indicator and console, feed the watchdog, and sleep when the
light is off.lx_ui.cpp maps (layout, state, gesture) to an action. Changing the
user interface means editing a table, not rewriting logic, and
user-interface.md is generated from it.requested level (UI) -> min(thermal cap, low-voltage cap, cell-current cap, USB cap)
-> smoothing (SMOOTH_MS per level)
-> ramp::power_of_level() (150 geometric levels between LX_OUT_MIN_Q16 and full scale)
-> hal::set_main(power) (PWM duty, current set point, or both)
| Block | Contents | Written when |
|---|---|---|
| Parameters | “LX”, hash of the parameter list, values, CRC-16 | save, factory reset |
| Calibration | BAT_CAL, THM_OFFSET, CRC-16, in its own 64-byte page |
with every parameter save; survives the factory reset and a changed parameter list |
| State ring | 8 slots: lockout, memory level, CRC-8 | when the light goes off after a change (spreads the wear) |
You need make and a C++ compiler for the PC; the cross-compilers only for the MCUs you build.
# Ubuntu 24.04 (on Windows: install WSL with Ubuntu and use the same commands)
sudo apt install g++ make gcc-avr binutils-avr avr-libc gcc-riscv64-unknown-elf python3-matplotlib
cd firmware
make test # unit and behaviour tests on the PC
make sim # build/luxsim, the simulator
make docs # regenerate parameters.md and user-interface.md
make tiny1616 BOARD=example-tiny1616-pwm # build/tiny1616-example-tiny1616-pwm.hex
make ch32v003 BOARD=example-ch32v003-buck # build/ch32v003-example-ch32v003-buck.bin
The ESP32-C3 port builds with ESP-IDF 5.x:
cd firmware/ports/esp32c3
idf.py set-target esp32c3
idf.py -DLX_BOARD=flashka-keychain-v05 build # or flashka-keychain-v06, flashka-keychain-v07, edco-v01
Details and the current limitations of that port: ports/esp32c3/README.md.
| MCU | Programmer | Command |
|---|---|---|
| ATtiny1616 | UPDI: a USB-serial adapter with a resistor (“serial UPDI”) or a dedicated UPDI programmer | pymcuprog write -t uart -u /dev/ttyUSB0 -d attiny1616 -f build/tiny1616-<board>.hex --erase, and once per chip the brown-out fuse: make tiny1616-fuses UPDI_PORT=/dev/ttyUSB0 |
| CH32V003 | WCH-LinkE on the one-wire SWIO pin | minichlink -w build/ch32v003-<board>.bin flash -b |
| ESP32-C3 | USB (built-in USB serial/JTAG) | idf.py -p <port> flash |
Before the first flash of a new board, do the bring-up checks in testing.md: a current-limited supply and a check that every output stays off during reset (DRV-7).
Boards with LX_HAS_CONSOLE (the FlashKa: over USB) accept text commands, one per line. Any
serial terminal works.
| Command | Does |
|---|---|
help |
list the commands |
ver |
firmware version and board name |
status |
state, output and requested level, estimated cell current, temperature, cell voltage, charge %, thermal and low-voltage caps |
list |
all parameters with their values |
get NAME |
one parameter |
set NAME VALUE |
change a parameter (refused outside its range) |
save |
store the parameters (and the calibration) |
defaults |
parameters back to defaults, calibration kept (not saved until save) |
defaults all |
the same, calibration included |
level N / off |
switch the light to level N (1-150) or off; refused in lockout |
reboot |
restart the MCU |
wdtest |
watchdog test: moon on, then the firmware stops; the watchdog must restart the MCU (DRV-8) |
An example session:
> ver
LuxLibre 0.5.0 / FlashKa Keychain v0.6
ok
> get THM_LIMIT
THM_LIMIT=50
ok
> set THM_LIMIT 47
THM_LIMIT=47
ok
> save
ok
The console stays awake for 30 s after the last input; while USB is connected the light does not
sleep and the main LED is capped at CHG_MAXLVL (it is also a charger connection).
Every user setting is one line in core/lx_params_list.h: name (at most 10 characters), default,
range, unit and description. The list generates the C++ enum, the defaults table, the console
names and parameters.md. Boards change defaults with LX_DEF_<NAME> in their
header. Ranges keep every setting safe (FW-5): for example
CHG_TMIN cannot go below 0 °C.
make test runs three programs:
CI runs these on every push, plus the ATtiny1616, CH32V003 and ESP32-C3 builds (the ESP32-C3 job may fail without failing the run until it has passed once) and a check that the generated documents are up to date (.github/workflows/firmware.yml).