Repository navigation
Firmware Programs
Snapshot of
docs/firmware/programs.mdat commit707b4a0, preserved before the docs were compacted. History only: the current specs are in the repository.
Game programs are JSON files on the central's file system (SPIFFS). They are not compiled
into the firmware. To add or change a game, you edit files in firmware/data/, check them, and
upload the file system image:
python tools/validate_programs.py # checks types, fields, links, colours, path lengths
pio run -d firmware -e NewBoard -t uploadfs # writes firmware/data/ to the SPIFFS partition (USB)
python tools/reactpod_config.py push firmware/data # or: upload over BLE to a central in the menuPrograms uploaded over BLE spread to all pods of the set automatically. See config-api.md.
Flashing the firmware (
-t upload) does not updatefirmware/data/. After a fresh flash runuploadfsonce, otherwise the central shows the red error blink ("No programs found").
firmware/data/
├── programs.json menu: list and order of programs
└── prog/
├── solo.json complete program, all blocks inline
├── weiche.json small programs: metadata + includes
├── …
└── lib/
├── loop.json shared cycle: round counter, cycle timer, summary, report
├── star.json star game logic
├── random.json random game logic
└── multi.json run / avoid game logic
SPIFFS paths (including the leading /) must be at most 31 characters long.
{ "programs": ["/prog/solo.json", "/prog/weiche.json", "..."] }The order in this list is the order in the menu. Short press = next entry, long press = start.
{
"version": 1,
"name": "Dreieck",
"description": "free text, ignored by the firmware",
"peripherals": 2,
"menu": [ { "leds": [7, 9, 11], "color": "blue" } ],
"variables": { "MAX_LOOPS": 12 },
"include": ["/prog/lib/loop.json", "/prog/lib/random.json"],
"start": "StartCycle",
"blocks": [ ]
}| Field | Meaning |
|---|---|
peripherals |
Number of peripherals the central connects before the game. Sets the variables REQUIRED_PERIPHERALS (= peripherals) and NODE_COUNT (= peripherals + 1) |
menu |
LEDs lit in the program menu. Each group has leds (indices 0–17) and color
|
variables |
Initial values (integers). Override values from included files |
include |
Fragment files with more variables and blocks. Loaded first, in order. Max nesting depth 3 |
start |
Name of the first block. Default: the first block |
blocks |
The block graph (see §2) |
Block names must be unique across the program and all its includes. A fragment can link
to blocks it does not define itself. For example, lib/star.json ends in EndLoop, which
comes from lib/loop.json.
Peripherals have ids 0 … peripherals-1, in connection order. The central itself has id
peripherals (= REQUIRED_PERIPHERALS). Results use the same ids.
Every block has name, type and its output ports. A port is a JSON key whose
value is the name of the next block:
{ "name": "Wait", "type": "button", "pressed": "FalseStart", "timeout": "Go" }Rules:
- If a block leaves through a port that is not connected, the engine uses
nextinstead, if the block has anextport. - If there is no target at all, the program ends. The validator prints a note for such blocks.
- Blocks that finish immediately (LED, setVariable, if, …) run in the same loop pass as the following block. Waiting blocks never block the main loop.
Values: numeric parameters marked value accept a number (12) or a variable
name ("MAX_LOOPS"). Variables are read when the block runs.
Colours: "red", "#FF8800" or [255, 136, 0]. Named colours: off/black, red, green,
blue, yellow, magenta, cyan, white. Colours sent to peripherals must be named
colours, because the BLE protocol only transports these 8.
programFormat ≥ 2 (§6). Any block, of any type, may also carry:
| Field | Default | |
|---|---|---|
timerStart |
timer name. Starts the timer when the block is entered | – |
timerStop |
timer name. When the block is left, records now − start (ms) as the metric <name> with the central's pod id, then clears the timer |
– |
{ "name": "Go", "type": "remotePress", "pod": 0, "timerStart": "lap", "next": "Back" },
{ "name": "Finish", "type": "button", "timerStop": "lap", "next": "Report" }- A timer name is a non-empty string, like a metric name. A program (with its includes) may use at most 16 different timer names.
- The time is recorded exactly like
record:RESULTline, statistics,summary. - On a waiting block (
button,remotePress, …) the stop time is the moment the block is left, i.e. the press. For remote blocks that is when the central receives the press (a few ms after the press on the peripheral). - A block with both fields and the same name measures its own duration.
- Engine details (restart, orphan stops, pause): §7.9.
programFormat ≥ 3 (§6). delay, button, remotePress, remoteMultiPress, charge, release and toggle (≥ 4) may show how
much of their time limit is left on the LED ring:
| Field | Default | |
|---|---|---|
countdown |
secret, drain, fill, dim, colorShift or trafficLight
|
secret (no animation, as before) |
countdownColor |
named colour, end colour of colorShift
|
red |
countdownTarget |
delay only: "central", "all", "peripherals" or a pod id (value) |
"central" |
countdownFrom |
delay and button only: named start colour (these blocks have no lit colour of their own) |
green |
{ "name": "Hit", "type": "remotePress", "pod": "P", "timeoutMs": 1500, "countdown": "drain", "next": "Ok", "timeout": "Miss" },
{ "name": "Ready", "type": "delay", "minMs": 2000, "maxMs": 4000, "countdown": "trafficLight", "countdownTarget": "all", "next": "Go" }- The countdown runs over the block's time limit: the duration of a
delay(ms, or the random duration), the timeout of the others (timeoutMs, or the random timeout ofbutton). It starts with the lit colour of the block (colorofremotePress,hitColor/avoidColorofremoteMultiPress,countdownFromofdelayandbutton). - Styles:
drain= the ring is full and empties LED by LED (empty at the end);fill= it starts empty and fills up;dim= full brightness fades to dark;colorShift= the lit colour blends intocountdownColor;trafficLight= green, yellow, red, one third of the time each (the lit colour is not used). - A
button,remotePressorremoteMultiPresswithout a time limit and withcountdown≠secretis rejected (validator and firmware). - On peripherals the countdown runs on the pod itself (ARM carries the effect), so it ends
with the pod's timeout. On
remoteMultiPressall lit pods and the central count down together. - With a countdown
timeoutMsis read once when the block is entered (§7.2). - A press stops the countdown.
colorAfter, press handling and reaction times are unchanged (reaction times count from the first lighting). - Pods with older firmware (no
effects) count downsecret: they stay lit with the lit colour. -
charge: the countdown runs overtimeoutMson the idle ring and ends with the first hold.release: overmaxHoldMsfrom the start of the hold (trigger), or overtimeoutMsfrom the colour switch (signal);countdownFromis not used. Details: §7.13, §7.14. -
toggle: the countdown runs over the whole game on every pod and the central, in the colour of the pod's current team (a flip changes the colour, the animation goes on). Details: §7.15. - Details: §7.12.
programFormat ≥ 3 (§6). Plays an animation on the ring of the central and / or the peripherals.
| Field | Default | |
|---|---|---|
target |
"central", "all" (central + all peripherals), "peripherals" or a pod id (value; the central's own id = the central) |
"central" |
effect |
solid, fill, drain, dim, colorShift, trafficLight, blink, pulse, spin, off
|
required |
color, color2
|
named colours | white, off |
durationMs |
value, length of the effect. For blink, pulse, spin: 0 / missing = endless (until the next LED change) |
– |
periodMs |
value, one cycle of blink, pulse, spin
|
500 |
brightness |
value 0 … 255 | 255 |
leds |
LED indices, central only (ignored on pods) | all 18 |
syncStartMs |
value: all targets start together this long after the block starts | 20 |
wait |
false: start the effect and continue at once. true: continue when the effect has ended |
false |
Ports: next
{ "name": "Ready", "type": "effect", "target": "all", "effect": "pulse", "color": "blue",
"periodMs": 500, "durationMs": 2000, "wait": true, "next": "Go" }effect |
Shows |
|---|---|
solid |
the ring in color
|
fill |
LEDs light one by one in color (background color2) over durationMs, ends with a full ring |
drain |
full ring in color that empties LED by LED over durationMs, ends with the background color2
|
dim |
color fades from brightness to dark over durationMs
|
colorShift |
color blends into color2 over durationMs
|
trafficLight |
green, yellow, red, one third of durationMs each (colours ignored) |
blink |
color and color2 alternate, one cycle per periodMs
|
pulse |
color fades down to dark and up again, one cycle per periodMs
|
spin |
a segment of 3 LEDs in color turns once per periodMs on color2
|
off |
the ring goes dark |
-
fill,drain,dim,colorShiftandtrafficLightare timed: withoutdurationMsthey jump to their end frame.blink,pulseandspinwith adurationMsend in a steadycolor. -
wait: truewith an endless effect (blink,pulse,spinwithout a constantdurationMs> 0) is an error. - A later
led,remotePress,remoteResetAll, … on the same ring replaces the effect. Withwait: falsethe effect keeps running while the next blocks execute. - Pods without effect support (older firmware) show
colorstatically (offswitches them off). - Details: §7.11.
| Field | Default | |
|---|---|---|
color |
colour | required |
leds |
list of LED indices | all 18 |
Ports: next
| Field | Default | |
|---|---|---|
ms |
value | – |
minMs, maxMs
|
value, random duration in [min, max] | – |
countdown, countdownColor, countdownTarget, countdownFrom
|
optional ring countdown over the duration (see Countdown) | secret |
Ports: next
| Field | Default | |
|---|---|---|
shortMs |
value, short/long threshold | 1000 |
timeoutMs |
value | no timeout |
minTimeoutMs, maxTimeoutMs
|
value, random timeout | – |
record |
metric name: records press time − block entry | – |
storeAs |
variable for the same time | – |
countdown, countdownColor, countdownFrom
|
optional ring countdown over the timeout, needs a timeout (see Countdown) | secret |
Ports: pressed (press edge), short, long (on release), next (fallback for
short/long), timeout.
Only presses that start after the block was entered count. A button that is still held from the previous step is ignored.
| Field | Default | |
|---|---|---|
left, right
|
value | required |
op |
== != > < >= <=
|
== |
Ports: true, false (no fallback)
| Field | Default | |
|---|---|---|
var |
variable name | required |
op |
=, +, -, random (0 … value−1), randomOther (like random but ≠ current value) |
= |
value |
value | required |
Ports: next
| Field | Default | |
|---|---|---|
pod |
value, peripheral id | required |
mode |
hit / avoid
|
hit |
color / colorAfter
|
named colour while waiting / after press | green (red for avoid) / off |
timeoutMs |
value | no timeout |
countdown, countdownColor
|
optional countdown on the pod, needs timeoutMs (see Countdown) |
secret |
record, storeAs
|
reaction time measured on the peripheral | – |
Ports: next (pressed), timeout (also taken if the pod does not exist).
Only the press that belongs to this activation counts. Presses of earlier activations
are ignored.
| Field | Default | |
|---|---|---|
pods |
value, number of peripherals | all connected |
avoid |
value, number of red "do not press" pods (central included in the draw) | 0 |
includeCentral |
central takes part | true |
hitColor, avoidColor
|
named colours | green, red |
syncStartMs |
value: all pods (and the central) light together this long after the block starts. 0 = each pod as soon as its command arrives | 20 |
timeoutMs |
value | no timeout |
countdown, countdownColor
|
optional countdown on all lit pods and the central, needs timeoutMs (see Countdown) |
secret |
record |
metric for every green pod's reaction time | – |
Ports: next (all green pods pressed), avoidHit, timeout. Pods that were not pressed
stay lit, so follow with remoteResetAll.
programFormat ≥ 3 (§6). The ring shows how long the button has been held. Needs the hold link feature on
peripherals (firmware ≥ 2.9, link-protocol.md §2.2).
| Field | Default | |
|---|---|---|
pod |
"central" or a peripheral id (value). The central's own id also means the central |
"central" |
onsetMs |
value. Minimum hold before charging starts | 0 |
maxMs |
value, ≥ 1. Hold time (after the onset) for a full charge | required |
style |
fill, brightness, trafficLight, colorShift
|
fill |
color |
named colour while charging | green |
color2 |
colorShift only: end colour |
red |
mode |
release: releasing ends the block. accumulate: releasing pauses (or drains, see decayMs) the charge, the block ends at full / timeout
|
release |
decayMs |
value, accumulate only: time to drain a full charge while released. 0 = keep the charge |
0 |
timeoutMs, or minTimeoutMs + maxTimeoutMs
|
value, from the entry of the block (random like button). 0 / missing = none. countdown applies |
none |
countdown, countdownColor
|
optional countdown over the timeout on the idle ring (see Countdown); needs a timeout | secret |
storeAs |
variable: charge in ms (0 … maxMs) |
– |
storePercent |
variable: charge 0 … 100 | – |
record |
metric for the charge in ms | – |
Ports: released (released after the onset), early (released before the onset; falls back to released
with charge 0), full (maximum reached: the block ends at once, the ring flashes), timeout, next (fallback
for the others).
{ "name": "Charge", "type": "charge", "pod": "central", "onsetMs": 200, "maxMs": 2000, "style": "fill", "color": "green",
"storeAs": "CHARGE", "record": "charge", "released": "Score", "full": "Overcharged", "timeout": "Missed" }- As with
button, a hold that started before the block was entered does not count: the button must be released and pressed again. - Ring:
filllights the LEDs one by one,brightnessfades from dim to full,trafficLightgoes green, yellow, red,colorShiftblendscolorintocolor2. Before the first hold the ring shows the start frame (fill: dark,brightness: dim,trafficLight: green,colorShift:color) or the countdown. - The hold is measured and shown on the pod itself (it reports PRESSED when a hold starts and RELEASED when it ends), so the charge is exact to about 1 ms and the ring is smooth.
- A pod that does not exist, or has older firmware (no
holdlink feature), makes the block taketimeoutat once. - Details: §7.13.
programFormat ≥ 3 (§6). Two modes. Needs the hold link feature on peripherals.
-
trigger: the athlete holds the pod, letting go is the event, e.g. the start of a run. -
signal: the athlete holds, the colour switches after a fixed or random time, and the athlete must let go as fast as possible (start reaction with false-start detection).
| Field | Mode | Default | |
|---|---|---|---|
mode |
both |
trigger or signal
|
trigger |
pod |
both |
"central" or a peripheral id (value) |
"central" |
holdColor |
both | named colour while waiting for the hold | yellow |
heldColor |
both | named colour once the hold is detected ("you're set") | = holdColor
|
allowHeld |
both | a hold that is already active on entry counts (it starts on entry) |
true for trigger, false for signal
|
holdTimeoutMs |
both | value. No hold within this time from the entry → noHold. 0 / missing = none |
none |
maxHoldMs |
trigger | value. Held longer than this (from the start of the hold) → timeout. 0 / missing = none. countdown applies |
none |
storeAs |
trigger | variable: hold duration in ms | – |
goColor |
signal | named colour of the switch | green |
goMs, or minGoMs + maxGoMs
|
signal | value. Time from the start of the hold to the switch; random in [min, max] (inclusive), drawn once on entry. Required | – |
timeoutMs |
signal | value. Not released this long after the switch → timeout. 0 / missing = none. countdown applies |
none |
record, storeAs
|
signal | reaction = release − switch, measured on the pod (ms) | – |
countdown, countdownColor
|
both | optional countdown (see Countdown): trigger over maxHoldMs from the start of the hold, signal over timeoutMs from the switch |
secret |
Ports: next (released; in signal released after the switch, reaction recorded), noHold, falseStart
(signal only: released before the switch), timeout (trigger: held too long, signal: released too late).
Unconnected noHold, falseStart and timeout fall back to next.
{ "name": "Go", "type": "release", "mode": "trigger", "pod": 0, "holdColor": "yellow",
"storeAs": "HOLD_MS", "next": "Run", "noHold": "Idle" },
{ "name": "Start", "type": "release", "mode": "signal", "pod": 0, "holdColor": "yellow", "goColor": "green",
"minGoMs": 1500, "maxGoMs": 3000, "timeoutMs": 1000, "record": "start",
"next": "Run", "falseStart": "Foul", "noHold": "Idle", "timeout": "Late" }-
triggerleaves the block at the release (the central's receive time of RELEASED, as for remote presses), so atimerStarton the next block starts at the release. - A pod that does not exist, or has older firmware, makes the block take
timeoutat once (nextif unconnected). - Details: §7.14.
programFormat ≥ 4 (§6). Team and capture games ("domination"): the pods stay lit and every press flips a pod between
two team colours. Needs the toggle link feature on peripherals (older ones are left out of the game).
| Field | Default | |
|---|---|---|
pods |
value, number of peripherals | all connected |
includeCentral |
the central's own button takes part | true |
colors |
the two team colours [A, B] (named) |
["red", "blue"] |
start |
a, b, alternate (every other pod, A first), random (each pod), off (unowned and dark until the first press, which makes the pod A) |
alternate |
durationMs, or minDurationMs + maxDurationMs
|
value. Game length; random in [min, max], drawn once on entry. Required | – |
cooldownMs |
value. Minimum time between two flips of one pod (0 … 65535) | 300 |
syncStartMs |
value. All pods and the central start this long after the block was entered (as remoteMultiPress) |
20 |
countdown, countdownColor
|
optional countdown over the game, see Countdown | secret |
storeA, storeB
|
variables: number of pods per team at the end | – |
storeTimeA, storeTimeB
|
variables: owned time per team at the end, summed over all pods (ms) | – |
storeFlips |
variable: total number of flips | – |
Ports: next (time over), allA, allB (every pod and the central has team A / B, allA / allB are optional: an
unconnected one does not fall back to next, the game goes on). After the block the pods stay lit in their final
colour and a press no longer changes it; follow with remoteResetAll.
{ "name": "Domination", "type": "toggle", "pods": "NODE_COUNT", "includeCentral": true,
"colors": ["red", "blue"], "start": "alternate", "durationMs": 60000, "cooldownMs": 500, "countdown": "secret",
"storeA": "RED_PODS", "storeB": "BLUE_PODS", "storeTimeA": "RED_MS", "storeTimeB": "BLUE_MS", "storeFlips": "FLIPS",
"next": "Result", "allA": "RedWins", "allB": "BlueWins" }- Details: §7.15.
| Field | Default | |
|---|---|---|
pods |
value | all connected |
ledsOff |
also switch the central off | true |
Ports: next
| Field | Default | |
|---|---|---|
metric |
metric name | required |
storeAverage, storeBest, storeWorst, storeCount
|
variable names | – |
reset |
clear the statistics afterwards | true |
show |
bar on the central's ring, see below. programFormat ≥ 6 (§6) |
false |
targetMs |
value, 1 … 60000: the target time of the bar (half full). Required with show
|
– |
showMs |
value, 0 … 60000: how long the bar stays. 0 = until the next light change, no wait | 3000 |
Ports: next (with show and showMs > 0: after the bar)
{ "name": "Summary", "type": "summary", "metric": "reaction", "storeAverage": "AVG_REACTION",
"show": true, "targetMs": "TARGET_MS", "showMs": 3000, "next": "Report" }With show, the block waits showMs while the bar is on the ring and then continues. The bar
is the average of the metric (§7.7). A pod with an older format rejects a program that uses
these fields (fieldFormatTooOld), it does not ignore them.
programFormat ≥ 2 (§6).
| Field | Default | |
|---|---|---|
count |
value, number of passes through body
|
required |
storeIndex |
variable that gets the current pass (1 … count) |
– |
Ports: body (one more pass), done (after count passes). Both fall back to next.
{ "name": "Rounds", "type": "repeat", "count": "MAX_LOOPS", "storeIndex": "RUNS", "body": "OneRound", "done": "Summary" }The last block of the body links back to the repeat block. Entering the block again after
done starts a new series of count passes. Details: §7.10.
programFormat ≥ 5 (§6). Central only.
| Field | Default | |
|---|---|---|
timeoutMs |
value; 0 = no timeout | no timeout |
metrics |
list of metric names included in the report (max 8); [] = all |
all |
vars |
list of variable names whose values are included in the report (max 8) | – |
editable |
object: variable name → { "min": int, "max": int }. Variables the client may change with setVariables while the block waits (max 8) |
– |
color |
colour of the central's LEDs while waiting | "cyan" |
Ports: next (client command continue), pressed (central's button), timeout,
repeat (client command repeat), stop (client command stop). Unconnected ports fall
back to next (§2), so a program without a phone runs on after a press or the timeout.
{ "name": "Report", "type": "communicate",
"timeoutMs": 3000, "metrics": ["reaction", "cycle"], "vars": ["RUNS", "MAX_LOOPS"],
"editable": { "MAX_LOOPS": { "min": 1, "max": 50 } },
"next": "StartCycle", "repeat": "ResetRuns", "stop": "End" }On enter the block turns the measurements since the last report into a report, keeps it
in the report buffer and sends it as a report event (config-api.md §5),
then waits for a client command, a press or the timeout. Details: §7.16.
-
varsand the names ineditablemust be variables of the program (variables, also from includes).min≤max. - Metric and variable names: non-empty, at most 32 characters.
Blocks with record add a measurement to a named metric, and so does every timerStop
(metric = the timer name, pod id = the central's). Every measurement is printed on
the serial console (115200 baud):
RESULT,<millis>,<metric>,<podId>,<value_ms>
SUMMARY,<metric>,<count>,<average>,<best>,<worst>
The shipped programs use the metric reaction (run / run5 also record lap, the time
from the lights to the last press). lib/loop.json times each cycle (cycle, from
StartCycle to the end of the last round), prints a summary after every MAX_LOOPS rounds,
stores AVG_REACTION / BEST_REACTION and shows the average as a bar on the ring for 3 s
(show, §7.7). A program can then branch on those, for example
to show a colour by performance. Then its Report block (communicate, §2) sends a report
with all metrics and RUNS / MAX_LOOPS and waits up to 3 s with a blue ring: a press or
continue starts the next cycle, repeat starts it again (the report is discarded), stop
ends the program (block End). Without a phone the game goes on after 3 s as before.
Programs that include lib/loop.json start at StartCycle; a reset of RUNS must link to
StartCycle, not StartLoop, so the cycle timer restarts.
To add a new kind of measurement (e.g. press duration or misses), a block calls
ctx.results.record("<metric>", podId, value). No other code needs to change.
- Create
src/Block/MyBlock/MyBlock.{h,cpp}deriving fromBlock:-
configure(json, ctx): read parameters (useValuefor numbers,JsonUtilfor colours). Returnctx.fail("…")on bad input. -
enter(ctx): reset state, take timestamps. -
execute(ctx): returnBlockStatus::running()while waiting,BlockStatus::done(port)when finished. -
portCount()/portName(i)if the block has more thannext. -
onProgramStart()if the block keeps state across entries (e.g. a loop counter).
-
- Add one line to the table in BlockFactory.cpp.
- Add the type to
BLOCK_TYPESin tools/validate_programs.py and document it here.
BlockContext gives access to variables, LEDs, the button, results and the pod network
(IPodNetwork: arm, reset, pollEvent, nowUs). Blocks should not use the
singletons directly.
During the program menu the central offers a BLE characteristic MaxLoop. A number
0 written there replaces
MAX_LOOPSof the program that is selected next. After the selection BLE is switched off, so the value cannot be changed during a game.
The config API reports the revision of the program format a firmware understands as
programFormat in info (config-api.md §5). It is increased whenever a
block type gets new fields, ports or semantics, so configuration software can tell whether a
pod supports a program before it uploads it. The firmware constant is
BlockFactory::PROGRAM_FORMAT (BlockFactory.h),
the validator's is PROGRAM_FORMAT in tools/validate_programs.py.
"version": 1 in a program file is the file layout (§1.2), not this revision.
programFormat |
Firmware | Changes |
|---|---|---|
| 1 | 2.0 | first JSON program format: block types led, delay, button, if, setVariable, remotePress, remoteMultiPress, remoteResetAll, summary as described in §2 |
| 2 | 2.8 |
timerStart / timerStop on every block (§2 Timers, §7.9); block type repeat (§2, §7.10) |
| 3 | 2.9 | block type effect (§2, §7.11); countdown, countdownColor, countdownTarget, countdownFrom on delay, button, remotePress, remoteMultiPress (§2 Countdown, §7.12). With a countdown timeoutMs of remotePress / remoteMultiPress is read once on enter (§7.2). Block types charge (§2, §7.13) and release (§2, §7.14), with the hold reporting of link protocol revision 2 (link-protocol.md §2.2), also with countdown. Revision 3 is not released yet, so these were added to it without a new revision |
| 4 | 2.10 | block type toggle (§2, §7.15), with the toggle activations of link protocol revision 3 (link-protocol.md §2.3), also with countdown
|
| 5 | 2.11 | block type communicate (§2, §7.16) with the report buffer and the ops reports, ack, command, setVariables, session (config-api.md §5) |
| 6 | 2.13 |
summary fields show, targetMs, showMs: the average as a bar on the central's ring (§2, §7.7). The bar is the level effect of link-protocol.md §2.1 on the central only; with show the block waits showMs
|
Programs of an older revision run unchanged on newer firmware. A program that uses fields
or block types of a newer revision is rejected by older firmware only if it uses a new
block type; unknown fields are ignored there, so check programFormat before uploading.
Firmware before 2.3 does not report programFormat; treat a missing field as revision 1.
Exact runtime behaviour of the block engine, for simulators (e.g. the Config App) that must
behave like the pod. Code references are relative to src/. When one of these details
changes, increase programFormat (§6).
- The central calls
BlockManager::run()once per loop pass (loop()never sleeps, a pass takes µs while blocks wait, see timing.md §1.1). - In one pass the engine executes the current block. If it finishes, the block is left
(
exit), the next block is entered (enter) and executed in the same pass, and so on. - At most
MAX_STEPS_PER_PASS = 32blocks are executed per pass (BlockManager.h,BlockManager::run). When the limit is reached, the block that was entered last is executed in the next pass. Nothing is lost or skipped. An endless loop of instant blocks therefore never hangs the pod: it runs 32 steps per pass, and the button, the radio and the serial console keep working. -
enter()is called every time a block becomes current, also when a block links to itself. Times (entry time, random durations, random timeouts) are taken inenter(). - Port selection (
BlockManager::run): the block's result port is used if it is connected. Otherwise, if the block's port 0 is callednext(every type exceptif),nextis used. If that is not connected either, the program ends (running = false, no error). - After a pause (a peripheral was lost, or a phone
pause, state PAUSED) the central re-enters the current block (BlockManager::restartCurrentBlock:exit+enter). Its timers restart and random values are drawn again. A pause does not change the block;communicateis the exception (§7.16). - Values that name a variable are read when the block uses them (see the table below).
An unknown variable reads as 0 (
VariableManager::resolvecreates it with 0).
| Type | Finishes | Value read |
|---|---|---|
led, if, setVariable, remoteResetAll, repeat
|
immediately, in the pass it is entered | on execute |
summary |
immediately, with show after showMs (§7.7) |
on execute |
delay |
when millis() ≥ entry + duration (checked once per pass). A duration of 0 finishes in the entry pass |
on enter (also the countdown fields) |
button |
on press / release / timeout | timeout on enter, shortMs on release |
remotePress |
on the peripheral's press / timeout. Immediately through timeout if the pod does not exist |
pod on enter, timeoutMs every pass (on enter if countdown ≠ secret) |
remoteMultiPress |
when all green pods are pressed / an avoid pod is pressed / timeout |
pods, avoid, syncStartMs on enter, timeoutMs every pass (on enter if countdown ≠ secret) |
effect |
wait: false: immediately. wait: true: when now ≥ start + durationMs (checked once per pass) |
all fields in the entry pass (first execute) |
charge |
on release / full charge / timeout (§7.13). Immediately through timeout if the pod does not exist or cannot measure holds |
everything (also the random timeout) on enter |
release |
on release / timeout / no hold (§7.14). Immediately through timeout as for charge
|
everything (also the random switch delay) on enter |
toggle |
when the game time is over / all pods one colour (§7.15) | everything (also the random duration) on enter |
All random numbers come from esp_random() (Block/RandomUtil.h).
-
setVariablerandom: uniform in [0, value − 1].value≤ 1 gives 0. -
setVariablerandomOther(SetVariableBlock.cpp):-
value≥ 2 and the current value is in [0, value − 1]: uniform among the othervalue − 1numbers, never the current one. -
value≥ 2 and the current value is outside that range: likerandom. -
value≤ 1: always 0 (there is no other number to choose).
-
-
delayminMs/maxMsandbuttonminTimeoutMs/maxTimeoutMs: uniform in [min, max], both inclusive (RandomUtil::between). A negativemincounts as 0. Ifmax≤min, the result ismin.ms/timeoutMsis the same with min = max. -
chargeminTimeoutMs/maxTimeoutMsandreleaseminGoMs/maxGoMs: the same, drawn once on enter.goMsisminGoMs=maxGoMs.
Code: ButtonBlock.cpp, Input/Button.h.
- Only a press edge after the block was entered counts (the press counter is stored in
enter). A button held while the block is entered is ignored until it is released and pressed again. - Debounce: an edge is taken immediately, further edges within 20 ms are ignored.
- On the press edge the reaction time (press time − entry time, ms) is recorded
(
record) and stored (storeAs). - If the
pressedport is connected, the block leaves throughpressedon the press edge.short/longare never taken then. - Otherwise the block waits for the release and leaves through
shortif the press lasted less thanshortMs, elselong(a press of exactlyshortMsislong). Nothing happens whenshortMsis reached while the button is still held:longis taken only on the release. Unconnectedshort/longfall back tonext. -
timeoutis only checked before a new press. A held button from before the block does not stop the timeout. Once a new press has started, the timeout no longer applies and the block waits for the release, however long it takes. - A timeout of 0 (or a random timeout that comes out as 0, or a negative one) means no timeout.
Code: RemotePressBlock.cpp.
-
podis a peripheral id0 … connected − 1. Any higher id, including the central's own id (REQUIRED_PERIPHERALS), does not exist: nothing is lit and the block leaves throughtimeoutin the pass it is entered. The same happens in a program without peripherals. (podis taken modulo 256, so do not use negative ids.) - Only the press that answers this activation (command sequence number) counts.
- After the press the pod is reset (
colorAfter); the reaction time measured on the peripheral is recorded / stored. -
timeoutMscounts from the entry. 0 means an immediate timeout (unlikebutton). WithouttimeoutMsthe block waits forever. On timeout the pod is reset.
Code: RemoteMultiplePressBlock.cpp.
- Number of peripherals
n=pods(default: all connected), limited to [0, connected peripherals] (and 31).podslarger than the number of connected peripherals uses all connected ones, no error. Peripherals0 … n − 1take part. - The central takes part as id
nin the draw ifincludeCentral(default true); its results use its normal id (REQUIRED_PERIPHERALS). Totalt = n + (includeCentral ? 1 : 0). - The number of avoid pods is
avoidlimited to [0, t − 1], so at least one pod is always green. The avoid pods are drawn uniformly from alltids, the central included. -
t = 0(no peripherals, central excluded): the block leaves throughnextimmediately. - Peripherals light at network time entry +
syncStartMs(default 20 ms; 0 = when the command arrives). The central lights at the same time. Presses of the central before it is lit do not count. - A press of an avoid pod (peripheral or central) leaves through
avoidHitimmediately. The other pods stay lit. - A green peripheral is reset when pressed and its reaction time recorded. A green central switches its LEDs off when pressed; its reaction time is measured from the moment it lit.
- When all green pods are pressed the block leaves through
next.timeoutMscounts from the entry, 0 means an immediate timeout; without it there is no timeout. - Pods that were not pressed stay lit (also after
avoidHit/timeout).
Code: SummaryBlock.cpp, Results.cpp.
- Statistics are kept per metric since the last
summaryof that metric withreset(or theresultsAPI op withreset).averageis the integer average (truncated). -
No values for the metric:
count= 0,average= 0,best= 0,worst= 0. These zeros are printed (SUMMARY,<metric>,0,0,0,0) and stored in thestore…variables. -
bestis the smallest value,worstthe largest. - With
reset(default true) the metric is cleared afterwards.
Bar (show, programFormat 6). Code: SummaryBlock.cpp,
frame: level in LEDManager.cpp.
- Read once on entry, after the statistics are stored and before
reset: the averageA(ms, integer) and the targetT(targetMs, read as 1 … 60000) give the bar. Lettwice = 2 T.-
percent=(twice − A) · 100 / twice, 0 whenA ≥ twice, and at least 6 (one LED of 18 for any measured value). - Lit LEDs:
(percent · 18 + 50) / 100, from LED 0 on (the rest of the ring is dark). A full ring is 0 ms, half a ringT, an empty ring2T. - Colour: green if
A < T; yellow (amber) ifA ≤ 1.5 T; red otherwise.
-
- The bar is a
leveleffect (link-protocol.md §2.1,param= percent) on the central's ring, started at once. Peripherals show nothing. It replaces the ring's colours, as any light does. -
Wait.
showMs(default 3000, 0 … 60000) after the bar starts, the block leaves throughnext. When the bar ends the ring is switched off (asturnOff).showMs0: no wait, the bar stays until the next light change. -
No values for the metric (
count= 0): no bar, no wait, as withoutshow. - Pause. A pause during the bar keeps the bar; the block ends at the original time.
- Without
showthe block is as before: it does not wait and does not touch the ring.
-
ledwithoutledssets all 18 LEDs. Withleds, the other LEDs keep their colour. -
ifhas nonext: an unconnectedtrue/falseends the program. -
remoteResetAllresets peripherals0 … min(pods, connected) − 1(default all connected), and switches the central's LEDs off unlessledsOffis false.
Code: BlockManager.cpp (enterCurrent, stopTimer).
-
timerStartis applied when the block is entered, before itsenter(): the timer storesmillis()and is running. A start on a running timer restarts it. -
timerStopis applied when the block is left, after itsexit(), also when the program ends in that block. A running timer recordsmillis()− start (ms) for the central's pod id (REQUIRED_PERIPHERALS) and stops. A stop on a timer that is not running is ignored and counted (status.timers.orphanStops, config-api.md §5). - When a block is left and the next one entered, the stop of the old block comes before the start of the new one. A block that links to itself with both fields and the same name therefore records each of its passes.
- An instant block is entered and left in the same pass, so its own duration is 0 ms. Timer values are accurate to one loop pass.
- A pause (
restartCurrentBlock) does not leave the block: timers keep running (the pause is part of the measured time) and are neither started nor stopped. - All timers are stopped and the orphan count is 0 when the program starts.
Code: RepeatBlock.cpp.
- Every block has its own counter, 0 when the program starts (
onProgramStart). - On execute (once per entry) the counter is incremented and
countis read. If counter ≤count:storeIndex= counter, leave throughbody. Otherwise: counter = 0, leave throughdone;storeIndexkeeps its last value. - So
count= 3 givesbody,body,body,done, and the next entry starts again with pass 1.count≤ 0 leaves throughdoneon every entry and never setsstoreIndex. Acountlowered during the loop below the passes already made ends the loop at the next entry. - Unconnected
bodyordonefall back tonext(§7.1). With neither connected the program ends. - Nested repeats count independently: an outer
count2 around an innercount3 runs the inner body 6 times. - A pause (
restartCurrentBlock) does not change the counter: the counter changes only on execute, and a re-entered block has not been executed yet. - If the program leaves the body for good (never returns to the repeat block), the counter
keeps its value until the block next reaches
done. This is not detected.
Code: EffectBlock.cpp, EffectTarget.cpp,
effect player: LEDManager.cpp (renderEffect), frame formulas: link-protocol.md §2.1.
-
Start. In the first
executeafter the entry (the entry pass; a pause re-enters the block and starts it again) all fields are read once.durationMs< 0 counts as 0,periodMs≤ 0 as 500 (max 65535),brightnessis limited to [0, 255],syncStartMs< 0 counts as 0. Letlead = syncStartMs(default 20) andstart = entry time + leadon the network clock (= the central's clock). The effect is sent to the targets with start timestart(withlead= 0: "now", each pod as its command arrives). Untilstartthe ring keeps its old colours; fromstartthe frames are a function ofnow − start(a late pod joins the animation). -
Target.
central: the central only.peripherals: peripheral ids 0 … connected − 1.all: both. A pod id (value, read at start): that peripheral; the central's own id (REQUIRED_PERIPHERALS) means the central; any other id is ignored (nothing shown, the block continues). Without peripherals only the central is affected. -
Effect parameters. The effect runs for the whole ring.
leds(central only) limits the output to those LEDs, the others keep their colour; the geometry offill/drain/spinstays that of the whole ring.offis a steady dark ring (colour off).dimfades frombrightnessto 0, for all other effectsbrightnessscales the output. Colours are named colours for the central too. -
wait: false(instant block): continues in the same pass throughnext. The effect keeps running while the following blocks execute. -
wait: true: leaves throughnextwhennow ≥ start + durationMs(checked once per pass), so it takessyncStartMs + durationMsfrom the entry. Duration 0 (e.g. a variable that is 0) finishes right aftersyncStartMs.blink,pulse,spinwith a constantdurationMs≤ 0 or none are rejected when the program is loaded. The effect is not stopped when the block is left. -
End frames. Timed effects stay in their end frame (
fillfull,drainandoffdark,colorShift=color2,trafficLightred,dimdark).blink,pulseandspinwith a duration end in a steadycolor. Endless effects run until the ring is changed. -
Replacement. On the central any static LED call (
led,remoteResetAllwithledsOff, a pressed central inremoteMultiPress, …) or another effect replaces it. On a peripheral an ARM activation, a press result or RESET replaces it. -
Old pods. A peripheral without the
effectslink feature showscolorstatically atstart(an ARM withcolorActive;offis a RESET). It stays lit until the next ARM / RESET. The block still waits for the full duration.
Code: Countdown.cpp (effect for a style), the four blocks.
A countdown is a ring effect (link-protocol.md §2.1) with color = the lit colour L and
durationMs = the playing time D:
countdown |
Effect | Ring at the end |
|---|---|---|
drain |
drain, color L, background off |
dark |
fill |
fill, color L, background off |
full, colour L |
dim |
dim, color L, brightness 255 → 0 |
dark |
colorShift |
colorShift, L → countdownColor (default red) |
countdownColor |
trafficLight |
trafficLight (colours ignored) |
red |
D is the time limit T read on enter (minus the lead time where noted). If T ≤ 0 (a
timeout of 0 for remote…, a delay of 0, button without timeout) there is no countdown. secret
never plays an effect.
-
delay:T= the drawn duration,L=countdownFrom(green). Central only: the effect starts on enter (D = T). If the target includes peripherals andT> 20 ms, it starts 20 ms after the entry on all targets andD = T − 20, so that it ends with the delay.countdownTargetas for theeffectblock, pods without effect support show nothing. When the block is left the central stops its effect (the ring keeps the last frame). -
button:T= the drawn timeout,L=countdownFrom. The effect starts on enter on the central (D = T). A press (the edge that counts) or leaving the block stops it; the ring keeps the last frame. Timeout and reaction time are unchanged. -
remotePress:T=timeoutMsread once on enter,L= the pod'scolor(colorActive). The ARM command carries the effect (D = T), which the pod plays from its activation (the ARM command's arrival, as before). Because the central times outTafter its entry, the pod's countdown ends a few ms (link latency) after the timeout is taken. On press or timeout the pod is reset as before (the countdown ends). A pod without effect support stays lit withL. -
remoteMultiPress:T=timeoutMsread once on enter,Lper pod = its lit colour (hitColor/avoidColor). All lit pods (hit and avoid) and the central (if it takes part, with its own lit colour) start the effect at the common start timeentry + syncStartMsandD = T − syncStartMs, so they run out together at the timeout. IfD≤ 0 there is no countdown. A pressed green pod is reset, a pressed central switches off (both end their countdown); the other pods keep counting down until the end frame, also afteravoidHit/timeout(follow withremoteResetAllas before). - Without a countdown
timeoutMsis still read every pass (§7.2). - The central's countdown uses the same effect player as the
effectblock: a later static LED call replaces it.
Code: ChargeBlock.cpp, HoldBlock.cpp, the hold itself: Link/HoldSession.cpp (the central's own button and the peripheral run the same code), wire format: link-protocol.md §2.2.
Enter. Read once: pod, onsetMs (< 0 counts as 0), maxMs (< 1 counts as 1), decayMs (< 0 as 0; ignored
unless accumulate) and the timeout T: timeoutMs, or a random value in [minTimeoutMs, maxTimeoutMs] (§7.3);
T ≤ 0 or no timeout field means no timeout (as button, unlike remotePress). pod is the central if it is
"central", missing, or equals the central's own id (REQUIRED_PERIPHERALS). Otherwise it is a peripheral id
0 … connected − 1; a peripheral without the hold link feature, or any other id, cannot take part: nothing
is lit or stored and the block leaves through timeout in the pass it is entered. Then the idle ring is shown.
Hold. A hold starts at a press edge after the entry (the press counter is stored on enter, §7.4); a button
that is down on entry is ignored until it is released and pressed again. Let p be the press edge, r the release
edge (button timestamps, not the time a pass notices them; the debounce is 20 ms). Charge in ms:
- while held:
charge = min(maxMs, c0 + max(0, (t − p) − onsetMs)).c0is the charge atp: 0 inreleasemode, inaccumulatethe charge kept from earlier holds. The onset applies to every hold. - after a release in
accumulate:decayMs = 0:chargeis kept. Else it drains linearly,charge(t) = max(0, c − (t − r) · maxMs / decayMs). - the charge is rounded to whole ms when it is reported.
Ring (central and pod alike). Idle (before the first hold): fill dark, brightness the colour at brightness 40
(of 255), trafficLight green, colorShift color; if the block has a countdown and T > 0, the countdown
(§7.12, lit colour color, duration T, started on enter) instead. At the start of a hold the ring shows the frame
for the current charge (this replaces a countdown, which does not come back). From p + onsetMs the charge effect
runs, a function of charge / maxMs: fill: LED n lights at n/18 of the charge, brightness: 40 → 255, trafficLight:
green, yellow, red in thirds, colorShift: color → color2 (default red). In accumulate a release freezes the
ring at the charge (drawn about 30 times a second while it drains).
Ending, release mode. The first of:
-
full: the charge reachesmaxMs, i.e. atp + onsetMs + maxMs. The pod / central reports it at that moment, without waiting for the release (a release edge at or after that time also counts as full). The ring flashes (color;trafficLight: red;colorShift:color2; 600 ms, 200 ms period, dark afterwards) and is not reset. The block leaves throughfull; stored:maxMs, 100 %,recordmaxMs. - A release edge
rwithr − p < onsetMs: portearly, if it is not connectedreleased. Charge 0 is stored. - Any other release edge: port
released, charge =(r − p) − onsetMs(0 for an exactonsetMs). - Timeout (below).
storeAs (ms), storePercent ((charge · 100 + maxMs / 2) / maxMs, integer) and record (ms, with the pod's id, or
REQUIRED_PERIPHERALS for the central) are written for 1, 2 and 3. After released, early and timeout the pod / ring
is reset (dark).
Ending, accumulate mode. Only full (as above, with the charge that has accumulated: the full moment is
p + onsetMs + (maxMs − c0)) and timeout. released and early are never taken; a release only pauses the charge.
Timeout. T counts from the entry until the block ends, also during a hold (checked after the events of a pass;
an event received in the same pass wins). release mode: leave through timeout, nothing is stored, the pod is reset.
accumulate mode: the pod (or the central's own session) stops T ms after its activation and reports the charge it
has then (decayed if released); the block leaves through timeout with that charge stored (storeAs, storePercent,
record). The central waits up to 300 ms after its own T for the pod's report; if it does not come, nothing is stored.
Pause. restartCurrentBlock ends the activation and enters again: charge 0, a new random timeout. A peripheral
that cannot be reached keeps its ring until the new ARM.
Code: ReleaseBlock.cpp, HoldBlock.cpp, Link/HoldSession.cpp, wire format: link-protocol.md §2.2.
Enter. Read once: pod, holdTimeoutMs, maxHoldMs (trigger), goMs / minGoMs + maxGoMs (signal: the switch
delay G is drawn once, §7.3, negative values count as 0), timeoutMs (signal), allowHeld; for each value ≤ 0 or
missing means none. The pod is resolved as for charge (§7.13): a pod that does not exist or cannot measure holds
makes the block leave through timeout at once (next if timeout is unconnected). Then the ring shows holdColor.
Start of the hold. A press edge after the entry. With allowHeld a button that is already down on entry starts the
hold at the entry (not at the physical press, so storeAs of trigger counts from the entry). Without it a button
that is down is ignored until released and pressed again. Let s be the start of the hold. The ring shows heldColor
(trigger with a countdown and maxHoldMs > 0: the countdown over maxHoldMs, lit colour heldColor, from s).
The central learns of the start when it receives PRESSED (its own button: in the pass that sees it).
trigger. A release edge r: leave through next; storeAs = r − s in ms (rounded). The block is left in the
pass in which the central receives RELEASED (the central's own button: the pass that sees the release), so a
timerStart on the next block starts then, a few ms after the physical release for a peripheral. The pod / ring is reset.
signal. At s + G the ring switches to goColor (or, with a countdown and timeoutMs > 0, the countdown over
timeoutMs, lit colour goColor). A release edge r:
-
r < s + G: false start, portfalseStart. Nothing is stored or recorded. - otherwise: reaction =
r − switchwhereswitchis the moment the pod updated its LEDs ats + G(an ideal simulator:s + G); ≥ 0, rounded to ms.recordandstoreAsget it, portnext.
Timeouts (the central's clock, a few ms of link latency are not modelled by the simulator):
-
noHold: the hold has not startedholdTimeoutMsafter the entry. Only checked until a hold has started. -
triggertimeout: not releasedmaxHoldMsafter the start of the hold (where the central saw it start). -
signaltimeout: not releasedG + timeoutMsafter the start of the hold, i.e.timeoutMsafter the switch. - Events of a pass are handled before the timeouts. After
noHoldandtimeoutthe pod / ring is reset, nothing is stored.
An unconnected noHold, falseStart or timeout falls back to next (§7.1). Pause: as charge.
Code: ToggleBlock.cpp, the flip on the pod:
PeripheralRole.cpp (runToggle), wire format:
link-protocol.md §2.3.
Enter. Read once: pods, cooldownMs (clamped to 0 … 65535), syncStartMs (< 0 counts as 0), the duration D
(durationMs, or random in [minDurationMs, maxDurationMs], §7.3; D ≤ 0 ends the block at the start) and, for
start: "random", one draw per participant. The participants are the peripherals 0 … pods − 1 that have the toggle
link feature (the others are left out, the log has "cannot toggle") and, with includeCentral, the central (index
pods). The start owner is set per participant in this order (central last): a, b, alternate = A for the 1st, 3rd, …
participant and B for the others, random, off = nobody. Let t0 = entry + syncStartMs: all pods and the central show their
colour at t0 (as in remoteMultiPress, §7.6). The game runs from t0 to t0 + D.
Flip. A press edge after t0 flips the pod: A → B, B → A, nobody → A. A press less than cooldownMs after the
last accepted press of that pod is ignored (button timestamps, not the time a pass notices them); the first press always
counts. Several presses noticed in one pass count once. The pod flips on its own and reports {owner, flipCount} (the
central's own button flips in the pass that sees the press). The ring shows the new colour at once.
Central bookkeeping. For every participant the central keeps owner, flipCount and the time of the last owner change.
A PRESSED event counts if pod and cmdSeq match the activation, flipCount is above the last one seen (older and
duplicate frames are dropped) and it was received no later than t0 + D. A skipped flipCount (lost frame) adds to the
flip total; if the owner differs, the time since the last change goes to the old owner, with the central's receive
time as the moment of the change (a few ms after the press; an ideal simulator uses the press time). Owned time per team
is the sum over all participants, so with N pods it can reach N · D.
Ending. Checked each pass after the events:
-
allA/allB: after a flip, every participant (also the central) has the same team and the port is connected. Not connected: the game goes on. At least one participant is needed. This is checked only after a flip, never on entry. - Time over:
now ≥ t0 + D→next.
At the end the owned time of the current owners is closed at the end time (t0 + D for next, the current time for
allA / allB) and storeA / storeB (pods per team, nobody is not counted), storeTimeA / storeTimeB
(rounded ms), storeFlips (sum of all flipCounts, central included) are written. Participants then show their final
colour frozen: the central's ring is set (a countdown ends), a peripheral gets a plain activation with that colour before and
after the press (so a press changes nothing). Nothing is reset; follow with remoteResetAll.
Pause. restartCurrentBlock enters again: new random duration, new start owners, flips and times start from zero. A pod
that is not reached keeps its state until the new ARM.
Code: Block/CommunicateBlock, Results/Reports.
Measurement log. Besides the statistics (§7.7), the central keeps every measurement
(record, timerStop, §3) since the last report: t (millis() when recorded), metric,
pod id, value in ms. The log holds at most 48 values; further values are not listed but
still counted in the report's stats and in valuesDropped. The log is cleared when the
program starts and when a report is built. summary and the results op do not touch it.
Enter.
- Build a report:
id(counts from 1 since boot, never reused until reboot),program(path),setId,athlete(only if asessionset one),cycle(reports built since the program started, 1 for the first),startMs(time of the previous report, or the program start),endMs(now),values(the log, oldest first, only the metrics inmetrics; all withoutmetrics),statsper listed metric that has values (count,averagetruncated,best= smallest,worst= largest; over all values of the log, also the dropped ones),vars(current values ofvars),valuesDropped(only if > 0). - Clear the log (all metrics, also those not listed).
- Put the report into the report buffer (8 reports, RAM). A full buffer drops its oldest
report and counts it (
status.reports.dropped). - Send it as a
reportevent line on USB serial (config-api.md §5). - Set all central LEDs to
color, readtimeoutMs(a value, read once), start waiting.
Wait. Checked each pass, the first that applies wins:
- A client
command(continue→next,repeat→repeat,stop→stop). - A press of the central's button (press edge after entering) →
pressed. -
timeoutMs> 0 and now − entry ≥timeoutMs→timeout.
Unconnected ports fall back to next (§7.1); with no next the program ends.
setVariables (config-api.md §5) changes the editable variables only while the block waits.
repeat only selects the port: the report stays as it was sent (the client marks it as
discarded).
Pause. While PAUSED the block does not wait: command and setVariables answer
not_ready, status has no waiting. This holds for a peripheral lost and for a phone pause
(pause / resume, config-api.md §5). A phone pause can start while the block waits.
restartCurrentBlock resumes the block (Block::resume(), by default enter()) without building
a second report: the block keeps the report of this entry, sets its LEDs again and restarts the
timeout. The client answers after resume, with the same report.
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