Anti-jam keyed FHSS: SipHash hop schedule, lockstep RX, jammer-resilience validation#262
Merged
Conversation
The SDR validator failed the strict 10-round keyed threshold while the two-adapter link decoded 99.38% of slots with zero loss. Root cause was entirely on the B210 analysis side — three stacked bugs, all now fixed and validated on hardware (B210 + RTL8812AU, ch36/40/44/48 @ 5210MHz, 61.44Msps): PASS, cycles=14, match_frac=0.814. - matcher: the keyed path required an exact contiguous match anchored at rle[0] with no gap tolerance, so a single dropped/weak dwell truncated the whole score. Replace with keyed_align() — slides over every start offset and resyncs, skipping dropped (drop_tol) and spurious (ins_tol) dwells; gate on match_frac (--match-frac). mirror_map() handles the B210's IQ-inverted spectrum as a channel involution instead of the old sequence-reversal. - classifier: the per-window dominant-channel decision averaged power, diluting the bursty near-field TX below the steady ~9dB band-edge pedestal on the edge channels (collapsing the sequence to a single channel). Use a high percentile (--seq-pct, default 90) to track the dwell peak. - orchestration: bounded TX self-exited during the B210's multi-second bring-up, so the capture caught only a sliver of hopping. Run TX continuously and treat the SDR capture as the bounded op; add SEED/SLOT_MS keyed-slot support. Also: drop the dead top-level `import uhd` (only gnuradio's gruhd is used), so --analyse-only and the new --self-test run offline. self_test() reproduces the matcher bug deterministically (old matcher 2/12 cycles, new 10/12). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the M5 experiments for #153 plus the plumbing they needed, all hardware-validated on the two-adapter + B210 bench. Library/demo: - HopSchedule::sequential() — a keyless public round-robin that shares the lockstep sync machinery, so "sequential" is a first-class peer of the keyed schedule (needed for the follower experiment: sequential is predictable, keyed is not). Fixed public fingerprint, selftest coverage. - rxdemo lockstep now engages on slot-mode with the seed optional (keyed → tracks the permutation, keyless → the sequential order); tx/streamtx emit the sequential schedule's marker too. - streamtx: fix a shutdown segfault — the device was destructed after libusb was torn down, so Jaguar1's async TX completions reaped against a freed handle (crash leaked the USB claim → "adapter in use" on the next run). reset() the device first. Also emit a lockstep sync marker on its own frame every DEVOURER_HOP_SYNC_EVERY data frames (default 4), so an RX can track the data path without touching the caller's FEC PSDUs. Experiments: - tests/jammer_resilience.py + run_jammer_resilience.sh — parked-jammer A/B/C/D delivery matrix (static clean/jammed vs sequential vs keyed) over the fused-FEC link. Result: static@jammed ~0.00, static@clean ~1.00, hopping ~0.95; sequential ≈ keyed vs a blind parked jammer. - tests/sdr_follower_jammer.py — full-duplex B210 follower (senses on RX while jamming on TX simultaneously), reactive vs predictive. Measured retune latency ~3.5 ms sets the dwell threshold where a reactive follower can no longer track a keyed hopper; reactive chase thrashes (~2100 retunes/20s) where predictive holds (~450), which is exactly the cost a keyed permutation imposes. - docs/jammer-resilience.md — methodology, numbers, and the B210 full-duplex / 60 MS/s tuning notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- docs/fhss.md: capstone article covering the hop engine, sequential/keyed SipHash schedule, single-adapter lockstep receive, the two-adapter (99.38%) and wideband-SDR validation, and the parked + following jammer-resilience results. - frequency-hopping.md: refresh the keyed/lockstep section to current state (sequential lockstep, streamtx sync markers, seed-optional RX) and link the article + jammer-resilience note. - CLAUDE.md: add a keyed-FHSS + lockstep + jammer-resilience paragraph. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MSVC's <windows.h> defines a `max` function-like macro, so `std::max(1, ...)` fails to compile (C2589). Use a plain clamp instead, matching how the lockstep RX path already avoids it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Anti-jam pseudo-random frequency hopping on top of
FastRetune:src/HopSchedule.h) — a header-only SipHash-2-4 keyed, stateless per-round Fisher-Yates permutation. With a seed the hop order is a keyed pseudo-random permutation; with no seed it is the public sequential round-robin. Both are stateless (channel = perm(slot/N)[slot%N]), so a receiver joins without replaying RNG state.txdemoandstreamtx(DEVOURER_HOP_SLOT_MS, mutually exclusive with frame-dwell).rxdemoacquisition, tracking, and reacquisition. Works for both keyed and sequential orders (seed optional).docs/fhss.md) plus refresheddocs/frequency-hopping.mdand a jammer-resilience methodology note.Implements milestones M1–M5 of #153. M6 (adaptive hopset exclusion — dropping a persistently-jammed channel from the set) is spun out as separate follow-up work.
Validation (hardware)
ctest: 18/18 passed (addshop_schedule_math— SipHash reference vectors, permutation coverage, key/round sensitivity, stateless-lookup identity, sequential round-robin, marker round-trip).--self-test.Full methodology + numbers:
docs/fhss.md,docs/jammer-resilience.md.Notable fixes folded in
reset()before libusb teardown.DEVOURER_HOP_SYNC_EVERYdata frames, default 4), so a lockstep RX can track the data path while the caller's FEC PSDUs stay byte-for-byte untouched.Compatibility
streamtx's caller-owned PSDU format is unchanged.Test plan
ctest --test-dir build --output-on-failure— 18/18python3 tests/hop_rx_probe.py --self-test(offline aligner regression guard)Closes #153.
🤖 Generated with Claude Code