13 — Build Firmware in Small, Verifiable Steps
The first useful firmware milestone is a small behavior you can explain and check. In this chapter, Sensor Monitor will accept explicitly synthetic readings, track whether they are usable, and report its state. You will follow the same policy source into two builds: a native host program and an Arduino sketch. Their results answer different questions, and your evidence record will keep those questions visible.
Start from CP05 in a new exercise folder after completing the interface plan in Chapter 12. Earlier CP01–04 snapshots contain the minimal fixed-reading baseline. CP05 introduces the actual replay adapter and normal/fault policy. It does not yet implement alerts, hysteresis, or threshold commands. Those features arrive in Chapter 17. A high temperature in this checkpoint can therefore remain NORMAL; that is not a missing capstone fix.
Establish the baseline before editing
Open the extracted sensor-monitor folder in the editor. Read its README, then inspect dependencies.json and scripts/common.cmd. The manifest identifies the toolchain and exact board target; the common wrapper names the shared sources used in native compilation. Generated executables belong under ignored build/, not among the source files you intend to review.
Run the initial host path from PowerShell:
.\scripts\replay.cmd fixtures/normal.csv
$replayExit = $LASTEXITCODE
Save the output and the exit value before running unrelated native commands. A successful process exits zero. The fixture contains eight events, and CP05's expected states are FAULT, NORMAL, NORMAL, NORMAL, NORMAL, FAULT, FAULT, NORMAL. The first and seventh samples are fresh but still recovering. The sixth contains an explicitly nonfinite synthetic input.
If the wrapper cannot find the compiler, inspect the actual executable location and set $env:CXX for the current PowerShell session. The supplied wrapper detects the recorded user-local LLVM-MinGW layout and adds its directory to the wrapper process PATH for runtime DLLs. It does not change system PATH or script execution policy. Diagnose a missing executable or runtime dependency before asking the assistant to rewrite policy code.
Now cross-compile the unmodified sketch:
.\scripts\build-firmware.cmd
$firmwareExit = $LASTEXITCODE
The command uses the recorded Feather target and produces firmware artifacts; it performs no upload. Retain the actual build output, including target options, errors if any, and exit. The final CP09 release has a verified Windows build from a fresh learner clone, but this chapter's CP05 stage and your local edit require their own receipts. A reader should run and record the build in their own environment rather than inherit a passing claim from another source version.
Follow the reading through four concerns
Open host/replay.cpp. Its job is to interpret fixture events, supply timestamps and sample values, and send the resulting status to output. It is an adapter between a file format and application logic. The policy does not need to know the filename, whether the line ending is LF or CRLF, or how the terminal displays the result.
Next open firmware/sensor-monitor/monitor_policy.cpp. This source decides whether a reading is current and whether the state is NORMAL or FAULT. The native wrapper compiles this file directly. The Arduino sketch directory also contains and compiles it. There is no separate imitation of the policy in the host test program.
output_format.cpp renders the snapshot as a bounded JSON line. It converts unavailable temperature to JSON null, names the state and error, and includes the synthetic label. The policy supplies meaning; the formatter supplies representation. Keeping those responsibilities separate lets you change a report without changing the recovery rule.
Finally inspect sensor-monitor.ino. It supplies a known sequence of synthetic values using Arduino's time and serial interfaces. No physical sensor library is included. The sketch provides a different acquisition/output environment around the shared logic, which is useful for compilation practice. It does not transform the host replay into a simulation of electronics or USB behavior.
Four concerns are enough here: acquire input, decide policy, render output, and drive the surrounding loop. They do not require a framework or a class hierarchy. A separation is useful when it makes a question easier to test or a change easier to inspect. Adding layers merely to make the diagram larger works against that goal.
Make failure a deliberate state
Sensor Monitor begins in FAULT with missing input. A first fresh valid reading is useful evidence, but the policy remains in FAULT/recovering. A second consecutive valid reading allows NORMAL. This rule makes recovery observable and avoids claiming that a single good result has erased the preceding uncertainty.
The distinction between state and current data is important. During recovery, a sample can have a numeric temperature while the state remains FAULT. With missing, nonfinite, or stale input, the temperature is unavailable and the output uses null. Do not infer data freshness solely from the word NORMAL or FAULT.
In the normal fixture, the token nan intentionally supplies a nonfinite floating-point value. The adapter recognizes that token as a valid fixture instruction for an invalid reading. The policy then enters FAULT/nonfinite. The process can still exit zero because it successfully executed the requested scenario. An expected fault state is not the same as a broken test runner.
By contrast, the token 28C is malformed in the fixture grammar. It is neither a valid numeric sample nor one of the explicit special tokens. The adapter rejects that row and reports a line-number diagnostic with exit two. Keeping input-file errors distinct from modeled sensor failures will make the next chapter's diagnosis much clearer.
Give time an explicit meaning
The fixture supplies a 32-bit timestamp named uptime_ms. In replay, that number is injected software input; it is not a measurement of how long the process took to run. The program can evaluate eight seconds of modeled events in a fraction of a wall-clock second. That speed is useful for tests but must not be reported as measured device timing.
A reading remains current at an age of exactly 3000 milliseconds. It becomes stale when its age is greater than 3000. The policy checks elapsed age using unsigned subtraction and assumes chronological calls within the documented horizon. Chapter 15 will test the equality boundary and rollover explicitly. For now, keep the rule beside the code so “three seconds” does not become an ambiguous comparison.
Run the supplied stale sequence:
.\scripts\replay.cmd fixtures/stale-recovery.csv
$staleExit = $LASTEXITCODE
The sample at timestamp 1000 is still current at timestamp 4000. At 4001, the output becomes FAULT/stale with a null temperature. A valid sample at 5000 begins recovery; a missing sample at 6000 resets it. The valid samples at 7000 and 8000 then complete a new recovery. Each event has a reason, so you can predict the sequence before running it.
The scheduler in monitor_policy.h addresses a different time question: when is another acquisition due? It allows one acquisition immediately and then no earlier than one second after the previous acquisition. A delayed loop does not manufacture a burst of catch-up readings. A scheduler, a freshness rule, and a driver's actual execution duration are three separate things.
Ask for one change with one observable outcome
A useful agent task names the smallest behavior that needs to change. “Improve the sensor monitor” gives little basis for rejecting a broad rewrite. “Expose the snapshot's current-data flag in each sample JSON object without changing policy” is a reviewable task with clear inputs and output.
For the worked example, add a JSON Boolean field named current. The snapshot already contains this flag, so acquisition and policy need no modification. A status with current data should print true; missing, nonfinite, or stale data should print false. A recovering sample can correctly print true while still reporting FAULT.
Use this brief:
In this disposable CP05 copy, add a Boolean current field to each sample JSON object. Use Snapshot.current and keep the existing fields. Change only the status formatter and the reporting description. Do not modify acquisition, normal/fault rules, timing, dependencies, or the board target. Run the normal and stale-recovery fixtures, inspect the diff, and report actual command results. Cross-build the shared-source change when the recorded toolchain is available.
Ask the assistant to inspect the source before proposing a patch. Its explanation should point to the existing snapshot field and the formatting call. If it proposes a new global variable, a sensor driver, or a threshold parser, return to the brief. Those changes do not help answer this task.
Review the formatter change
The status formatter already uses bounded snprintf. Extend its format string with ,"current":%s before the closing brace and supply v.current ? "true" : "false" as the corresponding argument. The Boolean values are unquoted JSON literals. The existing completion check still needs to confirm the whole line fits the output buffer.
The relevant ending of the format string changes conceptually from:
"\"state\":\"%s\",\"error\":\"%s\"}"
to:
"\"state\":\"%s\",\"error\":\"%s\",\"current\":%s}"
These are focused excerpts from a larger string expression, not standalone replacement functions. Read the full call and match every placeholder with its argument. An extra %s without its value is a formatting defect even if the surrounding code looks plausible. Your compiler warnings and actual output are part of the review.
Adding a field changes the output contract. Update the local exercise documentation to say which consumers should expect it. Do not claim compatibility with a strict parser that rejects additional fields unless that parser was checked. Later chapters start from their own published checkpoint; this disposable reporting extension does not silently become a requirement for CP07 or CP09.
LAB13 — Expose freshness without changing policy
Allow forty-five to sixty minutes. Record your clean CP05 baseline, create a branch if you initialized a local practice repository, and apply the bounded reporting change. Keep the original checkpoint ZIP available for recovery. If you do not yet have a Git repository in this exercise folder, compare the changed files with a separately extracted baseline rather than pretending an absent history exists.
Run normal and stale-recovery fixtures after the change. Inspect the full JSON lines first. Then use PowerShell's JSON parser to inspect the new field without relying on visual alignment:
$lines = .\scripts\replay.cmd fixtures/stale-recovery.csv
$runExit = $LASTEXITCODE
$records = $lines | ForEach-Object { $_ | ConvertFrom-Json }
$samples = $records | Where-Object { $_.type -eq 'sample' }
$samples | Format-Table uptime_ms, state, error, current
A parsing error matters; do not discard it because some lines look correct. For the stale fixture, expected current flags are true, true, true, false, true, false, true, true. In particular, the 5000 and 7000 recovery records have current data while still in FAULT. The new field must express the snapshot meaning, not simply test whether the state equals NORMAL.
Cross-compile the modified shared source with the unchanged manifest target. Save its actual exit and output separately from the host result. If the compiler or package is unavailable, record that exact open check. Do not substitute an older firmware file or a reassuring agent response.
Review the diff last as well as first. The intended implementation changes the status formatter and its documentation. A changed recovery counter, delay, FQBN, or library dependency requires an explanation against the brief and usually indicates scope drift. Remove unrelated edits before saving a teaching commit.
Failure, diagnosis, and recovery
Suppose an assistant implements current as state != FAULT. The normal case may look fine, but recovery records become false even though they contain fresh numeric readings. The stale-recovery fixture exposes that mistake without a board. Correct the mapping to the existing snapshot flag, rerun the same sequence, and preserve the before/after evidence.
Suppose the output contains "current":"true". That is a string, not a Boolean. A human can read it, but a machine consumer may treat it differently. Parse the JSON and inspect the field's type rather than merely searching for the word true. Stable machine-readable formats are contracts between programs, not just attractive terminal text.
If a patch causes widespread compiler errors, inspect the small format-string edit before adding dependencies or replacing the project. Restore the formatter from the known baseline if necessary, confirm the baseline still runs, and reapply the change in a smaller step. A recoverable failure is part of the workflow; a hidden failure is an evidence problem.
Worked answer and questions
A successful solution reads Snapshot.current directly, emits JSON Boolean literals, retains all existing sample fields, and leaves policy and acquisition unchanged. Its stale-recovery output matches the eight expected flags and preserves the previous state/error sequence. Its report identifies the source change, host results, firmware result or open gate, and unperformed physical checks.
Why can FAULT coexist with a current temperature? Recovery requires two consecutive valid samples. The first is fresh data, but the policy deliberately has not returned to NORMAL. Freshness and readiness are related but distinct properties.
What does a firmware build prove here? It establishes that the selected toolchain compiled the current source for the recorded target. It does not establish upload, USB enumeration, actual sensor input, or physical timing.
Why does the nan fixture scenario exit zero? The file is valid and the expected policy behavior is a nonfinite fault. A process failure would be a different event, such as a malformed row or failed compilation.
Why use the shared source on the host? It allows controlled input to exercise the actual policy being compiled into firmware. A separately reimplemented “simulation” could pass while the production policy retained the defect.
Transfer and evidence to keep
Read a saved log with the next maintainer in mind. A filename such as output.txt says little about its input or source. A short accompanying note can identify CP05, the exact fixture, the compiler, the formatter change, and the run's exit without putting personal machine details into the sample output. Keep configuration and evidence identity outside the stable sample schema unless consumers actually need them in each record.
Also preserve the distinction between an expected transcript and captured output. The state sequence printed in this chapter explains the required behavior; your terminal run establishes what your local executable did. If a result differs, retain the difference and investigate it. Replacing the captured text with a reference sequence would erase the information you need to debug. A useful handoff can include both, labeled clearly, with a short explanation of any unresolved mismatch.
Choose a reporting change in your own project that can be made without changing acquisition. Define its field name, type, meaning, and expected boundary behavior. Identify a sequence that would expose a misleading implementation, then ask for the smallest patch and inspect its actual output.
Keep the brief, reviewed diff, fixture identity, tool versions, command exits, and a short limitation statement together. This evidence is small enough to understand and complete enough to reproduce. Chapter 14 will use the same discipline when the starting behavior is wrong and several explanations compete.
<!-- Production figures: SS13-01 baseline build; SS13-02 synthetic acquisition/shared-source diff (no physical driver); SS13-03 exact-target cross-build; SS13-04 actual synthetic normal replay; SS13-05 actual invalid/missing FAULT replay. Full guest captures and stage-specific CP05/lab firmware receipt pending coordinator; final CP09 Windows build separately verified. HP04 optional/unperformed. -->



