- C++ 93.9%
- Python 5.2%
- CMake 0.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| components | ||
| documentation | ||
| licenses | ||
| main | ||
| test | ||
| tools | ||
| .clang-tidy | ||
| .gitattributes | ||
| .gitignore | ||
| .sync-exclude.lst | ||
| CLAUDE.md | ||
| CMakeLists.txt | ||
| dependencies.lock | ||
| LICENSE | ||
| partitions.csv | ||
| README.md | ||
| sdkconfig.defaults | ||
| sdkconfig.defaults.debug | ||
| THIRD-PARTY-NOTICES.md | ||
| VERSION | ||
Lumi
Lumi is a device for sport activities, workouts, and games.
Lumi can work on its own, or it can connect to up to 12 other devices for multi-device games. The main feature is a reaction game. The device lights up and the user touches it as fast as possible.
It is free and open source under the MIT license. See LICENSE and THIRD-PARTY-NOTICES.md. It was mostly generated using Claude Code.


Status
Working firmware, tested on real hardware with three pods. The Mobile phone companion app is not yet implemented.
Software
- Language: C++
- Framework: ESP-IDF
- OS: FreeRTOS
- GUI: LVGL
- Mesh technology: ESP-NOW
- Bluetooth: NimBLE (BLE only, for the phone app)
Hardware
| Hardware | GPIO | Description | Info |
|---|---|---|---|
| Wemos Lolin32 Lite | — | Microcontroller that runs the firmware. | Link |
| Button center | T3 (GPIO 15) | Large center capacitive button. | |
| Button left | T8 (GPIO 33) | Left capacitive button. | |
| Button right | T9 (GPIO 32) | Right capacitive button. | |
| Neopixel LED ring | 13 | Ring of 24 addressable WS2812B RGB LEDs. | |
| OLED SSD1306 | SDA 12, SCL 14 | 128 px wide, 32 px high. I2C address 0x3C. | Link |
| TOF sensor VL53L0X | SDA 12, SCL 14 | Time-of-flight distance sensor. I2C address 0x29. | Link |
| Piezo buzzer | 19 | Acoustic buzzer. | |
| 18650 battery cell | — | Power source. | |
| Power switch | — | Connect / disconnect the battery. |
Game modes
| Mode | What you do | Pods |
|---|---|---|
| Reaction Time | One pod lights up at a time. Touch it. You get your average, best, and worst time. | 1+ |
| Memory | The pods show a pattern that grows every round. Repeat it. | 1+ |
| Mine Sweeper | Green means touch it fast. Red is a mine, leave it alone. It gets faster every wave. | 1+ |
| Interval | The buzzer calls you to one pod after another. Wave at it or press it before time runs out. The clock speeds up. | 1+ |
| Reaction Multiplayer | Every player has their own color. Chase your color across the pods and collect points. First to get all points wins. | 2+ |
How the pods talk
Every frame a pod sends goes out twice: once to all pods at once, and once addressed to each pod it has heard from. The radio acknowledges and retries an addressed frame in hardware, and does neither for the all-pods copy. A pod keeps whichever copy arrives first and drops the other, so the second one costs airtime and nothing else.
Pods find each other with no setup and no network. A pod that is switched on joins a running group in about a second, and one that loses power rejoins just as fast.
How a game works
flowchart LR
A[Pick a game<br/>on any pod] --> B[That pod becomes<br/>the coordinator]
B --> C[It tells the other pods<br/>when to light up]
C --> D[Players react]
D --> E[Every pod shows<br/>the same result]
Repository layout
| Path | Contents |
|---|---|
components/lumi_core |
Games, mesh protocol, settings, menu logic. No hardware calls, host unit-tested. |
components/lumi_hal |
Drivers: touch, LEDs, buzzer, display, distance sensor, storage, radio. |
components/lumi_ui |
Screens and the app state machine. |
main |
Firmware entry point. |
test |
Host unit tests. |
documentation |
Requirements, game-mode diagrams, coding guidelines, CI setup. |
tools |
Version-header generator and the coverage report. |
Build and flash
You need ESP-IDF v6.0 and an ESP32 board.
idf.py set-target esp32
idf.py build
idf.py -p <PORT> flash
Flashing goes over the board's own USB port, not the charger.
The release build is silent: it writes no log output. To get diagnostics on the serial port:
idf.py fullclean
idf.py -DLUMI_DEBUG=ON build
idf.py -p <PORT> flash monitor
fullclean is needed because ESP-IDF only reads the sdkconfig.defaults files when no sdkconfig exists yet.
Run the tests
The game, mesh, and menu logic lives in components/lumi_core and has no hardware dependency, so it builds and runs on a normal PC.
cmake -S test -B test/build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build test/build
./test/build/lumi_core_tests
Line coverage, with a GCC build:
CC=gcc CXX=g++ cmake -S test -B test/build-coverage -G Ninja \
-DCMAKE_CXX_FLAGS="--coverage -O0" -DCMAKE_EXE_LINKER_FLAGS="--coverage"
cmake --build test/build-coverage
./test/build-coverage/lumi_core_tests
python3 tools/coverage_report.py test/build-coverage --min 80
Static analysis needs a compile_commands.json. The host build covers lumi_core:
cmake -S test -B test/build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
clang-tidy -p test/build components/lumi_core/src/*.cpp
For lumi_hal and lumi_ui, point clang-tidy at the ESP-IDF build directory instead.
Documentation
- Requirements — what the device must do, with IDs the code refers to.
- Game modes — one diagram per mode.
- Coding guidelines
- Networking