Statecharts · Problem 5 · capstone
P5 · Harel’s Digital Watch
The original worked example: five orthogonal regions, a deep-history entrance, a feature whose scope is a box, and a battery that has to make the watch forget. Fifteen tests.
The system
The Citizen Quartz Multi-Alarm III — Harel's own wristwatch, reverse-engineered
by observation and drawn as Figure 31 of the 1987 paper. Four buttons, a to
d. A display cycle: time/date, alarm 1, alarm 2, chime, stopwatch. Two independent alarms
and a chime, each of which can be enabled or disabled. A light. A battery.
And a beeper test, which is the feature the whole problem is built around. Press two buttons together and a healthy beeper beeps. Harel notes that Citizen's manual describes the beeper test and the light in exactly the same way — press so-and-so, this-and-that happens — “no indication of the scope is provided, despite the major difference between the two”. The light works everywhere. The beeper test does not.
What is reduced
Figure 31 is a fold-out. This version keeps the structure and drops the arithmetic:
no update modes, no time values, no button press/release pairs — Harel's b+d chord is collapsed
into a single beep_test event. Everything that makes the chart shaped the way it is
survives.
Step 1 — draw it
alive state is six orthogonal components: the main one holding the displays and the beeping modes, one for each alarm's enabled/disabled status, one for the chime, one for the light, one for power. This version uses five. Each is a fact that is true regardless of what is on screen — which is exactly the test from lesson 03.d transition, and broadcast delivers the event to all of them. The alarm and chime regions guard theirs on “am I the thing being displayed?” (Harel's in(state) condition). The time/date and stopwatch ones need no guard, because their states are only in the configuration when they are on screen. No dispatcher, no match.clear-history(state*) for exactly this and attaches it to the transitions into dead. The library has no such action; find where it keeps what history remembers.Step 2 — build it
exercises/p5_digital_watch/machine.py —
the skeleton. Its docstring is the contract: exact state ids, event names, and context attributes.exercises/p5_digital_watch/test_machine.py —
15 acceptance tests. Do not edit them.exercises/p5_digital_watch/_solution.py —
a reference solution with commentary. Open it after yours is green, not before.cd statecharts/exercises/p5_digital_watch pytest -q
Hints
initial marked, the first one declared wins. A nested-class state takes it as a class keyword: class alive(State.Parallel, initial=True):.self.alive.main.displays.alarm1_display.is_active is always False — chained access reaches the class attribute. Every state is published on the instance under its own id: self.alarm1_display.is_active.self.history_values is a plain dict on the instance, keyed by history pseudo-state id. .clear() in the action on the transition into dead.When it is green
Two things to go and look at.
First, test_one_button_press_does_two_unrelated_things. One
b, and the light comes on and the stopwatch starts, from two transitions in two
regions that do not know about each other. Harel's version of this scenario is better: from the update
mode, pressing d then b leaves you “in time, one month
ahead, with the beeper beeping and the light on” — three regions each answering the same
chord in their own terms, and the composite behaviour is something nobody wrote down.
Second, count the guards in your chart. There should be very few, and none of them should be about where the watch is. Everything positional is carried by the shape.
Sources: Harel 1987, Figs. 8–31 · Harel 2007 · RESOURCES.md