Repository navigation
Archive Improvement Plan
Snapshot of
docs/archive/improvement-plan.mdat commit707b4a0, preserved before the docs were compacted. History only: the current specs are in the repository.
This plan turns the findings in known-issues.md into work packages (WP). Each WP lists the issues it closes, a proposed design and a "done when" check. Effort: S ≈ < 1 day · M ≈ 1–3 days · L ≈ > 3 days (hobby-time estimates).
Progress: the block system rework (JSON programs) completed WP-S3, WP-S4, WP-S7, WP-T3 and WP-A5. It also completed most of WP-S1, WP-S2, WP-S5, WP-T2 and WP-A4. These are marked ✅ / ◐ below. See programs.md for the result.
Firmware 2.0 (ESP-NOW link) replaced the BLE pod link. It completes WP-S6 (pause + reconnect), WP-T5 (synchronous start), WP-F1 (pairing) and the protocol part of WP-A2. WP-T4 (BLE connection parameters) and WP-A1 (NimBLE) are obsolete. Details: link-protocol.md.
The order matters. Stabilise first (P0). Then make timing measurable and correct (P1). Then restructure (P2), and only then add features (P3). Quality work (Q) runs alongside.
◐ Done: connection/characteristic checks, only valid pods are counted. Open: freeing failed clients (M16).
-
Peripheralgets abool init()(or factory returningnullptr). Return false ifconnect(),getService()or any characteristic lookup fails, or ifcanWrite()/canNotify()are missing. -
CentralMode::clientScanForPeripheralsincrementsconnectedPeripheralsonly on success. On failure it deletes the client (pClient->disconnect(),delete). - Skip devices whose address is already in
peripherals[]. - Done when: switching a pod off during connection does not crash the central. A second central in config mode is ignored.
◐ Done: strtoll, link validation at load, signed If, randomOther, avoid clamp. Still open: the phone MaxLoop value is only read during CONFIRM.
- Replace
std::stollwithstrtoll+endptrcheck. Ignore invalid values. -
BlockManager: resolve allnextnames to indices once after the program is built (validate()). Unknown names → log + error pattern, never index 255.BlockFactoryreturningnullptr→ reject the block. -
IfBlock: useint64_t. -
SetVariable '!': ifchangeSize <= 1fall back to'?'. -
RemoteMultiplePress: clampavoidCounttopods. Replace the VLA with a fixed array or a bitmask. - Done when: a program with a typo produces a clear serial error instead of a crash.
- Initialise all members in class definitions (
uint8_t selectedModel = 0;…). -
RemoteMultiplePress: use a signed counter orif (remaining > 0) remaining--.
- Replace
std::queue<Peripheral*>with a FreeRTOS queue (xQueueCreate(16, sizeof(Event))). The notify callback sends withxQueueSendFromISR/xQueueSendand a timeout of 0. -
Event={ uint8_t podId; int32_t reactionMs; uint32_t rxTimeMs; }, so the value from H1 is carried along. - Done when: RUN5 with 4 pods hammered simultaneously runs for 30 min without corruption.
◐ Done:
Input/Button(immediate edge + 20 ms lockout, press counters). Game blocks only count new presses; the boot hold is ignored in the menu. Open: interrupt timestamps, use in menu and on peripherals.
- New
Buttondriver, shared by all roles:- GPIO interrupt on both edges. Timestamp with
esp_timer_get_time()(µs). - 15–20 ms debounce lockout after each edge.
- Events
PRESS(t),RELEASE(t, heldMs), plus a state queryisDown().
- GPIO interrupt on both edges. Timestamp with
-
ButtonBlockconsumes events instead of sampling. A release always produces a result:< shortLimit→ SHORT, otherwise LONG. No dead zone. - Arm-on-release rule: a block that waits for a press ignores a press that began before the block was entered. This fixes the confirm press and the boot-hold leaks.
- Model selector: draw program #0 immediately. Ignore the first release after boot.
- Done when: on hardware, SOLO and star mode start without a phantom press. A 1.5 s press on the central is accepted.
- Peripheral: register
BLEServerCallbacks. On disconnect → stateWAITING_FOR_CONNECTION, clear characteristics,startAdvertising(). - Central: on disconnect → new state
PAUSED(pods amber, missing pod's LED pair blinks). Rescan for the same address in the background. Resume the current round when it is back. A long press aborts to the menu. - Supervision timeout: set explicitly (e.g. 4 s) so a pod that has gone away is detected quickly.
- Done when: power-cycling one peripheral mid-game pauses the game, and the game continues after the pod is back.
-
DelayBlock::enterBlockdraws the random duration and storesuntil = now + d.executeBlockreturns "waiting" untilnow >= until. -
BlockManager::defineStartBlockcallsenterBlock()on the start block. Self-transitions are allowed to re-enter via an explicitrestartflag.
- Compile-time flag
REACTPOD_TRACE. Logesp_timer_get_time()for: block enter/exit, write sent/confirmed, notify received, button edges, LED show. Output as CSV over serial. - Log the actual connection parameters from
ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT. - Use a GPIO toggle on a spare pin for scope/logic-analyser measurements of LED onset vs. button edge. This is the ground truth for accuracy.
- Deliverable: a measured version of the tables in timing.md.
◐ Done:
Resultswith RESULT/SUMMARY CSV lines,summaryblock. Open: display on the ring / phone.
- Carry
reactionMsthrough the event (WP-S4) into aResultLog(round, podId, outcome {HIT, MISS, AVOID_HIT, FALSE_START}, ms). - Print a CSV line per round. Show a summary at the end of
MAX_LOOPS, for example an LED bar graph of the average time, or best/average via the phone characteristic (WP-F3).
-
ButtonBlockgets an optionalmeasureflag: it takest0when the LED is shown andt1from the button press edge timestamp (WP-S5). The block advances on press. - SOLO mode shows the result (e.g. number of lit LEDs ∝ time, or a colour band).
- After connecting each peripheral, call
esp_ble_gap_update_conn_paramswith min/max interval 7.5–15 ms (≤ 4 pods) or 15–30 ms (5–7 pods), latency 0, timeout 4 s. - Fix
setMinPreferred/setMaxPreferredin advertising (L3). - Done when: WP-T1 shows a stable CI and activation latency ≤ 2 CI.
- Use write-without-response without waiting for the confirmation (or NimBLE, WP-A1), so that all ARM commands go out within one or two connection events.
- Add a start delay to the ARM command: central timestamps each send
t_iand sendsdelay_i = D - (t_i - t_0)with D ≈ 50–100 ms. All pods then light at ≈t_0 + D. The pods measure from their own LED onset, so fairness only depends on the residual skew (~1 CI). - Optional later: clock sync (ping/pong, NTP-style offset per pod) for sub-ms alignment.
- The loop task blocks on the event queue with a timeout = "time until the next block
deadline" (
xQueueReceive). No more busy loop. -
LEDManagerkeeps a frame buffer and only callsshow()when the content changed. - Logging through a macro with compile-time level. Release builds use WARN and no
Serial.printin hot paths.
- NimBLE uses much less RAM than Bluedroid. It allows up to 9 connections and has
non-blocking writes and better callbacks. It may let the project drop the
arduino, espidfdual framework, which would mean faster builds and no sdkconfig handling. - Spike: port
ServerMode+Peripheralto NimBLE on a branch. Compare RAM, connection time and latency with WP-T1. - Decide based on numbers. If adopted, do it before WP-A2, so the protocol is only implemented once.
Replace the 5 polled characteristics with two:
| Characteristic | Dir | Frame |
|---|---|---|
Command |
central → pod, write w/o response | [ver][op][seq][len][payload…] |
Event |
pod → central, notify | [ver][type][seq][payload…] |
- Commands:
CLAIM(setId),ARM(seq, mode, colorActive, colorAfter, startDelayMs, timeoutMs),RESET(seq),LED(pattern),IDENTIFY,PING. - Events:
PRESSED(seq, reactionUs, flags),TIMEOUT(seq),STATUS(battery, fwVersion),PONG. - The peripheral handles commands in the
onWritecallback → queue → loop. No lost or reordered writes. -
seqlets the central drop stale or duplicate events.verallows mixed firmware to be detected. - Binary little-endian payloads. Reaction time in µs.
- New random 128-bit UUID base.
◐ Done in firmware 2.0:
RoleManager/CentralRole/PeripheralRole,IPodNetworkas the link abstraction. Open:IClock/IButton/ILedsfor host tests.
- Interfaces
IClock,IButton,ILeds,IRemotePods. The block engine and the game logic only depend on these. - Rename for clarity:
ServerMode→PeripheralRole,CentralMode→CentralRole, "model" → "program". -
ColorSetconstants becomeconstexprvalues (L2). - This is the base for host-side unit tests (WP-Q1).
◐ Done: ports,
BlockStatus, index links, instant chaining, timeouts on button/remote blocks, interned variables. Open: switching programs without reboot (engine supports it, CentralRole does not offer it).
- Links resolved to indices at load time (from WP-S2). A program is an immutable
Programobject (blocks + variables), so switching programs does not need a reboot. - Blocks return
Status { RUNNING, DONE(outputPort) }instead of each config overridinggetNextBlockName. Next block =ports[outputPort]. This removes theparentBlockback-pointers. - Run consecutive instant blocks in one pass (cap e.g. 32 steps, to catch accidental infinite loops).
- Generic timeouts on every waiting block (
timeoutMs→timeoutport). - Variables: interned IDs instead of
std::map<std::string>lookups on every pass.
- JSON schema v1 (
{"version":1,"name":…,"pods":3,"vars":{…},"start":…,"blocks":[…]}) that maps 1:1 to the WP-A4 block types. - Rewrite the loader (owned strings, all block types, validation errors with block name).
- Built-in programs move to JSON files in LittleFS (SPIFFS is deprecated), built from
data/. - The menu lists the programs found in the file system. Program count and menu LED pattern are taken from the file.
| WP | Feature | Effort | Notes |
|---|---|---|---|
| F1 ✅ | Set pairing | M | Central creates a random setId once (NVS). Peripherals store their owner on first CLAIM. Advertising carries manufacturer data {role, setId, battery}, so the central filters without connecting. Pair mode = hold button 5 s at boot on a peripheral. Closes H9, M6 |
| F2 | Battery monitoring | S |
analogReadMilliVolts(PIN_BATTERY_LEVEL), divider ratio from schematic, averaging. Status event + adv data. Low battery → amber blink, block game start below threshold |
| F3 | Phone interface | M | Extend the config service: program select, MAX_LOOPS, brightness, results notify (CSV/JSON per round), pod list with battery. Usable with a generic BLE app first, later a small web-bluetooth page |
| F4 | Power management | M | Brightness cap (e.g. 40 %), LED off when idle, BT modem sleep + auto light sleep on idle peripherals, deep sleep after N min idle with GPIO 3 wake-up (RTC-capable on C3) |
| F5 | New game modes | M each | COUNTER (hit as often as possible in T s), COUNTDOWN/timeout (miss if not hit in T), sequence/memory, two-player split, "go/no-go" with distractor colours. Built on WP-A4/A5 |
| F6 | Result display on the ring | S | End-of-cycle summary: average/best as LED bar, colour per performance band |
| F7 | OTA update | L | New partition table with ota_0/ota_1 (needs verified 4 MB flash, L5). Central via phone. Peripherals via central relay or one at a time. Rollback with esp_ota_mark_app_valid
|
| F8 | Identify pod | S |
IDENTIFY command blinks a pod so the user knows which physical pod is #n |
| F9 | Charging-dock awareness | S | Detect USB/charge state (if the HW exposes it) and show a charge animation instead of game states |
| WP | Item | Effort |
|---|---|---|
| L1 | Measure link latency, loss and start skew on hardware (timing.md §6) and replace the estimates | S |
| L2 |
Pre-arming: an ARM with trigger "after PRESSED of pod X" plus a pod-to-pod PRESSED broadcast saves one hop for remote → remote transitions. Needs program look-ahead (choose the next pod before the current press) |
M |
| L3 | ESP-NOW channel selectable via the phone, stored in NVS (M20) | S |
| L4 | Battery level in PING frames, shown on the central (with WP-F2) | S |
| L5 | Revert to the plain Arduino framework if the BLE connection limits in sdkconfig are no longer needed (faster builds) |
S |
| L6 | Optional ESP-NOW encryption (PMK/LMK per set) (M19) | M |
| L7 ✅ | BLE config API for program upload/download + program store sync between pods (config-api.md) | done |
| L8 | Config API: PIN protection, setMenu convenience op, upload of a whole program set as one transaction |
S–M |
| WP | Item | Effort |
|---|---|---|
| Q1 | PlatformIO native env with Unity tests for the block engine, variable manager, button state machine and protocol encode/decode (needs WP-A3) |
M |
| Q2 | GitHub Actions: pio run -e NewBoard + pio test -e native on every push |
S |
| Q3 |
pio check (cppcheck/clang-tidy) and a .clang-format
|
S |
| Q4 | Remove dead code (L4). Tidy .vscode and sdkconfig backups out of git |
S |
| Q5 | Hardware test checklist (connect 7 pods, disconnect/reconnect, 30 min soak per mode, battery run-down) run before each release | S |
| Q6 | Version string (FW_VERSION) printed at boot and reported in STATUS events |
S |
flowchart LR
S1[S1 safe connect] --> S6[S6 reconnect]
S4[S4 event queue] --> T2[T2 use results]
S5[S5 button driver] --> T3[T3 central timing]
T1[T1 instrumentation] --> T4[T4 conn params]
T1 --> A1[A1 NimBLE spike]
A1 --> A2[A2 protocol v2]
T4 --> T5[T5 simultaneous start]
A2 --> T5
A2 --> F1[F1 pairing]
A3[A3 HAL] --> Q1[Q1 unit tests]
A3 --> A4[A4 engine v2] --> A5[A5 programs as data] --> F5[F5 new modes]
S7[S7 non-blocking delay] --> T6[T6 event loop] --> F4[F4 power]
T2 --> F3[F3 phone UI]
T2 --> F6[F6 ring display]
For users and coaches
Repository and workflow
Firmware
- Firmware Overview
- Firmware Architecture
- Firmware Link Protocol
- Firmware Config API
- Firmware Programs
- Firmware Testing
- Firmware Timing
- Firmware Host Syntax Check
Config App
Community platform (on hold)
Decisions (ADRs)
- Decisions
- ADR 0001 Repo Layout and Workflow
- ADR 0002 Versions and Firmware Delivery
- ADR 0003 Community Platform
- ADR 0004 Platform Architecture
- ADR 0005 Live Results over BLE
- ADR 0006 No OTA
- ADR 0007 Platform on Hold
Archive