audit: the port's pass — the scan that could not walk, and the drift a generator ends

--scan's walk was unwalkable on POSIX: probe rates have no termios
B-constant, so the first off-nominal probe raised out of the loop. The port
speaks termios2 BOTHER now (red-proven on a pty at 9984 Bd), the probe's
open lives inside the walk's error handling, an fd no longer leaks on an
unmakeable rate, and the swallowed unknown-signature reply is named at
timeout instead of reported as silence. CMakePresets.json's generator emits
the submodule toolchain path it had drifted from — a hand edit on a
generated file, exactly the class rule 10 exists for — and presets.generated
gates the pair from here on (the  marker CMake rejects at the
presets root stayed out; the check is the guard). The over-slot image guard
the tsb runner gained reaches the pureboot runner too; the GPIO bridge's
delivery comment states the hardware truth (RXC at the stop bit's sampling
point); the hardware suite gains the scan check — the one place the rate
physics is real; and the libavr pin advances over both audit rounds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 10:12:17 +02:00
parent 1b18f10f4e
commit 7716e1e291
8 changed files with 193 additions and 53 deletions

View File

@@ -7,11 +7,14 @@ port's TUs compile identically; the sims prove nothing new there) exist for
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
is never built, one chip per hardware class and pack vintage is.
Run from the repo root: tools/make_presets.py
Run from the repo root: tools/make_presets.py — or with --check, which
verifies the committed file matches this generator and edits nothing (the
ctest entry `presets.generated` runs that, so drift reds the gate).
"""
import json
import os
import sys
CHIPS = [
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
@@ -41,7 +44,7 @@ def main():
"hidden": True,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
"toolchainFile": "${sourceDir}/libavr/cmake/avr-toolchain.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
@@ -72,6 +75,9 @@ def main():
for chip in REFLECT_SPOT:
add(chip, "reflect")
# CMake rejects unknown fields in the presets root, $comment included, so
# the file cannot carry a generated-file marker; the --check ctest is the
# whole of rule 10's guard here.
presets = {
"version": 8,
"configurePresets": configure,
@@ -79,12 +85,19 @@ def main():
"testPresets": test,
"workflowPresets": workflows,
}
rendered = json.dumps(presets, indent=1) + "\n"
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
if "--check" in sys.argv[1:]:
current = open(path).read() if os.path.exists(path) else ""
if current != rendered:
print("CMakePresets.json does not match its generator — run tools/make_presets.py")
return 1
return 0
with open(path, "w") as f:
json.dump(presets, f, indent=1)
f.write("\n")
f.write(rendered)
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
return 0
if __name__ == "__main__":
main()
sys.exit(main())

View File

@@ -72,6 +72,35 @@ class Suite:
except Exception:
pass
def scan(self) -> None:
"""The --scan walk against real termios and a real oscillator: every
probe rate must open a port (the off-nominal rates exist only through
termios2), and one probe must answer — the nominal on a healthy board,
a neighbor on a drifted one. The rig injects the one reset per probe
the operator supplies in the field; this is the rate physics the
simulator cannot arbitrate (a pty carries bytes at any rate), pinned
on silicon."""
module = pbrig.load_pureboot(self.rig.d.pureboot)
found = None
for pct in module.scan_ratios():
rate = module.scan_rate(self.rig.d.baud, pct)
self.rig.reset()
try:
port = module.Port(self.rig.d.port, rate)
except module.Error as error:
self.check("scan opens every probe rate", False, f"{rate} Bd: {error}")
return
try:
module.Loader(port).connect(min(self.rig.d.wait, 6.0))
found = pct
break
except module.Error:
continue
finally:
port.close()
self.check("scan finds the board's rate", found is not None,
"no probe answered" if found is None else f"{found:+d} % of {self.rig.d.baud} Bd")
def eeprom(self, info) -> None:
size = info.eeprom_size
if not size:
@@ -161,6 +190,10 @@ class Suite:
print("\nthe loader never answered; nothing below can be trusted")
return 1
if not self.rig.d.autobaud:
print("\nscan")
self.scan()
print("\nEEPROM")
self.eeprom(info)