17. Capstone: Ship a Documented Sensor Monitor
What you will be able to do
You will carry configurable alert behavior from requirements through implementation, review, verification, and a private release candidate. Start from CP08, whose normal/fault policy and basic tests already work. CP09 is the instructor reference solution and final behavior specification. Your submission includes the source, reviewed history, tests, synthetic replay, firmware-build evidence, documentation, and handoff. Physical equipment and public publication are optional and are not required to complete the core capstone.
Begin with a boundary you can defend
The feature sounds simple: alert when the temperature is high and allow the threshold to change. The engineering work lies in the details. What happens at equality? Can an invalid command alter the previous setting? Does a fault recover into an old alert state? Does reset preserve the setting? A useful requirement answers these questions before an agent generates a plausible implementation.
Keep the existing fault behavior. Missing, nonfinite, or stale input enters FAULT and invalidates the current reading. Two consecutive fresh valid samples recover; an invalid input or stale gap resets that count. Startup follows the same two-sample recovery rule. The first valid reading is current but still FAULT/recovering. Alert logic must not silently bypass this behavior.
The default threshold is 28.0 °C. While NORMAL, a valid temperature at or above the threshold enters ALERT. While ALERT, a temperature at or below threshold minus 1.0 °C clears to NORMAL. Between those boundaries, retain the prior non-fault state. Following FAULT recovery, classify from the fresh entry threshold rather than resurrecting a remembered alert.
These are teaching values, not sensor calibration or safety specifications. At the default threshold, 27.5 °C can legitimately be NORMAL or ALERT depending on the previous state. That history dependence is hysteresis. It reduces repeated transitions near one boundary, but the lesson does not claim measured noise performance or a physical control system.
Turn the contract into expected cases
Write expected states before editing the implementation. Use a small table that includes the starting condition and the input sequence:
| Starting condition | Input or action | Expected outcome |
|---|---|---|
| Startup FAULT | 25.0, then 26.0 at fresh ordered times | FAULT/recovering, then NORMAL |
| NORMAL, threshold 28.0 | 28.0 | ALERT |
| ALERT, threshold 28.0 | 27.5 | Remain ALERT |
| ALERT, threshold 28.0 | 27.0 | NORMAL |
| NORMAL, threshold 28.0 | 27.5 | Remain NORMAL |
| FAULT | Two fresh 27.5 readings | Recover to NORMAL |
| FAULT | Valid, invalid, valid | Remain FAULT; count restarted |
Add command expectations. Accept threshold <decimal> terminated by line feed. The allowed threshold is 0–50 °C inclusive and is stored only in RAM. Maximum line length is 64 bytes before the line feed, including an optional final carriage return. A malformed, oversized, truncated, or out-of-range command must leave the prior configuration intact.
Define numeric grammar instead of accepting whatever a conversion library happens to parse. The reference accepts an optional sign, at least one integer digit, and an optional decimal point followed by at least one fractional digit. It rejects exponents, extra whitespace, trailing text, and nonfinite words. Thus threshold 30.5 can be accepted, while threshold 28junk and threshold 2e1 are malformed.
A valid command immediately re-evaluates a current non-faulted reading against the new entry threshold. It cannot clear FAULT or advance recovery. Poll the monitor's injected time before applying a command so stale data cannot be treated as current. Reset restores the default threshold and FAULT/missing; no nonvolatile persistence is promised.
Establish a preserved baseline and a small plan
Create a new exercise branch from CP08 using the supplied history bundle, or initialize your own preserved exercise repository if starting from its ZIP. A ZIP provides files; the bundle provides the teaching history. Inspect status before making changes so unrelated work is not silently included.
git clone .\sensor-monitor-history.bundle capstone-work
Set-Location .\capstone-work
git switch -c capstone-alerts CP08
.\scripts\test.cmd
.\scripts\replay.cmd fixtures/normal.csv
These baseline checks establish the stage you are extending. CP08 has no ALERT state or command parser. Its normal fixture therefore has a different expected state sequence from CP09. Record the stage with the result; do not compare output against the final reference without noticing that the implementation is earlier.
Plan four small changes: policy/configuration, command parser, adapter/output integration, and expanded verification/documentation. Keep existing shared-source architecture. The host and firmware should compile the same policy, parser, and output implementation files rather than separate copies that can drift. Figure SS17-01 identifies the requirements and bounded plan.
CP08's native source list has no parser file. When you add command_parser.cpp, add it to REFERENCE_SOURCES in scripts/common.cmd so both Windows replay and test wrappers compile it. Update the source list in scripts/native.py if you also maintain the host appendix path. Keep the parser header and source inside firmware/sensor-monitor, where the Arduino sketch build includes them. Review these build-list changes with the feature; an unresolved parser symbol is an integration problem, not a reason to duplicate the parser inside a test.
Ask the agent to implement one slice at a time. Include the applicable requirement and acceptance criteria, but inspect the resulting diff before proceeding. A request to finish the whole capstone can produce a large patch whose assumptions are difficult to review. Small changes make it easier to connect a defect to the decision that introduced it.
Worked example: implement alert-entry equality
Begin with the state enum and configuration in the shared policy. Add ALERT, a default threshold, and the 1.0 °C hysteresis constant. Preserve the existing invalid-input checks and recovery gate. The alert comparison belongs after input validity and recovery handling, not before them.
The reference policy's relevant branches are:
if (state_ == State::Fault) {
if (++recovery_count_ < 2) {
error_ = Error::Recovering;
return;
}
state_ = temperature >= threshold_ ? State::Alert : State::Normal;
} else if (state_ == State::Alert) {
if (temperature <= threshold_ - kHysteresis)
state_ = State::Normal;
} else if (temperature >= threshold_) {
state_ = State::Alert;
}
Read the control flow before copying it. The first branch completes fault recovery and classifies the fresh temperature. The second handles clearing an existing alert. The last handles entry from NORMAL. Changing branch order or flattening these into one comparison can destroy hysteresis or make startup incorrectly leave FAULT after one sample.
For the instructor's demonstrated requirement, establish NORMAL with two fresh readings below the threshold, then supply exactly 28.0. Assert ALERT. Add a companion case immediately below the threshold. The expected outcomes come from the contract, not from the current comparison operator. Review this slice and run the applicable tests before adding parser work.
The learner then implements clearing equality, both directions through the hysteresis band, boundary jitter, and fresh fault recovery. Use ordered injected timestamps and keep each test's starting state explicit. A test at 27.5 without a known prior state is incomplete because the requirement permits two different outcomes there.
Add configuration without weakening input validation
The setter must reject nonfinite and out-of-range values without modifying the stored threshold. After a valid setting, re-evaluate a current non-faulted reading using the entry threshold. Keep FAULT unchanged. A command acknowledgement is a configuration result, not proof that a sample is valid or that recovery has occurred.
Build the parser as a bounded line collector plus a complete-line validator. Reserve storage for the maximum permitted bytes and the terminator needed by conversion. Once input exceeds the limit, drain to the next line feed and return too_long; do not let the remainder of the oversized line become a new command. Reset collector state at the line boundary so a subsequent valid command can be processed.
Check the entire numeric token before conversion. A conversion routine may accept the numeric prefix of 28junk; that is not the grammar this project promises. Validate in the wider numeric type before narrowing, so a value slightly above 50 cannot round into the allowed float range. The reference's complete parser is in CP09 for review after your attempt.
A partial line at host end-of-input returns truncated. Firmware waits for line feed without blocking the main loop; it does not invent a timeout or apply an incomplete command. The firmware limits serial work per loop so acquisition still gets a turn. These software design choices require code and tests; they do not establish observed physical serial timing.
Integrate output and replay
Add the threshold to the startup/configuration output and ALERT to state formatting. Keep the synthetic label in every relevant record. Missing, nonfinite, and stale readings produce JSON null for temperature. A recovering sample can contain a fresh numeric value while its state remains FAULT/recovering. Do not collapse all FAULT records into one misleading data-validity rule.
The full replay adapter supports sample, tick, command, truncated, and reset events. CP08 already supplies the normal, stale-recovery, and rollover fixtures, but it does not contain fixtures/commands.csv. After your initial parser and integration attempt, extract CP09 into a separate reference folder and inspect its docs/formats.md and fixtures/commands.csv. Copy only that command fixture into your exercise's fixtures folder, or author equivalent cases against the same contract; preserve your implementation. Use it for configuration cases and fixtures/normal.csv for alert transitions. Fixture syntax errors are different from intentionally invalid sensor samples. A malformed fixture exits 2 with a line-number diagnostic; a valid fixture that intentionally produces FAULT can complete with exit 0.
.\scripts\replay.cmd fixtures/normal.csv
.\scripts\replay.cmd fixtures/commands.csv
.\scripts\replay.cmd fixtures/stale-recovery.csv
.\scripts\replay.cmd fixtures/rollover.csv
At CP09, the normal fixture's expected states are FAULT, NORMAL, ALERT, ALERT, NORMAL, FAULT, FAULT, NORMAL. The first and seventh entries are recovering. This is an expected sequence derived from the fixture and requirements; save your actual output separately. Figure SS17-04 should show a genuine synthetic replay, with no suggestion of board measurement.
Verify boundaries and meaningful rejection
Run the full host assertions, inspect their result, and cross-build the reviewed source:
.\scripts\test.cmd
.\scripts\build-firmware.cmd
Cover inclusive entry and clearing, jitter, both directions through the band, invalid and stale input, interrupted recovery, timer rollover, valid threshold updates, malformed numeric suffixes, range endpoints, oversized lines, repeated commands, truncation, and reset defaults. The independent replay checks in the reference solution also parse JSON and compare expected sequences. State which checks you actually ran.
Use isolated FAULT-hysteresis and FAULT-command copies to demonstrate that tests reject meaningful defects. The hysteresis mutation changes inclusive clearing to strict clearing, missing exactly 27.0 °C. The command mutation accepts malformed trailing text. Both must compile before their behavioral rejection supports the intended lesson. Preserve a fresh good solution and show that it still passes.
Do not infer completeness from a large assertion count. A test suite that misses equality or validates only successful commands can look substantial while leaving the important defect undetected. Map the tests to requirements and name the limits of host execution. Figure SS17-03 shows actual outcomes, while the evidence note explains their scope.
Package a reproducible private candidate
Update the README, requirements, formats, decisions, evidence summary, handoff, and release notes to match the reviewed implementation. Record the release identity, source commit or hashes, exact target/tool inputs, available commands, and known limitations. Keep learner code and permitted fixtures separate from private authoring credentials, unrelated history, and production administration.
Create a fresh second checkout from the intended release source and follow the written setup. Run tests, replay, and firmware compilation there. This detects dependencies on untracked local files or incidental working-directory state. A clean status alone is not reproduction; the commands must actually run in the second copy. Preserve the receipt and distinguish it from checks in the original exercise folder.
A private release candidate satisfies the course task. Publishing a public repository, buying hardware, or uploading to a device is unnecessary. The candidate becomes reviewable through complete files and evidence, not through public visibility. Figure SS17-05 identifies release notes and source history without exposing unrelated authoring material.
LAB17: complete and defend the capstone
Allow several practice sessions. Begin from CP08, preserve the baseline, and implement the undemonstrated requirements using the plan above. Consult CP09 only after your own attempt or to diagnose a specific uncertainty. Submit the final source, bounded commits/diff, requirement-to-test map, actual host/replay/build receipts, seeded-defect evidence, fresh-checkout result, documentation, and handoff.
Use this 100-point rubric:
| Category | Points | Observable evidence |
|---|---|---|
| Requirements and sourced hardware facts | 15 | Explicit boundaries, exact documented target, traceable sources and unknowns |
| Git history and review | 15 | Coherent changes and inspected diff |
| Firmware behavior | 25 | Alerts, hysteresis, parser, faults, recovery, reset |
| Verification quality | 25 | Independent expectations, meaningful defects rejected, replay and limitations |
| Documentation and handoff | 10 | Reproducible setup and current evidence pointers |
| Agent instructions and skills | 10 | Actual fresh-session and skill case records |
The suggested pass is 80 points plus mandatory reproducible build, critical software behavior, host/replay checks, and honest evidence labeling. A high total cannot compensate for invented results or missing critical behavior. Physical validation has a separate optional checklist and is not required for the core score.
Failure and recovery: a correct program, an incorrect claim
Suppose the software checks pass and the release notes say “sensor hardware verified.” Correct the claim to name the actual host tests, synthetic replay, and firmware compilation. Then list the unperformed physical checks. The correction does not erase the successful software work; it prevents readers from relying on evidence that does not exist.
If a required check fails, preserve its original reproduction and narrow the next change. A parser error needs parser evidence; an electrical hypothesis cannot explain a host-only fixture failure without an additional argument. Repair the demonstrated cause, rerun the affected checks, then perform the final integrated verification after the last relevant change.
Completion check, questions, and transfer
Complete the core capstone when the mandatory behavior and evidence gates pass, the rubric reaches its target, and another checkout reproduces the result. Account or dependency limits leave affected gates open; they do not authorize a passing claim from prepared scripts.
Why can 27.5 °C have two valid states? Inside the hysteresis band, the prior non-fault state determines retention. Starting state belongs in the test.
Can a valid command clear FAULT? No. It changes configuration but does not supply the two fresh valid samples required for recovery.
Why reject 28junk? The entire command must match the promised grammar. Parsing only a numeric prefix violates that contract.
Why check a wider value before narrowing? A slightly out-of-range input must not round into the allowed range during conversion.
What does fresh-checkout reproduction add? It checks the completeness of the distributed source and setup, exposing reliance on untracked or incidental local state.
Transfer the workflow to one bounded feature in another project: explicit requirements, a small plan, independent expectations, reviewed changes, real checks, and a current handoff. Keep the evidence boundary as precise as the feature itself.
Sources and figures
The accompanying CP08 and CP09 source, requirements, formats, fixtures, and checkpoint notes define the executable examples. The documented target remains Adafruit product 5477; physical behavior is not established by that reference. Figures: SS17-01 requirements, SS17-02 diff, SS17-03 tests, SS17-04 synthetic replay, SS17-05 private candidate. Optional HP06 requires a separately performed physical lab and is not part of core completion.




