Requirements & pseudocode: design before code
420-302-VA · WEEK 5 · FALL 2026

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 sibling randint gives whole numbers; the docs page is worth ten minutes.)
  • break and continue: Python's spellings of EXIT WHILE and "continue to next round". break leaves the innermost loop; continue jumps 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:

IDCheck performedResult
R1Watched 3 round startsPass: LED off, message printed
R2Stopwatch on 5 roundsPass: delays 2.4–4.7 s, all different
R3Counted ~1 s before pressingPass: printed 1043 ms, plausibly near 1000
R4Pressed early, twicePass: FALSE START, no time, new round
R5See noteArgued from the error budget; direct test needs equipment we lack
R6Played 3 rounds, then Ctrl+CPass
C1, C2Bench demo · git logPass

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".

  1. 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.)

  2. 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.)

  3. 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.)

  4. 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.)

  5. 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()?
The wait must watch two things at once, elapsed time and the button, so it cannot block on either; polling checks both each lap. The measurement watches exactly one thing, so blocking is simpler and loses nothing.
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".
Why is time.monotonic() the right clock for the reaction time?
It only moves forward, immune to the wall-clock adjustments NTP makes. Intervals need a clock that cannot jump; dates need the wall clock. Wrong choice, and a clock sync mid-round could report a negative reaction time.
R5 got "argued, not measured" in the table. Why is that better than writing Pass?
Because no test was run that could have failed. Verification is only as good as its method; recording the limit keeps the table trustworthy and names what better equipment would enable. Faked passes are how systems ship broken.
In the door chime, what makes "a press during the flash" a spec question rather than a coding question?
Both ignoring it and queueing it are implementable; which one is correct is a decision about desired behaviour, and that decision belongs to a requirement. Code answers how; only the spec can answer which.