Skip to content

Anti-jam keyed FHSS: SipHash hop schedule, lockstep RX, jammer-resilience validation#262

Merged
josephnef merged 7 commits into
masterfrom
feature/153-keyed-fhss
Jul 11, 2026
Merged

Anti-jam keyed FHSS: SipHash hop schedule, lockstep RX, jammer-resilience validation#262
josephnef merged 7 commits into
masterfrom
feature/153-keyed-fhss

Conversation

@josephnef

@josephnef josephnef commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Anti-jam pseudo-random frequency hopping on top of FastRetune:

  • Keyed hop schedule (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.
  • Wall-clock slot hopping wired into txdemo and streamtx (DEVOURER_HOP_SLOT_MS, mutually exclusive with frame-dwell).
  • Single-adapter lockstep RX — versioned slot/epoch/phase sync markers plus rxdemo acquisition, tracking, and reacquisition. Works for both keyed and sequential orders (seed optional).
  • Jammer-resilience measurement — parked-jammer delivery matrix and a full-duplex B210 following jammer.
  • A comprehensive article (docs/fhss.md) plus refreshed docs/frequency-hopping.md and 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 (adds hop_schedule_math — SipHash reference vectors, permutation coverage, key/round sensitivity, stateless-lookup identity, sequential round-robin, marker round-trip).
  • Two-adapter endurance (RTL8812AU → RTL8812CU lockstep, keyed, 50 ms slots on ch 36/40/44/48): 598.3 s, 11 968 TX slots; acquired once, tracked all 11 965 unique slots with no loss/reacquire transition, decoded within 11 891 (99.38 %) — all 74 misses isolated singletons. Cross-channel RX retune 2.18 ms median / 2.65 ms p95; post-retune first decode 1.82 ms median.
  • Wideband SDR (USRP B210, ch 36/40/44/48 @ 5210 MHz, 61.44 Msps): confirms the keyed order end-to-end — 14 keyed rounds, overflow-free, 0.814 match fraction, past the strict 10-round threshold. The validator's analysis chain was hardened (gap-tolerant keyed aligner, high-percentile dominant-channel decision, continuous-TX capture, IQ-mirror handling) and ships an offline --self-test.
  • Jammer resilience (fused-FEC delivery):
    • Parked jammer (CW on ch 40, contested link margin): static@clean ~1.00, static@jammed ~0.00, sequential ~0.95, keyed ~0.95. Sequential ≈ keyed vs a blind parked jammer (reps 0.948/0.950 vs 0.947/0.950), as expected — order only matters against a reactive jammer.
    • Following jammer (full-duplex B210, sense on RX + jam on TX simultaneously): reaction latency ~3.5 ms (retune-dominated) sets the dwell threshold below which a reactive follower can no longer track a keyed hopper. The reactive chase (vs keyed) thrashes ~2100 retunes/20 s where the predictive chase (vs sequential) holds at ~450 — the cost a keyed permutation imposes on the jammer.

Full methodology + numbers: docs/fhss.md, docs/jammer-resilience.md.

Notable fixes folded in

  • streamtx shutdown segfault — the device was destructed after libusb teardown, so Jaguar1's async TX completions reaped against a freed handle (crash, and the crash leaked the USB claim → "adapter in use" on the next run). The device is now reset() before libusb teardown.
  • streamtx sync markers ride their own frame (every DEVOURER_HOP_SYNC_EVERY data frames, default 4), so a lockstep RX can track the data path while the caller's FEC PSDUs stay byte-for-byte untouched.

Compatibility

  • No seed preserves the existing sequential schedule; no slot-ms preserves frame-dwell hopping.
  • streamtx's caller-owned PSDU format is unchanged.
  • Slot mode rejects an explicitly-configured frame dwell.
  • Lockstep RX is opt-in and mutually exclusive with RX sweep mode.

Test plan

  • ctest --test-dir build --output-on-failure — 18/18
  • python3 tests/hop_rx_probe.py --self-test (offline aligner regression guard)
  • Two-adapter lockstep endurance (599 s, 99.38 %)
  • Wideband SDR keyed-order validation (14 rounds PASS)
  • Parked + following jammer-resilience runs

Closes #153.

🤖 Generated with Claude Code

josephnef and others added 6 commits July 11, 2026 16:18
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>
@josephnef josephnef changed the title Add keyed FHSS schedule and lockstep RX Anti-jam keyed FHSS: SipHash hop schedule, lockstep RX, jammer-resilience validation Jul 11, 2026
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>
@josephnef josephnef merged commit e001a15 into master Jul 11, 2026
15 checks passed
@josephnef josephnef deleted the feature/153-keyed-fhss branch July 11, 2026 15:49
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.

Anti-jam pseudo-random frequency hopping (FHSS): keyed hop schedule + lockstep RX on top of FastRetune

1 participant