Skip to content

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

Description

@josephnef

Summary

Devourer already has all the mechanics of frequency hopping: FastRetune does an intra-band retune in ~1.5 ms (~0.5 frame-times at the 345 fps chip-limited injection rate), send_packet honours a radiotap CHANNEL field per packet, and the outer-FEC combining layer treats a wiped channel as recoverable erasures (docs/frequency-hopping.md, docs/fused-fec.md). What it hops today, however, is a fixed, public list in fixed order (DEVOURER_HOP_CHANNELS="1,6,11", indexed (count / dwell) % N). A narrowband jammer that observes one round knows the whole pattern and can follow it at leisure.

This issue proposes the classic FHSS anti-jam upgrade: a keyed pseudo-random hop sequence that TX and RX derive independently from a shared seed, so a jammer must either spread its power across all N channels (−10·log₁₀(N) dB per channel) or build a follower that detects + retunes faster than our dwell — which at per-packet dwell is ~2.9 ms against our 1.5 ms retune.

Key framing: we do not need every hop to survive. The Reed-Solomon outer code already converts a jammed channel into ⌈N_symbols/N_ch⌉ erasures per block. The property pseudo-randomness buys is unpredictability of where the next dwell lands, which is what defeats both parked and follower jammers.

Design

1. Hop schedule — keyed PRF, per-round permutation, stateless by slot

round      = slot / N_ch
perm_round = keyed_permutation(key, round)      // Fisher-Yates driven by a PRF stream
channel(slot) = hopset[ perm_round[slot % N_ch] ]
  • Keyed PRF: SipHash-2-4 (or any small keyed PRF) over (round, i). The security property is sequence unpredictability, not confidentiality — but a keyed PRF (not a bare LCG/xorshift seeded with the key) is required, since LCG state is recoverable from a few observed outputs.
  • Permutation per round, not independent draws. Independent draws repeat channels and can starve one; a per-round permutation guarantees every channel is visited each round, which the frequency-diversity FEC analysis (erasure bound ⌈N/N_ch⌉) assumes.
  • Stateless by slot index. channel(slot) is a pure function of (key, slot) — RX can join/rejoin at any slot without replaying history, and a lost frame cannot desynchronise the sequence. This rules out any "advance an internal RNG per hop" design.
  • Pure header-only module (proposed src/HopSchedule.h), shared verbatim by TX and RX ends, unit-tested headlessly (determinism across instances, permutation coverage, statistical spread, key sensitivity) and registered with ctest like stream_stdin_binary.

2. Demo integration

  • DEVOURER_HOP_SEED=<hex, up to 128-bit> — presence switches the existing hop loop (both WiFiDriverTxDemo and StreamTxDemo) from sequential to pseudo-random indexing. All existing knobs (DEVOURER_HOP_CHANNELS as the hopset, DEVOURER_HOP_DWELL_FRAMES, DEVOURER_HOP_FAST, DEVOURER_HOP_RADIOTAP) compose unchanged — the change point is one line, the hop_channels[dwell_no % N] index.
  • DEVOURER_HOP_SLOT_MS=N — optional time-based slotting (slot = elapsed/N) instead of frame-count dwell, so the schedule is defined by wall time rather than TX pacing; prerequisite for lockstep RX, which has no frame counter of its own.
  • The <devourer-hop> marker gains the slot (and seed fingerprint) so a wideband observer can check the on-air sequence against the PRF prediction.

3. RX architectures (both, in parallel)

  • Asymmetric (available immediately): TX hops pseudo-randomly; RX is a wideband SDR (B210, validated in the hop work) or N adapters. No sync needed — hop_rx_combine.py already dedups by (block_id, symbol_index). This is also the measurement ground truth for everything below.
  • Lockstep single-adapter RX (the real feature): second Realtek adapter runs the same HopSchedule. Two unknowns, in dependency order:
    1. RX-side FastRetune viability — FastRetune is TX-validated only. RX adds AGC/IGI settle after the LO move: how long after a retune until decode works again? Cheap to answer with a fixed public pattern on both ends vs a static-RX baseline; gates the rest of the plan.
    2. Sync — frame-carried slot counter, one-way (matches the video-link topology): TX stamps (epoch, slot) in each frame; RX acquires by parking on one PRF-predicted channel until it hears anything, extracts phase, then tracks by free-running its own slot timer and correcting phase from each decoded frame. Loss of signal ⇒ back to acquisition. Wall-clock sync deliberately rejected (fragile for a drone link).

4. Dwell — the anti-jam knob

Start slot-based at 20–50 ms (easy tracking: many frames per dwell, one decode suffices to hold phase; already defeats human/slow-scan followers), then shrink toward per-packet as tracking proves robust. The follower-resistance floor is set by detect+retune time of the adversary vs our dwell.

5. Band plan

5 GHz is the natural home: UNII-1→3 is all intra-band for FastRetune's gate ⇒ hopset of a dozen+ channels (N=12 ≈ 11 dB against a spread jammer) vs three non-overlapping channels at 2.4 GHz. Caveat from docs/frequency-hopping.md: TX power is not re-tuned per hop, and a wide 5 GHz span may want a periodic full SetMonitorChannel — slot that refresh into the epoch boundary.

6. Out of scope for v1

Adaptive channel avoidance (sense jammed channels, exclude them keyed-consistently on both ends) — needs a return channel or TX-side CCA sensing, and naive designs leak predictability. Follow-on issue once v1 measures where it would pay.

Milestones

  • M1 — HopSchedule module + demo wiring (pure code, no hardware): PRF, per-round permutation, unit tests, DEVOURER_HOP_SEED / DEVOURER_HOP_SLOT_MS.
  • M2 — TX on-air validation: extend tests/hop_rx_probe.py to verify the B210-observed channel sequence matches the PRF prediction for the seed (not merely "cycles through the set"). This milestone is the asymmetric-RX deliverable.
  • M3 — RX-side FastRetune viability (the hardware unknown, gates M4): RX demo hops on a fixed public schedule; measure post-retune decode dead-time and delivery vs static RX.
  • M4 — lockstep sync: slot stamping TX-side; acquisition/tracking state machine RX-side; chip-to-chip validation; then shrink dwell to find the tracking floor.
  • M5 — jammer resilience measurement: USRP narrowband jammer (reuse the fused-fec interferer rig) parked on one hopset channel; end-to-end FEC-decoded delivery: static vs sequential-hop vs PR-hop. Then a scripted follower jammer (SDR detect + retune) to measure the dwell threshold where following breaks.
  • M6 (follow-on) — adaptive hopset exclusion.

Context

This is the defensive/link-robustness counterpart to the frequency-diversity work: same mechanics, plus a keyed schedule and an RX that can follow it. All numbers above (retune cost, fps, erasure bounds) are from the measured results in docs/frequency-hopping.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions