No description
  • C++ 93.9%
  • Python 5.2%
  • CMake 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
seluba 0d4cfd8b74
All checks were successful
CI / Host tests, coverage, static analysis (push) Successful in 1m1s
CI / Firmware build (push) Successful in 2m14s
Fix logo font (convert to path)
2026-08-30 14:09:19 +02:00
.claude add / update AI configs 2026-08-30 12:02:19 +02:00
.forgejo/workflows Initial commit 2026-08-27 17:13:30 +02:00
components Set version to 1.0.0 2026-08-30 14:00:53 +02:00
documentation Fix logo font (convert to path) 2026-08-30 14:09:19 +02:00
licenses Initial commit 2026-08-27 17:13:30 +02:00
main change value screens to markers, renumber game modes 2026-08-30 10:20:22 +02:00
test Set version to 1.0.0 2026-08-30 14:00:53 +02:00
tools Initial commit 2026-08-27 17:13:30 +02:00
.clang-tidy Initial commit 2026-08-27 17:13:30 +02:00
.gitattributes Initial commit 2026-08-27 17:13:30 +02:00
.gitignore Initial commit 2026-08-27 17:13:30 +02:00
.sync-exclude.lst fix arrow and wordmark rendering on mono display 2026-08-30 10:20:17 +02:00
CLAUDE.md add / update AI configs 2026-08-30 12:02:19 +02:00
CMakeLists.txt Initial commit 2026-08-27 17:13:30 +02:00
dependencies.lock Initial commit 2026-08-27 17:13:30 +02:00
LICENSE Initial commit 2026-08-27 17:13:30 +02:00
partitions.csv Initial commit 2026-08-27 17:13:30 +02:00
README.md Update documentation 2026-08-30 11:26:02 +02:00
sdkconfig.defaults change config text appearance, only number bold 2026-08-30 10:20:27 +02:00
sdkconfig.defaults.debug Initial commit 2026-08-27 17:13:30 +02:00
THIRD-PARTY-NOTICES.md Initial commit 2026-08-27 17:13:30 +02:00
VERSION Set version to 1.0.0 2026-08-30 14:00:53 +02:00

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+

Diagrams of each mode

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