Skip to content

USB exclusive access: claim-before-reset + advisory per-adapter lock#169

Merged
josephnef merged 1 commit into
masterfrom
feat/usb-exclusive-access
Jul 4, 2026
Merged

USB exclusive access: claim-before-reset + advisory per-adapter lock#169
josephnef merged 1 commit into
masterfrom
feat/usb-exclusive-access

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Problem

Launching two devourer processes on the same adapter wedges the chip. The open sequence was detach → libusb_reset_device → claim → assert(rc==0), so a second launch's reset ran before its claim failed — re-enumerating the adapter out from under the first process and crashing it — and the assert turned the doomed claim into a SIGABRT instead of a clean exit.

Fix

Use the OS's own exclusivity correctly, and add a cross-platform advisory lock as the primary cross-platform gate.

  • src/UsbOpen.{h,cpp}claim_interface_then_reset(), the recommended open sequence: take an advisory per-adapter lock first, then detach, configure, claim, and only then — as sole owner — reset. A second process is refused before it touches the device. The kernel interface claim (LIBUSB_ERROR_BUSY on Linux usbfs) is a redundant second gate; on WinUSB, where libusb_claim_interface does not arbitrate, the advisory lock is the guard.
  • src/UsbDeviceLock.{h,cpp}devourer::UsbDeviceLock: advisory exclusive lock keyed to the adapter's bus + port path. POSIX flock / Windows named mutex, auto-released on process exit including SIGKILL/crash, so there are never stale locks (the failure mode a PID file has). Held for the device lifetime via a shared_ptr in RtlUsbAdapter; WiFiDriver::CreateRtlDevice uses the lock the caller passes, or acquires its own as a best-effort second gate for direct library consumers.
  • All six demos call claim_interface_then_reset instead of the inline detach/reset/claim/assert block and hand the lock to CreateRtlDevice.

Validation

Second process on the same adapter, first process already running:

Before After
Linux (RTL8812AU) 2nd process's reset crashed the 1st USB adapter in use … refusing to open, exits before reset → 1st survives
macOS (RTL8822CU) 2nd process SIGABRT (assert) clean refuse, exit 1 → 1st survives

Single-process RX and TX bring-up unaffected. Build clean + ctest 2/2 on both platforms. The Windows/mingw paths (named-mutex lock) are exercised by this PR's CI matrix.

🤖 Generated with Claude Code

Two devourer processes on one adapter would wedge the chip: the second
launch's libusb_reset_device (issued before its claim) re-enumerated the
adapter out from under the first process and crashed it, and the demo
asserted on the doomed claim instead of exiting cleanly.

Fix the open ordering and add a cross-platform advisory lock:

- src/UsbOpen.{h,cpp}: claim_interface_then_reset() — the recommended open
  sequence. Takes an advisory per-adapter lock FIRST, then detaches,
  configures, claims, and only then (as sole owner) resets. A second process
  is refused before it touches the device. The kernel interface claim
  (LIBUSB_ERROR_BUSY on Linux usbfs) is a redundant second gate; on WinUSB,
  where the claim does not arbitrate, the lock is the guard.

- src/UsbDeviceLock.{h,cpp}: devourer::UsbDeviceLock — advisory exclusive
  lock keyed to the adapter's bus+port path. POSIX flock / Windows named
  mutex, auto-released on process exit (including SIGKILL / crash), so there
  are never stale locks. Held for the device lifetime via a shared_ptr in
  RtlUsbAdapter; WiFiDriver::CreateRtlDevice uses the lock the caller passes
  (from claim_interface_then_reset) or acquires its own as a best-effort
  second gate for direct library consumers.

- All six demos now call claim_interface_then_reset instead of the old inline
  detach/reset/claim/assert(rc==0) block, and hand the lock to CreateRtlDevice.

Validated on Linux (RTL8812AU) and macOS (RTL8822CU): a second process on
the same adapter is refused ("USB adapter in use ... refusing to open") and
exits cleanly BEFORE any reset, so the first process keeps running;
single-process RX and TX bring-up are unaffected. Build clean + ctest 2/2
on both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@josephnef josephnef merged commit 35bcdb7 into master Jul 4, 2026
13 checks passed
@josephnef josephnef deleted the feat/usb-exclusive-access branch July 4, 2026 20:36
josephnef added a commit that referenced this pull request Jul 4, 2026
Extends the `DEVOURER_CW_TONE` MP single-tone (bare RF local-oscillator
carrier, from #165) to the **Jaguar3** family — RTL8822CU (rtl8822c) and
RTL8812EU/8822EU (rtl8822e) — the last USB generation without it.
SDR-validated on a USRP B210. Builds on #167 (Jaguar1+Jaguar2) and
current master (#169).

## Why Jaguar3 needs a different recipe

The halbb/halrf generation needs two things the older chips don't
(ported from the vendor `phydm_mp_set_single_tone_jgr3` +
`phydm_set_pmac_txon_jgr3`):

1. **RF 0x00 is write-only through the HSSI 3-wire port**
(`0x1808`/`0x4108`). The direct BB→RF window (`0x3c00`) is a *read-only
shadow* for RF 0x00, so a window write is silently dropped. (RF 0x58 /
LO-enable is a normal reg and still uses the window.)
2. **The BB must be held in continuous TX** (`0x1c3c`/`0x1a00`/`0x1ca4`
+ PMAC/TX-OFDM `0x1d08`/`0x1e70`), or the RF state machine re-drives RF
0x00 back to RX within milliseconds and no carrier comes out.

`RtlJaguar3Device::StartCwTone`/`StopCwTone` implement the full recipe
(path A, CCA off, serialized on the coex `_reg_mu`); the coex runtime
thread isn't started for a tone.

## SDR results

| Device | 2.4 GHz | 5 GHz |
|---|---|---|
| **RTL8822CU** (2T2R) | +38 dBFS, carrier-to-spur **62 dB** | c2s
**51–58 dB** |
| **RTL8812EU** (BL-M8812EU2, 1T1R, rfe 21) | radiates (weak — **no 2.4
GHz PA**) | radiates, weaker (bare-LO output is low on this module),
center carrier, gain-responsive |

## 8812EU 5 GHz bring-up robustness (two fixes)

- **DACK/IQK USB-read crash:** the status-poll loops issue tens of
thousands of USB control-IN reads; a single glitch made `rtw_read` throw
`std::ios_base::failure` and aborted bring-up. Wrapped `dac_calibrate()`
+ `phy_iq_calibrate()` in a bounded retry (`HalJaguar3::retry_cal`),
mirroring `StartRxLoop`'s existing retry.
- **Post-bring-up read NAK:** on the 8812EU at 5 GHz the FW
power-mode/coex H2C steps and the `GPIO_MUXCFG` RFE-pin write leave the
chip NAKing control-IN reads, so `StartCwTone`'s BB reads failed. For a
CW tone, arm *early* (after the channel/RFE config, before those steps)
and return — a bare tone needs no per-rate power/coex/FW-coex. The 5 GHz
PA still needs its RFE-enable pins driven, so snapshot `0x40`/`0x64`,
arm, then **blind-write** the PA pins (the read-breaking write happens
last). The plain (non-CW) TX path is unchanged.

## Scope / notes

- `tests/cw_tone_sdr.sh` gains `8822cu`/`8822eu` cells.
- 8822CU regression-checked after the restructure: still +38 dBFS.
- The 8812EU intermittently drops control-IN under *rapid repeated* ch36
bring-ups (a device-USB trait); single clean launches work.

Closes the Jaguar3 follow-up noted in #165.

🤖 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