You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
RS-12.15: the streaming tractor's own clock drives its receiver between trains - #152
Merging changes no board. Both bench L072s still run 0c1bb0a9. Flashing them with this build and the on-air legs V, W and X need the operator's GO.
Before: on FHSS (profile 1), the base's commands rarely reached a streaming tractor. Legs H and I of RS-12.12 delivered 1 of 17 and 49 of 281. On v2, legs R, T and U delivered 19 of 72, 2 of 43 and 7 of 49. Profile 2 delivered 56 of 58. The tractor's receiver followed the hop clock only while its scan machine was LOCKED. Between camera trains it stayed on its last TX channel or on the scan walker's channel, while the base sent on the tractor's grid.
After: while the tractor holds originator authority (8 or more of its own transmissions, each less than 1 s apart), its own TX clock drives its receiver in any scan state, and the scan walker leaves the synthesizer alone. In the new SIL, the tractor hears every command that does not collide with its own transmission. The base, as a follower, behaves exactly as before.
SIL sequence
before (clean commands heard)
after
between trains, light
17/59
59/59
between trains, storm
81/85
85/85
leg H shape
15/28
28/28
leg I shape
159/182
182/182
scan FAILED, then commands
10/33
33/33
final scan FAIL with a command on air
0/1
1/1
The SIL channel is ideal, so these rates are higher than on air. The pattern is the point: every miss before the fix is a wrong channel (in the last row, a receiver in standby), and every miss after it is a half-duplex collision.
How
sx1276_rx_grid_policy.[ch]: a follow rule. follow = (LOCKED && adopted) || originator authority. Authority needs 8 own TXs each less than 1 s apart, and lapses 1 s after the last one.
sx1276_rx.c: sx1276_rx_slot_follow gates on that rule instead of LOCKED. The scan walker skips only its hardware retune while the node holds authority. Its state, timers and counters keep running, so it resumes within one 500 ms dwell after authority lapses. The rule reads a fresh platform_now_ms(). The pass's now_ms is read before a TX_DONE noted in the same pass, so with it the unsigned gap wrapped to about 49 days and dropped authority. At demotion, the followed-slot marker is cleared only when the clock is reset. The final scan FAIL no longer puts the radio in standby while the node holds authority, because the slot follower re-arms only at the next slot boundary. The FAILED state and its fault report are unchanged. A walker step skipped under authority still clears the scan window's IRQ and CRC flags, so each RX_SCAN_FAILED sub-byte describes its own window, as on main.
New make check-rx-reverse-delivery (bench/host_proto/rx_reverse_delivery.c), part of the make check aggregate. It links the real sx1276_rx.c and sx1276_tx.c against a simulated SX1276, clock and air, driven in main.c's loop order, with SPI timed at the 2 MHz clock. It has 10 sequences, plus structure checks on every pass. One structure check is that no scan action takes the receiver out of RX while the node holds authority. Sequence 10 sends commands that fail their CRC early in a scan attempt; the FAIL sub-bytes that follow are the same as main's firmware gives.
Docs: in DESIGN-CONTROLLER/TODO.md, RS-12.15's reverse-delivery half is marked in progress, with its status, the bench and on-air steps, PASS criteria and open items. The 2026-09-12 SHARPENED note, which the RS-12.14 title had split in two, is put back together. The D-VS9 line in v25 TODO.md is updated. murata_l072/README.md and l072-bench-md5.yml carry the bench PC's new md5 pair, and the README records that CI's Linux build does not match the bench PC's.
One deliberate change: a node LOCKED on its own grid without authority (the RS-13 decay window) no longer follows it. In sequence 7, with a 2.5 s camera stall every 9 s, the tractor followed its own clock 18 times inside the decay window before the fix, and 0 times after it. The window itself is unchanged.
With the four radio files put back to main, the new target fails 21 checks, with the "before" numbers in the table above. With both review fixes taken out it fails 4, and with only the flag fix taken out, 1.
Sequence 5 (the base as follower) hashes every radio-affecting call. The hash is the same before and after the fix (fd9e8250). The wire format, the STATS layout, and the adopt and demotion logic do not change.
The two changed .c files compile clean with gcc -std=c11 -pedantic -Wall -Wextra -Werror.
The bench PC built both images at 13f20aa1 with Arm GNU 12.2.MPACBTI-Rel1 (8a22657): bench e083ae46a64686031b39866eca42e094, production 148de24e1f9acce417c46c2c69a5be6b, 24 972 B each, the same after a clean rebuild, with mingw32-make check passing. The same toolchain first rebuilt main's pair (0c1bb0a9 / 589c1203). CI's Linux build of that release gives bench 81a98296… and production 70a6418f…, so the md5 job's warnings are the known difference; the committed build/firmware.bin is the bench PC's.
Open for the design owner
Decay window. At 1 fps, or about 3.5 s after a stall of 1 s or more, the tractor holds no authority, and it behaves as before. Closing that gap is a grid-policy change that touches the F6 recovery tier.
Lead tolerance. The SIL margin before the tractor adopts an echo is about 0.5 ms. Add a tolerance to the originator's leading-grid test only if validation shows the tractor adopting.
Scan FAILED. Self-follow also runs in scan FAILED, as coded. The alternative is to exclude it, and then the final FAIL would put the radio in standby again.
Collisions. The misses left are half-duplex collisions. D-VS9's reserved control slot is the place for them.
Not done here
On-air validation, legs V, W and X in TODO RS-12.15, on profile 1 with both L072s flashed. This needs the operator's GO.
…r between trains (reverse delivery)
RS-12.15 reverse-delivery half (opened 2026-09-12). sx1276_rx_slot_follow()
ran only while the scan machine was LOCKED, and LOCK demotes 2 s after the
last valid frame -- which the streaming tractor, transmitting most of the
time, rarely receives. Between trains its receiver sat on its last TX
channel (tx_cleanup re-arm) or on the scan walker's channel, while the base
hops every command onto the tractor's own grid: 1/17 and 49/281 commands
heard on profile 1 (RS-12.12 legs H/I), 19/72, 2/43 and 7/49 on v2 (RS-12.15
legs R/T/U), vs 56/58 on profile 2. v2 (PR #125) kept the self-anchored
clock valid across demotion but did not change what drives the receiver.
- sx1276_rx_grid_policy.[ch]: the follow rule.
follow_own(now) := is_originator(now, clock_valid, adopted)
follow(now, locked) := (locked && adopted) || follow_own(now)
The originator's own TX clock IS the shared grid, so while it holds
authority (>= 8 own TX_DONEs each < 1 s apart; decays 1 s after the
last) it drives the receiver in ANY scan state. An adopted grid drives
it only while LOCKED, as before.
- sx1276_rx.c: slot_follow gates on follow() instead of LOCKED; the scan
walker (BEGIN_SCAN / ADVANCE_CHANNEL dispatch) skips only its HW retune
while follow_own() holds -- the scan machine's state, anchors, timers and
counters run on, so a SCANNING walker resumes within one dwell (500 ms)
of the authority decaying. Both read a fresh platform_now_ms():
sx1276_tx_poll() notes the own TX_DONE with a later clock read than the
pass's now_ms, and the wrap-safe gap test read the older now_ms as a
~49-day gap (the SIL caught a walker step parking the receiver off the
grid for the rest of a slot that way).
- Demotion edge: forget the followed slot only when the clock is reset. A
kept clock may be driving the receiver; forgetting re-tuned it to the
same channel mid-slot, restarting any reception in flight. Unobservable
before (nothing followed a kept clock until the frame that re-LOCKed
re-marked the slot).
Unchanged: the wire format and STATS layout; the follower and recovery
paths. Only an accepted header LOCKs, and a node without authority adopts
every accepted header, so a node that never holds authority (the base:
tx_stream_streak_max = 1 on every leg on record) is LOCKED only on an
adopted grid -- its walker, LOCKED-only follower and demotion reset are
exactly as before (check-rx-reverse-delivery, next commit, finds its
radio calls identical to the pre-fix file, call for call and microsecond
for microsecond). consider_remote / adopt / on_demotion are untouched.
One deliberate change on the originator's own grid: LOCKED there WITHOUT
authority (an own-TX gap >= 1 s while an echo still holds the lock -- the
RS-13 decay window) no longer follows; a frame heard there is handed
UNANCHORED and adopted, so tracking the own grid only fed the
RS-12.12/14 renumbering.
make check and every check-* green with host gcc 13 (-Werror); the
changed TUs also compile clean with the production flags (-Os,
-Wno-unused-*, 32-bit freestanding, routed and unrouted). Production
binary NOT rebuilt (no arm-none-eabi-gcc in this container) -- rebuild
and record the md5 before flashing. Nothing flashed; on-air validation
needs GO.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xHffg93jSdYuYEWpSUWeR
…ainst the real sx1276_rx.c/sx1276_tx.c
bench/host_proto/rx_reverse_delivery.c (new) links the REAL sx1276_rx.c and
sx1276_tx.c (+ the HW-free TUs they call) against a simulated SX1276
(registers, FIFO, IRQs, modes, synthesiser; SPI timed at the 2 MHz SCK), a
clock that advances inside busy-waits and SPI traffic, and an air model,
and drives them in main.c's loop order. Peers are models: a camera-daemon
stream (pipeline depth 2, 40 ms train gap, raw RXCONT re-arm) whose base
learns the grid from every tractor header the way its own firmware does
(anchor_rx on its own drifting ms clock after the FIFO read; slot rule,
phase byte after the PLL settle), or an ideal originator for the
base-role sequences. The channel is ideal: a command is lost only to a
half-duplex collision or to the receiver not listening on its channel
from its preamble on.
8 sequences:
1/2 between trains, light / storm command planes
3/4 camera stream + aligned pump at train end (leg H / leg I shapes)
5 follower unchanged (base role)
6 authority bound (a base burst of 10 commands 120 ms apart)
7 decay window (2.5 s camera stall every 9 s): no own-clock follow
while LOCKED without authority
8 scan FAILED (150 s without commands), then commands
plus per-pass structure checks in every sequence: the walker retunes
exactly when the scan machine dispatches unless the node holds
authority; every follow retune is allowed by the rule and lands on the
clock's channel; every own TX is keyed on its header slot's channel.
Clean commands received while the tractor held authority, pre-fix -> fix:
1: 17/59 -> 59/59 2: 81/85 -> 85/85 3: 15/28 -> 28/28
4: 159/182 -> 182/182 8: 10/33 -> 33/33
(all sent: 17/62 -> 59/62, 81/86 -> 85/86, 15/34 -> 28/34,
159/225 -> 182/225, 10/35 -> 33/35; every remaining miss is a half-duplex
collision). Pre-fix misses are wrong-channel: the receiver on the walker's
or the last TX channel. The pre-fix rates are higher than on air (ideal
channel, 6 ms pump latency) but the mechanism is the one on record.
Against the pre-fix firmware the target fails 16 checks; against the fix
it passes, and sequence 5's FNV-1a trace over every radio call (time,
caller, argument) is identical to the pre-fix file's (fd9e8250).
Added to the `check` aggregate. CI's `make check` step runs it; the
workflow lists individually only targets outside the aggregate, so no
workflow edit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xHffg93jSdYuYEWpSUWeR
… stale
- DESIGN-CONTROLLER/TODO.md: RS-12.15's reverse-delivery half is in
progress. Its entry gives the fix (de2ae16 + f9cc98b), the SIL results,
the bench build step, the on-air legs V/W/X with PASS and stop criteria,
and the items open for the design owner (decay window, lead tolerance,
self-follow in scan FAILED, half-duplex collisions). The 2026-09-12
SHARPENED note had been split by the RS-12.14 title; it is back in one
piece under RS-12.15.
- v25 TODO.md: the D-VS9 line says the base -> tractor fix is coded and
waits for the bench build and the operator's GO.
- murata_l072/README.md: the expected md5 pair is stale until the bench PC
builds both images. CI's Linux build of the same toolchain release has
not matched the bench PC's (bench 6f5073c0..., production 7983744b... on
2026-10-11 and on #150), so the md5s come from the bench PC.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xHffg93jSdYuYEWpSUWeR
…e node holds authority
Copilot's review of #152 found that the final-FAIL branch of
scan_dispatch_action() still put the modem in standby while the originator
followed its own clock. sx1276_rx_slot_follow() re-arms only at the next
slot boundary, so a command on air at that moment was aborted, and one
later in the same slot was missed. The standby is now skipped under
authority. The FAILED commit and the fault URC are unchanged.
check-rx-reverse-delivery gains a per-pass structure check (no scan action
takes the receiver out of RX under authority) and sequence 9: a clean
command on air at the final FAIL, 0/1 before, 1/1 after. The base model's
key-up is split out of base_send_cmd() so a command can be keyed at a given
time. Sequences 1-8 print the same numbers; only sequence 8's trace hash
moves (one standby call fewer), and sequence 5's stays fd9e8250. Against
the pre-fix sx1276_rx.c the target now fails 20 checks; against the
previous head, 3.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xHffg93jSdYuYEWpSUWeR
This return also skips the per-window s_scan_got_any_irq and s_scan_crc_seen resets below. While originator authority persists, a CRC error can remain latched across later dwells and non-final FAIL retries, causing subsequent RX_SCAN_FAILED reports to claim CRC activity from an earlier window. Clear both flags before returning, while leaving the hardware untouched. Add a CRC-then-CRC-free retry case to the SIL to cover this path.
…he scan window flags
Copilot's re-review of #152: the authority early return in the BEGIN_SCAN /
ADVANCE dispatch also skipped the per-window got_any_irq / crc_seen reset.
A payload-CRC error then stayed latched until the next LOCK, and every
later RX_SCAN_FAILED sub-byte, retries included, reported crc_seen (and
never hw_suspect) for a window that had none. The flags are now cleared
on every dwell as before; only the HW retune is skipped.
check-rx-reverse-delivery models a payload-CRC error (RxDone + ValidHeader
+ PayloadCrcError, the packet still written to the FIFO) and adds sequence
10: CRC errors early in the first scan attempt, then a quiet base, with
authority on every pass after the error. The first two FAIL sub-bytes are
0x60 and 0x41, the same as main's firmware gives; without this fix they
are 0x50 and 0x51. Sequences 1-9 print the same output as before, trace
hashes included. Against the pre-fix sx1276_rx.c the target fails 21
checks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xHffg93jSdYuYEWpSUWeR
…roduction bin
Built at 13f20aa on the bench PC with Arm GNU Toolchain 12.2.MPACBTI-Rel1
and WinLibs mingw32-make (PowerShell): mingw32-make, mingw32-make check
(all vectors PASS, memory map invariants hold), mingw32-make bench. Same
md5s after mingw32-make clean and a rebuild.
bench build/firmware_bench_diag.bin e083ae46a64686031b39866eca42e094 24972 B (untracked)
production build/firmware.bin 148de24e1f9acce417c46c2c69a5be6b 24972 B (committed)
Toolchain checked first on main f6e4b46: it reproduced bench
0c1bb0a9573f813137f941dfa47177d0 and production
589c120323c2d5e7ef9f459d7a4ba42d.
The pair replaces the stale one in murata_l072/README.md and in
l072-bench-md5.yml; the RS-12.15 BENCH line in the TODO is marked done.
Nothing was flashed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The non-blocking l072-bench-md5 job built bench 81a98296... and production
70a6418f... at 13f20aa (push and pull_request runs alike), against the
bench PC's e083ae46... / 148de24e... It differed on the old source too, so
its warnings on this pair are that known difference. The committed
build/firmware.bin is the bench PC's 148de24e....
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xHffg93jSdYuYEWpSUWeR
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Timing-sensitive receiver changes and open decay-window and FAILED-state decisions need design-owner review, with on-air validation still pending.
0 open findings
🧠 Review effort: Balanced
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Dorkmo · project thread
Note
Merging changes no board. Both bench L072s still run
0c1bb0a9. Flashing them with this build and the on-air legs V, W and X need the operator's GO.Before: on FHSS (profile 1), the base's commands rarely reached a streaming tractor. Legs H and I of RS-12.12 delivered 1 of 17 and 49 of 281. On v2, legs R, T and U delivered 19 of 72, 2 of 43 and 7 of 49. Profile 2 delivered 56 of 58. The tractor's receiver followed the hop clock only while its scan machine was LOCKED. Between camera trains it stayed on its last TX channel or on the scan walker's channel, while the base sent on the tractor's grid.
After: while the tractor holds originator authority (8 or more of its own transmissions, each less than 1 s apart), its own TX clock drives its receiver in any scan state, and the scan walker leaves the synthesizer alone. In the new SIL, the tractor hears every command that does not collide with its own transmission. The base, as a follower, behaves exactly as before.
The SIL channel is ideal, so these rates are higher than on air. The pattern is the point: every miss before the fix is a wrong channel (in the last row, a receiver in standby), and every miss after it is a half-duplex collision.
How
sx1276_rx_grid_policy.[ch]: a follow rule.follow = (LOCKED && adopted) || originator authority. Authority needs 8 own TXs each less than 1 s apart, and lapses 1 s after the last one.sx1276_rx.c:sx1276_rx_slot_followgates on that rule instead of LOCKED. The scan walker skips only its hardware retune while the node holds authority. Its state, timers and counters keep running, so it resumes within one 500 ms dwell after authority lapses. The rule reads a freshplatform_now_ms(). The pass'snow_msis read before a TX_DONE noted in the same pass, so with it the unsigned gap wrapped to about 49 days and dropped authority. At demotion, the followed-slot marker is cleared only when the clock is reset. The final scan FAIL no longer puts the radio in standby while the node holds authority, because the slot follower re-arms only at the next slot boundary. The FAILED state and its fault report are unchanged. A walker step skipped under authority still clears the scan window's IRQ and CRC flags, so each RX_SCAN_FAILED sub-byte describes its own window, as on main.make check-rx-reverse-delivery(bench/host_proto/rx_reverse_delivery.c), part of themake checkaggregate. It links the realsx1276_rx.candsx1276_tx.cagainst a simulated SX1276, clock and air, driven inmain.c's loop order, with SPI timed at the 2 MHz clock. It has 10 sequences, plus structure checks on every pass. One structure check is that no scan action takes the receiver out of RX while the node holds authority. Sequence 10 sends commands that fail their CRC early in a scan attempt; the FAIL sub-bytes that follow are the same as main's firmware gives.murata_l072/README.mdandl072-bench-md5.ymlcarry the bench PC's new md5 pair, and the README records that CI's Linux build does not match the bench PC's.One deliberate change: a node LOCKED on its own grid without authority (the RS-13 decay window) no longer follows it. In sequence 7, with a 2.5 s camera stall every 9 s, the tractor followed its own clock 18 times inside the decay window before the fix, and 0 times after it. The window itself is unchanged.
How it was checked
make check(now includingcheck-rx-reverse-delivery),check-rx-grid-policy(sequences 1–11, including the RS-13: close the no-radio items before the desk check and range-edge session #150 switch-gap cases) and CI's static targets pass with host gcc (-Wall -Wextra -Werror).main, the new target fails 21 checks, with the "before" numbers in the table above. With both review fixes taken out it fails 4, and with only the flag fix taken out, 1.fd9e8250). The wire format, the STATS layout, and the adopt and demotion logic do not change..cfiles compile clean withgcc -std=c11 -pedantic -Wall -Wextra -Werror.13f20aa1with Arm GNU 12.2.MPACBTI-Rel1 (8a22657): benche083ae46a64686031b39866eca42e094, production148de24e1f9acce417c46c2c69a5be6b, 24 972 B each, the same after a clean rebuild, withmingw32-make checkpassing. The same toolchain first rebuilt main's pair (0c1bb0a9/589c1203). CI's Linux build of that release gives bench81a98296…and production70a6418f…, so the md5 job's warnings are the known difference; the committedbuild/firmware.binis the bench PC's.Open for the design owner
Not done here
🤖 Generated with Claude Code
https://claude.ai/code/session_017xHffg93jSdYuYEWpSUWeR