hackrf-nusb 0.2.0

Rust-native HackRF RX driver built on nusb.
Documentation

hackrf-nusb

hackrf-nusb is an experimental, Rust-native HackRF driver built directly on nusb. It does not link to libusb or libhackrf. Its control protocol follows the upstream libhackrf implementation, while its owned device/stream API follows the shape of hydrasdr-rs.

The driver is currently RX-only. HackRF hardware is half duplex, and TX is not exposed yet.

Status

Implemented:

  • discovery and opening of HackRF Jawbreaker (1d50:604b), HackRF One / HackRF Pro (1d50:6089), and rad1o (1d50:cc15);
  • exact 128-bit serial discovery and selection;
  • firmware, board, USB API version, and MCU serial queries;
  • frequency, sample rate, LNA gain, VGA gain, RF amplifier, and antenna bias controls, including live setters while RX is active;
  • one owned, pull-based RX stream with synchronous and executor-agnostic asynchronous APIs;
  • WebUSB support on wasm32-unknown-unknown;
  • direct conversion of HackRF interleaved signed 8-bit IQ into Complex32.

There is no resampling and no alternate raw/floating-point stream mode. The configured rate is the complex sample rate produced by the radio. Each signed 8-bit component is normalized by 128.0 as it is copied to the caller's Complex32 slice.

USB dependency and execution model

One-shot operations and receive operations implement nusb::MaybeFuture. On a native target, call .wait() for blocking operation or .await the same value in async code. WebUSB supports only the async form.

The receive stream owns endpoint 0x81 and a persistent queue of 16 transfers, each 256 KiB (4 MiB total). Its steady-state read path does not acquire the device lifecycle mutex and does not allocate per read. The stream is pull-based: the application must keep reading while RX is enabled. At 20 Msamples/s the queue represents about 105 ms of host-side buffering; at 10 Msamples/s it is about 210 ms. HackRF transfers do not include sequence numbers, so StreamingStats reports host completion failures and controlled discards but cannot measure samples already lost inside the device.

Awaited native operations normally need one of nusb's runtime integrations:

cargo check --features tokio
cargo check --features smol

Use the integration matching the application's executor. The default feature set has no async-runtime dependency and supports the blocking native API.

Configuration

Config::builder() and Device::builder() validate settings before USB is opened or touched:

  • frequency: 1 MHz through 6 GHz inclusive;
  • complex sample rate: 2 MHz through 20 MHz inclusive;
  • RX IF/LNA gain: 0 through 40 dB in 8 dB steps;
  • baseband/VGA gain: 0 through 62 dB in 2 dB steps.

The defaults are 900 MHz, 10 Msamples/s, 8 dB LNA gain, 20 dB VGA gain, and both the RF amplifier and antenna bias disabled. Whenever the sample rate is set, the driver requests a baseband-filter bandwidth equal to the full sample rate; firmware maps that request to a bandwidth supported by the installed radio.

Device::config() is the configuration last successfully sent by this driver, not hardware readback. A complete validated Config can be applied with configure, and individual settings can be changed while receiving.

Synchronous API

use hackrf_nusb::{Complex32, Device, MaybeFuture};
use std::time::Duration;

fn main() -> hackrf_nusb::Result<()> {
    let mut device = Device::builder()
        .frequency_hz(100_000_000)
        .sample_rate_hz(10_000_000)
        .lna_gain_db(16)
        .vga_gain_db(20)
        .open()
        .wait()?;

    println!(
        "opened {} firmware={} serial={:?}",
        device.info().board_name(),
        device.info().firmware_version,
        device.info().serial.map(|serial| format!("{serial:032x}")),
    );

    let mut rx = device.rx_stream()?;
    rx.start().wait()?;
    device.set_frequency_hz(101_000_000).wait()?;

    let mut samples = [Complex32::default(); 4096];
    let count = rx
        .read(&mut samples, Some(Duration::from_secs(1)))
        .wait()?;

    let stats = rx.stop().wait()?;
    drop(rx);
    device.shutdown().wait()?;
    println!("read {count} samples; {stats:?}");
    Ok(())
}

Only one RxStream can be claimed per device. stop pauses RX while preserving the async transfer queue for a later restart. A running stream makes explicit device shutdown return Busy; a stopped stream may remain owned, but cannot be restarted after shutdown.

Asynchronous API

The same API can be awaited:

use futures_lite::future::block_on;
use hackrf_nusb::{Complex32, Device};

fn main() -> hackrf_nusb::Result<()> {
    block_on(async {
        let mut device = Device::builder()
            .frequency_hz(144_500_000)
            .sample_rate_hz(10_000_000)
            .open()
            .await?;

        let mut rx = device.rx_stream()?;
        rx.start().await?;
        let mut samples = [Complex32::default(); 4096];
        let count = rx.read(&mut samples, None).await?;
        let stats = rx.stop().await?;
        drop(rx);
        device.shutdown().await?;
        println!("read {count} samples; {stats:?}");
        Ok(())
    })
}

Waiting the first stream operation selects blocking USB; awaiting it selects async USB. Do not mix the two styles on the same stream.

Serials

HackRF firmware exposes a 32-hex-digit serial. Discovery parses all 128 bits into DeviceDescriptor::serial, DeviceInfo::serial reports the firmware value, and Device::open_serial / DeviceBuilder::serial select by the exact value. Format it with {serial:032x} when leading zeroes must be shown.

WebUSB

On wasm32-unknown-unknown, use the async API and request access during a browser user gesture before opening:

Device::builder().serial(serial).request_permission().await?;
let device = Device::open_serial(serial).await?;

WebUSB requires a secure context, a supporting browser, and user permission. The final application crate must enable the unstable web-sys WebUSB bindings. This repository's .cargo/config.toml contains the required setting:

[target.wasm32-unknown-unknown]
rustflags = ["--cfg=web_sys_unstable_apis"]

Native endpoints can cancel retained transfers at stop. WebUSB cannot, so after a restart the stream first drains and discards every transfer that was pending at stop. buffers_discarded_on_restart reports that warm-up.

Linux permissions and RF safety

Install the upstream HackRF udev rules, or equivalent rules for these IDs:

SUBSYSTEM=="usb", ATTR{idVendor}=="1d50", ATTR{idProduct}=="604b", MODE="0660", GROUP="plugdev", TAG+="uaccess"
SUBSYSTEM=="usb", ATTR{idVendor}=="1d50", ATTR{idProduct}=="6089", MODE="0660", GROUP="plugdev", TAG+="uaccess"
SUBSYSTEM=="usb", ATTR{idVendor}=="1d50", ATTR{idProduct}=="cc15", MODE="0660", GROUP="plugdev", TAG+="uaccess"

Reload rules and replug the device. Group names vary by distribution.

The antenna bias setting can place DC power on the RF port. It is disabled by default. Prefer explicit stop and shutdown so cleanup errors are observable. Native drops attempt receiver-off cleanup; WebUSB drops schedule best-effort async cleanup, and neither drop path can report an error.

Examples and hardware tests are gated and do not touch USB unless explicitly requested:

cargo run --example rx_sync -- --run
cargo run --example rx_sync -- --run --rx
cargo run --features smol --example rx_async -- --run
cargo run --features smol --example rx_async -- --run --rx
cargo test --test hardware --features smol -- --ignored --test-threads=1 --nocapture

License

Licensed under either Apache-2.0 or MIT, at your option.