Skip to content

Over-the-air time distribution + infrastructure AP mode (hardware beacon)#227

Merged
josephnef merged 41 commits into
masterfrom
timesync-hwbeacon
Jul 10, 2026
Merged

Over-the-air time distribution + infrastructure AP mode (hardware beacon)#227
josephnef merged 41 commits into
masterfrom
timesync-hwbeacon

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Summary

Over-the-air time distribution (LTE-eNB style) built on a new hardware-timed
beacon
primitive, plus an infrastructure AP mode that grows on the same
beacon. A Realtek adapter driven by devourer becomes a clock source and/or a real
access point that a stock Linux rtw88/mac80211 station discovers, associates
with, and exchanges traffic with — open or WPA2-PSK encrypted.

All of it rides small, generation-agnostic additions to IRtlDevice; the bulk of
the diff is the demo (examples/timesync/), docs, and experimental bench harnesses
under tests/. The library core is ~400 lines.

Library API (src/)

New IRtlDevice virtuals (base no-ops; per-generation where the hardware allows):

  • StartBeacon(mpdu, len, interval_tu) — load a beacon into the reserved page and
    enable the MAC beacon function, so the chip auto-transmits it at every TBTT,
    hardware-TSF-stamped. Jaguar2/3 (HalMAC reserved-page download); Jaguar1 returns
    false (no rsvd-page path).
  • ReadTsf() / WriteTsf() — the 64-bit MAC TSF (adoption / uplink-TA timebase).
  • SetCcaMode(disabled) — the EDCCA gate, so a TBTT beacon airs on schedule
    instead of after CSMA backoff (the master owns the channel).
  • AdjustBeaconTiming(µs) / AdjustBeaconTimingFine(µs) — steer the beacon TBTT
    (coarse one-shot REG_BCN_INTERVAL tweak; µs-fine via the beacon-function toggle
    • TSF shift). Jaguar3 only — the Jaguar2 8822B engine drops the beacon on a
      TBTT re-latch, so J2 gates these to 0 rather than silently kill the beacon.

HAL plumbing: HalMAC send_fw_page gains a beacon TX-descriptor path (BMC/HWSEQ/
DISQSELSEQ + bcn-valid latch) and the reserved-page boundary is propagated from
MacInit to the FW-download stage (J2 + J3).

Time distribution (examples/timesync/, docs/time-distribution.md)

Master → slave/UE clock distribution off the beacon TSF: sub-µs downlink, TSF
adoption, and a closed-loop uplink timing advance that converges to ~1.3 ms
(USB-actuator-jitter-limited) using the µs-fine steering.

AP mode (tests/ap_*, docs/ap-mode.md)

Experimental harnesses that make devourer a real AP: beacon (accepted by a kernel
iw scan on both bands) → probe/auth/assoc responses → DHCP/ARP/ICMP, so a Linux
station associates, leases an IP, and pings it. ap_wpa2 adds the WPA2-PSK 4-way
handshake
(openssl) and a software CCMP data path — a real station connects
encrypted and pings at 0% loss.

Validation

Bench-tested against a second Realtek adapter running stock kernel rtw88:
beacon accepted by iw scan (2.4 + 5 GHz), full open + WPA2 association, encrypted
DHCP lease and ping. Headless ctest (timesync selftest + existing math guards)
green; multi-platform CI builds clean.

Scope / caveats

  • StartBeacon is Jaguar2/3; beacon-TBTT steering is Jaguar3-only (gated off on
    J2). Jaguar1 has no reserved-page path.
  • The AP-mode WPA2/CCMP crypto is software (openssl) in the test harness —
    hardware CCMP offload (J3 security TX/RX descriptor fields) is not ported.
  • The tests/ap_* / beacon_* harnesses are experimental bench tools (manual-build,
    like beacon_tbtt.cpp), not library API; only the timesync selftest is a ctest.
  • No regulatory enforcement — the caller owns compliance (unchanged devourer policy).
  • The library reads no environment (audited — the beacon work's experimental
    getenv and dead state were removed).

🤖 Generated with Claude Code

josephnef and others added 30 commits July 9, 2026 15:25
Idea-6 experiment: can the MAC auto-transmit a beacon at each TBTT (TSF-timed,
host-jitter-free) from devourer's monitor/injection mode? Adds an experimental
IRtlDevice::BeaconTbttSpike (Jaguar1) + tests/beacon_tbtt.cpp harness, and a
behaviour-neutral _tx_qsel override on the J1 TX descriptor (default 0x12 mgmt,
unchanged; 0x10 = beacon queue).

RESULT: NO — the minimal path does not work. BeaconTbttSpike loads the frame via
a QSEL_BEACON bulk-OUT and enables the beacon function (BCN_CTRL 0x10 -> 0x1a,
EN_BCN_FUNCTION|DIS_BCNQ_SUB), but a second RX (8822C) saw ZERO beacons over
18 s. A plain QSEL_BEACON send does NOT load the persistent reserved-page buffer
the TBTT engine re-transmits from.

Root cause + full sequence (for future reference, from reference/rtl8812au):
- _BeaconFunctionEnable = BCN_CTRL (DIS_TSF_UDT|EN_BCN_FUNCTION|DIS_BCNQ_SUB =
  0x1a) + REG_RD_CTRL+1 = 0x6F. (My 0x1a matched; RD_CTRL was missing.)
- The beacon must be DOWNLOADED to the reserved page via HalDownloadRSVDPage8812
  with a BCN_VALID handshake (poll REG_DWBCN1_CTRL_8812+2) + beacon head-page
  setup — which devourer implements ONLY for the 8814 (3081/IDDMA), not the 8812.

Conclusion: hardware supports TBTT TX, but it's a real reserved-page-download
port, not a quick enable; the quick path is empirically ruled out, and
monitor-mode RX coexistence remains unverified. Spike branch — NOT for merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the beacon-TBTT experiment to Jaguar2 (RtlJaguar2Device::BeaconTbttSpike).
Unlike the 8812 (which lacks any reserved-page download), J2/J3 HAVE a real
halmac rsvd-page download (HalmacJaguar2Fw::download_rsvd_page), so the beacon
LOAD is reachable — the J1 blocker is gone.

RESULT: still negative. The download returns OK and the beacon function enables
(BCN_CTRL 0x1c->0x1e, EN_BCN_FUNCTION already set), but the 8822B TX airs ZERO
beacons over 18 s (8822C RX). Root cause, from the MacInit comment: post-init
the beacon boundary is set to rsvd_boundary(1938), which "makes the page-0
rsvd-page beacon download fail its bcn-valid latch" — so the beacon is never
validly latched for the TBTT engine. Making it fire needs the beacon-boundary
manipulation (init_trx_cfg's set_bcn_boundary path) + the correct beacon page,
plus DRVERLYINT/TBTT_PROHIBIT (set during init, not here).

Verdict across all generations: hardware-timed beacon-at-TBTT TX is a genuine
reserved-page/beacon-queue feature build, NOT a quick enable — J1 blocked at the
download, J2/J3 blocked at the boundary/latch. Monitor-mode RX coexistence still
unverified. Spike branch — NOT for merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A worked demonstration of the hardware-TSF primitives (#225) as an LTE eNB→UE
timebase: one MASTER broadcasts its hardware TSF periodically (a sync beacon,
ReadTsf()-stamped — reliable TX-side), and any number of SLAVES lock their
notion of the master clock to it from the beacons alone. No GPS at the slaves,
no shared host clock — only the master holds a reference, exactly the eNB→UE
model (master TSF = SFN, each slave a UE slaving to it).

Each slave relates the master's broadcast TSF to its OWN per-frame hardware TSF
(rx_pkt_attrib::tsfl) with a running least-squares fit (both clean MAC-latched
µs clocks), PREDICTS each beacon before it arrives, and emits the prediction
error. Two slaves predicting the same beacon (matched by seq) agree far tighter
than either's absolute lock, because the master's software stamp→air jitter is
common to both and cancels in the difference.

Bench (master 8812AU, slaves 8822CU + 8822EU, ch36, 50 ms beacons, 30 s):
  per-slave absolute lock : ~94 us RMS  (bounded by the master stamp jitter)
  inter-slave (inter-UE)  : 4.71 us RMS, max 17.6 us, mean +1.70 us
Two UEs agree on the eNB clock to <5 us over the air, no GPS. The inter-slave
error tightens with beacon rate (17.8 us at 100 ms → 4.7 us at 50 ms) as the
fits densify and balance.

New: examples/timesync/{main.cpp,timesync.h} (master/slave roles, reusing the
tdma TD frame tag), tests/timesync_selftest.cpp (headless LinFit ppm-recovery +
inter-slave-agreement + TSF-wrap math, wired into ctest as timesync_math and the
mingw target list), tests/timesync_demo.sh + timesync_analyze.py (the bench).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the LTE closed-loop half to the timesync example (DEVOURER_TSYNC_UPLINK=1,
roles master + ue): a full-duplex master phase-measures each UE uplink against
its own TSF slot grid (arrival tsfl mod slot — reliable per-frame, no ReadTsf the
RX loop would starve) and integrates a timing advance it broadcasts back in the
beacon; a full-duplex UE transmits one uplink per beacon, TA-corrected. Adds
LinFit::inverse(), the uplink/TA frame-class codes (reusing the tdma TD tag), a
headless TA-loop convergence test (timesync_math), and the bench harness
(tests/timesync_ta_demo.sh + timesync_ta_analyze.py).

STATUS — experimental, does NOT converge on the bench. The control math
converges in the selftest and the full-duplex plumbing works on-air (with a
FIXED advance, uplinks arrive tightly clustered, ~±0.2 ms). But a fixed-advance
authority test shows the TA shifts the UE's send-CALL time (~44%) yet NOT the
master-measured arrival phase. Root cause: under full-duplex, send_packet queues
the frame and the chip airs it on its own schedule, so userspace call-timing has
no sub-slot control over TX air departure — the loop has nothing fine to
actuate. (This bench also has only one clean full-duplex Jaguar2/3 adapter; the
8822E desenses its RX in TX+RX.) The measurement/feedback/math are in place;
closing the loop needs hardware-timed TX departure this path lacks. Kept behind
the opt-in flag and clearly marked; the downlink distribution (default) is
unaffected and validated.

docs/time-distribution.md documents the working downlink (eNB→UE, the bench
numbers) and the honest uplink-TA status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…find, still no air

Adds RtlJaguar3Device::BeaconTbttSpike (8822C/8822E) mirroring the J2 path, via a
new HalmacJaguar3Fw::download_rsvd_page + rsvd_boundary() and a HalJaguar3::
download_beacon_page passthrough. Beacon is downloaded to the rsvd_boundary head
page (where BCN_HEAD points after send_fw_page restores it).

NEW FINDING: the TBTT engine only pulls the beacon queue when the port's network
type is AP/Ad-hoc — monitor bring-up leaves REG_MSR (0x0102) port-0 at NoLink
(read back 0x00), so a validly-latched beacon never airs no matter what BCN_CTRL
says. Set port-0 = AP. Also dropped a bogus REG_TCR "TSF reset" write from the J2
spike (0x0604 bit0 is NOT the TSF reset — that's REG_DUAL_TSF_RST 0x0553; the old
write likely disabled a TX function / froze state).

RESULT: still negative on the 8822C. With the beacon loaded (bcn-valid latched),
MSR=AP, BCN_HEAD at rsvd_boundary, EN_BCN_FUNCTION on (already set at init,
BCN_CTRL 0x1c), and the interval set, ZERO beacons air (8822E RX sees only the
~12/s ambient rate, no added ~10/s at 100 TU; the RX path is proven — it hears
the 8822C's normal txdemo at 22 hits). The remaining blocker is a deeper AP-beacon
enable (beacon-queue scheduler / TBTT trigger) that needs a golden register-dump
comparison against a working AP (the DEVOURER_REPLAY_WSEQ methodology) — beyond a
blind register sweep. Spike branch, NOT for merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…chestrated)

Captured the kernel rtw88_8822c IBSS beacon-enable/refresh register stream on the
8822CU (usbmon, ch6), diffed against devourer's BeaconTbttSpike, and SDR-confirmed
(modulation-agnostic burst detector) the reference beacons on air at the ~102 ms
interval.

VERDICT: devourer's static "download beacon + set EN_BCN_FUNCTION, hardware
auto-transmits at TBTT" is the wrong model for this combo chip. rtw88 orchestrates
the beacon through the FW:
 1. re-downloads the beacon EVERY beacon interval (~94 ms) — 53 downloads in ~5 s,
 2. sends an H2C RSVD_PAGE command (REG_HMEBOX0 0x1d0=0x690c0100, cmd 0x00) telling
    the FW the rsvd-page locations,
 3. uses BCNQ_BDNY (0x0424)=0x9207 -> boundary page 0x207 (519), vs devourer's
    computed rsvd_boundary=1938 (page-allocation mismatch),
 4. cycles BCN_CTRL (0x0550) 0x10/0x14/0x18/0x1c across the download bracket (the
    spike wrote a static 0x1e),
 5. stages multiple rsvd pages (beacon+probe-rsp+null) at heads 0x207/0x287/...

So cracking beacon-TBTT here = porting rtw88's FW-orchestrated beacon path
(rsvd-page download to the right boundary + H2C RSVD_PAGE + periodic refresh), a
real feature build now precisely scoped by the dump — not a register tweak. The
capture/parse tooling (IBSS on the kernel driver + usbmon) and the SDR burst
detector are reproducible. tests/golden_dump_beacon.md has the distilled delta.
Spike branch, NOT for merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… still no air

From the rtw88 source (~/rtw88_build/rtw88, fw.c/mac80211.c) the beacon-enable
path is: net_type=AP in REG_CR[17:16], BCN_CTRL=EN_BCN_FUNCTION|DIS_TSF_UDT
(0x18), BIT_EN_BCNQ_DL (BIT22) in REG_FWHW_TXQ_CTRL (BSS_CHANGED_BEACON_ENABLED),
the rsvd-page download bracket (matches devourer's send_fw_page: BIT_ENSWBCN =
CR+1|0x1, FIFOPAGE_CTRL_2 pg|valid, restore), and a per-interval beacon
re-download. Applied all of it here + a periodic-refresh thread.

RESULT: still negative. EN_BCNQ_DL was ALREADY set at devourer init (TXQ
0x00711f83, bit22=1), BCN_CTRL now 0x18, net_type=AP, refresh every interval —
the 8822E RX sees ZERO beacon-subtype frames on ch6 OR ch36. Ruled out: the
CCK-rate-at-5GHz theory (dead on ch6 too where 1M CCK is valid), EN_BCNQ_DL
missing, BCN_CTRL value, MSR-only. (SDR burst detection on ch36 is unreliable —
an ambient AP beacons at 100 TU there, false-positive.)

Remaining blocker is deeper than register setup: rtw88 builds the FULL rsvd-page
blob (beacon+probe-rsp+null+... as one buffer) then re-downloads JUST the beacon
to fix its TX descriptor ("correct tx_desc for the beacon"), sends H2C RSVD_PAGE
with the page offsets, and its fifo.rsvd_boundary (519) differs from devourer's
(1938). So the 8822C beacon is FW-orchestrated end-to-end — cracking it needs the
full rsvd-page/TX-descriptor/H2C port, not the register enables. Spike branch,
NOT for merge; genuine progress + precise remaining scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… — still no air

Key correction: the earlier "kernel boundary 519 vs devourer 1938" was an
ENDIANNESS MISREAD of usbmon's raw bytes — "9207" is bytes 92 07 = LE 0x0792 =
1938, the SAME as devourer. Beacon heads 9287/a287/ab87 decode to pages
1938/1954/1963. So devourer's beacon lands at the correct page; the fifo-layout
reconciliation was a red herring (rtw88 rtw_set_trx_fifo_info: rsvd_boundary =
txff_pg_num(2048) - rsvd_pg_num = 1938 with page_size 128 — matches
HalmacJaguar3MacInit::priority_queue_cfg exactly).

Added an H2C RSVD_PAGE probe (cmd 0x00, captured payload). Still negative — but
the payload's probe/pspoll/null page offsets are for rtw88's FULL rsvd blob,
which devourer doesn't build (beacon only), so it's approximate/inert.

RULED OUT now: page layout, net_type/BCN_CTRL/EN_BCNQ_DL enables, CCK-rate,
periodic refresh, approximate H2C. The one remaining untested area is the beacon
TX DESCRIPTOR: devourer's send_fw_page builds a FW-DOWNLOAD descriptor (routes a
bulk chunk to QSEL_BEACON for iddma), NOT a complete beacon-TX descriptor the
TBTT engine transmits from — rtw88 explicitly re-downloads the beacon "to replace
the TX desc header (correct tx_desc for the beacon)". Cracking it needs a proper
beacon-TX-descriptor rsvd-page write path (reuse send_packet's descriptor logic,
routed to the beacon queue) + the real rsvd-page blob + a correctly-offset H2C.
That's a new code path, not a register tweak. Spike branch, NOT for merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…still no air

send_fw_page gains a beacon_desc mode: for the beacon rsvd-page it now sets
SET_TX_DESC_BMC/EN_HWSEQ/DISQSELSEQ (rtw_tx_rsvd_page_pkt_info_update RSVD_BEACON:
bmc=true, en_hwseq=true, dis_qselseq=true) on top of QSEL_BEACON/USE_RATE/LS —
the FW-download descriptor lacked all three. download_rsvd_page passes
beacon_desc=true; FW-download chunks keep the original descriptor.

STILL NEGATIVE on ch6. This exhausts the individual-piece hypotheses: page layout
(correct — endianness fix), net_type=AP, BCN_CTRL=0x18, EN_BCNQ_DL, download
bracket, CCK-rate, periodic refresh, H2C RSVD_PAGE (approx), and now the real
beacon TX descriptor. Each matches rtw88 yet the beacon does not leave the queue.

The blocker is now SYSTEMIC, not a single register/field: either devourer's
pure-monitor-mode MAC bring-up (RCR/filters/no-port — a mode the vendor driver
never combines with AP beaconing) leaves the beacon scheduler off, or the combo
FW only enters a beacon-transmitting state via the full integrated rtw88 flow
(build the complete rsvd-page blob + a correctly-offset H2C + the port/BSSID
config, as one sequence). Cracking it now means an integrated port of rtw88's
AP/beacon bring-up (or verifying monitor+beacon are incompatible on this chip),
not more piecewise fixes. Spike branch, NOT for merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… at page 0

Stopped asserting "matches rtw88" and actually diffed devourer's USB register
writes against the kernel rtw88_8822c IBSS beacon-enable (both on the 8822CU,
usbmon). The diff found concrete mismatches the reasoning missed:

1. BEACON DOWNLOADED TO PAGE 0 (the real bug). HalmacJaguar3Fw::_rsvd_boundary is
   never assigned — MacInit computes 1938 and writes the REGISTERS but never
   propagates it to the FW class, so _fw.rsvd_boundary() returned 0. The beacon
   armed to FIFOPAGE_CTRL_2=0x8000 (head 0) while BCNQ_BDNY=1938 — beacon and
   TBTT-engine head in different places. Fix: MacInit exposes rsvd_boundary(),
   HalJaguar3 propagates it via new _fw.set_rsvd_boundary() after init_mac_cfg.
   Verified via usbmon: devourer now arms to 0x8792 (page 1938), matching kernel.

2. NO PORT IDENTITY. The kernel writes REG_MACID (0x0610) + REG_BSSID (0x0618);
   devourer's monitor bring-up writes NEITHER. Added both from the beacon's
   addr2/addr3 (rtw88 rtw_vif_port_config).

3. EN_BCNQ_DL cleared during download on USB. send_fw_page cleared BIT_EN_BCNQ_DL
   every download; rtw88 does that ONLY for PCIe. A refresh overlapping a TBTT
   would suppress the beacon. Kept set on the beacon path.

STATUS: all three are real, usbmon-verified corrections (the page-0 bug matters
for ANY beacon work), but the beacon still does not air. Remaining diff = ~a
dozen registers the kernel's IBSS config writes that devourer's MONITOR bring-up
omits (REG_MACID tail, RD_CTRL, INT_MIG, RRSR/rate, coex tables). The gap is now
"monitor-mode MAC config != beaconing-station MAC config" — matchable, but many
registers. Spike branch, NOT for merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… 8822C

The final two bugs, both found by actually DIFFING devourer's usbmon writes
against the kernel rtw88 IBSS (not by asserting a match):

1. RADIOTAP PREFIX (the last blocker). The harness passes [radiotap][802.11
   beacon]; BeaconTbttSpike downloaded the WHOLE buffer into the rsvd page, so the
   on-air beacon was [radiotap][802.11] — malformed, and the readback showed
   REG_MACID/BSSID = garbage (0x80.../0xffff, i.e. radiotap+FC bytes). Strip the
   radiotap (it_len = bytes[2:3]) so the rsvd page holds the raw 802.11 MPDU, and
   read MACID/BSSID from the MPDU's addr2/addr3.

Combined with the earlier diff-found fixes (beacon page 0 -> 1938 via
_rsvd_boundary propagation; REG_MACID/BSSID port identity; EN_BCNQ_DL kept on the
USB beacon path) plus net_type=AP, BCN_CTRL=0x18, EN_BCNQ_DL, interval:

RESULT: the 8822C now AUTO-TRANSMITS the beacon at TBTT — ~200 canonical-SA
beacons decoded by the 8822E over 24 s (~8.3/s ≈ 100 TU), fully hardware-timed
(one download + periodic refresh, the MAC fires at each TBTT). This is the
hardware-timed TX primitive that unlocks LTE-style uplink-TA and a sub-µs
downlink beacon stamp.

Lesson: diff the actual register traffic, don't reason about whether it matches.
Spike branch — the crack; productization (strip the spike scaffolding, wire a
clean API) is follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w opt-in

Bench-verified: ONE beacon download airs beacons at TBTT indefinitely on both
bands (ch6 and ch36, no refresh) — the periodic re-download was never needed to
keep it airing. rtw88 re-downloads each interval only to refresh the beacon
CONTENT (TSF/TIM), not the airing, which is pure hardware TBTT. So the refresh
thread is now opt-in (BEACON_REFRESH=1); the default is the clean single-download
HW-TBTT path, which is truly kernel-like at the hardware level.

Status: beacon-TBTT works on 2.4 + 5 GHz. Remaining to full parity: the 5 GHz
decode count is lower than 2.4 (RX-side loss / beacon rate — to investigate),
and productization (clean API, J2 same fixes, timesync uplink-TA integration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verification (tests/beacon_ts_check.cpp): the 8822C's TBTT beacon body timestamp
(MPDU bytes 24..31) steps by ~102364 us (= 100 TU) each beacon, and the receiver's
arrival tsfl steps by the same — the MAC inserts the current hardware TSF into the
beacon at TX time. So devourer's beacon is now a fully functional, kernel-
equivalent 802.11 beacon: hardware-timed at TBTT, hardware-stamped with the live
TSF, on both 2.4 and 5 GHz.

Payoff for timesync: the master can use hardware-timed + hardware-stamped beacons.
A slave reads the beacon's TSF (master clock) and its own arrival tsfl (slave
clock) -> sub-µs downlink sync, dropping the ~94 us software-ReadTsf-stamp floor
that bounded the current downlink. That's the sub-µs floor + the uplink-TA
hardware actuator the timesync work was missing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retires the BeaconTbttSpike scaffolding into a proper IRtlDevice::StartBeacon
(non-experimental): load a full 802.11 beacon MPDU into the beacon reserved-page
and enable HW-TBTT auto-TX, hardware-timed + hardware-TSF-stamped. Renamed across
IRtlDevice + all three generations + the harness.

Jaguar1: now returns false (unsupported) — the 8812/8821 have no HalMAC
reserved-page download (QSEL-beacon transmits once; bench-confirmed negative), the
8814 IDDMA path is unported. Removed the dead QSEL-send attempt.

Jaguar2 (RtlJaguar2Device::StartBeacon): ported the two J3 bug-fixes verbatim —
(1) download to the real _fw.rsvd_boundary() not page 0 (added the
_fw.set_rsvd_boundary(_macinit.rsvd_boundary()) propagation after init_mac_cfg,
which was missing exactly as on J3); (2) strip a leading radiotap header so the
rsvd page holds the raw MPDU. Plus REG_MACID/BSSID port identity, net_type=AP,
BCN_CTRL=0x18, EN_BCNQ_DL, and the send_fw_page beacon_desc mode (BMC/EN_HWSEQ/
DISQSELSEQ + keep EN_BCNQ_DL on the USB beacon path). By-construction identical to
the validated J3 flow but UNTESTED (no 8822B/8821C on the bench).

Jaguar3 (the validated impl): unchanged behavior, verified still airs post-rename
(~100 beacons / 10 s on ch6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…4ns path

Wires the cracked StartBeacon (hardware-timed, hardware-TSF-stamped beacon) into
the timesync master/slave, so the downlink runs on real MAC-hardware timestamps
instead of the ~94 us software ReadTsf+send_packet stamp.

- master: build_std_beacon (byte-identical to the validated beacon_tbtt beacon —
  an extended body broke the hardware TSF insertion) + StartBeacon, then idle. The
  MAC auto-transmits at TBTT and inserts the live 64-bit TSF into the beacon.
- slave: reads the STANDARD 802.11 beacon timestamp (MPDU bytes 24..31 = master's
  hardware TSF) instead of the TD-tag tx_tsf, fit against its own arrival tsfl.

Bench (c812 master, a81a slave, ch36, HW beacon): 169 beacons, slave locks,
master-slave TSF offset stable to ~120 us on this shared channel (dominated by
CSMA air-time jitter + predict-over-gap). A direct clean-stretch measurement
(tests/beacon_ts_check) showed the floor is 0.254 us — vs the ~94 us software
stamp. Both timestamps are pure hardware; the residual is now channel-condition-
limited, not driver-limited.

Integration branch (timesync-hwbeacon = timesync + beacon merged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…downlink

The hardware-beacon downlink's residual was CSMA-limited: on a shared channel the
TBTT beacon defers to carrier-sense (EDCCA) and airs after a variable backoff, so
the master's TSF-stamp-at-scheduled-TBTT vs the delayed actual air time jitter by
~hundreds of us. In a time-distribution setup the master OWNS the channel, so the
backoff is pure loss.

Exposes SetCcaMode(bool) on IRtlDevice (J3 already implemented the vendor dis_cca
recipe: MAC BIT_DIS_EDCCA 0x520[15] + EDCCA-mask 0x524[11]) and adds
DEVOURER_TSYNC_NO_CSMA: the HW-beacon master disables EDCCA so the beacon airs
exactly on the TBTT schedule.

Bench (c812 master + a81a slave, HW beacon, CROWDED ch6, 146 beacons):
  without NO_CSMA: 472 us RMS   (CSMA backoff jitter)
  with    NO_CSMA: 0.31 us RMS, max 0.56 us   <- sub-µs on a crowded channel
The master-slave TSF offset goes rock-stable (moves only with the ~1.3 ppm
crystal drift). ~1500x tighter than the ~470us CSMA case, ~300x below the ~94us
software-ReadTsf-stamp floor. Full LTE-eNB-style sub-µs time distribution.

(EDCCA-disable is a NULL-op for injected TX — which bypasses TX deferral — but the
TBTT beacon goes through the MAC's normal deferral, so this is exactly where it
bites. Idea + 88x2EU edcca_threshold_override recipe from the user.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… _CSMA=1)

The master owns the channel in a time-distribution setup, so deferring the beacon
to CSMA is pure loss. Flip the default: DEVOURER_TSYNC_HWBEACON now disables EDCCA
unless DEVOURER_TSYNC_CSMA=1 is set. Default-on verified sub-µs (0.30 us RMS) on
crowded ch6 with no env; the opt-out keeps CSMA for anyone who needs to coexist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… parity

An RTL8812BU (2357:012d, Jaguar2) and RTL8811CU (0bda:c811) turned up on the
bench, so the previously by-construction-only J2 port is now hardware-validated:

- J2 StartBeacon: the 8812BU auto-transmits its beacon at TBTT, decoded by an
  8822E RX (~100 beacons / 12 s) — the page-0 + radiotap fixes ported from J3 work.
- Added RtlJaguar2Device::SetCcaMode (the EDCCA registers 0x520/0x524 are
  HalMAC-common with J3) so the no-CSMA path works on J2 too.
- End-to-end timesync (8812BU master, HW beacon + default no-CSMA, 8822E slave,
  crowded ch6, 149 beacons): 0.39 us RMS downlink sync — matching J3.

So hardware-timed, hardware-TSF-stamped, master-owns-channel sub-µs beacon time
distribution now works on BOTH HalMAC generations (8822B/8812BU/8821C and
8822C/8822E). Dropped the "UNTESTED" caveat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The doc still called the hardware-timed master beacon "a separate build" and
capped the downlink at the ~94 us software stamp. Updates it to the shipped
reality: StartBeacon (HW-timed + HW-TSF-stamped beacon at TBTT, both HalMAC
gens), the slave reading the standard 802.11 timestamp, and no-CSMA
(master-owns-channel, default on) giving 0.31 us (8822C) / 0.39 us (8812BU) on a
crowded channel. Adds the DEVOURER_TSYNC_HWBEACON / _CSMA knobs, the
minimal-beacon-body caveat, and notes the HW beacon is the sub-slot TX actuator
the uplink-TA loop was missing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uator

Adds IRtlDevice::WriteTsf(uint64_t) (REG_TSFTR 0x0560/0x0564) — the kernel-side
primitive devourer was missing next to ReadTsf. Sets the free-running MAC µs
clock, which continues from the written value. This is what a slave needs to
ADOPT the master's clock (slew its TSF onto the beacon's), and the actuator the
LTE-style uplink timing-advance needs (shift the port TSF so a TBTT-scheduled
uplink lands in its slot — the sub-slot air-departure control send_packet can't
give).

Validated on hardware both gens: WriteTsf(0x50_0000_0000) then ReadTsf 200 ms
later returns wrote + ~200692 us (8822C) — the counter took the value and ran on.
Implemented on Jaguar2 (8822B/8812BU/8821C) + Jaguar3 (8822C/8822E); no-op base.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The uplink timing-advance loop needs a way to shift a hardware-timed TX in
sub-slot increments. WriteTsf was the assumed lever, but it moves only the
reported TSF (and the beacon-body timestamp) — the beacon TBTT air-time is
driven by a separate per-port timer and is deaf to REG_TSFTR (bench-proven:
writing +25 ms to the TSF left the observed beacon arrival phase rock-constant).

The actuator that DOES steer the TBTT is a one-shot beacon-interval tweak: run
one interval at (nominal ± Δ) TU via REG_BCN_INTERVAL (0x0554) then restore, and
the next TBTT — and the cadence thereafter — advances/retards by Δ TU. This is
the 802.11 IBSS/TSF-merge mechanism. Add IRtlDevice::AdjustBeaconTiming(us) on
Jaguar2/3 (base no-op) built on the bare-0x0554 path (no beacon re-download);
requires an active StartBeacon, blocks ~one interval, TU-quantized (1 TU =
1.024 ms).

Bench-validated on 8822C (8822CU master + 8822EU observer, ch36): a single
AdjustBeaconTiming(-20480) advanced the beacon TBTT by exactly 20 TU — the
observer's arrival phase stepped 88018 -> 67565 µs at the tweak (predicted
-20480 µs), and held. tests/beacon_interval_shift.sh <short_TU> characterizes it.

Correct the WriteTsf doc-comment + docs/time-distribution.md accordingly
(WriteTsf = TSF adoption for the downlink; AdjustBeaconTiming = the TBTT/uplink-TA
actuator, coarse at 1-TU granularity).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The TU-quantized AdjustBeaconTiming (1.024 ms grain) is too coarse for a real
uplink timing-advance. Root cause of why WriteTsf couldn't move the TBTT: the
TBTT counter is latched while the beacon function runs — the vendor reset_tsf
path clears EN_BCN_FUNCTION first. Add AdjustBeaconTimingFine, which toggles the
beacon function off, shifts the port-0 TSF by the requested µs, and toggles it
back on so the TBTT re-derives from the shifted TSF at MICROSECOND resolution.

Bench-validated on 8822C (8822CU master + 8822EU observer, ch36), arrival-phase
steps all sub-TU: -5000 -> -4479 µs, -10000 -> -8997 µs, +6000 -> +7172 µs
(retard). Magnitude undershoots/overshoots by a sub-ms USB read->write latency
(~0.5-1.2 ms, the real TSF advances during the register sequence) — a systematic
offset a closed timing-advance loop absorbs; the resolution is µs. It also shifts
this port's TSF + beacon-body timestamp, the intended UE-advances-its-own-timebase
behaviour.

Jaguar2 correction: TBTT steering is NOT viable on the 8822B. Bench-proven on the
8812BU, BOTH the beacon-function toggle AND the one-shot REG_BCN_INTERVAL tweak
drop the beacon (the bcn-valid latch is lost, and J2 doesn't retain the beacon
bytes to re-download). The prior commit shipped the J2 interval tweak as working
(it was only J3-validated). Gate both J2 actuators to return 0 rather than
silently kill the beacon; the downlink (StartBeacon + SetCcaMode) is unaffected.
Steering is Jaguar3-only.

tests/beacon_interval_shift.sh gains a FINE_US=<µs> mode (+ DEVOURER_VID support)
to characterize the fine actuator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… was a misdiagnosis)

docs/time-distribution.md claimed the beacon body had to stay minimal — an
extended body (long SSID / extra IEs) was reported to break the hardware TSF
insertion (the MAC writing a per-beacon counter instead). Kernel APs beacon with
full bodies and the HW inserts the TSF fine, so the limit was suspect.

Bench-tested on an 8822C: a well-formed 70-byte beacon body (10-char SSID +
8 supported rates + DS param + TIM + ERP IEs) inserts the live hardware TSF
correctly — the observed timestamp field steps ~102400 µs per beacon exactly as
the minimal body does (4608054 -> 4710436 -> 4812890 -> ... via a second-adapter
tests/beacon_ts_check, isolating the fresh low-TSF source from a stale autonomous
beacon). So there is NO minimal-body requirement; the earlier failure was a
malformed test frame (bad IE lengths), not a hardware constraint. devourer
beacons can carry full kernel-AP content with the live TSF.

Add tests/beacon_fullbody.cpp (the full-body validation harness) and correct the
doc + the timesync build_std_beacon comment. No code change — StartBeacon already
accepts arbitrary beacon bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ve TSF, both gens)

Confirm devourer's hardware beacon looks like a real kernel-AP beacon on the air,
not just at the TSF level. tests/beacon_wire_check.cpp dumps, per canonical-SA
beacon, the frame control, the 802.11 sequence-control field and the beacon-body
timestamp. Bench-verified on both HalMAC generations:
  J3 8822C : FC=0x0080, seq 16,17,18,19,20,21..., ts steps ~102400 us
  J2 8812BU: FC=0x0080, seq 730,731,732,733...,   ts steps ~102400 us
So the hardware sequence numbering (EN_HWSEQ) increments the beacon seq by 1 per
beacon on both gens — kernel-equivalent. (A frozen seq seen earlier was a J2
beacon left in the post-drop state after an AdjustBeaconTiming tweak, which J2
does not survive — a degraded engine, not normal behaviour.) Also reconfirms the
full-body TSF insertion on J2 (last iteration validated it on J3 only).

No code change — a verification tool + a docs note. StartBeacon already produces
a fully kernel-equivalent beacon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e steering

The uplink-TA loop never converged because the UE's send_packet uplink has no
sub-slot air-departure control (the chip airs the queued frame on its own
schedule) — the doc's fixed-TA authority test showed TA shifts the send-CALL time
but not the measured arrival (bench baseline: phase RMS ~5.4 ms, drifting across
the whole slot).

Close it: DEVOURER_TSYNC_HWBEACON=1 on the UE airs the uplink from the BEACON
engine (StartBeacon stores a tdma Uplink frame in the rsvd page; the engine airs
it verbatim at the UE's TBTT) and steers that TBTT with AdjustBeaconTimingFine per
the master's TA — the sub-slot control send_packet lacked. The master's Uplink
measurement path is unchanged (matches by class).

The bench was NOT actually blocked: the master needs full-duplex but no fine
steering, so it can be the rock-solid 8812AU (Jaguar1 does TX+RX) — only the UE
must be a clean full-duplex J3 (8822CU). Bench-proven convergence (ch36, slot
20 ms, gain 0.30): arrival phase RMS drops from ~5.5 ms to a bounded ~1.4 ms
steady-state (mean ~0) — a 4x improvement over the non-converging baseline. The
residual is the fine actuator's USB read->write jitter (~0.5-1.2 ms/correction),
the floor for a userspace-USB TSF write.

tests/timesync_ta_demo.sh gains HWBEACON=1 (8812AU master + 8822CU UE, INT=100).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pts devourer's AP

The strongest "behaves like a kernel" proof: a real Linux 802.11 station (rtw88 /
mac80211) scans for devourer's beacon and lists it as a valid AP, parsing every
element. Bench-proven — devourer airs a full-content beacon (8812BU, ch6), a
second Realtek adapter bound to rtw88 (managed) runs `iw scan` and returns:

  BSS 57:42:75:05:d6:00  TSF <live>  freq 2437  beacon interval 25 TUs
  capability: ESS  SSID: devourerAP
  Supported rates: 1.0* 2.0* 5.5* 11.0* 18.0 24.0 36.0 54.0
  DS Parameter set: channel 6  TIM: DTIM Count 0 Period 1  ERP: <no flags>

The live hardware TSF, capability, SSID, all 8 rates (basic-rate flags), DS param,
TIM/DTIM and ERP are all interpreted correctly by the kernel BSS parser — devourer's
beacon is a genuine kernel-equivalent AP beacon, not just wire-correct bytes.

Add tests/beacon_kernel_scan.sh (the interop harness) + a DEVOURER_BCN_TU interval
knob on tests/beacon_fullbody.cpp (a dense interval is caught reliably by the scan
dwell) + a docs note. No library change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier ch36 miss was the sparse 100 TU interval, not a band issue: with a
dense interval (25 TU) the rtw88 station lists devourer's beacon at ch36
(5180 MHz) — SSID/rates/DS-param parsed, same as ch6. Control: the station sees
8 ambient 5 GHz APs, so its 5 GHz scan works. External kernel-AP interop is now
validated on both 2.4 GHz (ch6) and 5 GHz (ch36). Doc + harness note updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…be response)

Open the AP-side handshake and de-risk it with the first step: devourer responds
to active-scan probe requests fast enough that a real station discovers the AP via
the PROBE RESPONSE, with NO beacon aired. Full-duplex responder (InitWrite +
StartRxLoop): the RX callback matches a probe-request and queues the requester; the
main thread send_packet's a probe-response addressed to it. (send_packet must run
off the RX event thread — calling the synchronous bulk transfer from inside the RX
callback returns libusb BUSY, rc=-6; queue + main-thread TX fixes it.)

Bench-proven: devourer probe-responder on c812 (ch6, no beacon); a second Realtek
adapter bound to rtw88 (wlp13s0u1u4) runs `iw scan freq 2437` and lists
"devourerAP" in every scan — discovery purely via the probe response (4 reqs seen,
4 resps sent, 0 errors). This works because management-frame timing is tens of ms
(the userspace RX->TX round-trip is a few ms), unlike the SIFS-timed ACK a data
frame needs. It de-risks the full AP handshake (probe-resp -> auth -> assoc).

tests/probe_responder.cpp + a docs note. No library change (RX callback + send_packet).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ICATING

Extend the AP frontier from probe-response (tests/probe_responder.cpp) to the full
open-network handshake: ap_responder beacons (StartBeacon) and answers probe /
auth / (re)assoc requests from its RX callback (built in the callback, SENT from
the main thread — send_packet from the RX event thread returns libusb BUSY).

Bench findings:
- With a DENSE beacon (DEVOURER_BCN_TU=25 — a fast channel-hopping scan misses a
  100 TU beacon + the ~ms-delayed probe response), wpa_supplicant discovers
  devourerAP, selects it, and reaches "SME: Trying to authenticate".
- Full association did NOT complete on this rig, and it is NOT a devourer-side
  gap: the test station's rtw88 (2357:0120) never puts the auth frame on air.
  Two independent promiscuous monitors (the AP's own RX + an 8812AU sniffer) saw
  ZERO auth-to-BSSID, and devourer's monitor RX is confirmed promiscuous (52
  ambient unicast-to-other-MAC frames in 10 s), so a transmitted auth would be
  seen. The auth/assoc responder paths are built but unexercised end to end,
  pending a station that actually transmits auth.

The AP side (beacon accepted by a real kernel scan on both bands + active-scan
probe response + reaching wpa_supplicant AUTHENTICATING) behaves like a kernel AP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
josephnef and others added 11 commits July 10, 2026 00:36
…t BSSID fix)

FULL ASSOCIATION ACHIEVED — the ultimate "behaves like a kernel AP". A real Linux
station (rtw88 8822cu) authenticates, associates, and stays Connected to devourer's
AP: wpa_supplicant CTRL-EVENT-CONNECTED, `iw link` Connected, stable for 8 s+. The
station's AUTH/ASSOC requests arrive with retry=0 — devourer HARDWARE-ACKs them
(the ACK engine matches REG_MACID, which StartBeacon sets to the BSSID). So the
auth/assoc responder paths (built last commit) are now exercised end to end.

ROOT CAUSE of the multi-iteration association block: the BSSID was MULTICAST. The
canonical test SA 57:42:75:05:d6:00 has the I/G bit set in 0x57 (bit 0 = 1 =
group/multicast), which is invalid as a BSSID — a station cannot unicast-auth to a
multicast address, so rtw88 silently dropped the auth before it hit the air. Two
independent promiscuous monitors saw ZERO auth-to-BSSID with 0x57 across two
station chips; flipping the first octet to 0x02 (locally-administered UNICAST) put
the auth on air and the whole handshake completed. ap_responder's kBssid is now
0x02.. and the header/doc explain the requirement.

Not a code change to the library — StartBeacon already sets MACID from the beacon
addr2 and the hardware ACKs; the fix is using a valid unicast BSSID.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The capstone of the AP frontier: after a real Linux station associates (prior
commit), it now exchanges IP traffic with devourer's AP over the air. ap_responder
answers the post-association data plane — ARP requests for the AP IP -> ARP
replies, ICMP echo requests -> echo replies, over 802.11 from-DS data frames.

Bench-proven: an rtw88 8822cu station associates, gets a static IP (192.168.99.2),
and `ping 192.168.99.1` returns 0% packet loss, ~1.8 ms RTT. So devourer is a
real, associable, PINGABLE AP — a Linux station discovers it, authenticates,
associates, and exchanges IP traffic with it, exactly like a kernel-driven AP.
The station's data frames are hardware-ACKed (MACID = BSSID).

tests/ap_ping_demo.sh runs the whole thing (associate + static IP + ping) and
reports the loss/RTT. A full data path (DHCP, forwarding) is an AP-stack concern
beyond the driver. No library change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… AP)

Complete the "just works like a real AP" chain: ap_responder now runs a minimal
DHCP server (DISCOVER->OFFER, REQUEST->ACK leasing 192.168.99.2, with subnet/
router/DNS options) alongside the ARP + ICMP data plane. So a Linux station
associates and dhcpcd AUTOMATICALLY leases 192.168.99.2 from devourer — no manual
static IP — then `ping 192.168.99.1` returns 0% loss, ~1.9 ms RTT.

Bench-proven end to end via tests/ap_ping_demo.sh: beacon -> auth -> assoc ->
DHCP (offered/leased) -> ARP/ICMP -> ping 6/6. devourer is a self-service
infrastructure AP in one userspace process, like hostapd+dnsmasq. Beyond this
(routing/NAT, WPA2) is AP-stack scope, not driver parity. No library change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…th devourer

Take the AP to WPA2: tests/ap_wpa2.cpp advertises an RSN IE (WPA2-PSK-CCMP) and
runs the 4-way handshake authenticator. A real Linux station completes it —
wpa_supplicant: "WPA: Key negotiation completed ... [PTK=CCMP GTK=CCMP]" +
CTRL-EVENT-CONNECTED; the AP logs msg2-MIC-verified -> msg3 -> msg4-OK. This is the
deepest driver-protocol milestone (WPA2 key negotiation).

The crypto is openssl in userspace: PMK = PBKDF2-HMAC-SHA1(psk, ssid), PTK = PRF,
MIC = HMAC-SHA1(KCK), GTK AES-key-wrapped with the KEK. The 4-way handshake needs
no CCMP (EAPOL-Key frames are cleartext, MIC-protected), so it works without the
chip crypto engine — which matters because J3 has no HW-crypto TX/RX descriptor
fields (only J1 does). Encrypted CCMP *data* is the follow-on (software AES-CCM on
the data path); the station disconnects post-handshake since data isn't yet
encrypted.

Two details that mattered: msg3 key-data pad is 0xDD then 0x00s (a 2nd 0xDD
mis-parses as a KDE -> station rejects msg3); a prior WRONG_KEY failure
temp-disables the SSID in wpa_supplicant (cold-cycle the station for a clean run).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…2 (0% loss)

Complete the WPA2 AP: after the 4-way handshake, ap_wpa2 encrypts/decrypts the
data plane with software AES-CCM (openssl) using the negotiated TK (PTK[32:48]).
It decrypts the station's CCMP data frames and answers ARP + ICMP echo encrypted,
so a real Linux station pings the AP over WPA2/CCMP at 0% packet loss, ~2.5 ms RTT.
The station runs hardware CCMP, devourer software CCMP — they interoperate.

So devourer is a COMPLETE WPA2-PSK infrastructure AP: a real station discovers it,
authenticates, associates, negotiates CCMP keys (4-way), and exchanges ENCRYPTED
IP traffic with it — the full encrypted management + data plane, validated against
the Linux WiFi stack.

CCMP framing details: AAD masks FC subtype/retry/pm/moredata + sets protected, and
masks the seq number (keeps frag); nonce = 0|A2|PN(6, big-endian); CCMP header
carries the 48-bit PN + ext-IV keyid; from-DS data frames set the protected bit.
(HW CCMP offload would need the J3 security TX/RX descriptor fields, absent in
devourer; software CCMP sidesteps that. Static IP used; DHCP-over-CCMP is the same
handle_plain extension.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pted lease)

Route the DHCP server through the CCMP data path (handle_plain): the AP decrypts
the station's DHCP DISCOVER/REQUEST, builds the OFFER/ACK, and encrypts it, so the
station leases 192.168.99.2 over ENCRYPTED DHCP — no static IP. Validated: dhcpcd
reports "leased 192.168.99.2 for 86400 seconds" over the WPA2/CCMP link (the
decrypt log shows the DISCOVER=332B UDP -> OFFER, plus ARP + ICMP over CCMP).

So devourer is a complete zero-config WPA2-PSK AP: a real Linux station discovers
it, authenticates, associates, negotiates CCMP keys (4-way), auto-leases an IP over
encrypted DHCP, and pings it encrypted at 0% loss — the full encrypted management +
data plane like a hostapd+dnsmasq WPA2 AP, in one userspace process.

(Each capability is individually bench-validated; a single clean end-to-end run of
all of it is flaky after many cycles on this rig — the 8812BU AP is on an xhci root
hub with no VBUS reset. IPv6 ND frames from the station are ignored.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…de in README

The infrastructure-AP work (probe/auth/assoc, DHCP/ARP/ICMP, WPA2 4-way + software
CCMP) had accreted inside docs/time-distribution.md, which is about time sync —
incoherent. Move it to a standalone docs/ap-mode.md (a real Linux station
associates → gets an IP → pings devourer, open or WPA2-PSK encrypted), leaving a
cross-reference in the time-distribution doc. Add both docs to the README index —
neither was listed.

Docs only; no code change. All test harnesses (beacon_fullbody, beacon_wire_check,
probe_responder, ap_responder, ap_wpa2, + the *.sh) compile clean, ctest 15/15.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pted DHCP + ping)

The WPA2 flow had no orchestration script (open mode has ap_ping_demo.sh). Add the
matching harness: builds ap_wpa2 (-lcrypto), brings devourer up as a WPA2-PSK AP,
connects a real station, leases an IP over encrypted DHCP, and pings over
WPA2/CCMP — reporting the 4-way, lease, and loss/RTT. Completes the AP harness set.
Docs only / test harness; no library change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The library must read no environment (CLAUDE.md: construction knobs go in
DeviceConfig, mid-session knobs are IRtlDevice setters) — but StartBeacon gated an
experimental periodic beacon re-download thread on std::getenv("BEACON_REFRESH"),
the only getenv in src/. Worse, the thread re-downloaded the SAME static beacon
bytes every interval: there is no content-update path, so even when enabled it did
nothing useful (a single download airs the beacon at every TBTT indefinitely).

Remove it entirely — the getenv, the thread, and its only-there-for-it state
(_bcn_bytes, _bcn_interval_ms, _bcn_run, _bcn_thread) plus the destructor join. The
default path (no refresh) is unchanged, and _bcn_interval_tu (used by
AdjustBeaconTiming) is untouched. A real dynamic-beacon-content refresh would need
a content-update path behind a DeviceConfig knob, not env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cn_interval_tu)

Two more write-only leftovers from the beacon-TBTT work, same class as the
BEACON_REFRESH getenv:

- Jaguar1 `_tx_qsel`: the spike made the TX-desc QUEUE_SEL dynamic (0x10 for a
  beacon-TX experiment, 0x12 for mgmt), but the beacon path was abandoned
  (StartBeacon returns false on J1) and the `_tx_qsel = 0x10/0x12` assignments
  were dropped — freezing the member at 0x12, exactly the original hardcode.
  Inline `SET_TX_DESC_QUEUE_SEL_8812(usb_frame, 0x12)` and drop the member.

- Jaguar2 `_bcn_interval_tu`: set in StartBeacon but never read — J2's
  AdjustBeaconTiming is gated (TBTT steering drops the 8822B beacon, returns 0),
  so the interval is unused. Remove the member and its assignment.

J3's `_bcn_interval_tu` is untouched (its AdjustBeaconTiming/Fine genuinely read
it — steering works there). No behaviour change; build clean, ctest 15/15.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef josephnef merged commit dad6d6d into master Jul 10, 2026
15 checks passed
@josephnef josephnef deleted the timesync-hwbeacon branch July 10, 2026 05:24
josephnef added a commit that referenced this pull request Jul 10, 2026
…t-latency bench (#228)

## Summary

Answers the recurring question *"how accurate is the TSF time
distribution vs NTP/PTP
over Wi-Fi?"* with **measured** numbers from a two-node bench, and lands
a reusable
transport-latency microbench. Follow-up to #227 (over-the-air time
distribution).

Two small, self-contained additions — a benchmark tool and a docs
writeup; no
library change.

## `tests/reglat.cpp` — register round-trip latency (USB vs PCIe)

One binary that times `RtlAdapter::rtw_read32` over whichever transport
it opens —
USB vendor-control by default, PCIe BAR2 MMIO when `DEVOURER_PCIE_BDF`
is set (the
PCIe path compiles only with `DEVOURER_PCIE`, via the `devourer` PUBLIC
`DEVOURER_HAVE_PCIE` define). Added as a plain `add_executable` (USB
path builds in
all configs). Measured on the same binary:

| Transport | mean/op | jitter (p99−p50) |
|---|---|---|
| USB (8822EU) | ~68 µs | ~64 µs |
| PCIe 8821CE (MMIO) | ~2.2 µs | ~0.08 µs |

→ **~30× faster, ~600–900× lower jitter**. The `AdjustBeaconTimingFine`
actuator
chains ~5–7 register ops, so the ~0.5–1.2 ms USB jitter (and the ~1.3 ms
closed-loop
uplink-TA residual) is a **transport** floor — a PCIe MMIO path would
collapse it
toward µs.

## `docs/timing-accuracy.md` — NTP / PTP over Wi-Fi vs TSF

Measured on two machines associated to the same AP:

| Method | Precision over Wi-Fi | Timestamp source |
|---|---|---|
| NTP (`chrony -Q`, software TS) | ~0.76 ms RMS (quiet); tens of ms
under load | software |
| PTP (`ptp4l`) | **does not run** | — |
| devourer TSF downlink | ~0.25 µs RMS | 802.11 MAC hardware |

- **~3000× tighter than NTP**, because the MAC latches the TSF *below*
the
CSMA/queue/power-save layer whose RTT jitter (measured **1.6 → 110 ms**
under
  load) bounds software-timestamped protocols.
- **PTP can't run**: `ethtool -T` = `PTP Hardware Clock: none`; grepping
the
`reference/` vendor drivers for
`SOF_TIMESTAMPING`/`SIOCSHWTSTAMP`/`skb_hwtstamps`/
`get_ts_info` finds nothing — neither vendor nor in-tree driver exposes
the HW
timestamp to the kernel API, though both read the TSF internally.
devourer
  surfaces it (`RxPacket.tsfl`), which is what enables sub-µs sync.

Caveats kept explicit in the doc: devourer's figure is one-way
*relative* precision
(TSF is 1 µs resolution, sub-µs via averaging), NTP's is the
healthy-link daemon
offset, and FTM ranging can beat a one-way TSF on *absolute* accuracy.

## Testing

`ctest` green; multi-config build clean (`reglat` builds in USB-only and
PCIe
configs); PCIe path verified on a Radxa X4 (RTL8821CE) via vfio-pci.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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