Stage 5 of 6 · Lab, kit and Pi · about 50 minutes
Design lab
The spec exists, the design traced clean. Now the cheap part: typing it in. Then the part that makes this week different from last week: proving, requirement by requirement, that what runs is what was specified, and doing the whole cycle again on systems you invent.
Set up the project
Same circuit as Week 4, LED on GPIO17 (physical 11), button on GPIO27 (physical 13), wired with outputs off and traced aloud (the diagram). On the Pi, over SSH:
$ mkdir -p ~/iot/week5/docs && cd ~/iot/week5
$ python3 -m venv --system-site-packages .venv && source .venv/bin/activate
$ nano .gitignore # .venv/ __pycache__/ *.pyc, as in Week 4
Then the week's characteristic move: the documents go in first. Put the requirements table in docs/requirements.md (Markdown tables from Week 1: pipes and dashes) and the pseudocode in docs/design.md, and commit them before any Python exists. The commit history should read like the method: spec, design, code, verification.
Translate: pseudocode to Python
With the construct map open, the translation is close to mechanical. reaction.py:
from gpiozero import LED, Button
from time import monotonic, sleep
from random import uniform
led = LED(17)
button = Button(27, bounce_time=0.05)
while True: # R6
led.off() # R1
print("Get ready...") # R1
delay = uniform(2.0, 5.0) # R2
waited = 0.0
false_start = False
while waited < delay: # the watchful wait
if button.is_pressed:
false_start = True
break # EXIT WHILE
sleep(0.01)
waited = waited + 0.01
if false_start: # R4
print("FALSE START")
button.wait_for_release()
continue # next round
led.on() # R2 completed
t0 = monotonic()
button.wait_for_press() # R3
reaction = (monotonic() - t0) * 1000
led.off()
print(f"Reaction: {reaction:.0f} ms") # R3
button.wait_for_release()
The new pieces, each earned by a design decision:
uniform(2.0, 5.0), from the standard library's random module: a random float in the range, R2's literal translation. (Its siblingrandintgives whole numbers; the docs page is worth ten minutes.)breakandcontinue: Python's spellings of EXIT WHILE and "continue to next round".breakleaves the innermost loop;continuejumps to that loop's next lap. Both are covered in the tutorial's control-flow chapter, this week's core language reading.button.wait_for_press()/wait_for_release(): gpiozero's blocking waits, the exact WAIT UNTIL of the design, documented with the rest of Button's API. Blocking (the program stands still) is precisely right here and precisely wrong in the watchful wait, which is why the design polls there instead: the wait must watch two things at once.
Background: measuring time honestly
Why monotonic() and not time()
The time module offers several clocks. time.time() is the wall clock: it can jump when NTP adjusts the system time, and Week 3's clock-sync discussion showed how real those adjustments are on a Pi. A stopwatch must never jump, so intervals are measured with time.monotonic(), a clock guaranteed only ever to move forward, meaningless as a date, perfect for durations. Rule for the whole course, ESP32 telemetry included: wall clock for timestamps, monotonic clock for intervals.
And how accurate is the measurement, for R5? Budget the error like the instrumentation problem it is: the debounce window (50 ms) can delay press recognition; the OS may schedule the process a few ms late; the polling in the watchful wait quantizes only the delay, not the measurement. Total plausible error: a few tens of ms, so R5's 100 ms is honest, and "to the microsecond" never was. Software timing is a measurement chain, and you already know how to think about those.
Verify against the spec
Run the acceptance criterion for every row of the spec, both partners, and record the result in docs/requirements.md as a fourth column. This table is the week's real deliverable:
| ID | Check performed | Result |
|---|---|---|
| R1 | Watched 3 round starts | Pass: LED off, message printed |
| R2 | Stopwatch on 5 rounds | Pass: delays 2.4–4.7 s, all different |
| R3 | Counted ~1 s before pressing | Pass: printed 1043 ms, plausibly near 1000 |
| R4 | Pressed early, twice | Pass: FALSE START, no time, new round |
| R5 | See note | Argued from the error budget; direct test needs equipment we lack |
| R6 | Played 3 rounds, then Ctrl+C | Pass |
| C1, C2 | Bench demo · git log | Pass |
R5 is the honest row
How would you truly verify ±100 ms? Film the LED and the press at a known frame rate and count frames; or drive the "button" from a second GPIO under software control with a known delay. Neither fits today's bench, and writing "argued, not measured" in the table is the professional answer: verification methods are themselves designed, and admitting a limit beats faking a pass. Assignment 2's timing requirements will deserve the same honesty.
The design ladder
Now the roles flip: the spec is yours to write. For each rung, the cycle is fixed and the artifacts are committed at every step: requirements (3 to 5, with IDs and acceptance criteria) → pseudocode or flowchart → trace one path → Python → verification table. The navigator writes the spec, the driver codes it, and you swap each rung; a rung is done when its table says so, not when it "seems to work".
Patience light
The LED lights only after the button has been held for a full 2 s; releasing early cancels. Spec question your requirements must answer: what happens during the hold? (Timing + one decision.)
Door chime
Each press triggers exactly one double-flash (short-short), then the system is ready again. Spec question: what does a press during the flash do? Your R-line decides; your code obeys. (Events + edge case.)
Two-mode night light
A press toggles between mode NORMAL (LED off, presses ignored for 1 s after switching) and mode NIGHT (LED on). Write the state diagram first; implement state as a variable. (Your first named state machine.)
Lockout
Extend the reaction timer: three false starts in one session triggers LOCKED for 10 s, LED flashing, presses ignored, then normal play resumes. Spec, diagram, trace the third false start, build. (Counters + states + everything.)
Stretch: best of five
The timer plays exactly five valid rounds, then reports the best and average times and exits cleanly. Needs a list or running totals; the control-flow chapter has what the design will ask for.
Checklist for this stage
Check yourself
Why does the watchful wait poll while the measurement uses wait_for_press()?
What do break and continue each do, in one sentence?
break exits the innermost loop immediately; continue skips to that loop's next lap. They are the Python spellings of the design's EXIT WHILE and "continue to next round".