From 87a2315258ae9cde8c45bdb6e2cf894065510303 Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Mon, 2 Sep 2024 11:19:22 -0700 Subject: [PATCH 01/16] base driver --- Cargo.toml | 4 +- crates/hackrf-one-rs/.gitignore | 1 + crates/hackrf-one-rs/Cargo.lock | 165 ++++++ crates/hackrf-one-rs/Cargo.toml | 13 + crates/hackrf-one-rs/examples/info.rs | 10 + crates/hackrf-one-rs/examples/rx.rs | 54 ++ crates/hackrf-one-rs/src/lib.rs | 703 ++++++++++++++++++++++++++ src/impls/hackrfone.rs | 438 ++++++++++++++++ src/impls/mod.rs | 5 + src/lib.rs | 3 + 10 files changed, 1395 insertions(+), 1 deletion(-) create mode 100644 crates/hackrf-one-rs/.gitignore create mode 100644 crates/hackrf-one-rs/Cargo.lock create mode 100644 crates/hackrf-one-rs/Cargo.toml create mode 100644 crates/hackrf-one-rs/examples/info.rs create mode 100644 crates/hackrf-one-rs/examples/rx.rs create mode 100644 crates/hackrf-one-rs/src/lib.rs create mode 100644 src/impls/hackrfone.rs diff --git a/Cargo.toml b/Cargo.toml index 7486176..926b169 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,9 @@ license = "Apache-2.0" repository = "https://github.com/FutureSDR/seify" [features] -default = ["soapy"] +default = ["soapy", "hackrfone"] rtlsdr = ["dep:seify-rtlsdr"] +hackrfone = ["dep:seify-hackrfone"] aaronia = ["dep:aaronia-rtsa"] aaronia_http = ["dep:ureq"] soapy = ["dep:soapysdr"] @@ -32,6 +33,7 @@ thiserror = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] once_cell = "1.19" seify-rtlsdr = { path = "crates/rtl-sdr-rs", version = "0.0.3", optional = true } +seify-hackrfone = { path = "crates/hackrf-one-rs", optional = true } soapysdr = { version = "0.4", optional = true } ureq = { version = "2.9", features = ["json"], optional = true } diff --git a/crates/hackrf-one-rs/.gitignore b/crates/hackrf-one-rs/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/crates/hackrf-one-rs/.gitignore @@ -0,0 +1 @@ +/target diff --git a/crates/hackrf-one-rs/Cargo.lock b/crates/hackrf-one-rs/Cargo.lock new file mode 100644 index 0000000..f2302c6 --- /dev/null +++ b/crates/hackrf-one-rs/Cargo.lock @@ -0,0 +1,165 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "atomic_enum" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e1aca718ea7b89985790c94aad72d77533063fe00bc497bb79a7c2dae6a661" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "cc" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +dependencies = [ + "shlex", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libusb1-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rusb" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4" +dependencies = [ + "libc", + "libusb1-sys", +] + +[[package]] +name = "seify-hackrfone" +version = "0.1.0" +dependencies = [ + "anyhow", + "atomic_enum", + "num-complex", + "rusb", + "thiserror", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" diff --git a/crates/hackrf-one-rs/Cargo.toml b/crates/hackrf-one-rs/Cargo.toml new file mode 100644 index 0000000..64a776a --- /dev/null +++ b/crates/hackrf-one-rs/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "seify-hackrfone" +version = "0.1.0" +edition = "2021" + +[dependencies] +rusb = "0.9" +num-complex = "0.4" +thiserror = "1.0.63" +atomic_enum = "0.3.0" + +[dev-dependencies] +anyhow = "1.0.86" diff --git a/crates/hackrf-one-rs/examples/info.rs b/crates/hackrf-one-rs/examples/info.rs new file mode 100644 index 0000000..0759274 --- /dev/null +++ b/crates/hackrf-one-rs/examples/info.rs @@ -0,0 +1,10 @@ +use seify_hackrfone::HackRf; + +fn main() { + let context = rusb::Context::new().expect("Failed to create rusb Context"); + let radio = HackRf::new(context).expect("Failed to open Hackrf"); + + println!("Board ID: {:?}", radio.board_id()); + println!("Version: {:?}", radio.version()); + println!("Device version: {:?}", radio.device_version()); +} diff --git a/crates/hackrf-one-rs/examples/rx.rs b/crates/hackrf-one-rs/examples/rx.rs new file mode 100644 index 0000000..0634591 --- /dev/null +++ b/crates/hackrf-one-rs/examples/rx.rs @@ -0,0 +1,54 @@ +use anyhow::{Context, Result}; +use num_complex::Complex32; +use seify_hackrfone::{HackRf, RxConfig}; +use std::time::Instant; + +fn main() -> Result<()> { + let context = rusb::Context::new().context("Failed to create rusb Context")?; + let radio = HackRf::new(context).context("Failed to open Hackrf")?; + + println!("Board ID: {:?}", radio.board_id()); + println!("Version: {:?}", radio.version()); + println!("Device version: {:?}", radio.device_version()); + + radio + .start_rx(&RxConfig { + frequency_hz: 2_410_000_000, + amp_enable: true, + antenna_enable: false, + ..Default::default() + }) + .context("Failed to receive on hackrf")?; + + const MTU: usize = 128 * 1024; + let mut buf = vec![0u8; MTU]; + let mut samples = vec![]; + + let collect_count = 100_000_000; + let mut last_print = Instant::now(); + + while samples.len() < collect_count { + let n = radio.rx(&mut buf).context("Failed to receive samples")?; + assert_eq!(n, buf.len()); + for iq in buf.chunks_exact(2) { + samples.push(Complex32::new( + (iq[0] as f32 - 127.0) / 128.0, + (iq[1] as f32 - 127.0) / 128.0, + )); + } + + if last_print.elapsed().as_millis() > 500 { + println!( + " read {} samples ({:.1}%)", + samples.len(), + samples.len() as f64 / collect_count as f64 * 100.0 + ); + last_print = Instant::now(); + } + } + println!("Collected {} samples", samples.len()); + + println!("First 100 {:#?} samples", &samples[..100]); + + Ok(()) +} diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs new file mode 100644 index 0000000..3928d65 --- /dev/null +++ b/crates/hackrf-one-rs/src/lib.rs @@ -0,0 +1,703 @@ +#![deny(unsafe_code)] + +//! HackRF One API. +//! +//! To get started take a look at [`HackRfOne::new`]. +#![cfg_attr(docsrs, feature(doc_cfg), feature(doc_auto_cfg))] +// TODO(tjn): re-enable +// #![warn(missing_docs)] + +use rusb::{request_type, Context, Direction, Recipient, RequestType, UsbContext, Version}; +use std::{ + sync::atomic::{AtomicU64, Ordering}, + time::Duration, +}; + +/// HackRF USB vendor ID. +const HACKRF_USB_VID: u16 = 0x1D50; +/// HackRF One USB product ID. +const HACKRF_ONE_USB_PID: u16 = 0x6089; + +#[allow(dead_code)] +#[repr(u8)] +enum Request { + SetTransceiverMode = 1, + Max2837Write = 2, + Max2837Read = 3, + Si5351CWrite = 4, + Si5351CRead = 5, + SampleRateSet = 6, + BasebandFilterBandwidthSet = 7, + Rffc5071Write = 8, + Rffc5071Read = 9, + SpiflashErase = 10, + SpiflashWrite = 11, + SpiflashRead = 12, + BoardIdRead = 14, + VersionStringRead = 15, + SetFreq = 16, + AmpEnable = 17, + BoardPartidSerialnoRead = 18, + SetLnaGain = 19, + SetVgaGain = 20, + SetTxvgaGain = 21, + AntennaEnable = 23, + SetFreqExplicit = 24, + UsbWcidVendorReq = 25, + InitSweep = 26, + OperacakeGetBoards = 27, + OperacakeSetPorts = 28, + SetHwSyncMode = 29, + Reset = 30, + OperacakeSetRanges = 31, + ClkoutEnable = 32, + SpiflashStatus = 33, + SpiflashClearStatus = 34, + OperacakeGpioTest = 35, + CpldChecksum = 36, + UiEnable = 37, +} + +#[allow(dead_code)] +#[repr(u16)] +enum TransceiverMode { + Off = 0, + Receive = 1, + Transmit = 2, + Ss = 3, + CpldUpdate = 4, + RxSweep = 5, +} + +#[atomic_enum::atomic_enum] +#[derive(PartialEq)] +pub enum Mode { + Idle = 0, + Receive, + Transmit, +} + +/// Configuration for TX gain settings +/// The LNA is always turned off for TX operations +#[derive(Debug)] +pub struct TxConfig { + /// Baseband gain, 0-62dB in 2dB increments + pub vga_db: u16, + /// 0 - 47 dB in 1dB increments + pub txvga_db: u16, + /// RF amplifier (on/off) + pub amp_enable: bool, + /// Antenna power port control + pub antenna_enable: bool, + /// Frequency in hz + pub frequency_hz: u64, +} + +impl Default for TxConfig { + fn default() -> Self { + Self { + vga_db: 16, + txvga_db: 40, + amp_enable: true, + antenna_enable: true, + // set within ITU americas 900mhz ISM + // as punishment for calling ::default() + frequency_hz: 908_000_000, + } + } +} + +/// Configuration for RX gain settings +/// The TXVGA is always turned off for RX operations +#[derive(Debug)] +pub struct RxConfig { + /// Baseband gain, 0-62dB in 2dB increments + pub vga_db: u16, + /// Low-noise amplifier gain, in 0-40dB in 8dB increments + pub lna_db: u16, + /// RF amplifier (on/off) + pub amp_enable: bool, + /// Antenna power port control + pub antenna_enable: bool, + /// Frequency in hz + pub frequency_hz: u64, +} + +impl Default for RxConfig { + fn default() -> Self { + Self { + vga_db: 8, + lna_db: 16, + amp_enable: true, + antenna_enable: true, + // set within global 900mhz ISM band to avoid sending our engineers to foreign prisons + // as punishment for calling ::default() + frequency_hz: 908_000_000, + } + } +} + +/// HackRF One errors. +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("rusb")] + Usb(#[from] rusb::Error), + #[error("transfer")] + Transfer { + /// Control transfer direction. + dir: Direction, + /// Actual amount of bytes transferred. + actual: usize, + /// Excepted number of bytes transferred. + expected: usize, + }, + /// An API call is not supported by your hardware. + /// + /// Try updating the firmware on your device. + #[error("no api")] + NoApi { + /// Current device version. + device: Version, + /// Minimum version required. + min: Version, + }, + #[error("{0}")] + Argument(&'static str), + #[error("Hackrf in invalid mode. Required: {required:?}, actual: {actual:?}")] + WrongMode { required: Mode, actual: Mode }, +} + +pub type Result = std::result::Result; + +/// HackRF One software defined radio. +pub struct HackRf { + handle: rusb::DeviceHandle, + discriptor: rusb::DeviceDescriptor, + mode: AtomicMode, + timeout_nanos: AtomicU64, +} + +impl HackRf { + pub fn new(ctx: Context) -> Option { + // TODO: use seify args + let devices = match ctx.devices() { + Ok(d) => d, + Err(_) => return None, + }; + + for device in devices.iter() { + let desc = match device.device_descriptor() { + Ok(d) => d, + Err(_) => continue, + }; + + if desc.vendor_id() == HACKRF_USB_VID && desc.product_id() == HACKRF_ONE_USB_PID { + match device.open() { + Ok(handle) => { + return Some(HackRf { + handle, + discriptor: desc, + timeout_nanos: AtomicU64::new( + Duration::from_millis(500).as_nanos() as u64 + ), + mode: AtomicMode::new(Mode::Idle), + }) + } + Err(_) => continue, + } + } + } + + None + } + + pub fn wrap(handle: rusb::DeviceHandle, desc: rusb::DeviceDescriptor) -> HackRf { + HackRf { + handle, + discriptor: desc, + timeout_nanos: AtomicU64::new(Duration::from_millis(500).as_nanos() as u64), + mode: AtomicMode::new(Mode::Idle), + } + } + + pub fn reset(self) -> Result<()> { + self.check_api_version(Version::from_bcd(0x0102))?; + self.write_control(Request::Reset, 0, 0, &[])?; + + Ok(()) + } + + pub fn device_version(&self) -> Version { + self.discriptor.device_version() + } + + pub fn board_id(&self) -> Result { + let data: [u8; 1] = self.read_control(Request::BoardIdRead, 0, 0)?; + Ok(data[0]) + } + + /// Read the firmware version. + pub fn version(&self) -> Result { + let mut buf: [u8; 16] = [0; 16]; + let n: usize = self.handle.read_control( + request_type(Direction::In, RequestType::Vendor, Recipient::Device), + Request::VersionStringRead as u8, + 0, + 0, + &mut buf, + self.timeout(), + )?; + Ok(String::from_utf8_lossy(&buf[0..n]).into()) + } + + /// Set the center frequency. + pub fn set_freq(&self, hz: u64) -> Result<()> { + let buf: [u8; 8] = freq_params(hz); + self.write_control(Request::SetFreq, 0, 0, &buf) + } + + /// Enable the RX/TX RF amplifier. + /// + /// In GNU radio this is used as the RF gain, where a value of 0 dB is off, + /// and a value of 14 dB is on. + pub fn set_amp_enable(&self, enable: bool) -> Result<()> { + self.write_control(Request::AmpEnable, enable.into(), 0, &[]) + } + + /// Set the baseband filter bandwidth. + /// + /// This is automatically set when the sample rate is changed with + /// [`set_sample_rate`]. + pub fn set_baseband_filter_bandwidth(&self, hz: u32) -> Result<()> { + self.write_control( + Request::BasebandFilterBandwidthSet, + (hz & 0xFFFF) as u16, + (hz >> 16) as u16, + &[], + ) + } + + /// Set the sample rate. + /// + /// For anti-aliasing, the baseband filter bandwidth is automatically set to + /// the widest available setting that is no more than 75% of the sample rate. + /// This happens every time the sample rate is set. + /// If you want to override the baseband filter selection, you must do so + /// after setting the sample rate. + /// + /// Limits are 8MHz - 20MHz. + /// Preferred rates are 8, 10, 12.5, 16, 20MHz due to less jitter. + pub fn set_sample_rate(&self, hz: u32, div: u32) -> Result<()> { + let hz: u32 = hz.to_le(); + let div: u32 = div.to_le(); + let buf: [u8; 8] = [ + (hz & 0xFF) as u8, + ((hz >> 8) & 0xFF) as u8, + ((hz >> 16) & 0xFF) as u8, + ((hz >> 24) & 0xFF) as u8, + (div & 0xFF) as u8, + ((div >> 8) & 0xFF) as u8, + ((div >> 16) & 0xFF) as u8, + ((div >> 24) & 0xFF) as u8, + ]; + self.write_control(Request::SampleRateSet, 0, 0, &buf)?; + self.set_baseband_filter_bandwidth((0.75 * (hz as f32) / (div as f32)) as u32) + } + + /// Set the LNA (low noise amplifier) gain. + /// + /// Range 0 to 40dB in 8dB steps. + /// + /// This is also known as the IF gain. + pub fn set_lna_gain(&self, gain: u16) -> Result<()> { + if gain > 40 { + Err(Error::Argument("lna gain must be less than 40")) + } else { + let buf: [u8; 1] = self.read_control(Request::SetLnaGain, 0, gain & !0x07)?; + if buf[0] == 0 { + // TODO(tjn): check hackrf docs + panic!(); + } else { + Ok(()) + } + } + } + + /// Set the VGA (variable gain amplifier) gain. + /// + /// Range 0 to 62dB in 2dB steps. + /// + /// This is also known as the baseband (BB) gain. + pub fn set_vga_gain(&self, gain: u16) -> Result<()> { + if gain > 62 || gain % 2 == 1 { + Err(Error::Argument( + "gain parameter out of range. must be even and less than or equal to 62", + )) + } else { + // TODO(tjn): read_control seems wrong here, investigate + let buf: [u8; 1] = self.read_control(Request::SetVgaGain, 0, gain & !0b1)?; + if buf[0] == 0 { + panic!("What is this return value?") + } else { + Ok(()) + } + } + } + + /// Set the transmit VGA gain. + /// + /// Range 0 to 47dB in 1db steps. + pub fn set_txvga_gain(&self, gain: u16) -> Result<()> { + if gain > 47 { + Err(Error::Argument("gain parameter out of range. max is 47")) + } else { + // TODO(tjn): read_control seems wrong here, investigate + let buf: [u8; 1] = self.read_control(Request::SetTxvgaGain, 0, gain)?; + if buf[0] == 0 { + panic!("What is this return value?") + } else { + Ok(()) + } + } + } + + /// Antenna power port control. Dhruv's guess: is this DC bias? + /// + /// The source docs are a little lacking in terms of explanations here. + pub fn set_antenna_enable(&self, value: bool) -> Result<()> { + let value = value.then(|| 1).unwrap_or(0); + self.write_control(Request::AntennaEnable, value, 0, &[]) + } + + /// Transitions the radio into transmit mode. + /// Call this function before calling [`Self::tx`]. + /// + /// Previous state set via `set_xxx` functions will be overriden with the parameters set in `config`. + /// + /// # Errors + /// This function will return an error if a tx or rx operation is already in progress or if an + /// I/O error occurs + pub fn start_tx(&self, config: &TxConfig) -> Result<()> { + // NOTE: perform atomic exchange first so that we only change the transceiver mode once if + // other therads are racing to change the mode + if let Err(actual) = self.mode.compare_exchange( + Mode::Idle, + Mode::Transmit, + Ordering::AcqRel, + Ordering::Relaxed, + ) { + return Err(Error::WrongMode { + required: Mode::Idle, + actual, + }); + } + + self.set_lna_gain(0)?; + self.set_vga_gain(config.vga_db)?; + self.set_txvga_gain(config.txvga_db)?; + self.set_freq(config.frequency_hz)?; + self.set_amp_enable(config.amp_enable)?; + self.set_antenna_enable(config.antenna_enable)?; + + self.write_control( + Request::SetTransceiverMode, + TransceiverMode::Transmit as u16, + 0, + &[], + )?; + + //released when devh goes out of scope. may pose an issue when switching between rx and tx + self.handle.claim_interface(0)?; + + Ok(()) + } + + /// Transitions the radio into receive mode. + /// Call this function before calling [`Self::rx`]. + /// + /// Previous state set via `set_xxx` functions will be overriden with the parameters set in `config`. + /// + /// # Errors + /// This function will return an error if a tx or rx operation is already in progress or if an + /// I/O error occurs + pub fn start_rx(&self, config: &RxConfig) -> Result<()> { + // NOTE: perform the atomic exchange first so that we only change the hackrf's state once if + // other threads are racing with us + if let Err(actual) = self.mode.compare_exchange( + Mode::Idle, + Mode::Receive, + Ordering::AcqRel, + Ordering::Relaxed, + ) { + return Err(Error::WrongMode { + required: Mode::Idle, + actual, + }); + } + + self.set_freq(config.frequency_hz)?; + self.set_vga_gain(config.vga_db)?; + self.set_txvga_gain(0)?; + self.set_amp_enable(config.amp_enable)?; + self.set_antenna_enable(config.antenna_enable)?; + + self.write_control( + Request::SetTransceiverMode, + TransceiverMode::Receive as u16, + 0, + &[], + )?; + + //released when devh goes out of scope. may pose an issue when switching between rx and tx + self.handle.claim_interface(0)?; + + Ok(()) + } + + pub fn stop_tx(&self) -> Result<()> { + // NOTE: perform atomic exchange last so that we prevent other threads from racing to + // start tx/rx with the delivery of our TransceiverMode::Off request + // + // This means that multiple threads can race on stop_tx/stop_rx, however in the worst case + // the hackrf will receive multiple TransceiverMode::Off requests, but will always end up + // in a valid state with the transceiver disabled. A mutex or other mode variants like + // Mode::IdlePending would solve this, however quickly this begins to look like a manually implemented mutex. + // + // To keep this crate low-level and low-overhead, this solution is fine and we expect + // consumers to wrap our type in an Arc and be smart enough to not enable / disable tx / rx + // from multiple threads at the same time. + + self.handle.release_interface(0)?; + + self.write_control( + Request::SetTransceiverMode, + TransceiverMode::Off as u16, + 0, + &[], + )?; + + if let Err(actual) = self.mode.compare_exchange( + Mode::Transmit, + Mode::Idle, + Ordering::AcqRel, + Ordering::Relaxed, + ) { + return Err(Error::WrongMode { + required: Mode::Idle, + actual, + }); + } + + Ok(()) + } + + pub fn stop_rx(&self) -> Result<()> { + // NOTE: perform atomic exchange last so that we prevent other threads from racing to + // start tx/rx with the delivery of our TransceiverMode::Off request + + self.handle.release_interface(0)?; + + self.write_control( + Request::SetTransceiverMode, + TransceiverMode::Off as u16, + 0, + &[], + )?; + + if let Err(actual) = self.mode.compare_exchange( + Mode::Receive, + Mode::Idle, + Ordering::AcqRel, + Ordering::Relaxed, + ) { + return Err(Error::WrongMode { + required: Mode::Idle, + actual, + }); + } + + Ok(()) + } + + /// Read samples from the radio. + /// + /// # Panics + /// This function panics if samples is not a multiple of 512 + pub fn rx(&self, samples: &mut [u8]) -> Result { + self.ensure_mode(Mode::Receive)?; + + if samples.len() % 512 != 0 { + panic!("samples must be a multiple of 512"); + } + + const ENDPOINT: u8 = 0x81; + Ok(self.handle.read_bulk(ENDPOINT, samples, self.timeout())?) + } + + /// Writes samples to the radio. + /// + /// # Panics + /// This function panics if samples is not a multiple of 512 + pub fn tx(&self, samples: &[u8]) -> Result { + self.ensure_mode(Mode::Transmit)?; + + if samples.len() % 512 != 0 { + panic!("samples must be a multiple of 512"); + } + + const ENDPOINT: u8 = 0x02; + Ok(self.handle.write_bulk(ENDPOINT, samples, self.timeout())?) + } +} + +impl HackRf { + fn ensure_mode(&self, expected: Mode) -> Result<()> { + let actual = self.mode.load(Ordering::Acquire); + if actual != expected { + return Err(Error::WrongMode { + required: expected, + actual, + }); + } + Ok(()) + } + + fn timeout(&self) -> Duration { + Duration::from_nanos(self.timeout_nanos.load(Ordering::Acquire)) + } + + fn read_control( + &self, + request: Request, + value: u16, + index: u16, + ) -> Result<[u8; N]> { + let mut buf: [u8; N] = [0; N]; + let n: usize = self.handle.read_control( + request_type(Direction::In, RequestType::Vendor, Recipient::Device), + request as u8, + value, + index, + &mut buf, + self.timeout(), + )?; + if n != buf.len() { + Err(Error::Transfer { + dir: Direction::In, + actual: n, + expected: buf.len(), + }) + } else { + Ok(buf) + } + } + + fn write_control(&self, request: Request, value: u16, index: u16, buf: &[u8]) -> Result<()> { + let n: usize = self.handle.write_control( + request_type(Direction::Out, RequestType::Vendor, Recipient::Device), + request as u8, + value, + index, + buf, + self.timeout(), + )?; + if n != buf.len() { + Err(Error::Transfer { + dir: Direction::Out, + actual: n, + expected: buf.len(), + }) + } else { + Ok(()) + } + } + + fn check_api_version(&self, min: Version) -> Result<()> { + fn version_to_u32(v: Version) -> u32 { + ((v.major() as u32) << 16) | ((v.minor() as u32) << 8) | (v.sub_minor() as u32) + } + + let v = self.discriptor.device_version(); + + if version_to_u32(v) >= version_to_u32(min) { + Ok(()) + } else { + Err(Error::NoApi { device: v, min }) + } + } +} + +// Helper for set_freq +fn freq_params(hz: u64) -> [u8; 8] { + const MHZ: u64 = 1_000_000; + + let l_freq_mhz: u32 = u32::try_from(hz / MHZ).unwrap_or(u32::MAX).to_le(); + let l_freq_hz: u32 = u32::try_from(hz - u64::from(l_freq_mhz) * MHZ) + .unwrap_or(u32::MAX) + .to_le(); + + [ + (l_freq_mhz & 0xFF) as u8, + ((l_freq_mhz >> 8) & 0xFF) as u8, + ((l_freq_mhz >> 16) & 0xFF) as u8, + ((l_freq_mhz >> 24) & 0xFF) as u8, + (l_freq_hz & 0xFF) as u8, + ((l_freq_hz >> 8) & 0xFF) as u8, + ((l_freq_hz >> 16) & 0xFF) as u8, + ((l_freq_hz >> 24) & 0xFF) as u8, + ] +} + +#[cfg(test)] +mod test { + use std::time::Duration; + + use crate::TxConfig; + + use super::{freq_params, HackRf}; + + #[test] + fn nominal() { + assert_eq!(freq_params(915_000_000), [0x93, 0x03, 0, 0, 0, 0, 0, 0]); + assert_eq!(freq_params(915_000_001), [0x93, 0x03, 0, 0, 1, 0, 0, 0]); + assert_eq!( + freq_params(123456789), + [0x7B, 0, 0, 0, 0x55, 0xF8, 0x06, 0x00] + ); + } + + #[test] + fn min() { + assert_eq!(freq_params(0), [0; 8]); + } + + #[test] + fn max() { + assert_eq!(freq_params(u64::MAX), [0xFF; 8]); + } + + #[test] + fn abc() { + let context = rusb::Context::new().expect("Failed to get libusb context"); + let radio = HackRf::new(context).expect("Failed to open hackrf. No radio connected?"); + radio.start_tx(&Default::default()).unwrap(); + std::thread::sleep(Duration::from_millis(50)); + + radio.stop_tx().unwrap(); + assert!(radio.stop_tx().is_err()); + assert!(radio.stop_tx().is_err()); + assert!(radio.stop_rx().is_err()); + assert!(radio.stop_rx().is_err()); + + std::thread::sleep(Duration::from_millis(50)); + + radio.start_rx(&Default::default()).unwrap(); + std::thread::sleep(Duration::from_millis(50)); + + radio.stop_rx().unwrap(); + assert!(radio.stop_rx().is_err()); + assert!(radio.stop_rx().is_err()); + assert!(radio.stop_tx().is_err()); + assert!(radio.stop_tx().is_err()); + } +} diff --git a/src/impls/hackrfone.rs b/src/impls/hackrfone.rs new file mode 100644 index 0000000..4a0f2ad --- /dev/null +++ b/src/impls/hackrfone.rs @@ -0,0 +1,438 @@ +use std::sync::Arc; + +use crate::Error; + +pub struct HackRfOne { + dev: Arc, +} + +impl seify::RxStreamer for Rx { + fn mtu(&self) -> Result { + // TOOD(tjn): verify + Ok(128 * 1024) + } + + fn activate_at(&mut self, time_ns: Option) -> Result<(), Error> { + // TODO(tjn): sleep precisely for `time_ns` + + let cfg = &self.inner.rx_config; + self.inner.set_freq(cfg.frequency_hz)?; + self.inner.set_vga_gain(cfg.vga_db)?; + self.inner.set_txvga_gain(0)?; + self.inner.set_amp_enable(cfg.amp_enable)?; + self.inner.set_antenna_enable(cfg.antenna_enable)?; + + self.inner.write_control( + Request::SetTransceiverMode, + TranscieverMode::Receive as u16, + 0, + &[], + ); + self.dh.claim_interface(0)?; + + Ok(()) + } + + fn deactivate_at(&mut self, time_ns: Option) -> Result<(), Error> { + // TODO(tjn): sleep precisely for `time_ns` + self.inner.dh.release_interface(0).unwrap(); + self.inner.set_transceiver_mode(TranscieverMode::Off).unwrap(); + Ok(()) + } + + fn read( + &mut self, + buffers: &mut [&mut [num_complex::Complex32]], + timeout_us: i64, + ) -> Result { + const ENDPOINT: u8 = 0x81; + assert_eq!(buffers.len(), 1); + let dst = buffers[0]; + self.buf.resize(dst.len() * 2, 0); + + let n = self.inner.dh.read_bulk(ENDPOINT, &mut self.buf, self.inner.to).unwrap(); + assert_eq!(n, self.buf.len()); + } +} + +struct Tx { + inner: Arc, +} + +impl seify::TxStreamer for Tx { + fn mtu(&self) -> Result { + // TOOD(tjn): verify + Ok(128 * 1024) + } + + fn activate_at(&mut self, time_ns: Option) -> Result<(), Error> { + // TODO(tjn): sleep precisely for `time_ns` + + let cfg = &self.inner.tx_config; + self.inner.set_lna_gain(0)?; + self.inner.set_vga_gain(cfg.vga_db)?; + self.inner.set_txvga_gain(cfg.txvga_db)?; + self.inner.set_freq(cfg.frequency_hz)?; + self.inner.set_amp_enable(cfg.amp_enable)?; + self.inner.set_antenna_enable(cfg.antenna_enable)?; + + self.inner. + /* + self.write_control( + Request::SetTransceiverMode, + TranscieverMode::Transmit as u16, + 0, + &[], + ); + */ + + Ok(()) + } + + fn deactivate_at(&mut self, time_ns: Option) -> Result<(), Error> { + self.dh.release_interface(0)?; + self.set_transceiver_mode(TranscieverMode::Off)?; + todo!() + } + + fn write( + &mut self, + buffers: &[&[num_complex::Complex32]], + at_ns: Option, + end_burst: bool, + timeout_us: i64, + ) -> Result { + + // TODO: + self.dh + .write_bulk(ENDPOINT, samples, Duration::from_millis(1)) + .map_err(Error::Usb) + todo!() + } + + fn write_all( + &mut self, + buffers: &[&[num_complex::Complex32]], + at_ns: Option, + end_burst: bool, + timeout_us: i64, + ) -> Result<(), Error> { + todo!() + } +} + +impl seify::DeviceTrait for HackRf { + type RxStreamer = Rx; + + type TxStreamer = Tx; + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn driver(&self) -> seify::Driver { + // ??? no enum variant that makes sense for us + todo!() + } + + fn id(&self) -> Result { + todo!() + } + + fn info(&self) -> Result { + Ok(Default::default()) + } + + fn num_channels(&self, _: seify::Direction) -> Result { + Ok(1) + } + + + + + fn full_duplex(&self, _direction: Direction, _channel: usize) -> Result { + Ok(false) + } + + fn rx_streamer(&self, channels: &[usize], _args: Args) -> Result { + if channels != [0] { + Err(Error::ValueError) + } else { + Ok(RxStreamer::new(self.dev.clone())) + } + } + + fn tx_streamer(&self, _channels: &[usize], _args: Args) -> Result { + Err(Error::NotSupported) + } + + fn antennas(&self, direction: Direction, channel: usize) -> Result, Error> { + self.antenna(direction, channel).map(|a| vec![a]) + } + + fn antenna(&self, direction: Direction, channel: usize) -> Result { + if matches!(direction, Rx) && channel == 0 { + Ok("RX".to_string()) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn set_antenna(&self, direction: Direction, channel: usize, name: &str) -> Result<(), Error> { + if matches!(direction, Rx) && channel == 0 && name == "RX" { + Ok(()) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn gain_elements(&self, direction: Direction, channel: usize) -> Result, Error> { + if matches!(direction, Rx) && channel == 0 { + Ok(vec!["TUNER".to_string()]) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn supports_agc(&self, direction: Direction, channel: usize) -> Result { + if matches!(direction, Rx) && channel == 0 { + Ok(true) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn enable_agc(&self, direction: Direction, channel: usize, agc: bool) -> Result<(), Error> { + let gains = self.dev.get_tuner_gains().or(Err(Error::DeviceError))?; + if matches!(direction, Rx) && channel == 0 { + let mut inner = self.i.lock().unwrap(); + if agc { + inner.gain = TunerGain::Auto; + Ok(self.dev.set_tuner_gain(inner.gain.clone())?) + } else { + inner.gain = TunerGain::Manual(gains[gains.len() / 2]); + Ok(self.dev.set_tuner_gain(inner.gain.clone())?) + } + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn agc(&self, direction: Direction, channel: usize) -> Result { + if matches!(direction, Rx) && channel == 0 { + let inner = self.i.lock().unwrap(); + Ok(matches!(inner.gain, TunerGain::Auto)) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn set_gain(&self, direction: Direction, channel: usize, gain: f64) -> Result<(), Error> { + self.set_gain_element(direction, channel, "TUNER", gain) + } + + fn gain(&self, direction: Direction, channel: usize) -> Result, Error> { + self.gain_element(direction, channel, "TUNER") + } + + fn gain_range(&self, direction: Direction, channel: usize) -> Result { + self.gain_element_range(direction, channel, "TUNER") + } + + fn set_gain_element( + &self, + direction: Direction, + channel: usize, + name: &str, + gain: f64, + ) -> Result<(), Error> { + let r = self.gain_range(direction, channel)?; + if r.contains(gain) && name == "TUNER" { + let mut inner = self.i.lock().unwrap(); + inner.gain = TunerGain::Manual((gain * 10.0) as i32); + Ok(self.dev.set_tuner_gain(inner.gain.clone())?) + } else { + log::warn!("Gain out of range"); + Err(Error::OutOfRange(r, gain)) + } + } + + fn gain_element( + &self, + direction: Direction, + channel: usize, + name: &str, + ) -> Result, Error> { + if matches!(direction, Rx) && channel == 0 && name == "TUNER" { + let inner = self.i.lock().unwrap(); + match inner.gain { + TunerGain::Auto => Ok(None), + TunerGain::Manual(i) => Ok(Some(i as f64)), + } + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn gain_element_range( + &self, + direction: Direction, + channel: usize, + name: &str, + ) -> Result { + if matches!(direction, Rx) && channel == 0 && name == "TUNER" { + Ok(Range::new(vec![RangeItem::Interval(0.0, 50.0)])) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn frequency_range(&self, direction: Direction, channel: usize) -> Result { + self.component_frequency_range(direction, channel, "TUNER") + } + + fn frequency(&self, direction: Direction, channel: usize) -> Result { + self.component_frequency(direction, channel, "TUNER") + } + + fn set_frequency( + &self, + direction: Direction, + channel: usize, + frequency: f64, + _args: Args, + ) -> Result<(), Error> { + self.set_component_frequency(direction, channel, "TUNER", frequency) + } + + fn frequency_components( + &self, + direction: Direction, + channel: usize, + ) -> Result, Error> { + if matches!(direction, Rx) && channel == 0 { + Ok(vec!["TUNER".to_string()]) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn component_frequency_range( + &self, + direction: Direction, + channel: usize, + name: &str, + ) -> Result { + if matches!(direction, Rx) && channel == 0 && name == "TUNER" { + Ok(Range::new(vec![RangeItem::Interval(0.0, 2e9)])) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn component_frequency( + &self, + direction: Direction, + channel: usize, + name: &str, + ) -> Result { + if matches!(direction, Rx) && channel == 0 && name == "TUNER" { + Ok(self.dev.get_center_freq() as f64) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn set_component_frequency( + &self, + direction: Direction, + channel: usize, + name: &str, + frequency: f64, + ) -> Result<(), Error> { + if matches!(direction, Rx) + && channel == 0 + && self + .frequency_range(direction, channel)? + .contains(frequency) + && name == "TUNER" + { + self.dev.set_center_freq(frequency as u32)?; + Ok(self.dev.reset_buffer()?) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn sample_rate(&self, direction: Direction, channel: usize) -> Result { + if matches!(direction, Rx) && channel == 0 { + Ok(self.dev.get_sample_rate() as f64) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn set_sample_rate( + &self, + direction: Direction, + channel: usize, + rate: f64, + ) -> Result<(), Error> { + if matches!(direction, Rx) + && channel == 0 + && self + .get_sample_rate_range(direction, channel)? + .contains(rate) + { + self.dev.set_tuner_bandwidth(rate as u32)?; + Ok(self.dev.set_sample_rate(rate as u32)?) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } + + fn get_sample_rate_range(&self, direction: Direction, channel: usize) -> Result { + if matches!(direction, Rx) && channel == 0 { + Ok(Range::new(vec![ + RangeItem::Interval(225_001.0, 300_000.0), + RangeItem::Interval(900_001.0, 3_200_000.0), + ])) + } else if matches!(direction, Rx) { + Err(Error::ValueError) + } else { + Err(Error::NotSupported) + } + } +} diff --git a/src/impls/mod.rs b/src/impls/mod.rs index d3f9bb8..6b10094 100644 --- a/src/impls/mod.rs +++ b/src/impls/mod.rs @@ -18,3 +18,8 @@ pub use rtlsdr::RtlSdr; pub mod soapy; #[cfg(all(feature = "soapy", not(target_arch = "wasm32")))] pub use soapy::Soapy; + +#[cfg(all(feature = "hackrfone", not(target_arch = "wasm32")))] +pub mod hackrfone; +#[cfg(all(feature = "hackrfone", not(target_arch = "wasm32")))] +pub use hackrfone::HackRfOne; diff --git a/src/lib.rs b/src/lib.rs index 5afc1e8..23d16ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,9 @@ pub enum Error { #[cfg(all(feature = "rtlsdr", not(target_arch = "wasm32")))] #[error("RtlSdr ({0})")] RtlSdr(#[from] seify_rtlsdr::error::RtlsdrError), + #[cfg(all(feature = "rtlsdr", not(target_arch = "wasm32")))] + #[error("RtlSdr ({0})")] + HackRfOne(#[from] seify_rtlsdr::error::RtlsdrError), } #[cfg(all(feature = "aaronia_http", not(target_arch = "wasm32")))] From b21e663374edb45f250f190bbad75a93d2d7c632 Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Mon, 2 Sep 2024 11:19:34 -0700 Subject: [PATCH 02/16] clippy --- crates/hackrf-one-rs/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs index 3928d65..f014ed3 100644 --- a/crates/hackrf-one-rs/src/lib.rs +++ b/crates/hackrf-one-rs/src/lib.rs @@ -365,7 +365,7 @@ impl HackRf { /// /// The source docs are a little lacking in terms of explanations here. pub fn set_antenna_enable(&self, value: bool) -> Result<()> { - let value = value.then(|| 1).unwrap_or(0); + let value = if value { 1 } else { 0 }; self.write_control(Request::AntennaEnable, value, 0, &[]) } @@ -652,7 +652,7 @@ fn freq_params(hz: u64) -> [u8; 8] { mod test { use std::time::Duration; - use crate::TxConfig; + use super::{freq_params, HackRf}; From 103ccc4ff37db321133ad83c53f42a66562609f4 Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Mon, 2 Sep 2024 11:35:41 -0700 Subject: [PATCH 03/16] clean tests --- crates/hackrf-one-rs/examples/rx.rs | 4 +- crates/hackrf-one-rs/src/lib.rs | 92 +++++++++++++---------------- src/impls/hackrfone.rs | 36 +++++------ 3 files changed, 59 insertions(+), 73 deletions(-) diff --git a/crates/hackrf-one-rs/examples/rx.rs b/crates/hackrf-one-rs/examples/rx.rs index 0634591..d6aa2a7 100644 --- a/crates/hackrf-one-rs/examples/rx.rs +++ b/crates/hackrf-one-rs/examples/rx.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use num_complex::Complex32; -use seify_hackrfone::{HackRf, RxConfig}; +use seify_hackrfone::{Config, HackRf}; use std::time::Instant; fn main() -> Result<()> { @@ -12,7 +12,7 @@ fn main() -> Result<()> { println!("Device version: {:?}", radio.device_version()); radio - .start_rx(&RxConfig { + .start_rx(&Config { frequency_hz: 2_410_000_000, amp_enable: true, antenna_enable: false, diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs index f014ed3..7c5f204 100644 --- a/crates/hackrf-one-rs/src/lib.rs +++ b/crates/hackrf-one-rs/src/lib.rs @@ -80,39 +80,11 @@ pub enum Mode { /// Configuration for TX gain settings /// The LNA is always turned off for TX operations #[derive(Debug)] -pub struct TxConfig { +pub struct Config { /// Baseband gain, 0-62dB in 2dB increments pub vga_db: u16, /// 0 - 47 dB in 1dB increments pub txvga_db: u16, - /// RF amplifier (on/off) - pub amp_enable: bool, - /// Antenna power port control - pub antenna_enable: bool, - /// Frequency in hz - pub frequency_hz: u64, -} - -impl Default for TxConfig { - fn default() -> Self { - Self { - vga_db: 16, - txvga_db: 40, - amp_enable: true, - antenna_enable: true, - // set within ITU americas 900mhz ISM - // as punishment for calling ::default() - frequency_hz: 908_000_000, - } - } -} - -/// Configuration for RX gain settings -/// The TXVGA is always turned off for RX operations -#[derive(Debug)] -pub struct RxConfig { - /// Baseband gain, 0-62dB in 2dB increments - pub vga_db: u16, /// Low-noise amplifier gain, in 0-40dB in 8dB increments pub lna_db: u16, /// RF amplifier (on/off) @@ -123,11 +95,12 @@ pub struct RxConfig { pub frequency_hz: u64, } -impl Default for RxConfig { +impl Default for Config { fn default() -> Self { Self { - vga_db: 8, - lna_db: 16, + vga_db: 16, + lna_db: 0, + txvga_db: 40, amp_enable: true, antenna_enable: true, // set within global 900mhz ISM band to avoid sending our engineers to foreign prisons @@ -377,7 +350,7 @@ impl HackRf { /// # Errors /// This function will return an error if a tx or rx operation is already in progress or if an /// I/O error occurs - pub fn start_tx(&self, config: &TxConfig) -> Result<()> { + pub fn start_tx(&self, config: &Config) -> Result<()> { // NOTE: perform atomic exchange first so that we only change the transceiver mode once if // other therads are racing to change the mode if let Err(actual) = self.mode.compare_exchange( @@ -392,12 +365,7 @@ impl HackRf { }); } - self.set_lna_gain(0)?; - self.set_vga_gain(config.vga_db)?; - self.set_txvga_gain(config.txvga_db)?; - self.set_freq(config.frequency_hz)?; - self.set_amp_enable(config.amp_enable)?; - self.set_antenna_enable(config.antenna_enable)?; + self.apply_config(config)?; self.write_control( Request::SetTransceiverMode, @@ -420,7 +388,7 @@ impl HackRf { /// # Errors /// This function will return an error if a tx or rx operation is already in progress or if an /// I/O error occurs - pub fn start_rx(&self, config: &RxConfig) -> Result<()> { + pub fn start_rx(&self, config: &Config) -> Result<()> { // NOTE: perform the atomic exchange first so that we only change the hackrf's state once if // other threads are racing with us if let Err(actual) = self.mode.compare_exchange( @@ -435,11 +403,7 @@ impl HackRf { }); } - self.set_freq(config.frequency_hz)?; - self.set_vga_gain(config.vga_db)?; - self.set_txvga_gain(0)?; - self.set_amp_enable(config.amp_enable)?; - self.set_antenna_enable(config.antenna_enable)?; + self.apply_config(config)?; self.write_control( Request::SetTransceiverMode, @@ -551,6 +515,16 @@ impl HackRf { } impl HackRf { + fn apply_config(&self, config: &Config) -> Result<()> { + self.set_lna_gain(config.lna_db)?; + self.set_vga_gain(config.vga_db)?; + self.set_txvga_gain(config.txvga_db)?; + self.set_freq(config.frequency_hz)?; + self.set_amp_enable(config.amp_enable)?; + self.set_antenna_enable(config.antenna_enable)?; + Ok(()) + } + fn ensure_mode(&self, expected: Mode) -> Result<()> { let actual = self.mode.load(Ordering::Acquire); if actual != expected { @@ -652,8 +626,6 @@ fn freq_params(hz: u64) -> [u8; 8] { mod test { use std::time::Duration; - - use super::{freq_params, HackRf}; #[test] @@ -677,9 +649,29 @@ mod test { } #[test] - fn abc() { - let context = rusb::Context::new().expect("Failed to get libusb context"); - let radio = HackRf::new(context).expect("Failed to open hackrf. No radio connected?"); + fn device_states() { + let strict = true; + + let context = match rusb::Context::new() { + Ok(c) => c, + Err(e) => { + if strict { + panic!("{e:?}"); + } + println!("Failed to create rusb context, passing test anyway: {e:?}"); + return; + } + }; + let radio = match HackRf::new(context) { + Some(r) => r, + None => { + if strict { + panic!("Failed to open hackrf"); + } + println!("Failed to open hackrf, passing test anyway"); + return; + } + }; radio.start_tx(&Default::default()).unwrap(); std::thread::sleep(Duration::from_millis(50)); diff --git a/src/impls/hackrfone.rs b/src/impls/hackrfone.rs index 4a0f2ad..70c8546 100644 --- a/src/impls/hackrfone.rs +++ b/src/impls/hackrfone.rs @@ -1,12 +1,21 @@ use std::sync::Arc; -use crate::Error; +use crate::{Error, RxStreamer, TxStreamer}; pub struct HackRfOne { - dev: Arc, + dev: Arc, } -impl seify::RxStreamer for Rx { +struct HackRfInner { + dev: seify_hackrfone::HackRf, + +} + +pub struct Rx { + dev: Arc, +} + +impl RxStreamer for Rx { fn mtu(&self) -> Result { // TOOD(tjn): verify Ok(128 * 1024) @@ -15,21 +24,6 @@ impl seify::RxStreamer for Rx { fn activate_at(&mut self, time_ns: Option) -> Result<(), Error> { // TODO(tjn): sleep precisely for `time_ns` - let cfg = &self.inner.rx_config; - self.inner.set_freq(cfg.frequency_hz)?; - self.inner.set_vga_gain(cfg.vga_db)?; - self.inner.set_txvga_gain(0)?; - self.inner.set_amp_enable(cfg.amp_enable)?; - self.inner.set_antenna_enable(cfg.antenna_enable)?; - - self.inner.write_control( - Request::SetTransceiverMode, - TranscieverMode::Receive as u16, - 0, - &[], - ); - self.dh.claim_interface(0)?; - Ok(()) } @@ -55,11 +49,11 @@ impl seify::RxStreamer for Rx { } } -struct Tx { - inner: Arc, +pub struct Tx { + dev: Arc, } -impl seify::TxStreamer for Tx { +impl TxStreamer for Tx { fn mtu(&self) -> Result { // TOOD(tjn): verify Ok(128 * 1024) From e76c0cb85047fc3012c21deb6006476ea69bb549 Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Mon, 2 Sep 2024 17:00:41 -0700 Subject: [PATCH 04/16] fm-receiver works! --- crates/hackrf-one-rs/src/lib.rs | 366 +++++++++++++++----------- src/device.rs | 19 ++ src/impls/hackrfone.rs | 448 +++++++++++++++++++------------- src/lib.rs | 23 +- 4 files changed, 529 insertions(+), 327 deletions(-) diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs index 7c5f204..59a0d6d 100644 --- a/crates/hackrf-one-rs/src/lib.rs +++ b/crates/hackrf-one-rs/src/lib.rs @@ -77,35 +77,55 @@ pub enum Mode { Transmit, } -/// Configuration for TX gain settings -/// The LNA is always turned off for TX operations +/// Configurable parameters on the hackrf #[derive(Debug)] pub struct Config { - /// Baseband gain, 0-62dB in 2dB increments + /// Baseband gain, 0-62dB in 2dB increments (rx only) pub vga_db: u16, - /// 0 - 47 dB in 1dB increments + /// 0 - 47 dB in 1dB increments (tx only) pub txvga_db: u16, - /// Low-noise amplifier gain, in 0-40dB in 8dB increments + + /// Low-noise amplifier gain, in 0-40dB in 8dB increments (rx only) + // Pre baseband receive pub lna_db: u16, /// RF amplifier (on/off) - pub amp_enable: bool, + /// NOTE: called `amp_enable` in HackRf docs + pub power_port_enable: bool, + /// Antenna power port control + // Power enable on antenna pub antenna_enable: bool, /// Frequency in hz pub frequency_hz: u64, + pub sample_rate_hz: u32, + // TODO: provide helpers for setting this up + pub sample_rate_div: u32, } -impl Default for Config { - fn default() -> Self { +impl Config { + pub fn tx_default() -> Self { Self { - vga_db: 16, + vga_db: 0, lna_db: 0, txvga_db: 40, - amp_enable: true, - antenna_enable: true, - // set within global 900mhz ISM band to avoid sending our engineers to foreign prisons - // as punishment for calling ::default() + power_port_enable: false, + antenna_enable: false, frequency_hz: 908_000_000, + sample_rate_hz: 2_500_000, + sample_rate_div: 1, + } + } + + pub fn rx_default() -> Self { + Self { + vga_db: 24, + lna_db: 0, + txvga_db: 0, + power_port_enable: false, + antenna_enable: false, + frequency_hz: 908_000_000, + sample_rate_hz: 2_500_000, + sample_rate_div: 1, } } } @@ -138,6 +158,8 @@ pub enum Error { Argument(&'static str), #[error("Hackrf in invalid mode. Required: {required:?}, actual: {actual:?}")] WrongMode { required: Mode, actual: Mode }, + #[error("Device not found")] + NotFound, } pub type Result = std::result::Result; @@ -150,13 +172,13 @@ pub struct HackRf { timeout_nanos: AtomicU64, } +const DEFAULT_TIMEOUT_NANOS: u64 = Duration::from_millis(500).as_nanos() as u64; + impl HackRf { - pub fn new(ctx: Context) -> Option { - // TODO: use seify args - let devices = match ctx.devices() { - Ok(d) => d, - Err(_) => return None, - }; + /// Opens the first Hackrf One radio (if found) by scanning `ctx`. + pub fn open_first() -> Result { + let ctx = Context::new()?; + let devices = ctx.devices()?; for device in devices.iter() { let desc = match device.device_descriptor() { @@ -167,7 +189,7 @@ impl HackRf { if desc.vendor_id() == HACKRF_USB_VID && desc.product_id() == HACKRF_ONE_USB_PID { match device.open() { Ok(handle) => { - return Some(HackRf { + return Ok(HackRf { handle, discriptor: desc, timeout_nanos: AtomicU64::new( @@ -181,14 +203,64 @@ impl HackRf { } } - None + Err(Error::NotFound) + } + + pub fn scan() -> Result> { + let ctx = Context::new()?; + let devices = ctx.devices()?; + + let mut res = vec![]; + for device in devices.iter() { + let desc = match device.device_descriptor() { + Ok(d) => d, + Err(_) => continue, + }; + + if desc.vendor_id() == HACKRF_USB_VID && desc.product_id() == HACKRF_ONE_USB_PID { + res.push((device.bus_number(), device.address())); + } + } + Ok(res) + } + + /// Opens a hackrf with usb address `:
` + pub fn open_bus(bus_number: u8, address: u8) -> Result { + let ctx = Context::new()?; + + let devices = ctx.devices()?; + + println!("Opening bus addr: {bus_number}:{address}"); + for device in devices.iter() { + let desc = match device.device_descriptor() { + Ok(d) => d, + Err(_) => continue, + }; + + if desc.vendor_id() == HACKRF_USB_VID + && desc.product_id() == HACKRF_ONE_USB_PID + && device.bus_number() == bus_number + && device.address() == address + { + let handle = device.open()?; + return Ok(HackRf { + handle, + discriptor: desc, + timeout_nanos: AtomicU64::new(DEFAULT_TIMEOUT_NANOS), + mode: AtomicMode::new(Mode::Idle), + }); + } + } + + Err(rusb::Error::NoDevice.into()) } + /// Wraps an existing rusb device handle. pub fn wrap(handle: rusb::DeviceHandle, desc: rusb::DeviceDescriptor) -> HackRf { HackRf { handle, discriptor: desc, - timeout_nanos: AtomicU64::new(Duration::from_millis(500).as_nanos() as u64), + timeout_nanos: AtomicU64::new(DEFAULT_TIMEOUT_NANOS), mode: AtomicMode::new(Mode::Idle), } } @@ -223,125 +295,6 @@ impl HackRf { Ok(String::from_utf8_lossy(&buf[0..n]).into()) } - /// Set the center frequency. - pub fn set_freq(&self, hz: u64) -> Result<()> { - let buf: [u8; 8] = freq_params(hz); - self.write_control(Request::SetFreq, 0, 0, &buf) - } - - /// Enable the RX/TX RF amplifier. - /// - /// In GNU radio this is used as the RF gain, where a value of 0 dB is off, - /// and a value of 14 dB is on. - pub fn set_amp_enable(&self, enable: bool) -> Result<()> { - self.write_control(Request::AmpEnable, enable.into(), 0, &[]) - } - - /// Set the baseband filter bandwidth. - /// - /// This is automatically set when the sample rate is changed with - /// [`set_sample_rate`]. - pub fn set_baseband_filter_bandwidth(&self, hz: u32) -> Result<()> { - self.write_control( - Request::BasebandFilterBandwidthSet, - (hz & 0xFFFF) as u16, - (hz >> 16) as u16, - &[], - ) - } - - /// Set the sample rate. - /// - /// For anti-aliasing, the baseband filter bandwidth is automatically set to - /// the widest available setting that is no more than 75% of the sample rate. - /// This happens every time the sample rate is set. - /// If you want to override the baseband filter selection, you must do so - /// after setting the sample rate. - /// - /// Limits are 8MHz - 20MHz. - /// Preferred rates are 8, 10, 12.5, 16, 20MHz due to less jitter. - pub fn set_sample_rate(&self, hz: u32, div: u32) -> Result<()> { - let hz: u32 = hz.to_le(); - let div: u32 = div.to_le(); - let buf: [u8; 8] = [ - (hz & 0xFF) as u8, - ((hz >> 8) & 0xFF) as u8, - ((hz >> 16) & 0xFF) as u8, - ((hz >> 24) & 0xFF) as u8, - (div & 0xFF) as u8, - ((div >> 8) & 0xFF) as u8, - ((div >> 16) & 0xFF) as u8, - ((div >> 24) & 0xFF) as u8, - ]; - self.write_control(Request::SampleRateSet, 0, 0, &buf)?; - self.set_baseband_filter_bandwidth((0.75 * (hz as f32) / (div as f32)) as u32) - } - - /// Set the LNA (low noise amplifier) gain. - /// - /// Range 0 to 40dB in 8dB steps. - /// - /// This is also known as the IF gain. - pub fn set_lna_gain(&self, gain: u16) -> Result<()> { - if gain > 40 { - Err(Error::Argument("lna gain must be less than 40")) - } else { - let buf: [u8; 1] = self.read_control(Request::SetLnaGain, 0, gain & !0x07)?; - if buf[0] == 0 { - // TODO(tjn): check hackrf docs - panic!(); - } else { - Ok(()) - } - } - } - - /// Set the VGA (variable gain amplifier) gain. - /// - /// Range 0 to 62dB in 2dB steps. - /// - /// This is also known as the baseband (BB) gain. - pub fn set_vga_gain(&self, gain: u16) -> Result<()> { - if gain > 62 || gain % 2 == 1 { - Err(Error::Argument( - "gain parameter out of range. must be even and less than or equal to 62", - )) - } else { - // TODO(tjn): read_control seems wrong here, investigate - let buf: [u8; 1] = self.read_control(Request::SetVgaGain, 0, gain & !0b1)?; - if buf[0] == 0 { - panic!("What is this return value?") - } else { - Ok(()) - } - } - } - - /// Set the transmit VGA gain. - /// - /// Range 0 to 47dB in 1db steps. - pub fn set_txvga_gain(&self, gain: u16) -> Result<()> { - if gain > 47 { - Err(Error::Argument("gain parameter out of range. max is 47")) - } else { - // TODO(tjn): read_control seems wrong here, investigate - let buf: [u8; 1] = self.read_control(Request::SetTxvgaGain, 0, gain)?; - if buf[0] == 0 { - panic!("What is this return value?") - } else { - Ok(()) - } - } - } - - /// Antenna power port control. Dhruv's guess: is this DC bias? - /// - /// The source docs are a little lacking in terms of explanations here. - pub fn set_antenna_enable(&self, value: bool) -> Result<()> { - let value = if value { 1 } else { 0 }; - self.write_control(Request::AntennaEnable, value, 0, &[]) - } - /// Transitions the radio into transmit mode. /// Call this function before calling [`Self::tx`]. /// @@ -351,6 +304,8 @@ impl HackRf { /// This function will return an error if a tx or rx operation is already in progress or if an /// I/O error occurs pub fn start_tx(&self, config: &Config) -> Result<()> { + println!("Starting tx: {config:?}"); + // NOTE: perform atomic exchange first so that we only change the transceiver mode once if // other therads are racing to change the mode if let Err(actual) = self.mode.compare_exchange( @@ -487,7 +442,7 @@ impl HackRf { /// /// # Panics /// This function panics if samples is not a multiple of 512 - pub fn rx(&self, samples: &mut [u8]) -> Result { + pub fn read(&self, samples: &mut [u8]) -> Result { self.ensure_mode(Mode::Receive)?; if samples.len() % 512 != 0 { @@ -502,7 +457,7 @@ impl HackRf { /// /// # Panics /// This function panics if samples is not a multiple of 512 - pub fn tx(&self, samples: &[u8]) -> Result { + pub fn write(&self, samples: &[u8]) -> Result { self.ensure_mode(Mode::Transmit)?; if samples.len() % 512 != 0 { @@ -520,8 +475,10 @@ impl HackRf { self.set_vga_gain(config.vga_db)?; self.set_txvga_gain(config.txvga_db)?; self.set_freq(config.frequency_hz)?; - self.set_amp_enable(config.amp_enable)?; + self.set_amp_enable(config.power_port_enable)?; self.set_antenna_enable(config.antenna_enable)?; + self.set_sample_rate(config.sample_rate_hz, config.sample_rate_div)?; + Ok(()) } @@ -599,6 +556,125 @@ impl HackRf { Err(Error::NoApi { device: v, min }) } } + + /// Set the center frequency. + pub fn set_freq(&self, hz: u64) -> Result<()> { + let buf: [u8; 8] = freq_params(hz); + self.write_control(Request::SetFreq, 0, 0, &buf) + } + + /// Enable the RX/TX RF amplifier. + /// + /// In GNU radio this is used as the RF gain, where a value of 0 dB is off, + /// and a value of 14 dB is on. + pub fn set_amp_enable(&self, enable: bool) -> Result<()> { + self.write_control(Request::AmpEnable, enable.into(), 0, &[]) + } + + /// Set the baseband filter bandwidth. + /// + /// This is automatically set when the sample rate is changed with + /// [`set_sample_rate`]. + pub fn set_baseband_filter_bandwidth(&self, hz: u32) -> Result<()> { + self.write_control( + Request::BasebandFilterBandwidthSet, + (hz & 0xFFFF) as u16, + (hz >> 16) as u16, + &[], + ) + } + + /// Set the sample rate. + /// + /// For anti-aliasing, the baseband filter bandwidth is automatically set to + /// the widest available setting that is no more than 75% of the sample rate. + /// This happens every time the sample rate is set. + /// If you want to override the baseband filter selection, you must do so + /// after setting the sample rate. + /// + /// Limits are 8MHz - 20MHz. + /// Preferred rates are 8, 10, 12.5, 16, 20MHz due to less jitter. + pub fn set_sample_rate(&self, hz: u32, div: u32) -> Result<()> { + let hz: u32 = hz.to_le(); + let div: u32 = div.to_le(); + let buf: [u8; 8] = [ + (hz & 0xFF) as u8, + ((hz >> 8) & 0xFF) as u8, + ((hz >> 16) & 0xFF) as u8, + ((hz >> 24) & 0xFF) as u8, + (div & 0xFF) as u8, + ((div >> 8) & 0xFF) as u8, + ((div >> 16) & 0xFF) as u8, + ((div >> 24) & 0xFF) as u8, + ]; + self.write_control(Request::SampleRateSet, 0, 0, &buf)?; + self.set_baseband_filter_bandwidth((0.75 * (hz as f32) / (div as f32)) as u32) + } + + /// Set the LNA (low noise amplifier) gain. + /// + /// Range 0 to 40dB in 8dB steps. + /// + /// This is also known as the IF gain. + pub fn set_lna_gain(&self, gain: u16) -> Result<()> { + if gain > 40 { + Err(Error::Argument("lna gain must be less than 40")) + } else { + let buf: [u8; 1] = self.read_control(Request::SetLnaGain, 0, gain & !0x07)?; + if buf[0] == 0 { + // TODO(tjn): check hackrf docs + panic!(); + } else { + Ok(()) + } + } + } + + /// Set the VGA (variable gain amplifier) gain. + /// + /// Range 0 to 62dB in 2dB steps. + /// + /// This is also known as the baseband (BB) gain. + pub fn set_vga_gain(&self, gain: u16) -> Result<()> { + if gain > 62 || gain % 2 == 1 { + Err(Error::Argument( + "gain parameter out of range. must be even and less than or equal to 62", + )) + } else { + // TODO(tjn): read_control seems wrong here, investigate + let buf: [u8; 1] = self.read_control(Request::SetVgaGain, 0, gain & !0b1)?; + if buf[0] == 0 { + panic!("What is this return value?") + } else { + Ok(()) + } + } + } + + /// Set the transmit VGA gain. + /// + /// Range 0 to 47dB in 1db steps. + pub fn set_txvga_gain(&self, gain: u16) -> Result<()> { + if gain > 47 { + Err(Error::Argument("gain parameter out of range. max is 47")) + } else { + // TODO(tjn): read_control seems wrong here, investigate + let buf: [u8; 1] = self.read_control(Request::SetTxvgaGain, 0, gain)?; + if buf[0] == 0 { + panic!("What is this return value?") + } else { + Ok(()) + } + } + } + + /// Antenna power port control. Dhruv's guess: is this DC bias? + /// + /// The source docs are a little lacking in terms of explanations here. + pub fn set_antenna_enable(&self, value: bool) -> Result<()> { + let value = if value { 1 } else { 0 }; + self.write_control(Request::AntennaEnable, value, 0, &[]) + } } // Helper for set_freq @@ -662,7 +738,7 @@ mod test { return; } }; - let radio = match HackRf::new(context) { + let radio = match HackRf::open_first() { Some(r) => r, None => { if strict { diff --git a/src/device.rs b/src/device.rs index 95e8538..bf19fb9 100644 --- a/src/device.rs +++ b/src/device.rs @@ -229,6 +229,7 @@ impl Device { Err(Error::NotFound) => None, Err(e) => return Err(e), }; + dbg!(&args); #[cfg(all(feature = "aaronia", any(target_os = "linux", target_os = "windows")))] { if driver.is_none() || matches!(driver, Some(Driver::Aaronia)) { @@ -301,6 +302,24 @@ impl Device { } } } + #[cfg(all(feature = "hackrfone", not(target_arch = "wasm32")))] + { + if driver.is_none() || matches!(driver, Some(Driver::HackRf)) { + match crate::impls::HackRfOne::open(&args) { + Ok(d) => { + return Ok(Device { + dev: Arc::new(DeviceWrapper { dev: d }), + }) + } + Err(Error::NotFound) => { + if driver.is_some() { + return Err(Error::NotFound); + } + } + Err(e) => return Err(e), + } + } + } Err(Error::NotFound) } } diff --git a/src/impls/hackrfone.rs b/src/impls/hackrfone.rs index 70c8546..d23e052 100644 --- a/src/impls/hackrfone.rs +++ b/src/impls/hackrfone.rs @@ -1,124 +1,212 @@ -use std::sync::Arc; +use std::sync::{Arc, Mutex}; -use crate::{Error, RxStreamer, TxStreamer}; +use num_complex::Complex32; +use seify_hackrfone::Config; + +use crate::{Args, Direction, Error, Range, RangeItem}; pub struct HackRfOne { - dev: Arc, + inner: Arc, +} + +const MTU: usize = 64 * 1024; + +impl HackRfOne { + pub fn probe(_args: &Args) -> Result, Error> { + panic!(); + let mut devs = vec![]; + for (bus_number, address) in seify_hackrfone::HackRf::scan()? { + println!("probing {bus_number}:{address}"); + devs.push( + format!( + "driver=hackrfone, bus_number={}, address={}", + bus_number, address + ) + .try_into()?, + ); + } + Ok(devs) + } + + /// Create a Hackrf One devices + pub fn open>(args: A) -> Result { + let args: Args = args.try_into().or(Err(Error::ValueError))?; + + let bus_number = args.get("bus_number"); + let address = args.get("address"); + let dev = match (bus_number, address) { + (Ok(bus_number), Ok(address)) => { + dbg!(bus_number, address); + seify_hackrfone::HackRf::open_bus(bus_number, address)? + } + (Err(Error::NotFound), Err(Error::NotFound)) => { + println!("Opening first hackrf device"); + seify_hackrfone::HackRf::open_first()? + } + (bus_number, address) => { + println!("HackRfOne::open received invalid args: bus_number: {bus_number:?}, address: {address:?}"); + return Err(Error::ValueError); + } + }; + + Ok(Self { + inner: Arc::new(HackRfInner { + dev, + tx_config: Mutex::new(Config::tx_default()), + rx_config: Mutex::new(Config::rx_default()), + }), + }) + } + + pub fn with_config(&self, direction: Direction, f: F) -> R + where + F: FnOnce(&mut Config) -> R, + { + let config = match direction { + Direction::Tx => self.inner.tx_config.lock(), + Direction::Rx => self.inner.rx_config.lock(), + }; + f(&mut config.unwrap()) + } } struct HackRfInner { dev: seify_hackrfone::HackRf, + tx_config: Mutex, + rx_config: Mutex, +} +pub struct RxStreamer { + inner: Arc, + buf: Vec, } -pub struct Rx { - dev: Arc, +impl RxStreamer { + fn new(inner: Arc) -> Self { + Self { + inner, + buf: vec![0u8; MTU], + } + } } -impl RxStreamer for Rx { +impl crate::RxStreamer for RxStreamer { fn mtu(&self) -> Result { - // TOOD(tjn): verify - Ok(128 * 1024) + Ok(MTU) } - fn activate_at(&mut self, time_ns: Option) -> Result<(), Error> { + fn activate_at(&mut self, _time_ns: Option) -> Result<(), Error> { // TODO(tjn): sleep precisely for `time_ns` + let config = self.inner.rx_config.lock().unwrap(); + self.inner.dev.start_rx(&config)?; Ok(()) } - fn deactivate_at(&mut self, time_ns: Option) -> Result<(), Error> { + fn deactivate_at(&mut self, _time_ns: Option) -> Result<(), Error> { // TODO(tjn): sleep precisely for `time_ns` - self.inner.dh.release_interface(0).unwrap(); - self.inner.set_transceiver_mode(TranscieverMode::Off).unwrap(); + + self.inner.dev.stop_rx()?; Ok(()) } fn read( &mut self, buffers: &mut [&mut [num_complex::Complex32]], - timeout_us: i64, + _timeout_us: i64, ) -> Result { - const ENDPOINT: u8 = 0x81; - assert_eq!(buffers.len(), 1); - let dst = buffers[0]; - self.buf.resize(dst.len() * 2, 0); + debug_assert_eq!(buffers.len(), 1); - let n = self.inner.dh.read_bulk(ENDPOINT, &mut self.buf, self.inner.to).unwrap(); - assert_eq!(n, self.buf.len()); + // make len multiple of 256 to make u multiple of 512 + let len = std::cmp::min(buffers[0].len(), MTU / 2); + let len = len & !0xff; + if len == 0 { + return Ok(0); + } + let n = self.inner.dev.read(&mut self.buf[0..len * 2])?; + debug_assert_eq!(n % 2, 0); + + for i in 0..n / 2 { + buffers[0][i] = Complex32::new( + (self.buf[i * 2] as f32 - 127.0) / 128.0, + (self.buf[i * 2 + 1] as f32 - 127.0) / 128.0, + ); + } + Ok(n / 2) } } -pub struct Tx { - dev: Arc, +pub struct TxStreamer { + inner: Arc, + buf: Vec, } -impl TxStreamer for Tx { +impl TxStreamer { + fn new(inner: Arc) -> Self { + Self { + inner, + buf: vec![0u8; MTU], + } + } +} + +impl crate::TxStreamer for TxStreamer { fn mtu(&self) -> Result { - // TOOD(tjn): verify - Ok(128 * 1024) + Ok(MTU) } - fn activate_at(&mut self, time_ns: Option) -> Result<(), Error> { + fn activate_at(&mut self, _time_ns: Option) -> Result<(), Error> { // TODO(tjn): sleep precisely for `time_ns` - let cfg = &self.inner.tx_config; - self.inner.set_lna_gain(0)?; - self.inner.set_vga_gain(cfg.vga_db)?; - self.inner.set_txvga_gain(cfg.txvga_db)?; - self.inner.set_freq(cfg.frequency_hz)?; - self.inner.set_amp_enable(cfg.amp_enable)?; - self.inner.set_antenna_enable(cfg.antenna_enable)?; - - self.inner. - /* - self.write_control( - Request::SetTransceiverMode, - TranscieverMode::Transmit as u16, - 0, - &[], - ); - */ - + let config = self.inner.tx_config.lock().unwrap(); + self.inner.dev.start_rx(&config)?; + Ok(()) } - fn deactivate_at(&mut self, time_ns: Option) -> Result<(), Error> { - self.dh.release_interface(0)?; - self.set_transceiver_mode(TranscieverMode::Off)?; - todo!() + fn deactivate_at(&mut self, _time_ns: Option) -> Result<(), Error> { + // TODO(tjn): sleep precisely for `time_ns` + + self.inner.dev.stop_tx()?; + Ok(()) } fn write( &mut self, buffers: &[&[num_complex::Complex32]], - at_ns: Option, - end_burst: bool, - timeout_us: i64, + _at_ns: Option, + _end_burst: bool, + _timeout_us: i64, ) -> Result { + debug_assert_eq!(buffers.len(), 1); + todo!(); - // TODO: - self.dh - .write_bulk(ENDPOINT, samples, Duration::from_millis(1)) - .map_err(Error::Usb) - todo!() + // self.inner.dev.write(samples) } fn write_all( &mut self, buffers: &[&[num_complex::Complex32]], - at_ns: Option, - end_burst: bool, - timeout_us: i64, + _at_ns: Option, + _end_burst: bool, + _timeout_us: i64, ) -> Result<(), Error> { - todo!() + debug_assert_eq!(buffers.len(), 1); + + let mut n = 0; + while n < buffers[0].len() { + let buf = &buffers[0][n..]; + n += self.write(&[buf], None, false, 0)?; + } + + Ok(()) } } -impl seify::DeviceTrait for HackRf { - type RxStreamer = Rx; +impl crate::DeviceTrait for HackRfOne { + type RxStreamer = RxStreamer; - type TxStreamer = Tx; + type TxStreamer = TxStreamer; fn as_any(&self) -> &dyn std::any::Any { self @@ -128,26 +216,22 @@ impl seify::DeviceTrait for HackRf { self } - fn driver(&self) -> seify::Driver { - // ??? no enum variant that makes sense for us - todo!() + fn driver(&self) -> crate::Driver { + crate::Driver::HackRf } fn id(&self) -> Result { todo!() } - fn info(&self) -> Result { + fn info(&self) -> Result { Ok(Default::default()) } - fn num_channels(&self, _: seify::Direction) -> Result { + fn num_channels(&self, _: crate::Direction) -> Result { Ok(1) } - - - fn full_duplex(&self, _direction: Direction, _channel: usize) -> Result { Ok(false) } @@ -156,12 +240,16 @@ impl seify::DeviceTrait for HackRf { if channels != [0] { Err(Error::ValueError) } else { - Ok(RxStreamer::new(self.dev.clone())) + Ok(RxStreamer::new(Arc::clone(&self.inner))) } } - fn tx_streamer(&self, _channels: &[usize], _args: Args) -> Result { - Err(Error::NotSupported) + fn tx_streamer(&self, channels: &[usize], _args: Args) -> Result { + if channels != [0] { + Err(Error::ValueError) + } else { + Ok(TxStreamer::new(Arc::clone(&self.inner))) + } } fn antennas(&self, direction: Direction, channel: usize) -> Result, Error> { @@ -169,84 +257,81 @@ impl seify::DeviceTrait for HackRf { } fn antenna(&self, direction: Direction, channel: usize) -> Result { - if matches!(direction, Rx) && channel == 0 { - Ok("RX".to_string()) - } else if matches!(direction, Rx) { - Err(Error::ValueError) + if channel == 0 { + Ok(match direction { + Direction::Rx => "RX".to_string(), + Direction::Tx => "TX".to_string(), + }) } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } fn set_antenna(&self, direction: Direction, channel: usize, name: &str) -> Result<(), Error> { - if matches!(direction, Rx) && channel == 0 && name == "RX" { - Ok(()) - } else if matches!(direction, Rx) { - Err(Error::ValueError) + if channel == 0 { + if direction == Direction::Rx && name == "RX" + || direction == Direction::Tx && name == "TX" + { + Ok(()) + } else { + Err(Error::NotSupported) + } } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } fn gain_elements(&self, direction: Direction, channel: usize) -> Result, Error> { - if matches!(direction, Rx) && channel == 0 { - Ok(vec!["TUNER".to_string()]) - } else if matches!(direction, Rx) { - Err(Error::ValueError) + if channel == 0 { + // TODO(tjn): add support for other gains (RF and baseband) + // See: https://hackrf.readthedocs.io/en/latest/faq.html#what-gain-controls-are-provided-by-hackrf + match direction { + Direction::Tx => Ok(vec!["IF".into()]), + // TODO: add rest + Direction::Rx => Ok(vec!["IF".into()]), + } } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } - fn supports_agc(&self, direction: Direction, channel: usize) -> Result { - if matches!(direction, Rx) && channel == 0 { - Ok(true) - } else if matches!(direction, Rx) { - Err(Error::ValueError) + fn supports_agc(&self, _direction: Direction, channel: usize) -> Result { + if channel == 0 { + Ok(false) } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } - fn enable_agc(&self, direction: Direction, channel: usize, agc: bool) -> Result<(), Error> { - let gains = self.dev.get_tuner_gains().or(Err(Error::DeviceError))?; - if matches!(direction, Rx) && channel == 0 { - let mut inner = self.i.lock().unwrap(); - if agc { - inner.gain = TunerGain::Auto; - Ok(self.dev.set_tuner_gain(inner.gain.clone())?) - } else { - inner.gain = TunerGain::Manual(gains[gains.len() / 2]); - Ok(self.dev.set_tuner_gain(inner.gain.clone())?) - } - } else if matches!(direction, Rx) { - Err(Error::ValueError) - } else { + fn enable_agc(&self, _direction: Direction, channel: usize, agc: bool) -> Result<(), Error> { + if channel == 0 { Err(Error::NotSupported) + } else { + Err(Error::ValueError) } } - fn agc(&self, direction: Direction, channel: usize) -> Result { - if matches!(direction, Rx) && channel == 0 { - let inner = self.i.lock().unwrap(); - Ok(matches!(inner.gain, TunerGain::Auto)) - } else if matches!(direction, Rx) { - Err(Error::ValueError) - } else { + fn agc(&self, _direction: Direction, channel: usize) -> Result { + if channel == 0 { Err(Error::NotSupported) + } else { + Err(Error::ValueError) } } fn set_gain(&self, direction: Direction, channel: usize, gain: f64) -> Result<(), Error> { - self.set_gain_element(direction, channel, "TUNER", gain) + println!("set_gain"); + self.set_gain_element(direction, channel, "IF", gain) } fn gain(&self, direction: Direction, channel: usize) -> Result, Error> { - self.gain_element(direction, channel, "TUNER") + println!("gain"); + self.gain_element(direction, channel, "IF") } fn gain_range(&self, direction: Direction, channel: usize) -> Result { - self.gain_element_range(direction, channel, "TUNER") + println!("gain_range"); + self.gain_element_range(direction, channel, "IF") } fn set_gain_element( @@ -256,11 +341,20 @@ impl seify::DeviceTrait for HackRf { name: &str, gain: f64, ) -> Result<(), Error> { + println!("set_gain_element"); + dbg!(direction, channel, name, gain); + let r = self.gain_range(direction, channel)?; - if r.contains(gain) && name == "TUNER" { - let mut inner = self.i.lock().unwrap(); - inner.gain = TunerGain::Manual((gain * 10.0) as i32); - Ok(self.dev.set_tuner_gain(inner.gain.clone())?) + if r.contains(gain) && name == "IF" { + match direction { + Direction::Tx => todo!(), + Direction::Rx => { + let mut config = self.inner.rx_config.lock().unwrap(); + println!("setting lna_db to {gain}"); + config.lna_db = gain as u16; + Ok(()) + } + } } else { log::warn!("Gain out of range"); Err(Error::OutOfRange(r, gain)) @@ -273,16 +367,17 @@ impl seify::DeviceTrait for HackRf { channel: usize, name: &str, ) -> Result, Error> { - if matches!(direction, Rx) && channel == 0 && name == "TUNER" { - let inner = self.i.lock().unwrap(); - match inner.gain { - TunerGain::Auto => Ok(None), - TunerGain::Manual(i) => Ok(Some(i as f64)), + dbg!(direction, channel, name); + if channel == 0 && name == "IF" { + match direction { + Direction::Tx => todo!(), + Direction::Rx => { + let config = self.inner.rx_config.lock().unwrap(); + Ok(Some(config.lna_db as f64)) + } } - } else if matches!(direction, Rx) { - Err(Error::ValueError) } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } @@ -292,12 +387,14 @@ impl seify::DeviceTrait for HackRf { channel: usize, name: &str, ) -> Result { - if matches!(direction, Rx) && channel == 0 && name == "TUNER" { - Ok(Range::new(vec![RangeItem::Interval(0.0, 50.0)])) - } else if matches!(direction, Rx) { - Err(Error::ValueError) + // TODO(tjn): add support for other gains + if channel == 0 && name == "IF" { + match direction { + Direction::Tx => Ok(Range::new(vec![RangeItem::Step(0.0, 47.0, 1.0)])), + Direction::Rx => Ok(Range::new(vec![RangeItem::Step(0.0, 40.0, 8.0)])), + } } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } @@ -321,30 +418,27 @@ impl seify::DeviceTrait for HackRf { fn frequency_components( &self, - direction: Direction, + _direction: Direction, channel: usize, ) -> Result, Error> { - if matches!(direction, Rx) && channel == 0 { + if channel == 0 { Ok(vec!["TUNER".to_string()]) - } else if matches!(direction, Rx) { - Err(Error::ValueError) } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } fn component_frequency_range( &self, - direction: Direction, + _direction: Direction, channel: usize, name: &str, ) -> Result { - if matches!(direction, Rx) && channel == 0 && name == "TUNER" { - Ok(Range::new(vec![RangeItem::Interval(0.0, 2e9)])) - } else if matches!(direction, Rx) { - Err(Error::ValueError) + if channel == 0 && name == "TUNER" { + // up to 7.25GHz + Ok(Range::new(vec![RangeItem::Interval(0.0, 7_270_000_000.0)])) } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } @@ -354,12 +448,10 @@ impl seify::DeviceTrait for HackRf { channel: usize, name: &str, ) -> Result { - if matches!(direction, Rx) && channel == 0 && name == "TUNER" { - Ok(self.dev.get_center_freq() as f64) - } else if matches!(direction, Rx) { - Err(Error::ValueError) + if channel == 0 && name == "TUNER" { + self.with_config(direction, |config| Ok(config.frequency_hz as f64)) } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } @@ -370,29 +462,28 @@ impl seify::DeviceTrait for HackRf { name: &str, frequency: f64, ) -> Result<(), Error> { - if matches!(direction, Rx) - && channel == 0 + if channel == 0 && self .frequency_range(direction, channel)? .contains(frequency) && name == "TUNER" { - self.dev.set_center_freq(frequency as u32)?; - Ok(self.dev.reset_buffer()?) - } else if matches!(direction, Rx) { - Err(Error::ValueError) + self.with_config(direction, |config| { + config.frequency_hz = frequency as u64; + }); + Ok(()) } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } fn sample_rate(&self, direction: Direction, channel: usize) -> Result { - if matches!(direction, Rx) && channel == 0 { - Ok(self.dev.get_sample_rate() as f64) - } else if matches!(direction, Rx) { - Err(Error::ValueError) + // NOTE: same state for both "directions" lets hope future sdr doesnt assume there are two + // values here, should be fine since we told it were not full duplex + if channel == 0 { + self.with_config(direction, |config| Ok(config.sample_rate_hz as f64)) } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } @@ -402,31 +493,30 @@ impl seify::DeviceTrait for HackRf { channel: usize, rate: f64, ) -> Result<(), Error> { - if matches!(direction, Rx) - && channel == 0 + if channel == 0 && self .get_sample_rate_range(direction, channel)? .contains(rate) { - self.dev.set_tuner_bandwidth(rate as u32)?; - Ok(self.dev.set_sample_rate(rate as u32)?) - } else if matches!(direction, Rx) { - Err(Error::ValueError) + self.with_config(direction, |config| { + // TODO: use sample rate div to enable lower effective sampling rate + config.sample_rate_hz = rate as u32; + config.sample_rate_div = 1; + }); + Ok(()) } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } - fn get_sample_rate_range(&self, direction: Direction, channel: usize) -> Result { - if matches!(direction, Rx) && channel == 0 { - Ok(Range::new(vec![ - RangeItem::Interval(225_001.0, 300_000.0), - RangeItem::Interval(900_001.0, 3_200_000.0), - ])) - } else if matches!(direction, Rx) { - Err(Error::ValueError) + fn get_sample_rate_range(&self, _direction: Direction, channel: usize) -> Result { + if channel == 0 { + Ok(Range::new(vec![RangeItem::Interval( + 1_000_000.0, + 20_000_000.0, + )])) } else { - Err(Error::NotSupported) + Err(Error::ValueError) } } } diff --git a/src/lib.rs b/src/lib.rs index 23d16ff..10279a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,9 +55,9 @@ pub enum Error { #[cfg(all(feature = "rtlsdr", not(target_arch = "wasm32")))] #[error("RtlSdr ({0})")] RtlSdr(#[from] seify_rtlsdr::error::RtlsdrError), - #[cfg(all(feature = "rtlsdr", not(target_arch = "wasm32")))] - #[error("RtlSdr ({0})")] - HackRfOne(#[from] seify_rtlsdr::error::RtlsdrError), + #[cfg(all(feature = "hackrfone", not(target_arch = "wasm32")))] + #[error("Hackrf ({0})")] + HackRfOne(#[from] seify_hackrfone::Error), } #[cfg(all(feature = "aaronia_http", not(target_arch = "wasm32")))] @@ -73,6 +73,7 @@ impl From for Error { pub enum Driver { Aaronia, AaroniaHttp, + HackRf, RtlSdr, Soapy, } @@ -94,6 +95,9 @@ impl FromStr for Driver { if s == "soapy" || s == "soapysdr" { return Ok(Driver::Soapy); } + if s == "hackrf" || s == "hackrfone" { + return Ok(Driver::HackRf); + } Err(Error::ValueError) } } @@ -183,6 +187,19 @@ pub fn enumerate_with_args>(a: A) -> Result, Error> { } } + #[cfg(all(feature = "hackrfone", not(target_arch = "wasm32")))] + { + if driver.is_none() || matches!(driver, Some(Driver::HackRf)) { + devs.append(&mut impls::HackRfOne::probe(&args)?) + } + } + #[cfg(not(all(feature = "hackrfone", not(target_arch = "wasm32"))))] + { + if matches!(driver, Some(Driver::HackRf)) { + return Err(Error::FeatureNotEnabled); + } + } + let _ = &mut devs; Ok(devs) } From cafc2631c50d9297738a99a798bbce9c6b44c6ec Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Mon, 2 Sep 2024 20:00:38 -0700 Subject: [PATCH 05/16] switch to nusb --- Cargo.toml | 1 + crates/hackrf-one-rs/Cargo.lock | 296 +++++++++++++++++++++++++++++--- crates/hackrf-one-rs/Cargo.toml | 3 +- crates/hackrf-one-rs/src/lib.rs | 282 +++++++++++++++++------------- src/impls/hackrfone.rs | 8 +- 5 files changed, 441 insertions(+), 149 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 926b169..5b7472e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ futures = "0.3" log = "0.4" nom = "7.1" num-complex = "0.4" +nusb = "0.1.10" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.6" diff --git a/crates/hackrf-one-rs/Cargo.lock b/crates/hackrf-one-rs/Cargo.lock index f2302c6..ed66d60 100644 --- a/crates/hackrf-one-rs/Cargo.lock +++ b/crates/hackrf-one-rs/Cargo.lock @@ -8,6 +8,12 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atomic_enum" version = "0.3.0" @@ -26,12 +32,76 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] -name = "cc" -version = "1.1.15" +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "io-kit-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" dependencies = [ - "shlex", + "core-foundation-sys", + "mach2", ] [[package]] @@ -41,15 +111,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] -name = "libusb1-sys" -version = "0.7.0" +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "mach2" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" dependencies = [ - "cc", "libc", - "pkg-config", - "vcpkg", ] [[package]] @@ -71,10 +150,39 @@ dependencies = [ ] [[package]] -name = "pkg-config" -version = "0.3.30" +name = "nusb" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "2d8beeee5c0ad012e1eaca540f5610d09a3af58ec435537d511b67e0be36ea66" +dependencies = [ + "atomic-waker", + "core-foundation", + "core-foundation-sys", + "io-kit-sys", + "log", + "once_cell", + "rustix", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "proc-macro2" @@ -95,13 +203,16 @@ dependencies = [ ] [[package]] -name = "rusb" -version = "0.9.4" +name = "rustix" +version = "0.38.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4" +checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" dependencies = [ + "bitflags", + "errno", "libc", - "libusb1-sys", + "linux-raw-sys", + "windows-sys 0.52.0", ] [[package]] @@ -110,16 +221,20 @@ version = "0.1.0" dependencies = [ "anyhow", "atomic_enum", + "futures-lite", "num-complex", - "rusb", + "nusb", "thiserror", ] [[package]] -name = "shlex" -version = "1.3.0" +name = "slab" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] [[package]] name = "syn" @@ -159,7 +274,140 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] -name = "vcpkg" -version = "0.2.15" +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/crates/hackrf-one-rs/Cargo.toml b/crates/hackrf-one-rs/Cargo.toml index 64a776a..ef8e5c5 100644 --- a/crates/hackrf-one-rs/Cargo.toml +++ b/crates/hackrf-one-rs/Cargo.toml @@ -4,10 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] -rusb = "0.9" num-complex = "0.4" thiserror = "1.0.63" atomic_enum = "0.3.0" +nusb = "0.1.10" +futures-lite = "2.3.0" [dev-dependencies] anyhow = "1.0.86" diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs index 59a0d6d..c9b7d8f 100644 --- a/crates/hackrf-one-rs/src/lib.rs +++ b/crates/hackrf-one-rs/src/lib.rs @@ -7,12 +7,17 @@ // TODO(tjn): re-enable // #![warn(missing_docs)] -use rusb::{request_type, Context, Direction, Recipient, RequestType, UsbContext, Version}; use std::{ sync::atomic::{AtomicU64, Ordering}, time::Duration, }; +use futures_lite::future::block_on; +use nusb::{ + transfer::{ControlIn, ControlOut, ControlType, Recipient, RequestBuffer}, + DeviceInfo, +}; + /// HackRF USB vendor ID. const HACKRF_USB_VID: u16 = 0x1D50; /// HackRF One USB product ID. @@ -133,12 +138,12 @@ impl Config { /// HackRF One errors. #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("rusb")] - Usb(#[from] rusb::Error), + #[error("io")] + Io(#[from] std::io::Error), #[error("transfer")] - Transfer { - /// Control transfer direction. - dir: Direction, + Transfer(#[from] nusb::transfer::TransferError), + #[error("transfer truncated")] + TransferTruncated { /// Actual amount of bytes transferred. actual: usize, /// Excepted number of bytes transferred. @@ -150,9 +155,9 @@ pub enum Error { #[error("no api")] NoApi { /// Current device version. - device: Version, + device: UsbVersion, /// Minimum version required. - min: Version, + min: UsbVersion, }, #[error("{0}")] Argument(&'static str), @@ -164,10 +169,63 @@ pub enum Error { pub type Result = std::result::Result; +/// A three-part version consisting of major, minor, and sub minor components. +/// +/// The intended use case of `Version` is to extract meaning from the version fields in USB +/// descriptors, such as `bcdUSB` and `bcdDevice` in device descriptors. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +// Taken from rusb::Version: https://github.com/a1ien/rusb/blob/8f8c3c6bff6a494a140da4d93dd946bf1e564d66/src/fields.rs#L142-L203 +pub struct UsbVersion(pub u8, pub u8, pub u8); + +impl UsbVersion { + /// Extracts a version from a binary coded decimal (BCD) field. BCD fields exist in USB + /// descriptors as 16-bit integers encoding a version as `0xJJMN`, where `JJ` is the major + /// version, `M` is the minor version, and `N` is the sub minor version. For example, 2.0 is + /// encoded as `0x0200` and 1.1 is encoded as `0x0110`. + pub fn from_bcd(mut raw: u16) -> Self { + let sub_minor: u8 = (raw & 0x000F) as u8; + raw >>= 4; + + let minor: u8 = (raw & 0x000F) as u8; + raw >>= 4; + + let mut major: u8 = (raw & 0x000F) as u8; + raw >>= 4; + + major += (10 * raw) as u8; + + UsbVersion(major, minor, sub_minor) + } + + /// Returns the major version. + pub fn major(self) -> u8 { + let UsbVersion(major, _, _) = self; + major + } + + /// Returns the minor version. + pub fn minor(self) -> u8 { + let UsbVersion(_, minor, _) = self; + minor + } + + /// Returns the sub minor version. + pub fn sub_minor(self) -> u8 { + let UsbVersion(_, _, sub_minor) = self; + sub_minor + } +} + +impl std::fmt::Display for UsbVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major(), self.minor(), self.sub_minor()) + } +} + /// HackRF One software defined radio. pub struct HackRf { - handle: rusb::DeviceHandle, - discriptor: rusb::DeviceDescriptor, + interface: nusb::Interface, + version: UsbVersion, mode: AtomicMode, timeout_nanos: AtomicU64, } @@ -175,29 +233,25 @@ pub struct HackRf { const DEFAULT_TIMEOUT_NANOS: u64 = Duration::from_millis(500).as_nanos() as u64; impl HackRf { + fn open(info: DeviceInfo) -> Result { + let device = info.open()?; + // TODO(tjn): verify interface + let interface = device.claim_interface(0)?; + + return Ok(HackRf { + interface, + version: UsbVersion::from_bcd(info.device_version()), + timeout_nanos: AtomicU64::new(Duration::from_millis(500).as_nanos() as u64), + mode: AtomicMode::new(Mode::Idle), + }); + } + /// Opens the first Hackrf One radio (if found) by scanning `ctx`. pub fn open_first() -> Result { - let ctx = Context::new()?; - let devices = ctx.devices()?; - - for device in devices.iter() { - let desc = match device.device_descriptor() { - Ok(d) => d, - Err(_) => continue, - }; - - if desc.vendor_id() == HACKRF_USB_VID && desc.product_id() == HACKRF_ONE_USB_PID { - match device.open() { - Ok(handle) => { - return Ok(HackRf { - handle, - discriptor: desc, - timeout_nanos: AtomicU64::new( - Duration::from_millis(500).as_nanos() as u64 - ), - mode: AtomicMode::new(Mode::Idle), - }) - } + for device in nusb::list_devices()? { + if device.vendor_id() == HACKRF_USB_VID && device.product_id() == HACKRF_ONE_USB_PID { + match Self::open(device) { + Ok(dev) => return Ok(dev), Err(_) => continue, } } @@ -207,18 +261,10 @@ impl HackRf { } pub fn scan() -> Result> { - let ctx = Context::new()?; - let devices = ctx.devices()?; - let mut res = vec![]; - for device in devices.iter() { - let desc = match device.device_descriptor() { - Ok(d) => d, - Err(_) => continue, - }; - - if desc.vendor_id() == HACKRF_USB_VID && desc.product_id() == HACKRF_ONE_USB_PID { - res.push((device.bus_number(), device.address())); + for device in nusb::list_devices()? { + if device.vendor_id() == HACKRF_USB_VID && device.product_id() == HACKRF_ONE_USB_PID { + res.push((device.bus_number(), device.device_address())); } } Ok(res) @@ -226,54 +272,40 @@ impl HackRf { /// Opens a hackrf with usb address `:
` pub fn open_bus(bus_number: u8, address: u8) -> Result { - let ctx = Context::new()?; - - let devices = ctx.devices()?; - - println!("Opening bus addr: {bus_number}:{address}"); - for device in devices.iter() { - let desc = match device.device_descriptor() { - Ok(d) => d, - Err(_) => continue, - }; - - if desc.vendor_id() == HACKRF_USB_VID - && desc.product_id() == HACKRF_ONE_USB_PID + for device in nusb::list_devices()? { + if device.vendor_id() == HACKRF_USB_VID + && device.product_id() == HACKRF_ONE_USB_PID && device.bus_number() == bus_number - && device.address() == address + && device.device_address() == address { - let handle = device.open()?; - return Ok(HackRf { - handle, - discriptor: desc, - timeout_nanos: AtomicU64::new(DEFAULT_TIMEOUT_NANOS), - mode: AtomicMode::new(Mode::Idle), - }); + return Self::open(device); } } - Err(rusb::Error::NoDevice.into()) + Err(Error::NotFound) } + /* /// Wraps an existing rusb device handle. pub fn wrap(handle: rusb::DeviceHandle, desc: rusb::DeviceDescriptor) -> HackRf { HackRf { - handle, + interface: handle, discriptor: desc, timeout_nanos: AtomicU64::new(DEFAULT_TIMEOUT_NANOS), mode: AtomicMode::new(Mode::Idle), } } + */ pub fn reset(self) -> Result<()> { - self.check_api_version(Version::from_bcd(0x0102))?; + self.check_api_version(UsbVersion::from_bcd(0x0102))?; self.write_control(Request::Reset, 0, 0, &[])?; Ok(()) } - pub fn device_version(&self) -> Version { - self.discriptor.device_version() + pub fn device_version(&self) -> UsbVersion { + self.version } pub fn board_id(&self) -> Result { @@ -283,16 +315,18 @@ impl HackRf { /// Read the firmware version. pub fn version(&self) -> Result { - let mut buf: [u8; 16] = [0; 16]; - let n: usize = self.handle.read_control( - request_type(Direction::In, RequestType::Vendor, Recipient::Device), - Request::VersionStringRead as u8, - 0, - 0, - &mut buf, - self.timeout(), - )?; - Ok(String::from_utf8_lossy(&buf[0..n]).into()) + let buf = block_on(self.interface.control_in(ControlIn { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: Request::VersionStringRead as u8, + value: 0x0, + index: 0x0, + length: 64, + })) + .into_result() + .expect("transfer failed?"); + + Ok(String::from_utf8_lossy(&buf).into()) } /// Transitions the radio into transmit mode. @@ -329,9 +363,6 @@ impl HackRf { &[], )?; - //released when devh goes out of scope. may pose an issue when switching between rx and tx - self.handle.claim_interface(0)?; - Ok(()) } @@ -367,9 +398,6 @@ impl HackRf { &[], )?; - //released when devh goes out of scope. may pose an issue when switching between rx and tx - self.handle.claim_interface(0)?; - Ok(()) } @@ -386,8 +414,6 @@ impl HackRf { // consumers to wrap our type in an Arc and be smart enough to not enable / disable tx / rx // from multiple threads at the same time. - self.handle.release_interface(0)?; - self.write_control( Request::SetTransceiverMode, TransceiverMode::Off as u16, @@ -414,8 +440,6 @@ impl HackRf { // NOTE: perform atomic exchange last so that we prevent other threads from racing to // start tx/rx with the delivery of our TransceiverMode::Off request - self.handle.release_interface(0)?; - self.write_control( Request::SetTransceiverMode, TransceiverMode::Off as u16, @@ -450,7 +474,15 @@ impl HackRf { } const ENDPOINT: u8 = 0x81; - Ok(self.handle.read_bulk(ENDPOINT, samples, self.timeout())?) + // TODO(tjn): dont allocate, dont block + let buf = block_on( + self.interface + .bulk_in(ENDPOINT, RequestBuffer::new(samples.len())), + ) + .into_result()?; + samples[..buf.len()].copy_from_slice(&buf); + + Ok(buf.len()) } /// Writes samples to the radio. @@ -465,7 +497,11 @@ impl HackRf { } const ENDPOINT: u8 = 0x02; - Ok(self.handle.write_bulk(ENDPOINT, samples, self.timeout())?) + let buf = Vec::from(samples); + // TODO(tjn): dont allocate, dont block + let n = block_on(self.interface.bulk_out(ENDPOINT, buf)).into_result()?; + + Ok(n.actual_length()) } } @@ -503,39 +539,42 @@ impl HackRf { value: u16, index: u16, ) -> Result<[u8; N]> { - let mut buf: [u8; N] = [0; N]; - let n: usize = self.handle.read_control( - request_type(Direction::In, RequestType::Vendor, Recipient::Device), - request as u8, + let mut res: [u8; N] = [0; N]; + let buf = block_on(self.interface.control_in(ControlIn { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: request as u8, value, index, - &mut buf, - self.timeout(), - )?; - if n != buf.len() { - Err(Error::Transfer { - dir: Direction::In, - actual: n, - expected: buf.len(), - }) - } else { - Ok(buf) + length: N as u16, + })) + .into_result()?; + + if buf.len() != N { + return Err(Error::TransferTruncated { + actual: buf.len(), + expected: N, + }); } + + res.copy_from_slice(&buf); + Ok(res) } fn write_control(&self, request: Request, value: u16, index: u16, buf: &[u8]) -> Result<()> { - let n: usize = self.handle.write_control( - request_type(Direction::Out, RequestType::Vendor, Recipient::Device), - request as u8, + let out = block_on(self.interface.control_out(ControlOut { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: request as u8, value, index, - buf, - self.timeout(), - )?; - if n != buf.len() { - Err(Error::Transfer { - dir: Direction::Out, - actual: n, + data: buf, + })) + .into_result()?; + + if out.actual_length() != buf.len() { + Err(Error::TransferTruncated { + actual: out.actual_length(), expected: buf.len(), }) } else { @@ -543,17 +582,18 @@ impl HackRf { } } - fn check_api_version(&self, min: Version) -> Result<()> { - fn version_to_u32(v: Version) -> u32 { + fn check_api_version(&self, min: UsbVersion) -> Result<()> { + fn version_to_u32(v: UsbVersion) -> u32 { ((v.major() as u32) << 16) | ((v.minor() as u32) << 8) | (v.sub_minor() as u32) } - let v = self.discriptor.device_version(); - - if version_to_u32(v) >= version_to_u32(min) { + if version_to_u32(self.version) >= version_to_u32(min) { Ok(()) } else { - Err(Error::NoApi { device: v, min }) + Err(Error::NoApi { + device: self.version, + min, + }) } } diff --git a/src/impls/hackrfone.rs b/src/impls/hackrfone.rs index d23e052..2aee13f 100644 --- a/src/impls/hackrfone.rs +++ b/src/impls/hackrfone.rs @@ -303,7 +303,7 @@ impl crate::DeviceTrait for HackRfOne { } } - fn enable_agc(&self, _direction: Direction, channel: usize, agc: bool) -> Result<(), Error> { + fn enable_agc(&self, _direction: Direction, channel: usize, _agc: bool) -> Result<(), Error> { if channel == 0 { Err(Error::NotSupported) } else { @@ -469,9 +469,11 @@ impl crate::DeviceTrait for HackRfOne { && name == "TUNER" { self.with_config(direction, |config| { + println!("Set frequency to {frequency}"); config.frequency_hz = frequency as u64; - }); - Ok(()) + self.inner.dev.set_freq(frequency as u64)?; + Ok(()) + }) } else { Err(Error::ValueError) } From ffd8d9f55503336a4365349fb41cc2f726dd298a Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Mon, 2 Sep 2024 21:16:13 -0700 Subject: [PATCH 06/16] async approach works! --- Cargo.toml | 1 + crates/hackrf-one-rs/Cargo.lock | 32 ++++++++++++++++ crates/hackrf-one-rs/Cargo.toml | 1 + crates/hackrf-one-rs/src/lib.rs | 66 ++++++++++++++++++++++++++++++++- src/impls/hackrfone.rs | 32 +++++++--------- 5 files changed, 112 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5b7472e..c7d34de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.6" thiserror = "1.0" +tracing = "0.1.40" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] once_cell = "1.19" diff --git a/crates/hackrf-one-rs/Cargo.lock b/crates/hackrf-one-rs/Cargo.lock index ed66d60..974f335 100644 --- a/crates/hackrf-one-rs/Cargo.lock +++ b/crates/hackrf-one-rs/Cargo.lock @@ -225,6 +225,7 @@ dependencies = [ "num-complex", "nusb", "thiserror", + "tracing", ] [[package]] @@ -267,6 +268,37 @@ dependencies = [ "syn", ] +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/crates/hackrf-one-rs/Cargo.toml b/crates/hackrf-one-rs/Cargo.toml index ef8e5c5..837e78a 100644 --- a/crates/hackrf-one-rs/Cargo.toml +++ b/crates/hackrf-one-rs/Cargo.toml @@ -9,6 +9,7 @@ thiserror = "1.0.63" atomic_enum = "0.3.0" nusb = "0.1.10" futures-lite = "2.3.0" +tracing = "0.1.40" [dev-dependencies] anyhow = "1.0.86" diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs index c9b7d8f..8a62749 100644 --- a/crates/hackrf-one-rs/src/lib.rs +++ b/crates/hackrf-one-rs/src/lib.rs @@ -14,7 +14,7 @@ use std::{ use futures_lite::future::block_on; use nusb::{ - transfer::{ControlIn, ControlOut, ControlType, Recipient, RequestBuffer}, + transfer::{ControlIn, ControlOut, ControlType, Queue, Recipient, RequestBuffer}, DeviceInfo, }; @@ -338,7 +338,7 @@ impl HackRf { /// This function will return an error if a tx or rx operation is already in progress or if an /// I/O error occurs pub fn start_tx(&self, config: &Config) -> Result<()> { - println!("Starting tx: {config:?}"); + tracing::info!("Starting tx: {config:?}"); // NOTE: perform atomic exchange first so that we only change the transceiver mode once if // other therads are racing to change the mode @@ -503,6 +503,68 @@ impl HackRf { Ok(n.actual_length()) } + + pub fn start_rx_stream(&self, transfer_size: usize) -> Result { + if transfer_size % 512 != 0 { + panic!("transfer_size must be a multiple of 512"); + } + + const ENDPOINT: u8 = 0x81; + Ok(RxStream { + interface: self.interface.clone(), + queue: self.interface.bulk_in_queue(ENDPOINT), + in_flight_transfers: 3, + transfer_size, + buf_pos: transfer_size, + buf: vec![0u8; transfer_size], + }) + } +} + +pub struct RxStream { + interface: nusb::Interface, + queue: Queue, + in_flight_transfers: usize, + transfer_size: usize, + buf_pos: usize, + buf: Vec, +} + +impl RxStream { + pub fn read_sync(&mut self, count: usize) -> Result<&[u8]> { + let buffered_remaining = self.buf.len() - self.buf_pos; + if buffered_remaining > 0 { + let to_consume = std::cmp::min(count, buffered_remaining); + let ret = &self.buf[self.buf_pos..self.buf_pos + to_consume]; + self.buf_pos += ret.len(); + //tracing::info!(" returning {to_consume} buffered bytes"); + if self.buf_pos == self.buf.len() { + //tracing::info!(" this is the last of buffered bytes"); + } + return Ok(ret); + } + + while self.queue.pending() < self.in_flight_transfers { + //tracing::info!("Submitting async transfer"); + self.queue.submit(RequestBuffer::new(self.transfer_size)); + } + let completion = block_on(self.queue.next_complete()); + //tracing::info!("Read {} bytes", completion.data.len()); + + if let Err(e) = completion.status { + //tracing::error!("transfer error: {e:?}"); + return Err(e.into()); + } + + let reuse = std::mem::replace(&mut self.buf, completion.data); + self.buf_pos = 0; + + self.queue + .submit(RequestBuffer::reuse(reuse, self.transfer_size)); + + // bytes are now buffered, use tail recursion for code above to return subslice + self.read_sync(count) + } } impl HackRf { diff --git a/src/impls/hackrfone.rs b/src/impls/hackrfone.rs index 2aee13f..0f63b42 100644 --- a/src/impls/hackrfone.rs +++ b/src/impls/hackrfone.rs @@ -13,7 +13,6 @@ const MTU: usize = 64 * 1024; impl HackRfOne { pub fn probe(_args: &Args) -> Result, Error> { - panic!(); let mut devs = vec![]; for (bus_number, address) in seify_hackrfone::HackRf::scan()? { println!("probing {bus_number}:{address}"); @@ -78,14 +77,14 @@ struct HackRfInner { pub struct RxStreamer { inner: Arc, - buf: Vec, + stream: Option, } impl RxStreamer { fn new(inner: Arc) -> Self { Self { inner, - buf: vec![0u8; MTU], + stream: None, } } } @@ -100,12 +99,16 @@ impl crate::RxStreamer for RxStreamer { let config = self.inner.rx_config.lock().unwrap(); self.inner.dev.start_rx(&config)?; + self.stream = Some(self.inner.dev.start_rx_stream(MTU)?); + Ok(()) } fn deactivate_at(&mut self, _time_ns: Option) -> Result<(), Error> { // TODO(tjn): sleep precisely for `time_ns` + println!("dropping stream"); + let _ = self.stream.take().unwrap(); self.inner.dev.stop_rx()?; Ok(()) } @@ -117,36 +120,29 @@ impl crate::RxStreamer for RxStreamer { ) -> Result { debug_assert_eq!(buffers.len(), 1); - // make len multiple of 256 to make u multiple of 512 - let len = std::cmp::min(buffers[0].len(), MTU / 2); - let len = len & !0xff; - if len == 0 { + if buffers[0].len() == 0 { return Ok(0); } - let n = self.inner.dev.read(&mut self.buf[0..len * 2])?; - debug_assert_eq!(n % 2, 0); + let buf = self.stream.as_mut().unwrap().read_sync(buffers[0].len())?; - for i in 0..n / 2 { + let samples = buf.len() / 2; + for i in 0..samples { buffers[0][i] = Complex32::new( - (self.buf[i * 2] as f32 - 127.0) / 128.0, - (self.buf[i * 2 + 1] as f32 - 127.0) / 128.0, + (buf[i * 2] as f32 - 127.0) / 128.0, + (buf[i * 2 + 1] as f32 - 127.0) / 128.0, ); } - Ok(n / 2) + Ok(samples) } } pub struct TxStreamer { inner: Arc, - buf: Vec, } impl TxStreamer { fn new(inner: Arc) -> Self { - Self { - inner, - buf: vec![0u8; MTU], - } + Self { inner } } } From 2f1eddacf9f4af5cc18522115faa4a9f3feff22e Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Thu, 5 Sep 2024 15:32:37 +0000 Subject: [PATCH 07/16] debugging hackrf on android --- Cargo.toml | 3 ++- crates/hackrf-one-rs/Cargo.toml | 3 ++- crates/hackrf-one-rs/src/lib.rs | 30 +++++++++++++++--------------- src/device.rs | 2 +- src/impls/hackrfone.rs | 20 +++++++++++++++++++- 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c7d34de..58add3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,8 @@ futures = "0.3" log = "0.4" nom = "7.1" num-complex = "0.4" -nusb = "0.1.10" +# nusb = "0.1.10" +nusb = { path = "../nusb" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.6" diff --git a/crates/hackrf-one-rs/Cargo.toml b/crates/hackrf-one-rs/Cargo.toml index 837e78a..50c2831 100644 --- a/crates/hackrf-one-rs/Cargo.toml +++ b/crates/hackrf-one-rs/Cargo.toml @@ -7,7 +7,8 @@ edition = "2021" num-complex = "0.4" thiserror = "1.0.63" atomic_enum = "0.3.0" -nusb = "0.1.10" +# nusb = "0.1.10" +nusb = { path = "../../../nusb" } futures-lite = "2.3.0" tracing = "0.1.40" diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs index 8a62749..344c768 100644 --- a/crates/hackrf-one-rs/src/lib.rs +++ b/crates/hackrf-one-rs/src/lib.rs @@ -233,9 +233,21 @@ pub struct HackRf { const DEFAULT_TIMEOUT_NANOS: u64 = Duration::from_millis(500).as_nanos() as u64; impl HackRf { + pub fn wrap(device: nusb::Device) -> Result { + let interface = device.claim_interface(0)?; + // Need a nice way to get version + + return Ok(HackRf { + interface, + // TODO(tjn): Actually read version + version: UsbVersion::from_bcd(0x0102), + timeout_nanos: AtomicU64::new(Duration::from_millis(500).as_nanos() as u64), + mode: AtomicMode::new(Mode::Idle), + }); + } + fn open(info: DeviceInfo) -> Result { let device = info.open()?; - // TODO(tjn): verify interface let interface = device.claim_interface(0)?; return Ok(HackRf { @@ -264,7 +276,7 @@ impl HackRf { let mut res = vec![]; for device in nusb::list_devices()? { if device.vendor_id() == HACKRF_USB_VID && device.product_id() == HACKRF_ONE_USB_PID { - res.push((device.bus_number(), device.device_address())); + res.push((device.busnum(), device.device_address())); } } Ok(res) @@ -275,7 +287,7 @@ impl HackRf { for device in nusb::list_devices()? { if device.vendor_id() == HACKRF_USB_VID && device.product_id() == HACKRF_ONE_USB_PID - && device.bus_number() == bus_number + && device.busnum() == bus_number && device.device_address() == address { return Self::open(device); @@ -285,18 +297,6 @@ impl HackRf { Err(Error::NotFound) } - /* - /// Wraps an existing rusb device handle. - pub fn wrap(handle: rusb::DeviceHandle, desc: rusb::DeviceDescriptor) -> HackRf { - HackRf { - interface: handle, - discriptor: desc, - timeout_nanos: AtomicU64::new(DEFAULT_TIMEOUT_NANOS), - mode: AtomicMode::new(Mode::Idle), - } - } - */ - pub fn reset(self) -> Result<()> { self.check_api_version(UsbVersion::from_bcd(0x0102))?; self.write_control(Request::Reset, 0, 0, &[])?; diff --git a/src/device.rs b/src/device.rs index bf19fb9..700d862 100644 --- a/src/device.rs +++ b/src/device.rs @@ -229,7 +229,6 @@ impl Device { Err(Error::NotFound) => None, Err(e) => return Err(e), }; - dbg!(&args); #[cfg(all(feature = "aaronia", any(target_os = "linux", target_os = "windows")))] { if driver.is_none() || matches!(driver, Some(Driver::Aaronia)) { @@ -320,6 +319,7 @@ impl Device { } } } + panic!("DRIVER NOT FOUND (cfg block?)"); Err(Error::NotFound) } } diff --git a/src/impls/hackrfone.rs b/src/impls/hackrfone.rs index 0f63b42..44073e6 100644 --- a/src/impls/hackrfone.rs +++ b/src/impls/hackrfone.rs @@ -1,4 +1,7 @@ -use std::sync::{Arc, Mutex}; +use std::{ + os::fd::{FromRawFd, OwnedFd}, + sync::{Arc, Mutex}, +}; use num_complex::Complex32; use seify_hackrfone::Config; @@ -30,6 +33,21 @@ impl HackRfOne { /// Create a Hackrf One devices pub fn open>(args: A) -> Result { let args: Args = args.try_into().or(Err(Error::ValueError))?; + log::info!("HackRfOne::open called: {args}"); + + if let Ok(fd) = args.get::("fd") { + log::info!("device open got special fd arg"); + let fd = unsafe { OwnedFd::from_raw_fd(fd) }; + let dev = nusb::Device::from_fd(fd)?; + + return Ok(Self { + inner: Arc::new(HackRfInner { + dev: seify_hackrfone::HackRf::wrap(dev)?, + tx_config: Mutex::new(Config::tx_default()), + rx_config: Mutex::new(Config::rx_default()), + }), + }); + } let bus_number = args.get("bus_number"); let address = args.get("address"); From 9bdab4a09946531f55201ec66eef09197d05946d Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Wed, 11 Sep 2024 00:20:24 -0700 Subject: [PATCH 08/16] nicer better logging --- src/device.rs | 2 +- src/impls/hackrfone.rs | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/device.rs b/src/device.rs index 700d862..8938767 100644 --- a/src/device.rs +++ b/src/device.rs @@ -319,7 +319,7 @@ impl Device { } } } - panic!("DRIVER NOT FOUND (cfg block?)"); + Err(Error::NotFound) } } diff --git a/src/impls/hackrfone.rs b/src/impls/hackrfone.rs index 44073e6..b41bab9 100644 --- a/src/impls/hackrfone.rs +++ b/src/impls/hackrfone.rs @@ -18,7 +18,7 @@ impl HackRfOne { pub fn probe(_args: &Args) -> Result, Error> { let mut devs = vec![]; for (bus_number, address) in seify_hackrfone::HackRf::scan()? { - println!("probing {bus_number}:{address}"); + log::debug!("probing {bus_number}:{address}"); devs.push( format!( "driver=hackrfone, bus_number={}, address={}", @@ -53,7 +53,6 @@ impl HackRfOne { let address = args.get("address"); let dev = match (bus_number, address) { (Ok(bus_number), Ok(address)) => { - dbg!(bus_number, address); seify_hackrfone::HackRf::open_bus(bus_number, address)? } (Err(Error::NotFound), Err(Error::NotFound)) => { @@ -235,11 +234,13 @@ impl crate::DeviceTrait for HackRfOne { } fn id(&self) -> Result { - todo!() + Ok(self.inner.dev.board_id()?.to_string()) } fn info(&self) -> Result { - Ok(Default::default()) + let mut args = crate::Args::default(); + args.set("firmware version", self.inner.dev.version()?); + Ok(args) } fn num_channels(&self, _: crate::Direction) -> Result { @@ -355,9 +356,6 @@ impl crate::DeviceTrait for HackRfOne { name: &str, gain: f64, ) -> Result<(), Error> { - println!("set_gain_element"); - dbg!(direction, channel, name, gain); - let r = self.gain_range(direction, channel)?; if r.contains(gain) && name == "IF" { match direction { @@ -381,7 +379,6 @@ impl crate::DeviceTrait for HackRfOne { channel: usize, name: &str, ) -> Result, Error> { - dbg!(direction, channel, name); if channel == 0 && name == "IF" { match direction { Direction::Tx => todo!(), From 8860d485e7a1f358f95016cc40bb83a84533ebaa Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Sun, 6 Oct 2024 10:38:29 -0700 Subject: [PATCH 09/16] cleanup --- Cargo.toml | 6 +- crates/hackrf-one-rs/Cargo.lock | 5 +- crates/hackrf-one-rs/Cargo.toml | 3 +- crates/hackrf-one-rs/examples/info.rs | 3 +- crates/hackrf-one-rs/examples/rx.rs | 13 +++-- crates/hackrf-one-rs/src/lib.rs | 79 +++++++++++---------------- examples/rx_typed.rs | 2 - 7 files changed, 49 insertions(+), 62 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 58add3f..10dc98e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,8 @@ license = "Apache-2.0" repository = "https://github.com/FutureSDR/seify" [features] -default = ["soapy", "hackrfone"] +# default = ["soapy", "hackrfone"] +default = ["hackrfone"] rtlsdr = ["dep:seify-rtlsdr"] hackrfone = ["dep:seify-hackrfone"] aaronia = ["dep:aaronia-rtsa"] @@ -25,8 +26,7 @@ futures = "0.3" log = "0.4" nom = "7.1" num-complex = "0.4" -# nusb = "0.1.10" -nusb = { path = "../nusb" } +nusb = { git = "https://github.com/kevinmehall/nusb.git" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.6" diff --git a/crates/hackrf-one-rs/Cargo.lock b/crates/hackrf-one-rs/Cargo.lock index 974f335..bc32bd0 100644 --- a/crates/hackrf-one-rs/Cargo.lock +++ b/crates/hackrf-one-rs/Cargo.lock @@ -152,13 +152,14 @@ dependencies = [ [[package]] name = "nusb" version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8beeee5c0ad012e1eaca540f5610d09a3af58ec435537d511b67e0be36ea66" +source = "git+https://github.com/kevinmehall/nusb.git#809228038f43de2d7a636825d17439b445f612b7" dependencies = [ "atomic-waker", "core-foundation", "core-foundation-sys", + "futures-core", "io-kit-sys", + "libc", "log", "once_cell", "rustix", diff --git a/crates/hackrf-one-rs/Cargo.toml b/crates/hackrf-one-rs/Cargo.toml index 50c2831..f183bc3 100644 --- a/crates/hackrf-one-rs/Cargo.toml +++ b/crates/hackrf-one-rs/Cargo.toml @@ -7,8 +7,7 @@ edition = "2021" num-complex = "0.4" thiserror = "1.0.63" atomic_enum = "0.3.0" -# nusb = "0.1.10" -nusb = { path = "../../../nusb" } +nusb = { git = "https://github.com/kevinmehall/nusb.git" } futures-lite = "2.3.0" tracing = "0.1.40" diff --git a/crates/hackrf-one-rs/examples/info.rs b/crates/hackrf-one-rs/examples/info.rs index 0759274..36bb0e7 100644 --- a/crates/hackrf-one-rs/examples/info.rs +++ b/crates/hackrf-one-rs/examples/info.rs @@ -1,8 +1,7 @@ use seify_hackrfone::HackRf; fn main() { - let context = rusb::Context::new().expect("Failed to create rusb Context"); - let radio = HackRf::new(context).expect("Failed to open Hackrf"); + let radio = HackRf::open_first().expect("Failed to open Hackrf"); println!("Board ID: {:?}", radio.board_id()); println!("Version: {:?}", radio.version()); diff --git a/crates/hackrf-one-rs/examples/rx.rs b/crates/hackrf-one-rs/examples/rx.rs index d6aa2a7..101eeed 100644 --- a/crates/hackrf-one-rs/examples/rx.rs +++ b/crates/hackrf-one-rs/examples/rx.rs @@ -4,8 +4,7 @@ use seify_hackrfone::{Config, HackRf}; use std::time::Instant; fn main() -> Result<()> { - let context = rusb::Context::new().context("Failed to create rusb Context")?; - let radio = HackRf::new(context).context("Failed to open Hackrf")?; + let radio = HackRf::open_first().context("Failed to open Hackrf")?; println!("Board ID: {:?}", radio.board_id()); println!("Version: {:?}", radio.version()); @@ -13,10 +12,14 @@ fn main() -> Result<()> { radio .start_rx(&Config { - frequency_hz: 2_410_000_000, - amp_enable: true, + vga_db: 0, + txvga_db: 0, + lna_db: 0, + power_port_enable: false, antenna_enable: false, - ..Default::default() + frequency_hz: 915_000_000, + sample_rate_hz: 2_000_000, + sample_rate_div: 0, }) .context("Failed to receive on hackrf")?; diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs index 344c768..79f35e4 100644 --- a/crates/hackrf-one-rs/src/lib.rs +++ b/crates/hackrf-one-rs/src/lib.rs @@ -230,8 +230,6 @@ pub struct HackRf { timeout_nanos: AtomicU64, } -const DEFAULT_TIMEOUT_NANOS: u64 = Duration::from_millis(500).as_nanos() as u64; - impl HackRf { pub fn wrap(device: nusb::Device) -> Result { let interface = device.claim_interface(0)?; @@ -341,7 +339,7 @@ impl HackRf { tracing::info!("Starting tx: {config:?}"); // NOTE: perform atomic exchange first so that we only change the transceiver mode once if - // other therads are racing to change the mode + // other threads are racing to change the mode if let Err(actual) = self.mode.compare_exchange( Mode::Idle, Mode::Transmit, @@ -511,7 +509,6 @@ impl HackRf { const ENDPOINT: u8 = 0x81; Ok(RxStream { - interface: self.interface.clone(), queue: self.interface.bulk_in_queue(ENDPOINT), in_flight_transfers: 3, transfer_size, @@ -522,7 +519,6 @@ impl HackRf { } pub struct RxStream { - interface: nusb::Interface, queue: Queue, in_flight_transfers: usize, transfer_size: usize, @@ -537,22 +533,15 @@ impl RxStream { let to_consume = std::cmp::min(count, buffered_remaining); let ret = &self.buf[self.buf_pos..self.buf_pos + to_consume]; self.buf_pos += ret.len(); - //tracing::info!(" returning {to_consume} buffered bytes"); - if self.buf_pos == self.buf.len() { - //tracing::info!(" this is the last of buffered bytes"); - } return Ok(ret); } while self.queue.pending() < self.in_flight_transfers { - //tracing::info!("Submitting async transfer"); self.queue.submit(RequestBuffer::new(self.transfer_size)); } let completion = block_on(self.queue.next_complete()); - //tracing::info!("Read {} bytes", completion.data.len()); if let Err(e) = completion.status { - //tracing::error!("transfer error: {e:?}"); return Err(e.into()); } @@ -591,7 +580,7 @@ impl HackRf { Ok(()) } - fn timeout(&self) -> Duration { + pub fn timeout(&self) -> Duration { Duration::from_nanos(self.timeout_nanos.load(Ordering::Acquire)) } @@ -804,53 +793,40 @@ fn freq_params(hz: u64) -> [u8; 8] { mod test { use std::time::Duration; - use super::{freq_params, HackRf}; + use super::*; #[test] - fn nominal() { + fn test_freq_params() { assert_eq!(freq_params(915_000_000), [0x93, 0x03, 0, 0, 0, 0, 0, 0]); assert_eq!(freq_params(915_000_001), [0x93, 0x03, 0, 0, 1, 0, 0, 0]); assert_eq!( freq_params(123456789), [0x7B, 0, 0, 0, 0x55, 0xF8, 0x06, 0x00] ); - } - #[test] - fn min() { assert_eq!(freq_params(0), [0; 8]); - } - #[test] - fn max() { assert_eq!(freq_params(u64::MAX), [0xFF; 8]); } - #[test] + // NOTE: make sure you can transmit on the frequency below in your country! + // #[test] + #[allow(dead_code)] fn device_states() { - let strict = true; - - let context = match rusb::Context::new() { - Ok(c) => c, - Err(e) => { - if strict { - panic!("{e:?}"); - } - println!("Failed to create rusb context, passing test anyway: {e:?}"); - return; - } - }; - let radio = match HackRf::open_first() { - Some(r) => r, - None => { - if strict { - panic!("Failed to open hackrf"); - } - println!("Failed to open hackrf, passing test anyway"); - return; - } - }; - radio.start_tx(&Default::default()).unwrap(); + let radio = HackRf::open_first().expect("Failed to open hackrf"); + + radio + .start_tx(&Config { + vga_db: 0, + txvga_db: 0, + lna_db: 0, + power_port_enable: false, + antenna_enable: false, + frequency_hz: 915_000_000, + sample_rate_hz: 2_000_000, + sample_rate_div: 0, + }) + .unwrap(); std::thread::sleep(Duration::from_millis(50)); radio.stop_tx().unwrap(); @@ -861,7 +837,18 @@ mod test { std::thread::sleep(Duration::from_millis(50)); - radio.start_rx(&Default::default()).unwrap(); + radio + .start_rx(&Config { + vga_db: 0, + txvga_db: 0, + lna_db: 0, + power_port_enable: false, + antenna_enable: false, + frequency_hz: 915_000_000, + sample_rate_hz: 2_000_000, + sample_rate_div: 0, + }) + .unwrap(); std::thread::sleep(Duration::from_millis(50)); radio.stop_rx().unwrap(); diff --git a/examples/rx_typed.rs b/examples/rx_typed.rs index 8681585..102b88b 100644 --- a/examples/rx_typed.rs +++ b/examples/rx_typed.rs @@ -20,8 +20,6 @@ pub fn main() -> Result<(), Box> { let rtl = rtlsdr::RtlSdr::open(cli.args)?; let dev = Device::from_impl(rtl); - // Get typed reference to device impl - // let _r : &seify::impls::RtlSdr = dev.impl_ref().unwrap(); dev.enable_agc(Rx, 0, true)?; dev.set_frequency(Rx, 0, 101e6)?; From bdc4189c65ee83c61af8761b2086c363b4209bfb Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Sun, 6 Oct 2024 10:42:52 -0700 Subject: [PATCH 10/16] fix examples --- crates/hackrf-one-rs/examples/info.rs | 6 ++++-- crates/hackrf-one-rs/examples/rx.rs | 2 +- crates/hackrf-one-rs/src/lib.rs | 11 ++++++----- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/hackrf-one-rs/examples/info.rs b/crates/hackrf-one-rs/examples/info.rs index 36bb0e7..03e300a 100644 --- a/crates/hackrf-one-rs/examples/info.rs +++ b/crates/hackrf-one-rs/examples/info.rs @@ -1,9 +1,11 @@ +use anyhow::{Context, Result}; use seify_hackrfone::HackRf; -fn main() { - let radio = HackRf::open_first().expect("Failed to open Hackrf"); +fn main() -> Result<()> { + let radio = HackRf::open_first().context("Failed to open Hackrf")?; println!("Board ID: {:?}", radio.board_id()); println!("Version: {:?}", radio.version()); println!("Device version: {:?}", radio.device_version()); + Ok(()) } diff --git a/crates/hackrf-one-rs/examples/rx.rs b/crates/hackrf-one-rs/examples/rx.rs index 101eeed..9efdf97 100644 --- a/crates/hackrf-one-rs/examples/rx.rs +++ b/crates/hackrf-one-rs/examples/rx.rs @@ -31,7 +31,7 @@ fn main() -> Result<()> { let mut last_print = Instant::now(); while samples.len() < collect_count { - let n = radio.rx(&mut buf).context("Failed to receive samples")?; + let n = radio.read(&mut buf).context("Failed to receive samples")?; assert_eq!(n, buf.len()); for iq in buf.chunks_exact(2) { samples.push(Complex32::new( diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs index 79f35e4..0e10789 100644 --- a/crates/hackrf-one-rs/src/lib.rs +++ b/crates/hackrf-one-rs/src/lib.rs @@ -256,7 +256,7 @@ impl HackRf { }); } - /// Opens the first Hackrf One radio (if found) by scanning `ctx`. + /// Opens the first Hackrf One found via USB. pub fn open_first() -> Result { for device in nusb::list_devices()? { if device.vendor_id() == HACKRF_USB_VID && device.product_id() == HACKRF_ONE_USB_PID { @@ -270,6 +270,8 @@ impl HackRf { Err(Error::NotFound) } + /// Scans the usb bus for hackrf devices, returning the pair of (bus_num, bus_addr) for each + /// device. pub fn scan() -> Result> { let mut res = vec![]; for device in nusb::list_devices()? { @@ -321,8 +323,7 @@ impl HackRf { index: 0x0, length: 64, })) - .into_result() - .expect("transfer failed?"); + .into_result()?; Ok(String::from_utf8_lossy(&buf).into()) } @@ -824,7 +825,7 @@ mod test { antenna_enable: false, frequency_hz: 915_000_000, sample_rate_hz: 2_000_000, - sample_rate_div: 0, + sample_rate_div: 1, }) .unwrap(); std::thread::sleep(Duration::from_millis(50)); @@ -846,7 +847,7 @@ mod test { antenna_enable: false, frequency_hz: 915_000_000, sample_rate_hz: 2_000_000, - sample_rate_div: 0, + sample_rate_div: 1, }) .unwrap(); std::thread::sleep(Duration::from_millis(50)); From 8305ec1eb4678a02c3b400e11b8bc3e117a227a9 Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Sun, 6 Oct 2024 10:47:51 -0700 Subject: [PATCH 11/16] clean docs --- crates/hackrf-one-rs/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs index 0e10789..49da968 100644 --- a/crates/hackrf-one-rs/src/lib.rs +++ b/crates/hackrf-one-rs/src/lib.rs @@ -2,7 +2,7 @@ //! HackRF One API. //! -//! To get started take a look at [`HackRfOne::new`]. +//! To get started take a look at [`HackRf::open_first`]. #![cfg_attr(docsrs, feature(doc_cfg), feature(doc_auto_cfg))] // TODO(tjn): re-enable // #![warn(missing_docs)] @@ -329,7 +329,7 @@ impl HackRf { } /// Transitions the radio into transmit mode. - /// Call this function before calling [`Self::tx`]. + /// Call this function before calling [`Self::write`]. /// /// Previous state set via `set_xxx` functions will be overriden with the parameters set in `config`. /// @@ -366,7 +366,7 @@ impl HackRf { } /// Transitions the radio into receive mode. - /// Call this function before calling [`Self::rx`]. + /// Call this function before calling [`Self::read`]. /// /// Previous state set via `set_xxx` functions will be overriden with the parameters set in `config`. /// @@ -503,6 +503,7 @@ impl HackRf { Ok(n.actual_length()) } + /// Setup the device to stream samples. pub fn start_rx_stream(&self, transfer_size: usize) -> Result { if transfer_size % 512 != 0 { panic!("transfer_size must be a multiple of 512"); @@ -528,6 +529,7 @@ pub struct RxStream { } impl RxStream { + /// Read samples from the device, blocking until more are available. pub fn read_sync(&mut self, count: usize) -> Result<&[u8]> { let buffered_remaining = self.buf.len() - self.buf_pos; if buffered_remaining > 0 { @@ -666,7 +668,7 @@ impl HackRf { /// Set the baseband filter bandwidth. /// /// This is automatically set when the sample rate is changed with - /// [`set_sample_rate`]. + /// [`Self::set_sample_rate`]. pub fn set_baseband_filter_bandwidth(&self, hz: u32) -> Result<()> { self.write_control( Request::BasebandFilterBandwidthSet, From 6ee8198060180cb9c8b8e3ff9d6d4a74a75e7382 Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Sun, 6 Oct 2024 10:52:20 -0700 Subject: [PATCH 12/16] rm complete TODOs --- crates/hackrf-one-rs/src/lib.rs | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs index 49da968..f36fea5 100644 --- a/crates/hackrf-one-rs/src/lib.rs +++ b/crates/hackrf-one-rs/src/lib.rs @@ -94,8 +94,7 @@ pub struct Config { // Pre baseband receive pub lna_db: u16, /// RF amplifier (on/off) - /// NOTE: called `amp_enable` in HackRf docs - pub power_port_enable: bool, + pub amp_enable: bool, /// Antenna power port control // Power enable on antenna @@ -113,7 +112,7 @@ impl Config { vga_db: 0, lna_db: 0, txvga_db: 40, - power_port_enable: false, + amp_enable: false, antenna_enable: false, frequency_hz: 908_000_000, sample_rate_hz: 2_500_000, @@ -126,7 +125,7 @@ impl Config { vga_db: 24, lna_db: 0, txvga_db: 0, - power_port_enable: false, + amp_enable: false, antenna_enable: false, frequency_hz: 908_000_000, sample_rate_hz: 2_500_000, @@ -233,11 +232,10 @@ pub struct HackRf { impl HackRf { pub fn wrap(device: nusb::Device) -> Result { let interface = device.claim_interface(0)?; - // Need a nice way to get version return Ok(HackRf { interface, - // TODO(tjn): Actually read version + // TODO: Actually read version, dont assume latest version: UsbVersion::from_bcd(0x0102), timeout_nanos: AtomicU64::new(Duration::from_millis(500).as_nanos() as u64), mode: AtomicMode::new(Mode::Idle), @@ -473,7 +471,6 @@ impl HackRf { } const ENDPOINT: u8 = 0x81; - // TODO(tjn): dont allocate, dont block let buf = block_on( self.interface .bulk_in(ENDPOINT, RequestBuffer::new(samples.len())), @@ -497,7 +494,7 @@ impl HackRf { const ENDPOINT: u8 = 0x02; let buf = Vec::from(samples); - // TODO(tjn): dont allocate, dont block + // TODO: dont allocate let n = block_on(self.interface.bulk_out(ENDPOINT, buf)).into_result()?; Ok(n.actual_length()) @@ -565,7 +562,7 @@ impl HackRf { self.set_vga_gain(config.vga_db)?; self.set_txvga_gain(config.txvga_db)?; self.set_freq(config.frequency_hz)?; - self.set_amp_enable(config.power_port_enable)?; + self.set_amp_enable(config.amp_enable)?; self.set_antenna_enable(config.antenna_enable)?; self.set_sample_rate(config.sample_rate_hz, config.sample_rate_div)?; @@ -716,8 +713,7 @@ impl HackRf { } else { let buf: [u8; 1] = self.read_control(Request::SetLnaGain, 0, gain & !0x07)?; if buf[0] == 0 { - // TODO(tjn): check hackrf docs - panic!(); + panic!("Unexpected return value from read_control(SetLnaGain)"); } else { Ok(()) } @@ -735,7 +731,6 @@ impl HackRf { "gain parameter out of range. must be even and less than or equal to 62", )) } else { - // TODO(tjn): read_control seems wrong here, investigate let buf: [u8; 1] = self.read_control(Request::SetVgaGain, 0, gain & !0b1)?; if buf[0] == 0 { panic!("What is this return value?") @@ -752,7 +747,6 @@ impl HackRf { if gain > 47 { Err(Error::Argument("gain parameter out of range. max is 47")) } else { - // TODO(tjn): read_control seems wrong here, investigate let buf: [u8; 1] = self.read_control(Request::SetTxvgaGain, 0, gain)?; if buf[0] == 0 { panic!("What is this return value?") @@ -823,7 +817,7 @@ mod test { vga_db: 0, txvga_db: 0, lna_db: 0, - power_port_enable: false, + amp_enable: false, antenna_enable: false, frequency_hz: 915_000_000, sample_rate_hz: 2_000_000, @@ -845,7 +839,7 @@ mod test { vga_db: 0, txvga_db: 0, lna_db: 0, - power_port_enable: false, + amp_enable: false, antenna_enable: false, frequency_hz: 915_000_000, sample_rate_hz: 2_000_000, From b4b750282dd810ca2129fc8283429d8ced114042 Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Sun, 6 Oct 2024 11:25:43 -0700 Subject: [PATCH 13/16] self review --- Cargo.toml | 8 +++----- crates/hackrf-one-rs/Cargo.toml | 1 + crates/hackrf-one-rs/src/lib.rs | 34 +++++++++++++++++++-------------- src/impls/hackrfone.rs | 28 +++++++++++---------------- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 10dc98e..fe93251 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,9 @@ license = "Apache-2.0" repository = "https://github.com/FutureSDR/seify" [features] -# default = ["soapy", "hackrfone"] -default = ["hackrfone"] +default = ["soapy"] rtlsdr = ["dep:seify-rtlsdr"] -hackrfone = ["dep:seify-hackrfone"] +hackrfone = ["dep:seify-hackrfone", "dep:nusb"] aaronia = ["dep:aaronia-rtsa"] aaronia_http = ["dep:ureq"] soapy = ["dep:soapysdr"] @@ -26,12 +25,11 @@ futures = "0.3" log = "0.4" nom = "7.1" num-complex = "0.4" -nusb = { git = "https://github.com/kevinmehall/nusb.git" } +nusb = { git = "https://github.com/kevinmehall/nusb.git", optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.6" thiserror = "1.0" -tracing = "0.1.40" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] once_cell = "1.19" diff --git a/crates/hackrf-one-rs/Cargo.toml b/crates/hackrf-one-rs/Cargo.toml index f183bc3..31fe579 100644 --- a/crates/hackrf-one-rs/Cargo.toml +++ b/crates/hackrf-one-rs/Cargo.toml @@ -2,6 +2,7 @@ name = "seify-hackrfone" version = "0.1.0" edition = "2021" +authors = ["Troy Neubauer "] [dependencies] num-complex = "0.4" diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs index f36fea5..e4a80d1 100644 --- a/crates/hackrf-one-rs/src/lib.rs +++ b/crates/hackrf-one-rs/src/lib.rs @@ -174,6 +174,7 @@ pub type Result = std::result::Result; /// descriptors, such as `bcdUSB` and `bcdDevice` in device descriptors. #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] // Taken from rusb::Version: https://github.com/a1ien/rusb/blob/8f8c3c6bff6a494a140da4d93dd946bf1e564d66/src/fields.rs#L142-L203 +// nusb doesnt currently have this pub struct UsbVersion(pub u8, pub u8, pub u8); impl UsbVersion { @@ -230,6 +231,8 @@ pub struct HackRf { } impl HackRf { + /// Wraps an existing nusb::Device. + /// Useful on platform like Android where you are forced to use [`nusb::Device::from_fd`]. pub fn wrap(device: nusb::Device) -> Result { let interface = device.claim_interface(0)?; @@ -242,6 +245,7 @@ impl HackRf { }); } + /// Opens `info` based on the result of a `nusb` scan. fn open(info: DeviceInfo) -> Result { let device = info.open()?; let interface = device.claim_interface(0)?; @@ -295,6 +299,7 @@ impl HackRf { Err(Error::NotFound) } + /// Resets the HackRf. pub fn reset(self) -> Result<()> { self.check_api_version(UsbVersion::from_bcd(0x0102))?; self.write_control(Request::Reset, 0, 0, &[])?; @@ -337,8 +342,8 @@ impl HackRf { pub fn start_tx(&self, config: &Config) -> Result<()> { tracing::info!("Starting tx: {config:?}"); - // NOTE: perform atomic exchange first so that we only change the transceiver mode once if - // other threads are racing to change the mode + // NOTE: perform atomic exchange first so that we only change the transceiver mode once if + // other threads are racing to change with us if let Err(actual) = self.mode.compare_exchange( Mode::Idle, Mode::Transmit, @@ -372,8 +377,8 @@ impl HackRf { /// This function will return an error if a tx or rx operation is already in progress or if an /// I/O error occurs pub fn start_rx(&self, config: &Config) -> Result<()> { - // NOTE: perform the atomic exchange first so that we only change the hackrf's state once if - // other threads are racing with us + // NOTE: perform atomic exchange first so that we only change the transceiver mode once if + // other threads are racing to change with us if let Err(actual) = self.mode.compare_exchange( Mode::Idle, Mode::Receive, @@ -402,14 +407,16 @@ impl HackRf { // NOTE: perform atomic exchange last so that we prevent other threads from racing to // start tx/rx with the delivery of our TransceiverMode::Off request // - // This means that multiple threads can race on stop_tx/stop_rx, however in the worst case - // the hackrf will receive multiple TransceiverMode::Off requests, but will always end up - // in a valid state with the transceiver disabled. A mutex or other mode variants like - // Mode::IdlePending would solve this, however quickly this begins to look like a manually implemented mutex. + // This means if multiple threads call stop_tx/stop_rx concurrently the hackrf may receive + // multiple TransceiverMode::Off requests, but will always end up in a valid state with the + // transceiver disabled. + // + // Adding something like Mode::IdlePending would solve this, + // however quickly this begins to look like a manually implemented mutex. // // To keep this crate low-level and low-overhead, this solution is fine and we expect // consumers to wrap our type in an Arc and be smart enough to not enable / disable tx / rx - // from multiple threads at the same time. + // from multiple threads at the same time on a single duplex radio. self.write_control( Request::SetTransceiverMode, @@ -425,7 +432,7 @@ impl HackRf { Ordering::Relaxed, ) { return Err(Error::WrongMode { - required: Mode::Idle, + required: Mode::Transmit, actual, }); } @@ -434,8 +441,7 @@ impl HackRf { } pub fn stop_rx(&self) -> Result<()> { - // NOTE: perform atomic exchange last so that we prevent other threads from racing to - // start tx/rx with the delivery of our TransceiverMode::Off request + // NOTE: same as above - perform atomic exchange last self.write_control( Request::SetTransceiverMode, @@ -451,7 +457,7 @@ impl HackRf { Ordering::Relaxed, ) { return Err(Error::WrongMode { - required: Mode::Idle, + required: Mode::Receive, actual, }); } @@ -806,7 +812,7 @@ mod test { assert_eq!(freq_params(u64::MAX), [0xFF; 8]); } - // NOTE: make sure you can transmit on the frequency below in your country! + // NOTE: make sure you can transmit on the frequency below before enabling! // #[test] #[allow(dead_code)] fn device_states() { diff --git a/src/impls/hackrfone.rs b/src/impls/hackrfone.rs index b41bab9..993c278 100644 --- a/src/impls/hackrfone.rs +++ b/src/impls/hackrfone.rs @@ -33,10 +33,10 @@ impl HackRfOne { /// Create a Hackrf One devices pub fn open>(args: A) -> Result { let args: Args = args.try_into().or(Err(Error::ValueError))?; - log::info!("HackRfOne::open called: {args}"); if let Ok(fd) = args.get::("fd") { - log::info!("device open got special fd arg"); + log::info!("Wrapping hackrf fd={fd}"); + // SAFETY: the caller intends to pass ownership to us let fd = unsafe { OwnedFd::from_raw_fd(fd) }; let dev = nusb::Device::from_fd(fd)?; @@ -56,11 +56,11 @@ impl HackRfOne { seify_hackrfone::HackRf::open_bus(bus_number, address)? } (Err(Error::NotFound), Err(Error::NotFound)) => { - println!("Opening first hackrf device"); + log::debug!("Opening first hackrf device"); seify_hackrfone::HackRf::open_first()? } (bus_number, address) => { - println!("HackRfOne::open received invalid args: bus_number: {bus_number:?}, address: {address:?}"); + info::warn!("HackRfOne::open received invalid args: bus_number: {bus_number:?}, address: {address:?}"); return Err(Error::ValueError); } }; @@ -112,7 +112,7 @@ impl crate::RxStreamer for RxStreamer { } fn activate_at(&mut self, _time_ns: Option) -> Result<(), Error> { - // TODO(tjn): sleep precisely for `time_ns` + // TODO: sleep precisely for `time_ns` let config = self.inner.rx_config.lock().unwrap(); self.inner.dev.start_rx(&config)?; @@ -122,9 +122,8 @@ impl crate::RxStreamer for RxStreamer { } fn deactivate_at(&mut self, _time_ns: Option) -> Result<(), Error> { - // TODO(tjn): sleep precisely for `time_ns` + // TODO: sleep precisely for `time_ns` - println!("dropping stream"); let _ = self.stream.take().unwrap(); self.inner.dev.stop_rx()?; Ok(()) @@ -169,7 +168,7 @@ impl crate::TxStreamer for TxStreamer { } fn activate_at(&mut self, _time_ns: Option) -> Result<(), Error> { - // TODO(tjn): sleep precisely for `time_ns` + // TODO: sleep precisely for `time_ns` let config = self.inner.tx_config.lock().unwrap(); self.inner.dev.start_rx(&config)?; @@ -178,7 +177,7 @@ impl crate::TxStreamer for TxStreamer { } fn deactivate_at(&mut self, _time_ns: Option) -> Result<(), Error> { - // TODO(tjn): sleep precisely for `time_ns` + // TODO: sleep precisely for `time_ns` self.inner.dev.stop_tx()?; Ok(()) @@ -298,7 +297,7 @@ impl crate::DeviceTrait for HackRfOne { fn gain_elements(&self, direction: Direction, channel: usize) -> Result, Error> { if channel == 0 { - // TODO(tjn): add support for other gains (RF and baseband) + // TODO: add support for other gains (RF and baseband) // See: https://hackrf.readthedocs.io/en/latest/faq.html#what-gain-controls-are-provided-by-hackrf match direction { Direction::Tx => Ok(vec!["IF".into()]), @@ -335,17 +334,14 @@ impl crate::DeviceTrait for HackRfOne { } fn set_gain(&self, direction: Direction, channel: usize, gain: f64) -> Result<(), Error> { - println!("set_gain"); self.set_gain_element(direction, channel, "IF", gain) } fn gain(&self, direction: Direction, channel: usize) -> Result, Error> { - println!("gain"); self.gain_element(direction, channel, "IF") } fn gain_range(&self, direction: Direction, channel: usize) -> Result { - println!("gain_range"); self.gain_element_range(direction, channel, "IF") } @@ -362,7 +358,6 @@ impl crate::DeviceTrait for HackRfOne { Direction::Tx => todo!(), Direction::Rx => { let mut config = self.inner.rx_config.lock().unwrap(); - println!("setting lna_db to {gain}"); config.lna_db = gain as u16; Ok(()) } @@ -398,7 +393,7 @@ impl crate::DeviceTrait for HackRfOne { channel: usize, name: &str, ) -> Result { - // TODO(tjn): add support for other gains + // TODO: add support for other gains if channel == 0 && name == "IF" { match direction { Direction::Tx => Ok(Range::new(vec![RangeItem::Step(0.0, 47.0, 1.0)])), @@ -480,7 +475,6 @@ impl crate::DeviceTrait for HackRfOne { && name == "TUNER" { self.with_config(direction, |config| { - println!("Set frequency to {frequency}"); config.frequency_hz = frequency as u64; self.inner.dev.set_freq(frequency as u64)?; Ok(()) @@ -492,7 +486,7 @@ impl crate::DeviceTrait for HackRfOne { fn sample_rate(&self, direction: Direction, channel: usize) -> Result { // NOTE: same state for both "directions" lets hope future sdr doesnt assume there are two - // values here, should be fine since we told it were not full duplex + // values here, should be fine since we told it we're not full duplex if channel == 0 { self.with_config(direction, |config| Ok(config.sample_rate_hz as f64)) } else { From b1dd445ace9236d333569a48087159bf36843445 Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Sun, 6 Oct 2024 13:39:40 -0700 Subject: [PATCH 14/16] bring back typed line --- examples/rx_typed.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/rx_typed.rs b/examples/rx_typed.rs index 102b88b..8681585 100644 --- a/examples/rx_typed.rs +++ b/examples/rx_typed.rs @@ -20,6 +20,8 @@ pub fn main() -> Result<(), Box> { let rtl = rtlsdr::RtlSdr::open(cli.args)?; let dev = Device::from_impl(rtl); + // Get typed reference to device impl + // let _r : &seify::impls::RtlSdr = dev.impl_ref().unwrap(); dev.enable_agc(Rx, 0, true)?; dev.set_frequency(Rx, 0, 101e6)?; From 27f5153a0c346aa1690984901d4dd27a8ca0539d Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Tue, 8 Oct 2024 01:02:47 -0700 Subject: [PATCH 15/16] address feedback --- .gitmodules | 3 + Cargo.toml | 8 +- crates/hackrf-one-rs/.gitignore | 1 - crates/hackrf-one-rs/Cargo.lock | 446 ------------- crates/hackrf-one-rs/Cargo.toml | 16 - crates/hackrf-one-rs/examples/info.rs | 11 - crates/hackrf-one-rs/examples/rx.rs | 57 -- crates/hackrf-one-rs/src/lib.rs | 863 -------------------------- crates/seify-hackrfone | 1 + examples/rx_generic.rs | 5 +- src/impls/hackrfone.rs | 7 +- 11 files changed, 14 insertions(+), 1404 deletions(-) delete mode 100644 crates/hackrf-one-rs/.gitignore delete mode 100644 crates/hackrf-one-rs/Cargo.lock delete mode 100644 crates/hackrf-one-rs/Cargo.toml delete mode 100644 crates/hackrf-one-rs/examples/info.rs delete mode 100644 crates/hackrf-one-rs/examples/rx.rs delete mode 100644 crates/hackrf-one-rs/src/lib.rs create mode 160000 crates/seify-hackrfone diff --git a/.gitmodules b/.gitmodules index a219b25..549cf26 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "crates/rtl-sdr-rs"] path = crates/rtl-sdr-rs url = https://github.com/bastibl/rtl-sdr-rs.git +[submodule "crates/seify-hackrfone"] + path = crates/seify-hackrfone + url = https://github.com/MerchGuardian/seify-hackrfone.git diff --git a/Cargo.toml b/Cargo.toml index fe93251..13acac9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,10 @@ license = "Apache-2.0" repository = "https://github.com/FutureSDR/seify" [features] -default = ["soapy"] +# default = ["soapy"] +default = ["hackrfone"] rtlsdr = ["dep:seify-rtlsdr"] -hackrfone = ["dep:seify-hackrfone", "dep:nusb"] +hackrfone = ["dep:seify-hackrfone"] aaronia = ["dep:aaronia-rtsa"] aaronia_http = ["dep:ureq"] soapy = ["dep:soapysdr"] @@ -25,7 +26,6 @@ futures = "0.3" log = "0.4" nom = "7.1" num-complex = "0.4" -nusb = { git = "https://github.com/kevinmehall/nusb.git", optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.6" @@ -34,7 +34,7 @@ thiserror = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] once_cell = "1.19" seify-rtlsdr = { path = "crates/rtl-sdr-rs", version = "0.0.3", optional = true } -seify-hackrfone = { path = "crates/hackrf-one-rs", optional = true } +seify-hackrfone = { path = "crates/seify-hackrfone", version = "0.1.0", optional = true } soapysdr = { version = "0.4", optional = true } ureq = { version = "2.9", features = ["json"], optional = true } diff --git a/crates/hackrf-one-rs/.gitignore b/crates/hackrf-one-rs/.gitignore deleted file mode 100644 index ea8c4bf..0000000 --- a/crates/hackrf-one-rs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/crates/hackrf-one-rs/Cargo.lock b/crates/hackrf-one-rs/Cargo.lock deleted file mode 100644 index bc32bd0..0000000 --- a/crates/hackrf-one-rs/Cargo.lock +++ /dev/null @@ -1,446 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "anyhow" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "atomic_enum" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e1aca718ea7b89985790c94aad72d77533063fe00bc497bb79a7c2dae6a661" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "fastrand" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" - -[[package]] -name = "futures-core" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[package]] -name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-lite" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "io-kit-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" -dependencies = [ - "core-foundation-sys", - "mach2", -] - -[[package]] -name = "libc" -version = "0.2.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" - -[[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "mach2" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" -dependencies = [ - "libc", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "nusb" -version = "0.1.10" -source = "git+https://github.com/kevinmehall/nusb.git#809228038f43de2d7a636825d17439b445f612b7" -dependencies = [ - "atomic-waker", - "core-foundation", - "core-foundation-sys", - "futures-core", - "io-kit-sys", - "libc", - "log", - "once_cell", - "rustix", - "slab", - "windows-sys 0.48.0", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - -[[package]] -name = "pin-project-lite" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustix" -version = "0.38.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - -[[package]] -name = "seify-hackrfone" -version = "0.1.0" -dependencies = [ - "anyhow", - "atomic_enum", - "futures-lite", - "num-complex", - "nusb", - "thiserror", - "tracing", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "syn" -version = "2.0.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/crates/hackrf-one-rs/Cargo.toml b/crates/hackrf-one-rs/Cargo.toml deleted file mode 100644 index 31fe579..0000000 --- a/crates/hackrf-one-rs/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "seify-hackrfone" -version = "0.1.0" -edition = "2021" -authors = ["Troy Neubauer "] - -[dependencies] -num-complex = "0.4" -thiserror = "1.0.63" -atomic_enum = "0.3.0" -nusb = { git = "https://github.com/kevinmehall/nusb.git" } -futures-lite = "2.3.0" -tracing = "0.1.40" - -[dev-dependencies] -anyhow = "1.0.86" diff --git a/crates/hackrf-one-rs/examples/info.rs b/crates/hackrf-one-rs/examples/info.rs deleted file mode 100644 index 03e300a..0000000 --- a/crates/hackrf-one-rs/examples/info.rs +++ /dev/null @@ -1,11 +0,0 @@ -use anyhow::{Context, Result}; -use seify_hackrfone::HackRf; - -fn main() -> Result<()> { - let radio = HackRf::open_first().context("Failed to open Hackrf")?; - - println!("Board ID: {:?}", radio.board_id()); - println!("Version: {:?}", radio.version()); - println!("Device version: {:?}", radio.device_version()); - Ok(()) -} diff --git a/crates/hackrf-one-rs/examples/rx.rs b/crates/hackrf-one-rs/examples/rx.rs deleted file mode 100644 index 9efdf97..0000000 --- a/crates/hackrf-one-rs/examples/rx.rs +++ /dev/null @@ -1,57 +0,0 @@ -use anyhow::{Context, Result}; -use num_complex::Complex32; -use seify_hackrfone::{Config, HackRf}; -use std::time::Instant; - -fn main() -> Result<()> { - let radio = HackRf::open_first().context("Failed to open Hackrf")?; - - println!("Board ID: {:?}", radio.board_id()); - println!("Version: {:?}", radio.version()); - println!("Device version: {:?}", radio.device_version()); - - radio - .start_rx(&Config { - vga_db: 0, - txvga_db: 0, - lna_db: 0, - power_port_enable: false, - antenna_enable: false, - frequency_hz: 915_000_000, - sample_rate_hz: 2_000_000, - sample_rate_div: 0, - }) - .context("Failed to receive on hackrf")?; - - const MTU: usize = 128 * 1024; - let mut buf = vec![0u8; MTU]; - let mut samples = vec![]; - - let collect_count = 100_000_000; - let mut last_print = Instant::now(); - - while samples.len() < collect_count { - let n = radio.read(&mut buf).context("Failed to receive samples")?; - assert_eq!(n, buf.len()); - for iq in buf.chunks_exact(2) { - samples.push(Complex32::new( - (iq[0] as f32 - 127.0) / 128.0, - (iq[1] as f32 - 127.0) / 128.0, - )); - } - - if last_print.elapsed().as_millis() > 500 { - println!( - " read {} samples ({:.1}%)", - samples.len(), - samples.len() as f64 / collect_count as f64 * 100.0 - ); - last_print = Instant::now(); - } - } - println!("Collected {} samples", samples.len()); - - println!("First 100 {:#?} samples", &samples[..100]); - - Ok(()) -} diff --git a/crates/hackrf-one-rs/src/lib.rs b/crates/hackrf-one-rs/src/lib.rs deleted file mode 100644 index e4a80d1..0000000 --- a/crates/hackrf-one-rs/src/lib.rs +++ /dev/null @@ -1,863 +0,0 @@ -#![deny(unsafe_code)] - -//! HackRF One API. -//! -//! To get started take a look at [`HackRf::open_first`]. -#![cfg_attr(docsrs, feature(doc_cfg), feature(doc_auto_cfg))] -// TODO(tjn): re-enable -// #![warn(missing_docs)] - -use std::{ - sync::atomic::{AtomicU64, Ordering}, - time::Duration, -}; - -use futures_lite::future::block_on; -use nusb::{ - transfer::{ControlIn, ControlOut, ControlType, Queue, Recipient, RequestBuffer}, - DeviceInfo, -}; - -/// HackRF USB vendor ID. -const HACKRF_USB_VID: u16 = 0x1D50; -/// HackRF One USB product ID. -const HACKRF_ONE_USB_PID: u16 = 0x6089; - -#[allow(dead_code)] -#[repr(u8)] -enum Request { - SetTransceiverMode = 1, - Max2837Write = 2, - Max2837Read = 3, - Si5351CWrite = 4, - Si5351CRead = 5, - SampleRateSet = 6, - BasebandFilterBandwidthSet = 7, - Rffc5071Write = 8, - Rffc5071Read = 9, - SpiflashErase = 10, - SpiflashWrite = 11, - SpiflashRead = 12, - BoardIdRead = 14, - VersionStringRead = 15, - SetFreq = 16, - AmpEnable = 17, - BoardPartidSerialnoRead = 18, - SetLnaGain = 19, - SetVgaGain = 20, - SetTxvgaGain = 21, - AntennaEnable = 23, - SetFreqExplicit = 24, - UsbWcidVendorReq = 25, - InitSweep = 26, - OperacakeGetBoards = 27, - OperacakeSetPorts = 28, - SetHwSyncMode = 29, - Reset = 30, - OperacakeSetRanges = 31, - ClkoutEnable = 32, - SpiflashStatus = 33, - SpiflashClearStatus = 34, - OperacakeGpioTest = 35, - CpldChecksum = 36, - UiEnable = 37, -} - -#[allow(dead_code)] -#[repr(u16)] -enum TransceiverMode { - Off = 0, - Receive = 1, - Transmit = 2, - Ss = 3, - CpldUpdate = 4, - RxSweep = 5, -} - -#[atomic_enum::atomic_enum] -#[derive(PartialEq)] -pub enum Mode { - Idle = 0, - Receive, - Transmit, -} - -/// Configurable parameters on the hackrf -#[derive(Debug)] -pub struct Config { - /// Baseband gain, 0-62dB in 2dB increments (rx only) - pub vga_db: u16, - /// 0 - 47 dB in 1dB increments (tx only) - pub txvga_db: u16, - - /// Low-noise amplifier gain, in 0-40dB in 8dB increments (rx only) - // Pre baseband receive - pub lna_db: u16, - /// RF amplifier (on/off) - pub amp_enable: bool, - - /// Antenna power port control - // Power enable on antenna - pub antenna_enable: bool, - /// Frequency in hz - pub frequency_hz: u64, - pub sample_rate_hz: u32, - // TODO: provide helpers for setting this up - pub sample_rate_div: u32, -} - -impl Config { - pub fn tx_default() -> Self { - Self { - vga_db: 0, - lna_db: 0, - txvga_db: 40, - amp_enable: false, - antenna_enable: false, - frequency_hz: 908_000_000, - sample_rate_hz: 2_500_000, - sample_rate_div: 1, - } - } - - pub fn rx_default() -> Self { - Self { - vga_db: 24, - lna_db: 0, - txvga_db: 0, - amp_enable: false, - antenna_enable: false, - frequency_hz: 908_000_000, - sample_rate_hz: 2_500_000, - sample_rate_div: 1, - } - } -} - -/// HackRF One errors. -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("io")] - Io(#[from] std::io::Error), - #[error("transfer")] - Transfer(#[from] nusb::transfer::TransferError), - #[error("transfer truncated")] - TransferTruncated { - /// Actual amount of bytes transferred. - actual: usize, - /// Excepted number of bytes transferred. - expected: usize, - }, - /// An API call is not supported by your hardware. - /// - /// Try updating the firmware on your device. - #[error("no api")] - NoApi { - /// Current device version. - device: UsbVersion, - /// Minimum version required. - min: UsbVersion, - }, - #[error("{0}")] - Argument(&'static str), - #[error("Hackrf in invalid mode. Required: {required:?}, actual: {actual:?}")] - WrongMode { required: Mode, actual: Mode }, - #[error("Device not found")] - NotFound, -} - -pub type Result = std::result::Result; - -/// A three-part version consisting of major, minor, and sub minor components. -/// -/// The intended use case of `Version` is to extract meaning from the version fields in USB -/// descriptors, such as `bcdUSB` and `bcdDevice` in device descriptors. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] -// Taken from rusb::Version: https://github.com/a1ien/rusb/blob/8f8c3c6bff6a494a140da4d93dd946bf1e564d66/src/fields.rs#L142-L203 -// nusb doesnt currently have this -pub struct UsbVersion(pub u8, pub u8, pub u8); - -impl UsbVersion { - /// Extracts a version from a binary coded decimal (BCD) field. BCD fields exist in USB - /// descriptors as 16-bit integers encoding a version as `0xJJMN`, where `JJ` is the major - /// version, `M` is the minor version, and `N` is the sub minor version. For example, 2.0 is - /// encoded as `0x0200` and 1.1 is encoded as `0x0110`. - pub fn from_bcd(mut raw: u16) -> Self { - let sub_minor: u8 = (raw & 0x000F) as u8; - raw >>= 4; - - let minor: u8 = (raw & 0x000F) as u8; - raw >>= 4; - - let mut major: u8 = (raw & 0x000F) as u8; - raw >>= 4; - - major += (10 * raw) as u8; - - UsbVersion(major, minor, sub_minor) - } - - /// Returns the major version. - pub fn major(self) -> u8 { - let UsbVersion(major, _, _) = self; - major - } - - /// Returns the minor version. - pub fn minor(self) -> u8 { - let UsbVersion(_, minor, _) = self; - minor - } - - /// Returns the sub minor version. - pub fn sub_minor(self) -> u8 { - let UsbVersion(_, _, sub_minor) = self; - sub_minor - } -} - -impl std::fmt::Display for UsbVersion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}.{}.{}", self.major(), self.minor(), self.sub_minor()) - } -} - -/// HackRF One software defined radio. -pub struct HackRf { - interface: nusb::Interface, - version: UsbVersion, - mode: AtomicMode, - timeout_nanos: AtomicU64, -} - -impl HackRf { - /// Wraps an existing nusb::Device. - /// Useful on platform like Android where you are forced to use [`nusb::Device::from_fd`]. - pub fn wrap(device: nusb::Device) -> Result { - let interface = device.claim_interface(0)?; - - return Ok(HackRf { - interface, - // TODO: Actually read version, dont assume latest - version: UsbVersion::from_bcd(0x0102), - timeout_nanos: AtomicU64::new(Duration::from_millis(500).as_nanos() as u64), - mode: AtomicMode::new(Mode::Idle), - }); - } - - /// Opens `info` based on the result of a `nusb` scan. - fn open(info: DeviceInfo) -> Result { - let device = info.open()?; - let interface = device.claim_interface(0)?; - - return Ok(HackRf { - interface, - version: UsbVersion::from_bcd(info.device_version()), - timeout_nanos: AtomicU64::new(Duration::from_millis(500).as_nanos() as u64), - mode: AtomicMode::new(Mode::Idle), - }); - } - - /// Opens the first Hackrf One found via USB. - pub fn open_first() -> Result { - for device in nusb::list_devices()? { - if device.vendor_id() == HACKRF_USB_VID && device.product_id() == HACKRF_ONE_USB_PID { - match Self::open(device) { - Ok(dev) => return Ok(dev), - Err(_) => continue, - } - } - } - - Err(Error::NotFound) - } - - /// Scans the usb bus for hackrf devices, returning the pair of (bus_num, bus_addr) for each - /// device. - pub fn scan() -> Result> { - let mut res = vec![]; - for device in nusb::list_devices()? { - if device.vendor_id() == HACKRF_USB_VID && device.product_id() == HACKRF_ONE_USB_PID { - res.push((device.busnum(), device.device_address())); - } - } - Ok(res) - } - - /// Opens a hackrf with usb address `:
` - pub fn open_bus(bus_number: u8, address: u8) -> Result { - for device in nusb::list_devices()? { - if device.vendor_id() == HACKRF_USB_VID - && device.product_id() == HACKRF_ONE_USB_PID - && device.busnum() == bus_number - && device.device_address() == address - { - return Self::open(device); - } - } - - Err(Error::NotFound) - } - - /// Resets the HackRf. - pub fn reset(self) -> Result<()> { - self.check_api_version(UsbVersion::from_bcd(0x0102))?; - self.write_control(Request::Reset, 0, 0, &[])?; - - Ok(()) - } - - pub fn device_version(&self) -> UsbVersion { - self.version - } - - pub fn board_id(&self) -> Result { - let data: [u8; 1] = self.read_control(Request::BoardIdRead, 0, 0)?; - Ok(data[0]) - } - - /// Read the firmware version. - pub fn version(&self) -> Result { - let buf = block_on(self.interface.control_in(ControlIn { - control_type: ControlType::Vendor, - recipient: Recipient::Device, - request: Request::VersionStringRead as u8, - value: 0x0, - index: 0x0, - length: 64, - })) - .into_result()?; - - Ok(String::from_utf8_lossy(&buf).into()) - } - - /// Transitions the radio into transmit mode. - /// Call this function before calling [`Self::write`]. - /// - /// Previous state set via `set_xxx` functions will be overriden with the parameters set in `config`. - /// - /// # Errors - /// This function will return an error if a tx or rx operation is already in progress or if an - /// I/O error occurs - pub fn start_tx(&self, config: &Config) -> Result<()> { - tracing::info!("Starting tx: {config:?}"); - - // NOTE: perform atomic exchange first so that we only change the transceiver mode once if - // other threads are racing to change with us - if let Err(actual) = self.mode.compare_exchange( - Mode::Idle, - Mode::Transmit, - Ordering::AcqRel, - Ordering::Relaxed, - ) { - return Err(Error::WrongMode { - required: Mode::Idle, - actual, - }); - } - - self.apply_config(config)?; - - self.write_control( - Request::SetTransceiverMode, - TransceiverMode::Transmit as u16, - 0, - &[], - )?; - - Ok(()) - } - - /// Transitions the radio into receive mode. - /// Call this function before calling [`Self::read`]. - /// - /// Previous state set via `set_xxx` functions will be overriden with the parameters set in `config`. - /// - /// # Errors - /// This function will return an error if a tx or rx operation is already in progress or if an - /// I/O error occurs - pub fn start_rx(&self, config: &Config) -> Result<()> { - // NOTE: perform atomic exchange first so that we only change the transceiver mode once if - // other threads are racing to change with us - if let Err(actual) = self.mode.compare_exchange( - Mode::Idle, - Mode::Receive, - Ordering::AcqRel, - Ordering::Relaxed, - ) { - return Err(Error::WrongMode { - required: Mode::Idle, - actual, - }); - } - - self.apply_config(config)?; - - self.write_control( - Request::SetTransceiverMode, - TransceiverMode::Receive as u16, - 0, - &[], - )?; - - Ok(()) - } - - pub fn stop_tx(&self) -> Result<()> { - // NOTE: perform atomic exchange last so that we prevent other threads from racing to - // start tx/rx with the delivery of our TransceiverMode::Off request - // - // This means if multiple threads call stop_tx/stop_rx concurrently the hackrf may receive - // multiple TransceiverMode::Off requests, but will always end up in a valid state with the - // transceiver disabled. - // - // Adding something like Mode::IdlePending would solve this, - // however quickly this begins to look like a manually implemented mutex. - // - // To keep this crate low-level and low-overhead, this solution is fine and we expect - // consumers to wrap our type in an Arc and be smart enough to not enable / disable tx / rx - // from multiple threads at the same time on a single duplex radio. - - self.write_control( - Request::SetTransceiverMode, - TransceiverMode::Off as u16, - 0, - &[], - )?; - - if let Err(actual) = self.mode.compare_exchange( - Mode::Transmit, - Mode::Idle, - Ordering::AcqRel, - Ordering::Relaxed, - ) { - return Err(Error::WrongMode { - required: Mode::Transmit, - actual, - }); - } - - Ok(()) - } - - pub fn stop_rx(&self) -> Result<()> { - // NOTE: same as above - perform atomic exchange last - - self.write_control( - Request::SetTransceiverMode, - TransceiverMode::Off as u16, - 0, - &[], - )?; - - if let Err(actual) = self.mode.compare_exchange( - Mode::Receive, - Mode::Idle, - Ordering::AcqRel, - Ordering::Relaxed, - ) { - return Err(Error::WrongMode { - required: Mode::Receive, - actual, - }); - } - - Ok(()) - } - - /// Read samples from the radio. - /// - /// # Panics - /// This function panics if samples is not a multiple of 512 - pub fn read(&self, samples: &mut [u8]) -> Result { - self.ensure_mode(Mode::Receive)?; - - if samples.len() % 512 != 0 { - panic!("samples must be a multiple of 512"); - } - - const ENDPOINT: u8 = 0x81; - let buf = block_on( - self.interface - .bulk_in(ENDPOINT, RequestBuffer::new(samples.len())), - ) - .into_result()?; - samples[..buf.len()].copy_from_slice(&buf); - - Ok(buf.len()) - } - - /// Writes samples to the radio. - /// - /// # Panics - /// This function panics if samples is not a multiple of 512 - pub fn write(&self, samples: &[u8]) -> Result { - self.ensure_mode(Mode::Transmit)?; - - if samples.len() % 512 != 0 { - panic!("samples must be a multiple of 512"); - } - - const ENDPOINT: u8 = 0x02; - let buf = Vec::from(samples); - // TODO: dont allocate - let n = block_on(self.interface.bulk_out(ENDPOINT, buf)).into_result()?; - - Ok(n.actual_length()) - } - - /// Setup the device to stream samples. - pub fn start_rx_stream(&self, transfer_size: usize) -> Result { - if transfer_size % 512 != 0 { - panic!("transfer_size must be a multiple of 512"); - } - - const ENDPOINT: u8 = 0x81; - Ok(RxStream { - queue: self.interface.bulk_in_queue(ENDPOINT), - in_flight_transfers: 3, - transfer_size, - buf_pos: transfer_size, - buf: vec![0u8; transfer_size], - }) - } -} - -pub struct RxStream { - queue: Queue, - in_flight_transfers: usize, - transfer_size: usize, - buf_pos: usize, - buf: Vec, -} - -impl RxStream { - /// Read samples from the device, blocking until more are available. - pub fn read_sync(&mut self, count: usize) -> Result<&[u8]> { - let buffered_remaining = self.buf.len() - self.buf_pos; - if buffered_remaining > 0 { - let to_consume = std::cmp::min(count, buffered_remaining); - let ret = &self.buf[self.buf_pos..self.buf_pos + to_consume]; - self.buf_pos += ret.len(); - return Ok(ret); - } - - while self.queue.pending() < self.in_flight_transfers { - self.queue.submit(RequestBuffer::new(self.transfer_size)); - } - let completion = block_on(self.queue.next_complete()); - - if let Err(e) = completion.status { - return Err(e.into()); - } - - let reuse = std::mem::replace(&mut self.buf, completion.data); - self.buf_pos = 0; - - self.queue - .submit(RequestBuffer::reuse(reuse, self.transfer_size)); - - // bytes are now buffered, use tail recursion for code above to return subslice - self.read_sync(count) - } -} - -impl HackRf { - fn apply_config(&self, config: &Config) -> Result<()> { - self.set_lna_gain(config.lna_db)?; - self.set_vga_gain(config.vga_db)?; - self.set_txvga_gain(config.txvga_db)?; - self.set_freq(config.frequency_hz)?; - self.set_amp_enable(config.amp_enable)?; - self.set_antenna_enable(config.antenna_enable)?; - self.set_sample_rate(config.sample_rate_hz, config.sample_rate_div)?; - - Ok(()) - } - - fn ensure_mode(&self, expected: Mode) -> Result<()> { - let actual = self.mode.load(Ordering::Acquire); - if actual != expected { - return Err(Error::WrongMode { - required: expected, - actual, - }); - } - Ok(()) - } - - pub fn timeout(&self) -> Duration { - Duration::from_nanos(self.timeout_nanos.load(Ordering::Acquire)) - } - - fn read_control( - &self, - request: Request, - value: u16, - index: u16, - ) -> Result<[u8; N]> { - let mut res: [u8; N] = [0; N]; - let buf = block_on(self.interface.control_in(ControlIn { - control_type: ControlType::Vendor, - recipient: Recipient::Device, - request: request as u8, - value, - index, - length: N as u16, - })) - .into_result()?; - - if buf.len() != N { - return Err(Error::TransferTruncated { - actual: buf.len(), - expected: N, - }); - } - - res.copy_from_slice(&buf); - Ok(res) - } - - fn write_control(&self, request: Request, value: u16, index: u16, buf: &[u8]) -> Result<()> { - let out = block_on(self.interface.control_out(ControlOut { - control_type: ControlType::Vendor, - recipient: Recipient::Device, - request: request as u8, - value, - index, - data: buf, - })) - .into_result()?; - - if out.actual_length() != buf.len() { - Err(Error::TransferTruncated { - actual: out.actual_length(), - expected: buf.len(), - }) - } else { - Ok(()) - } - } - - fn check_api_version(&self, min: UsbVersion) -> Result<()> { - fn version_to_u32(v: UsbVersion) -> u32 { - ((v.major() as u32) << 16) | ((v.minor() as u32) << 8) | (v.sub_minor() as u32) - } - - if version_to_u32(self.version) >= version_to_u32(min) { - Ok(()) - } else { - Err(Error::NoApi { - device: self.version, - min, - }) - } - } - - /// Set the center frequency. - pub fn set_freq(&self, hz: u64) -> Result<()> { - let buf: [u8; 8] = freq_params(hz); - self.write_control(Request::SetFreq, 0, 0, &buf) - } - - /// Enable the RX/TX RF amplifier. - /// - /// In GNU radio this is used as the RF gain, where a value of 0 dB is off, - /// and a value of 14 dB is on. - pub fn set_amp_enable(&self, enable: bool) -> Result<()> { - self.write_control(Request::AmpEnable, enable.into(), 0, &[]) - } - - /// Set the baseband filter bandwidth. - /// - /// This is automatically set when the sample rate is changed with - /// [`Self::set_sample_rate`]. - pub fn set_baseband_filter_bandwidth(&self, hz: u32) -> Result<()> { - self.write_control( - Request::BasebandFilterBandwidthSet, - (hz & 0xFFFF) as u16, - (hz >> 16) as u16, - &[], - ) - } - - /// Set the sample rate. - /// - /// For anti-aliasing, the baseband filter bandwidth is automatically set to - /// the widest available setting that is no more than 75% of the sample rate. - /// This happens every time the sample rate is set. - /// If you want to override the baseband filter selection, you must do so - /// after setting the sample rate. - /// - /// Limits are 8MHz - 20MHz. - /// Preferred rates are 8, 10, 12.5, 16, 20MHz due to less jitter. - pub fn set_sample_rate(&self, hz: u32, div: u32) -> Result<()> { - let hz: u32 = hz.to_le(); - let div: u32 = div.to_le(); - let buf: [u8; 8] = [ - (hz & 0xFF) as u8, - ((hz >> 8) & 0xFF) as u8, - ((hz >> 16) & 0xFF) as u8, - ((hz >> 24) & 0xFF) as u8, - (div & 0xFF) as u8, - ((div >> 8) & 0xFF) as u8, - ((div >> 16) & 0xFF) as u8, - ((div >> 24) & 0xFF) as u8, - ]; - self.write_control(Request::SampleRateSet, 0, 0, &buf)?; - self.set_baseband_filter_bandwidth((0.75 * (hz as f32) / (div as f32)) as u32) - } - - /// Set the LNA (low noise amplifier) gain. - /// - /// Range 0 to 40dB in 8dB steps. - /// - /// This is also known as the IF gain. - pub fn set_lna_gain(&self, gain: u16) -> Result<()> { - if gain > 40 { - Err(Error::Argument("lna gain must be less than 40")) - } else { - let buf: [u8; 1] = self.read_control(Request::SetLnaGain, 0, gain & !0x07)?; - if buf[0] == 0 { - panic!("Unexpected return value from read_control(SetLnaGain)"); - } else { - Ok(()) - } - } - } - - /// Set the VGA (variable gain amplifier) gain. - /// - /// Range 0 to 62dB in 2dB steps. - /// - /// This is also known as the baseband (BB) gain. - pub fn set_vga_gain(&self, gain: u16) -> Result<()> { - if gain > 62 || gain % 2 == 1 { - Err(Error::Argument( - "gain parameter out of range. must be even and less than or equal to 62", - )) - } else { - let buf: [u8; 1] = self.read_control(Request::SetVgaGain, 0, gain & !0b1)?; - if buf[0] == 0 { - panic!("What is this return value?") - } else { - Ok(()) - } - } - } - - /// Set the transmit VGA gain. - /// - /// Range 0 to 47dB in 1db steps. - pub fn set_txvga_gain(&self, gain: u16) -> Result<()> { - if gain > 47 { - Err(Error::Argument("gain parameter out of range. max is 47")) - } else { - let buf: [u8; 1] = self.read_control(Request::SetTxvgaGain, 0, gain)?; - if buf[0] == 0 { - panic!("What is this return value?") - } else { - Ok(()) - } - } - } - - /// Antenna power port control. Dhruv's guess: is this DC bias? - /// - /// The source docs are a little lacking in terms of explanations here. - pub fn set_antenna_enable(&self, value: bool) -> Result<()> { - let value = if value { 1 } else { 0 }; - self.write_control(Request::AntennaEnable, value, 0, &[]) - } -} - -// Helper for set_freq -fn freq_params(hz: u64) -> [u8; 8] { - const MHZ: u64 = 1_000_000; - - let l_freq_mhz: u32 = u32::try_from(hz / MHZ).unwrap_or(u32::MAX).to_le(); - let l_freq_hz: u32 = u32::try_from(hz - u64::from(l_freq_mhz) * MHZ) - .unwrap_or(u32::MAX) - .to_le(); - - [ - (l_freq_mhz & 0xFF) as u8, - ((l_freq_mhz >> 8) & 0xFF) as u8, - ((l_freq_mhz >> 16) & 0xFF) as u8, - ((l_freq_mhz >> 24) & 0xFF) as u8, - (l_freq_hz & 0xFF) as u8, - ((l_freq_hz >> 8) & 0xFF) as u8, - ((l_freq_hz >> 16) & 0xFF) as u8, - ((l_freq_hz >> 24) & 0xFF) as u8, - ] -} - -#[cfg(test)] -mod test { - use std::time::Duration; - - use super::*; - - #[test] - fn test_freq_params() { - assert_eq!(freq_params(915_000_000), [0x93, 0x03, 0, 0, 0, 0, 0, 0]); - assert_eq!(freq_params(915_000_001), [0x93, 0x03, 0, 0, 1, 0, 0, 0]); - assert_eq!( - freq_params(123456789), - [0x7B, 0, 0, 0, 0x55, 0xF8, 0x06, 0x00] - ); - - assert_eq!(freq_params(0), [0; 8]); - - assert_eq!(freq_params(u64::MAX), [0xFF; 8]); - } - - // NOTE: make sure you can transmit on the frequency below before enabling! - // #[test] - #[allow(dead_code)] - fn device_states() { - let radio = HackRf::open_first().expect("Failed to open hackrf"); - - radio - .start_tx(&Config { - vga_db: 0, - txvga_db: 0, - lna_db: 0, - amp_enable: false, - antenna_enable: false, - frequency_hz: 915_000_000, - sample_rate_hz: 2_000_000, - sample_rate_div: 1, - }) - .unwrap(); - std::thread::sleep(Duration::from_millis(50)); - - radio.stop_tx().unwrap(); - assert!(radio.stop_tx().is_err()); - assert!(radio.stop_tx().is_err()); - assert!(radio.stop_rx().is_err()); - assert!(radio.stop_rx().is_err()); - - std::thread::sleep(Duration::from_millis(50)); - - radio - .start_rx(&Config { - vga_db: 0, - txvga_db: 0, - lna_db: 0, - amp_enable: false, - antenna_enable: false, - frequency_hz: 915_000_000, - sample_rate_hz: 2_000_000, - sample_rate_div: 1, - }) - .unwrap(); - std::thread::sleep(Duration::from_millis(50)); - - radio.stop_rx().unwrap(); - assert!(radio.stop_rx().is_err()); - assert!(radio.stop_rx().is_err()); - assert!(radio.stop_tx().is_err()); - assert!(radio.stop_tx().is_err()); - } -} diff --git a/crates/seify-hackrfone b/crates/seify-hackrfone new file mode 160000 index 0000000..9f08ce5 --- /dev/null +++ b/crates/seify-hackrfone @@ -0,0 +1 @@ +Subproject commit 9f08ce5cf59492f484eedb02ae1fcc1af354f196 diff --git a/examples/rx_generic.rs b/examples/rx_generic.rs index a16dd78..7caddf6 100644 --- a/examples/rx_generic.rs +++ b/examples/rx_generic.rs @@ -21,7 +21,10 @@ pub fn main() -> Result<(), Box> { // Get typed reference to device impl // let r: &seify::impls::RtlSdr = dev.impl_ref().unwrap(); - dev.enable_agc(Rx, 0, true)?; + // HackRf doesnt support agc + if dev.supports_agc(Rx, 0)? { + dev.enable_agc(Rx, 0, true)?; + } dev.set_frequency(Rx, 0, 927e6)?; dev.set_sample_rate(Rx, 0, 3.2e6)?; diff --git a/src/impls/hackrfone.rs b/src/impls/hackrfone.rs index 993c278..d9d50a0 100644 --- a/src/impls/hackrfone.rs +++ b/src/impls/hackrfone.rs @@ -35,14 +35,11 @@ impl HackRfOne { let args: Args = args.try_into().or(Err(Error::ValueError))?; if let Ok(fd) = args.get::("fd") { - log::info!("Wrapping hackrf fd={fd}"); - // SAFETY: the caller intends to pass ownership to us let fd = unsafe { OwnedFd::from_raw_fd(fd) }; - let dev = nusb::Device::from_fd(fd)?; return Ok(Self { inner: Arc::new(HackRfInner { - dev: seify_hackrfone::HackRf::wrap(dev)?, + dev: seify_hackrfone::HackRf::from_fd(fd)?, tx_config: Mutex::new(Config::tx_default()), rx_config: Mutex::new(Config::rx_default()), }), @@ -60,7 +57,7 @@ impl HackRfOne { seify_hackrfone::HackRf::open_first()? } (bus_number, address) => { - info::warn!("HackRfOne::open received invalid args: bus_number: {bus_number:?}, address: {address:?}"); + log::warn!("HackRfOne::open received invalid args: bus_number: {bus_number:?}, address: {address:?}"); return Err(Error::ValueError); } }; From 09eeedd971dcf31c54e701d4edddb97f165de0e5 Mon Sep 17 00:00:00 2001 From: Troy Neubauer Date: Tue, 8 Oct 2024 01:03:12 -0700 Subject: [PATCH 16/16] clippy --- src/impls/hackrfone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/impls/hackrfone.rs b/src/impls/hackrfone.rs index d9d50a0..c219865 100644 --- a/src/impls/hackrfone.rs +++ b/src/impls/hackrfone.rs @@ -133,7 +133,7 @@ impl crate::RxStreamer for RxStreamer { ) -> Result { debug_assert_eq!(buffers.len(), 1); - if buffers[0].len() == 0 { + if buffers[0].is_empty() { return Ok(0); } let buf = self.stream.as_mut().unwrap().read_sync(buffers[0].len())?;