Skip to content

Archive the completed v1 roadmap, open the V2 gate board - #22

Merged
d3mocide merged 21 commits into
mainfrom
workstream-12-focus-survey
Sep 6, 2026
Merged

d3mocide merged 21 commits into
mainfrom
workstream-12-focus-survey

Conversation

@d3mocide

@d3mocide d3mocide commented Sep 5, 2026

Copy link
Copy Markdown
Owner

docs/ROADMAP.md becomes the forward-looking V2 board: five workstreams
(12-16) with an explicit status/gate model, where every workstream passes
design entry, engineering, device behavior, claim truth, and release in
order. The completed v1 phase narrative, feasibility analysis, and exit
criteria move unedited to docs/history/ROADMAP_V1.md at the stable v1.0.7
tag, so v1 questions still have an answer without the active board carrying
1,000 lines of finished history.

CLAUDE.md, AGENTS.md, and the README indexes point at the V2 board and its
design-entry documents first, and at the v1 archive only when a question is
actually about phases 0-11. version.h's versioning comment and build.yml's
one ROADMAP reference follow the same move; neither changes behavior.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

d3mocide and others added 21 commits September 4, 2026 17:02
docs/ROADMAP.md becomes the forward-looking V2 board: five workstreams
(12-16) with an explicit status/gate model, where every workstream passes
design entry, engineering, device behavior, claim truth, and release in
order. The completed v1 phase narrative, feasibility analysis, and exit
criteria move unedited to docs/history/ROADMAP_V1.md at the stable v1.0.7
tag, so v1 questions still have an answer without the active board carrying
1,000 lines of finished history.

CLAUDE.md, AGENTS.md, and the README indexes point at the V2 board and its
design-entry documents first, and at the v1 archive only when a question is
actually about phases 0-11. version.h's versioning comment and build.yml's
one ROADMAP reference follow the same move; neither changes behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Focus asks one narrow question -- at this selected frequency, what did the
receiver observe for how long -- and answers it with one bounded, radio-owned
request that restores Watch on every exit path. It is bench-image only: there
is no operator control yet, `coverage` is deliberately blank in focus.csv, and
no coverage or activity label can be displayed until its thresholds are earned.

Firmware: a Core-1 one-bin request with a fixed 141-bucket RSSI histogram
(median/P90/peak with no heap allocation), a Core-0 focus.csv writer, and a
restore-before-publish terminal row. A request is bounded in wall-clock time as
well as in samples -- every sample waits on the shared SPI bus, so a contended
bus could otherwise stretch a pass past its dwell with the radio away from home
and nothing to stop it. Mutual exclusion with Probe, Sweep, Cell, and Scope is
enforced in both directions.

Also fixes a latent silent-drop in Serial Control's STATUS: its argument buffer
was hand-sized to 240 bytes against a frame budget of ~230, and an over-long
frame is dropped rather than truncated, so a long session's wider counters
would have lost the whole frame -- newest fields first, with no error. The
budget is now derived from the frame size and tested at saturation.

Evidence, all on real hardware:

- Cancel, injected failure, timeout, and four-way arbitration each restore home
  before their durable row is written.
- The controlled dwell matrix ran 900 trials with zero transport errors, zero
  queue or row drops, and home restored every time. 14 of 15 arms separated a
  controlled source from ambient 30/30 vs 0/30.
- Budgets are measured off the built image rather than estimated: 188 B of
  working state against a 256 B target, a 189 B worst-case row against a 256 B
  buffer, 1,072 B radioTask frame in a 6,144 B stack.

The matrix's most consequential result is a limit, not a capability: detection
tracks the source's airtime against Focus's sample spacing, not dwell length.
With a fixed 8 samples the spacing is dwell/7, so a 2,000 ms dwell observes
eight instants rather than 2,000 ms, and worst-case detection degraded
monotonically as source airtime approached that spacing until one arm stopped
separating entirely. That is the design's own "observation time is not
coverage" rule with numbers behind it, and it means a later slice must scale
the sample budget with the dwell before any operator-facing dwell or
observation figure can mean what it reads.

Consequently no single fixed RSSI condition separates every arm, so the
qualifying condition gets a documented candidate rather than an accepted
constant, and the coverage thresholds stay unselected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The closing paragraph still described the radio request/recovery path and
bench harness as upcoming work; both shipped. It also predated the matrix,
so it implied thresholds could be selected next -- but §6.4 showed the
sampling design has to change first, and any threshold or radio-away budget
chosen before that change would be measured against sampling the product
will not ship.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 2026-09-04 matrix showed detection tracks the source's airtime against
dwell/(samples-1), so choosing a real sampling policy needs to sweep spacings
down to ~10 ms across a 2,000 ms dwell -- 201 samples, past the old ceiling
of 64. The new 208 is bounded by the histogram rather than by RAM: buckets are
uint8_t, so a pass whose samples all land in one 1 dB bucket saturates at 255
and the histogram then refuses to report a percentile at all. The histogram is
a fixed 141 bytes whatever the sample count, so this costs no static RAM.

Also documents a bench hazard that cost this session a wrong flash. The
Cardputer and the bench Heltec enumerate identically and their ttyACMn
numbering swaps between resets; targeting a raw path put LoRaTrace on the
Heltec, which then failed its IO-expander init and read exactly like a dead
receiver. The framed harnesses are self-checking, but flashing is not, so
HARDWARE_TESTING.md now specifies /dev/serial/by-id and records the failure
signature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bench receiver's channel lived only in config.txt on the bench SD card,
so replacing a failed card silently moved home from 918.5/SF8/BW125 to the
built-in 906.875/SF11/BW250 default. That is not a cosmetic difference: Focus
receives at the *home* bandwidth, so measurements would have continued at a
different bandwidth than the run they were being compared against, with
nothing in the output to show it -- and at BW250 the +-125 kHz bin-offset
analysis inverts, since that offset falls inside the passband instead of
outside it. The bench image now pins the channel through build flags and says
so at boot. Production is untouched: benchHomeChannelOverride() returns false
and changes nothing without both LORATRACE_BENCH_FAULTS and the -D flags.

Harness fixes, each from a failure this session rather than from review:

- The matrix waits for SD to mount instead of failing on one early poll. SD
  readiness lags HELLO by seconds, so a healthy device read as a broken one.
- It refuses to run unless the resolved home channel matches --expect-home-khz,
  which is what would have caught the card swap immediately.
- --samples is repeatable, so a spacing sweep is one process with one
  pre-flight gate rather than several invocations that could each half-run.
- HELLO gets a 45s window. Opening native USB-CDC resets the board, but when
  that reset lands varies, and the fixed 8.5s settle could expire before boot
  had even started -- which read as an unresponsive device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first bench slice fixed the sample count at 8, which made a longer dwell
strictly worse at catching bursts: spacing grows with the dwell, so a 2,000 ms
pass observed eight instants rather than 2,000 ms. A 360-trial sweep held the
dwell at 2,000 ms and varied only the spacing. A 94 ms source was missed at
286 ms and 100 ms spacing (8/15 and 2/15 trials) and caught in 15/15 at both
50 ms and 20 ms, with the worst-case reading improving from -97 dBm to -66 dBm
between those two -- so 50 ms is the edge of working, not a safe choice.
Detection collapses once spacing approaches the source's airtime.

20 ms keeps roughly a 2x margin against the ~40-50 ms airtime of the fastest
realistic mesh traffic. That margin is a judgement about what is worth
observing; the measurement itself says only that spacing at or above the
airtime fails and half of it works.

Finer sampling costs nothing measurable: radio-away was 2,073-2,075 ms across
every arm whether the pass took 8 samples or 101, and the histogram is a fixed
141 bytes regardless of sample count. The cost of a longer dwell is the dwell.

focusSamplesForDwell() rounds the interval count up, because truncating would
let a 30 ms dwell take two samples 30 ms apart and quietly violate the
constant it derives from. It is split into two constexpr functions because the
device toolchain builds as C++11, where a constexpr body must be a single
return -- the native test environment accepts locals and does not catch it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bench measurements need real path loss between the transmitter and the
receiver, and USB does not reach that far. The transmitter now optionally
joins a network and serves the identical @LTTX/1 framed protocol over TCP, so
a host can drive it from across a house. The harness needed no change at all:
bench_harness.py opens ports through pyserial's serial_for_url(), which
already accepts socket://host:port.

Replies follow whichever transport issued the command and stay there. That is
not a detail: ARM's TX_STARTED/TX_DONE are emitted from loop() long after the
command returns, so a bridge-driven pulse would otherwise send its completion
evidence out a USB port nobody is reading, and the harness gates every
source-on trial on exactly those frames.

Credentials live in NVS, set once over USB by scripts/heltec_wifi_setup.py,
which prompts locally so a password never reaches a transcript or a shell
history. WIFI_SSID/WIFI_PASS are refused over the bridge itself: a credential
change should not be possible through the transport it authorizes, and a
wrong one would disconnect the sender mid-command.

WiFi.setSleep(false) is applied after association rather than before. Setting
it ahead of WiFi.begin() is silently discarded, leaving the station in modem
sleep -- which keeps the association looking perfect to the AP while dropping
broadcast frames, so the board answers no ARP and appears dead to everything
except its own logs. The fixture is mains powered; there is nothing to save.

NET_TEST dials outward and reports the result, which is what finally
distinguished "our firmware is broken" from "the network is blocking these two
clients". It showed the board reaching the gateway and the internet while
unable to reach the host, in both directions -- wireless client isolation, not
a defect. Documented so the next person spends minutes on it, not an evening.

SECURITY: the bridge has no authentication, and its commands key a
transmitter. It is off unless explicitly enabled, output stays capped at
TX_POWER_DBM, and every transmit path is bounded -- but anything that can
reach the port can fire a pulse. Trusted networks only; --disable when idle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 900-trial matrix proposed `p90 >= -90 dBm` while the bench source sat ~70
dB above ambient -- a regime where every metric separates and any threshold
looks defensible. Repeating the question at a realistic level (transmitter
outdoors, driven over the new WiFi control bridge; receiver antenna fitted, so
the ambient floor is real; source peak ~-85 dBm against -99/-100 dBm) across
120 trials, the candidate fails outright.

So does every floor-relative variant: peak-median, peak-p90, p90-median. One
of ten metric/position combinations separates, by 2.0 dB, which is inside
ordinary RSSI variance and would not survive a temperature change or a
different antenna. Absolute and relative families fail together, so this is
not a matter of having picked the wrong statistic.

Two mechanisms explain it. At low SNR `p90` collapses even when the source is
present for most of the window -- at one position the transmitter radiated
57-71% of the pass while `p90` still read -94 to -97 dBm against a -100 floor,
because the instantaneous RSSI during a weak transmission mostly fails to
stand above noise and only its best moments do. And real MeshOregon traffic
reached -94 dBm during source-off control trials, which is not measurement
error but an actual transmission that no RSSI condition can distinguish from
a controlled fixture.

Coverage reporting is untouched: valid passes, observation time, and the
median/P90/peak summary remain honest. What the evidence refuses is the step
from "RSSI was elevated" to "something transmitted" -- exactly the step §3's
contract was written to prevent. The design was right and the constant was
wrong, which is the outcome that measurement is for.

`coverage` stays blank and the activity count stays unpopulated. Two routes
remain untested: a per-pass count of samples above an adaptive floor, for
which `FocusObservation` already reserves `qualifying_count` and never
populates it; and CAD or packet evidence, which §3 already contemplates as
the alternative basis for observed activity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… time

Two 240 s arms against one independently timed reference train, transmitter
outdoors on the WiFi bridge so the link is real rather than a bench-adjacent
one. Watch alone received 0.883 of the train; Watch with Focus interleaved at
a 48.1% away fraction received 0.463. The 95% intervals do not overlap.

The useful part is the arithmetic. If Focus merely stops the receiver hearing
anything while away and costs nothing else, the predicted reception is the
baseline scaled by the time left at home: 0.883 x (1 - 0.481) = 0.459.
Measured 0.463, well inside the interval. So the loss is accounted for
entirely by away time -- no extra penalty from retuning, from recovery, or
from any lingering effect after home listening is restored.

That makes the away duration Focus already records an honest proxy for what a
request costs, which is what would make an operator-facing away-time display
meaningful rather than decorative. It equally means there is no mitigation:
half the listening time away is half the packets, and time between restored
Watch windows reached 7.45 s.

The budget decision itself is deliberately not made here. §6.3 requires it to
be an operator product judgement taken after the measurement, not implied by
it; this supplies the exchange rate and stops there. Limits are recorded with
the evidence: a 0.883 baseline rather than a perfect link, one duty cycle at
one dwell, and a fixed-interval fixture rather than real bursty traffic.

Also hardens the harness's timeouts the way the matrix runner was hardened,
after native USB-CDC truncation cost that run an abort.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every RSSI summary statistic was rejected at field levels, and the follow-up
named one untested idea: `peak` is a single sample and therefore noise-prone,
so count the samples above an adaptive floor instead of taking an extreme of
them. A pass with one stray strong sample and a genuinely busy pass have the
same peak; only a count tells them apart, and there is a host test for exactly
that case.

Bench images now retain the pass histogram and report a ladder of counts at
median + 2/4/6/8/10/15/20 dB. That is deliberate: it lets one run evaluate any
candidate rule offline rather than reflashing per guess, and it keeps the rule
relative to each pass's own floor, since an absolute threshold is precisely
what the previous measurement rejected.

Across 120 trials at the same porch configuration, `C6 >= 2` -- two or more
samples at the pass median plus 6 dB -- detected 57/60 source-on trials, 95%
CI [0.863, 0.983], against 1/60 source-off. `C4 >= 5` performs identically, so
the ladder has a plateau rather than one lucky point.

The single flagged control trial read -63 dBm against a -101 dBm median with
six elevated samples. Nothing 38 dB above its own noise floor is noise: that
is a real transmission that was not ours, correctly caught by the detector and
mislabelled by an experiment that cannot tell "our fixture is quiet" from "the
band is quiet". The 1.7% false rate is an upper bound, not a measurement
against silence. The three misses are all within a couple of dB of the floor,
which is where a detector should miss.

This is a candidate, not a constant, and `qualifying_count` stays unpopulated.
The threshold is a count out of 101 samples, and the sampling policy now
scales sample count with dwell, so two samples is 2% of a 2,000 ms pass and
33% of a 100 ms one. Expressing it as a fraction of accepted samples, or
validating it per dwell, is the next measurement rather than a decision to
take at a keyboard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two sweeps, 480 trials, no arm failures, same porch configuration.

The dwell sweep answers whether the rule is a count or a fraction. Roughly
4-5% of accepted samples transfers between 500 ms and 2,000 ms passes, so it
is a fraction. It does not rescue a 100 ms pass: with six samples no threshold
both catches the source and rejects ambient, at any margin. That is a floor on
the instrument rather than a tuning problem, and it means a short pass may
report coverage honestly while reporting no activity at all.

The occupancy sweep matters more, because it bounds what the rule can claim.
Holding dwell fixed and varying only the source's duty within the pass,
detection runs 90-93% at 43-57% occupancy and collapses to 37-43% at 28.6%. A
single SF8 packet inside a 2,000 ms pass is 3-7% occupancy -- far below where
this already misses more than half the time. So the rule detects a
persistently occupied channel; it does not detect individual packets.

That is not a defect to tune away, it is the honest scope of sampling energy
rather than detecting a signal, and it makes CAD or packet reception the
better-supported basis for §3's observed activity. §3 has contemplated that
alternative from the start; it is now an evidenced preference rather than a
hunch.

Median-as-floor survived every occupancy tested -- the pass median read -101
dBm for source-on and source-off alike. The reason bounds the result: at these
levels most samples read like noise even while the transmitter radiates, the
same effect that made p90 useless. A strong source at the same occupancy would
pull the median up and break the assumption, and this sweep did not reach that
regime or license any claim about it.

qualifying_count stays unpopulated. What to populate it with is now a design
decision with evidence behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both files were left behind by a scripted edit that failed partway through
after the design doc had already been updated, so a4bc335 recorded the
finding in one place and not the other two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up 5 concluded that the count rule "detects a persistently occupied
channel, not individual packets", and argued from there that CAD or packet
reception was the better-supported basis for an activity claim. Both came from
a single link at roughly 12 dB SNR.

Repeating the SNR-sensitive tests at roughly 25 dB overturns it. At 28.6%
occupancy, detection went from 11/30 to 26/30. The cliff moved, so it was
never the statistical limit it was presented as -- it was where that
particular link stopped delivering samples above the margin. Detection depends
on occupancy and SNR together, and single-packet detection is untested rather
than excluded: a 150 ms packet at 20 ms sampling yields about seven elevated
samples, above the four or five the best rules require.

The link change is worth recording on its own. A matched whip replaced the
stubby and both antennas were stood vertical; vertical alignment mattered more
than the antenna, worth about 13 dB at 918.5 MHz where the horizontal pair had
been sitting in each other's pattern null.

Two conclusions survived both links. The 100 ms floor is real -- six samples
per pass detected 23/60 at 12 dB and 28/60 at 25 dB, so thirteen decibels
bought almost nothing and it is a sampling limit rather than a link limit. And
the rule is a fraction of accepted samples, 3.8-5.0% at 12 dB and 4.0-7.7% at
25 dB, not a fixed count.

Recorded as a method note as well: three conclusions were drawn from one link,
two held and the most consequential inverted. Where a conclusion depends on
signal level it needs two levels before it goes in the evidence file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same partial-failure as b74b4b5: a scripted multi-file edit asserted against
STATUS text that had already been rewritten, so 9d5a286 carried the correction
in the design doc and evidence file but left the withdrawn claim standing in
the two places most likely to be read first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Workstream 12's measurements bear on Sweep whether or not that was the intent.
Phase 9 already recorded that a short bin dwell can miss genuine traffic; what
12 adds is the quantitative form -- detection needs airtime above the sampling
spacing, six samples per pass is below a usable floor and 13 dB did not rescue
it, and a count beats the extreme statistics Pass A currently thresholds on.

Parked as a candidate rather than acted on, for two reasons written into the
entry. Sweep aims at persistent energy across 200+ bins, where short dwells
are a reasonable trade and Pass B's CAD step already exists because a Pass-A
peak is not packet evidence -- the architecture encodes the distinction 12
measured. And 12 itself drew three conclusions from one link, two of which
held while the most confident inverted on a second, so any claim about Sweep's
sensitivity needs the same two-baseline discipline before it is written down.

Entry criteria are concrete rather than a call to go and look: a controlled
per-bin sensitivity measurement at two or more signal levels, reusing 12's
transmitter fixture and benchSweepFloorQuery's existing readback, plus a
decision on whether Waterfall should distinguish "sampled and quiet" from
"barely sampled" in what it draws.

Also adds the matrix runner's single-pulse mode, which places one armed
transmission inside a long window instead of a burst -- the only way to ask
whether a single packet is detectable, since a burst answers a different
question however its duty is tuned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s stale

One armed 148 ms transmission placed inside a 2,000 ms pass -- 7.4% occupancy
-- was detected 29/30 with 0/30 false positives at the stronger link. That
settles the question the occupancy withdrawal left open: an RSSI-sampling pass
can support a packet-level activity signal. A 148 ms packet sampled every
20 ms yields about seven elevated samples against the four the rule needs, so
nothing about the instrument prevented this. The first link did.

It is not unconditional. The same rule at the weaker link failed on sources
occupying four times as much of the pass, so the rate depends on link quality
the device cannot know. And the companion position carrying real traffic
measured worse rather than better: a -57 dBm event in a source-off control
both produced false positives and suppressed counts by lifting the median,
which means Focus is least reliable exactly where a band is busiest. Wording
an activity indication that survives that is a product decision now, not an
open measurement.

A placement analysis was attempted and discarded rather than reported. It
tried to time each pulse against its window from log timestamps, but
TX_STARTED reaches the host only when the harness next polls the socket, so
the timestamps record host polling rather than RF timing -- and the output
said so, classifying "missed the window" trials as detected at 71-97% while
"inside" detected 0/2. Timing a pulse against the window needs a device-side
timestamp.

Also reconciles the design doc, which had silently missed its edit in each of
the last three documentation commits: in both a4bc335 and 9d5a286 the assert
that failed was the design doc's, not STATUS's, and the follow-up fixes
patched the wrong file. It now carries the consolidated current position --
the rule's fractional form, the 100 ms sampling floor, the withdrawn occupancy
claim, and this result -- rather than a state from three commits ago. The
governing gate document being the stale one is the worst place for that error
to land.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
version.h records an 11 dB under-read on Cell from sampling RSSI before the
AGC settles after a light retune, and says outright that whether Sweep shares
it is unknown. It does, and larger: adding a 5 ms settle moves Pass A's silent
floor for bin 66 from -115.0 to -101.5 dBm, and 20 ms holds it at -101.7.
Focus independently measures the ambient floor at that frequency as -99 to
-101 dBm across hundreds of passes, so the settled readings are the real ones
and the shipped configuration is about 14 dB low.

It matters because Pass A's peak decision compares a bin against a noise floor
plus a margin, and both terms come from under-read samples. The margin
constant was calibrated against those same values so it is not obviously
wrong, but it is calibrated on a scale that does not correspond to real dBm.

Deliberately does not claim the other half. Whether Pass A can flag a bin
carrying traffic is still open: even at 20 ms settle the source produced only
+3.5 dB where Focus reads the same transmitter 25 dB above the floor, and the
candidate explanations -- partial overlap with a 3 ms bin visit, bridge
latency making "on air during this specific 3 ms" unguaranteeable, and a FULL
begin() row that reads lower than the settled light path -- are untested. Two
earlier attempts were invalid because firing only for a lap's duration
radiates after the lap ends, which read as a silent band in every arm; both
are recorded so the next attempt does not repeat them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight settle values, five laps each, five bins, transmitter idle. Silence is
the design: the floor is a property of the receiver and its retune, so
measuring it with no source removes every timing problem that invalidated the
first attempts at Sweep's detection question.

Readings are flat from 3 ms through 40 ms. The shipped 0 ms configuration
under-reads by 9.5 dB at bin 0 and up to 17.6 dB at bin 40; 1 ms recovers only
about half. The cost is one settle per bin times 85 bins, so 3 ms predicts
0.26 s added per lap and the measured 1.70 -> 1.74 s is that within noise,
against 5.60 s at 40 ms. Roughly 15% of lap time for 10-18 dB of accuracy.

A designed control failed and is recorded as such. Bin 0 was meant to be
unaffected because it takes a full begin(), and it moved 9.5 dB. The settle is
skipped only when the global FULL retune mode is on -- the condition is
!benchSweepRetuneFullEveryBin(), not a per-bin test -- so in shipped LIGHT
mode bin 0 gets both the full begin and the settle. It shifted least, which is
consistent with a full begin settling partially, but that is an observation
rather than a demonstrated mechanism, and the clean control is FULL against
LIGHT+3 ms across all bins.

Not acted on. A 3 ms settle is the obvious fix and cannot be made alone: the
margin constant was calibrated against under-read values, so moving the settle
without recalibrating the margin changes Sweep's peak decisions in an untested
direction. Also unexplained: the settled floor is not flat across the band,
bin 40 sitting about 8 dB above bins 20 and 66 at every settle from 2 ms up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fully passive: 68 laps with two live repeaters as the sources and the bench
transmitter explicitly quieted, so this injected nothing into a live mesh.
Settle alternated 0 ms and 3 ms lap by lap, and seven control bins were read
in the same lap as the two traffic bins, so the comparison needs no external
ground truth.

The margin constant is 35 dB above the noise floor. Applying it to what was
measured: at the shipped 0 ms settle the best traffic reading sat 19.9 dB
above the floor and would not have been flagged; at 3 ms the same traffic sat
46.3 dB above and would have been. Pass A would have missed both repeaters as
shipped and caught both with the settle.

The reason a 13 dB floor error becomes a functional failure is that the signal
is under-read much harder than the floor: the same repeater burst read -95.3
dBm at 0 ms and -56.5 dBm at 3 ms. That matches version.h's note that the
under-read grows with signal strength, and it means the two errors do not
cancel -- the excursion the margin tests collapses from 46 dB to 20 dB and
falls under the threshold.

Deliberately claims no rate. Two detections in 34 laps per configuration is a
tiny count, the hit rate is bounded by a ~3 ms bin visit coinciding with a
burst rather than by sensitivity, and the 0 ms versus 3 ms hit counts are not
distinguishable at that size. The evidence here is the magnitude of the
excursion, not the number of hits.

Recommendation recorded but not applied: settle and re-derive the margin as
one change. Shipping the settle alone would apply a margin calibrated against
under-read values to correctly-read ones. Workstream 17 moves to Design entry.

The run ended at lap 68 of 200 on a USB-CDC transport timeout; this harness
lacks the retry hardening the Focus matrix runner has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
300 laps, zero skipped, 62.7 minutes, transmitting nothing -- two live
repeaters as the sources, alternating 0/3 ms settle so both configurations saw
the same traffic, and seven control bins read in the same lap.

Judged by Sweep's own 35 dB threshold: the shipped 0 ms configuration flagged
neither traffic bin in 150 laps, with a best excursion of 19.0 dB. The settled
configuration flagged both, 3/150 each, with a best excursion of 51.6 dB. The
shipped arm was not a near miss; it was short of its own threshold by a factor
of two in dB.

Specificity is clean in both arms. No adjacent bin and no control bin was ever
flagged -- about 1,650 non-traffic bin observations per arm, zero false flags.
When the settled configuration flags something it flags the two channels that
actually carry traffic.

The flag rates remain statistically unseparated and the writeup says so:
0/150 is [0.000, 0.025] and 3/150 is [0.007, 0.057], overlapping on [0.007,
0.025]. Separating those needs about 300 laps per arm and this run was sized
before that arithmetic was done. It does not weaken the conclusion, because
the rate follows from the margin arithmetic rather than evidencing it -- a
configuration whose best excursion is 19.0 dB cannot clear 35 dB at any sample
size. Also recorded: the ~2% rate at 3 ms is a coincidence rate set by the
~3 ms per-bin visit, not a sensitivity figure, and it binds a fixed Sweep too.

Fix still unapplied: settle and re-derive the margin as one change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@d3mocide
d3mocide merged commit c16a7b5 into main Sep 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant