Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lilo — a hands-free training assistant

Versão portuguesa

A self-contained training assistant running on an M5Stack Core S3. It guides you with sound, colour and light so you can train without looking at the screen. It needs no internet, no phone, no watch and no wristband.

One device, two kinds of training, same philosophy — alternating effort and rest:

  • Interval training. Configurable work and rest cycles, a countdown timer and a stopwatch. It started as an assistant for finger-strength work on a climbing hangboard, 10 s hang / 20 s rest — but that is just one exercise among many it can drive.
  • Reactive training. Two units light up as pads and you react: reaction speed, visual and auditory memory, and balance. Six games, synchronised over ESP-NOW.

Precise intervals, two synchronised units, signalling through sound, colour and light: all of it goes well beyond climbing. With paced stimuli you can train strength, balance, dexterity and memory, from children to older adults — general fitness, rehabilitation work, or exercise with a playful edge. The cycles adjust to the pace of each exercise and each person.

Main menu Reflex menu
Main menu Reflex games

This is not a medical device. It is an interval-training and stimulus-response aid, not a prescriber of load, and it is not clinically validated. Warm up, progress gradually, and stop on sharp pain.


Hardware

Component Role Required
M5Stack Core S3 Everything essential: touch screen, internal speaker, LTR-553 proximity sensor, BMI270 IMU, ESP32-S3 with ESP-NOW Yes
M5GO Bottom3 10 addressable RGB LEDs + battery No — recommended
GoPlus2 Battery only No

A bare Core S3 is enough. The bases are additive: the LED bar is bonus feedback and the batteries just make it portable. The second unit in the reactive games runs the exact same firmware with no LED base at all — the LED protocol is write-only, so nothing ever waits for a strip that isn't there.

Sound comes from the Core S3's internal speaker (M5.Speaker), with no accessories — that is what makes hands-free use work in the first place. If you want something more discreet, the sound can be turned off or turned down.


What it does

Interval training

  1. 10/20 — fixed cycle, zero configuration. 5 s prep, then 20 × (10 s work / 20 s rest).
  2. Custom — work, rest, reps per set, number of sets, rest between sets. Ships pre-filled as 7/3 repeaters. Saved to NVS.
  3. Timer — hours:minutes:seconds countdown with an end-of-time alert. Saved to NVS.
  4. Stopwatch — counts up, with a scrollable lap list.

Why 10/20? The fixed mode reproduces a published protocol rather than a number someone liked the sound of. In Gilmore et al., Sports Medicine – Open 2024, low-intensity hangs of 10 s on / 20 s off across a ~10-minute block — the "Abrahangs" routine, co-authored by Emil Abrahamsson — produced grip-strength gains equal to maximal hangs, and the two proved additive when combined. The ten minutes are not arbitrary either: collagen-synthesis signalling in loaded connective tissue peaks at around 10 minutes of loading and stays refractory for hours afterwards (Paxton et al., Tissue Eng Part A 2012; Baar, Sports Med 2017) — which is the argument for a short block repeated through the day rather than one long session.

Lilo supplies the cadence, not the load. That protocol asks for roughly 40% of maximum — "a light strain on the forearms" — and at least 6 hours between sessions. Both are yours to judge, and Custom will run any other work/rest ratio you want.

Editor Rest between sets Finished
Custom editor Set rest Done

The rounds counter shows what is left in the current set, and the set indicator only appears when there is more than one set. The final screen shows the work as you configured it — 1 round x 2 sets.

All three timed modes share one idle screen: mode name in the bar, total duration above the play button. While counting, the bar swaps the name for whatever is actually changing — the workout's remaining total in the cycle modes, nothing at all in the timer.

10/20 Custom Timer Stopwatch
10/20 walkthrough Custom walkthrough Timer walkthrough Stopwatch walkthrough

Reactive training (REFLEX) — two units

Both screens act as pads. The proximity sensor detects a hand approaching, so you can hit a pad without touching it.

Game Input What it trains
Reaction proximity or touch reaction time, target discrimination (green = go, other colours = decoy)
Survival proximity or touch same, sudden death, pace accelerates
Sequence touch only visual and auditory memory — Simon, growing sequence
Twist gyroscope reaction with a wrist-rotation gesture
Balance accelerometer continuous control — hold the pad level; first to tip loses
Duel proximity or touch 1v1 head-to-head, best of N

Every one runs the same shape — configure, get ready, react, see where you landed:

Reaction Survival Sequence
Reaction walkthrough Survival walkthrough Sequence walkthrough
Twist Balance Duel
Twist walkthrough Balance walkthrough Duel walkthrough

The settings

Each game has its own settings, saved to NVS. There are five in total, and each game uses the ones that make sense for it:

Setting What it controls Range Default
ROUNDS how many presentations before the game ends 3 to 20 5
WINDOW how long you have to react before it counts as a miss 0.1 to 10 s (±0.1) 2.5 s
INTERVAL the wait between presentations 0.1 to 10 s (±0.1) 2.0 s
MODE how the interval evolves through the game (see below) FIXED/RAND/DECR RAND
THRESHOLD the tilt that counts as tipping over, in Balance 3° to 30°
BEST OF rounds needed to win a Duel — odd numbers only, first to the majority 3 to 9 3

MODE is what changes the character of a game the most:

  • FIXED — the wait is always the same. You can settle into the rhythm and anticipate.
  • RAND — drawn between 0.1 s and the value you set, so there is nothing to anticipate. This is what measures reaction proper, rather than a sense of timing.
  • DECR — starts at the value you set and shortens by 0.15 s per round, down to a floor of 0.1 s. The game tightens on its own for as long as you last.
Game Settings Defaults
Reaction rounds · window · mode · interval 5 · 2.5 s · RAND · 2.0 s
Survival window · mode · interval 2.5 s · DECR · 2.0 s
Sequence window · mode · interval 3.0 s · FIXED · 0.6 s
Twist rounds · window · mode · interval 5 · 2.5 s · RAND · 2.0 s
Balance threshold · mode 7° · FIXED
Duel best of · window · mode · interval 3 · 2.5 s · RAND · 2.0 s

Three games read the settings their own way. Survival has no rounds — it is sudden death, and lasts as long as you do. In Sequence, the window is the time allowed per touch and the interval is the tempo the sequence is played back at, not a wait. And Balance has no RAND: a tilt limit tightening at random would be a lottery rather than a game — under DECR the threshold closes by 1° every 5 seconds, down to 3°.

The defaults are not uniform by accident either. Survival ships on DECR because accelerating is what it is, and Sequence ships on FIXED because what you memorise is a steady tempo — randomising it would make memory compete with surprise.

You configure one game at a time, each with its own fields:

Reaction Sequence Duel
Reaction settings Sequence settings Duel settings

Balance is the odd one out — instead of reacting to a flash, you hold the unit level and the screen tells you how close to tipping you are. Green is safe, amber is a warning, and the beeps get more insistent as you approach the threshold.


Pairing the two units

Flash the same firmware to both. On first entry into REFLEX each unit asks what it is:

  • HOST — runs the game logic, timing and scoring. It is also a pad.
  • PAD — lights up when told, detects hits and reports back.

The role is your choice and is remembered in NVS; tap the title bar to change it. The pairing itself is automatic: while unpaired, each unit broadcasts its role over ESP-NOW and adopts the first unit with the complementary role. No MAC addresses in the source, no per-unit builds.

If both units end up with the same role they will not pair, and both screens say so instead of waiting forever. A third unit in the room stays inert rather than interfering — this is a two-unit design.

Choosing a role Paired and waiting Both set the same
Role selector Pad waiting Role conflict

Feedback

These are the three channels from the introduction: the sound, the colour of the screen and the light of the LEDs. The state machine emits events (EnteredWork, FinalCountdown, EnteredDone, …) consumed in parallel by three independent layers, so changing one never touches the logic of the modes.

Sound — high tone entering work, low tone entering rest, triple beep over the last 3 seconds, a short melody on completion.

Screen — phase name and background colour, big phase seconds, rounds remaining, total-workout progress bar. Rendered to a single PSRAM canvas and pushed in one go, so transitions are atomic and flicker-free.

The top bar follows one rule everywhere in the app: back arrow left, actions right, and in the middle the name of what you picked while nothing is moving, live information while it counts. So 10/20 on the idle screen becomes the workout's remaining total once you start, and TIMER gives way to nothing at all. A name is something you chose seconds ago; the time is the thing that keeps changing, and it earns the space.

LEDs — colour says what (green = working, blue = rest, orange = long rest, yellow = about to start, red = last 3 seconds of work), and the number lit says how much is left. The bar empties like an hourglass rather than filling. Two axes of information at a glance, which is what makes hands-busy use work.

In the last 3 seconds the colour changes and stays — it does not blink. With only 3 seconds to read, blinking is noise.

Working Resting Three seconds left
Pull Rest Last three seconds

Settings

Brightness · sound on/off + volume · LEDs on/off + intensity · screen sleep (on/off and inactivity timeout) · theme (vivid / digital / earthy) · language (PT/EN). All persisted in NVS. The screen sleeps on idle screens only — never mid-workout — and wakes on a hand waved past the proximity sensor.

The theme changes the whole screen — phase backgrounds and every accent in the app. The LEDs always keep the vivid scheme: desaturating them would flatten the phase distinction and the red warning, which is exactly the channel you rely on when you are not looking at the screen.

The reflex games keep their own fixed colours for the same reason, only stronger: there the colour is the rule (green = touch, anything else = do not), and telling the two apart is a reaction measured in milliseconds. Making that discrimination depend on an appearance setting would change the difficulty of the game. Only the chrome, the countdown and the result screens follow the theme.

The settings screen itself, in all three, and the menu cycling through them:

Vivid Digital Earthy Cycling
Settings in vivid Settings in digital Settings in earthy Theme rotation

And the screen where it matters most — the last 3 seconds of work, where the background is the warning:

Vivid Digital Earthy
Vivid Digital Earthy

And every colour of the three palettes, side by side:

Colour themes


Build and flash

Built with PlatformIO (VS Code), Arduino framework, board m5stack-cores3. Dependencies (platformio.ini) are installed on first build: M5Unified, FastLED, Preferences.

Development was assisted by Claude Code, inside VS Code.

From scratch

  1. Install VS Code and the PlatformIO IDE extension — that is what brings the pio command.
  2. Clone the repository and open the folder in VS Code.
  3. Connect the Core S3 over USB-C.
  4. pio run -t upload.

The first build downloads the ESP32 toolchain and the libraries and takes a few minutes; later ones are quick. Nothing has to be installed by hand — platformio.ini handles the libraries. The Core S3 uses the ESP32-S3's native USB, so it shows up as a serial port with no extra driver; if it does not, pio device list shows what the system can see.

Action Command
Build pio run
Build and flash pio run -t upload
Flash and open serial monitor pio run -t upload -t monitor
List USB ports pio device list
Pick the port explicitly pio run -t upload --upload-port /dev/cu.usbmodem…

These all act on m5stack-cores3, the production firmware, because platformio.ini sets it as default_envs. Every other environment is a developer build and needs an explicit -e — see below.

Flash both units with the same command — roles are chosen on the device, not at build time.

Notes for reproducing the build:

  • The base LED strip is on GPIO5, 10 LEDs, GRB order. The base also needs 5 V on the bus: M5.Power.setExtOutput(true) (done in LedFeedback::begin()).
  • The proximity sensor (LTR-553ALS) sits at address 0x23 on M5.In_I2C. If it does not answer, the firmware degrades gracefully — touch still wakes the screen.
  • Flashing replaces the factory UiFlow2 firmware. It is reversible: reinstall UiFlow2 with M5Burner.

Developer builds

Isolated bring-up environments used while developing, each behind a build flag. They build the same tree with a different main, so they need an explicit -e:

Environment What it is for Flash with
m5stack-cores3 the real firmware (default) pio run -t upload
shot USB screen capture for the docs pio run -e shot -t upload
reflex-r1 ESP-NOW transport — PING/PONG + latency, both units pio run -e reflex-r1 -t upload
reflex-r2 hit detection over the proximity sensor, one unit pio run -e reflex-r2 -t upload
reflex-r3 bare reaction game, both units pio run -e reflex-r3 -t upload
reflex-r7-imu wrist-rotation tuning (BMI270), one unit pio run -e reflex-r7-imu -t upload
reflex-r8-balance tilt tuning for Balance (BMI270), one unit pio run -e reflex-r8-balance -t upload

Add -t monitor to any of them to open the serial monitor straight after flashing. shot runs at 921600 baud, the rest at 115200.

The environments do not coexist on the device. Each flash replaces the previous one entirely, so the list below is not a chain of installs — it is the complete set of commands, one at a time, depending on what you want on the unit at that moment. To get back to the normal firmware, flash the first one:

pio run -e m5stack-cores3    -t upload   # production firmware (same as `pio run -t upload`)
pio run -e shot              -t upload   # USB screen capture
pio run -e reflex-r1         -t upload   # ESP-NOW: PING/PONG + latency  (both units)
pio run -e reflex-r2         -t upload   # hit detection over proximity  (one unit)
pio run -e reflex-r3         -t upload   # bare reaction game            (both units)
pio run -e reflex-r7-imu     -t upload   # wrist rotation, BMI270        (one unit)
pio run -e reflex-r8-balance -t upload   # Balance tilt, BMI270          (one unit)

Because default_envs is set, a plain pio run only compiles the production firmware. To check that a change has not broken the bring-up builds, name them all:

pio run -e m5stack-cores3 -e shot -e reflex-r1 -e reflex-r2 -e reflex-r3 \
        -e reflex-r7-imu -e reflex-r8-balance

Documentation tools

Both run from the repo root and need pyserial + pillow. macOS system Python has neither, so activate whatever environment you installed them into first — otherwise they stop and tell you which interpreter is missing what.

Capturing screenshots with env:shot — see tools/screenshot.py:

pio run -e shot -t upload
pip install pyserial pillow
python3 tools/screenshot.py            # Enter captures whatever is on screen

Assembling the README's animated GIFs from those captures — see tools/gifs.py. No ffmpeg involved; the frames are real screens put in a row, and the sequences live at the top of the file:

python3 tools/gifs.py                  # all of them
python3 tools/gifs.py 1020 timer       # just these

Code layout

src/
  main.cpp          setup/loop — non-blocking loop (millis() only, never delay())
  Modes.*           orchestration: screens (menu/workout/settings), touch, events
  StateMachine.*    cycle modes: PREP → WORK → REST → SET_REST → DONE + events
  TimerMode.*       countdown engine; emits the same events
  StopwatchMode.*   stopwatch engine: counts up + laps
  AudioFeedback.*   beeps per event (M5.Speaker, non-blocking)
  ScreenUI.*        all rendering (M5Unified) + settings screen
  Textos.h          PT/EN strings, chosen inline where they are drawn
  Settings.*        global settings (brightness, sound, LEDs, sleep, theme, language) + NVS
  LedFeedback.*     colour per phase + emptying bar (FastLED)
  Proximity.*       LTR-553 proximity sensor — wakes the screen, feeds hit detection
  Config.*          WorkoutConfig (modes 1/2) + Timer namespace (mode 3) + NVS

  # REFLEX mode (two units)
  ReflexMode.*      game engine (host: logic + scoring; pad: reactive)
  Link.*            ESP-NOW transport, role-based automatic pairing
  HitDetector.*     hit detection (baseline + edge + debounce) over Proximity
  MotionDetector.*  wrist-rotation detection over the gyroscope (Twist)
  BalanceDetector.* tilt detection over the accelerometer (Balance)
  ReflexConfig.*    per-game settings + unit role + NVS
  Screenshot.*      USB screen capture (env:shot only)

Never delay(). Arduino runs a single loop(), and it is that loop which reads the touch screen, redraws the display, steps the LEDs and feeds the sound. delay() is not waiting while doing something else — it is stopping. A rest written as delay(20000) would leave 20 seconds with no touch, no countdown on screen and no LEDs going out. So time here is read rather than waited on: the moment a phase started is stored, and every pass through the loop asks how much of it has gone by.


Licence

Copyright (C) 2026 Paulo Semblano.

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 or (at your option) any later version. See LICENSE for the full text.

It is distributed in the hope that it will be useful, but without any warranty — without even the implied warranty of merchantability or fitness for a particular purpose.

In practice: use it, study it, modify it and build on it freely, including commercially. If you distribute a modified version, it has to stay open under the same licence, keep the copyright notices, and say what you changed.

About

Hands-free training assistant on an M5Stack Core S3, guided by sound, colour and light. One unit for interval training, two for reaction games over ESP-NOW. No network, no app.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages