From d3e6bdadc70c3fea36233d10fc8cb121af44cb48 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 2 Jan 2026 14:25:31 -0500 Subject: [PATCH 1/3] Start using nusb and fix typo in TransceiverMode --- Cargo.toml | 2 +- README.md | 7 +- src/lib.rs | 203 +++++++++++++++++++++++++++++++---------------------- 3 files changed, 126 insertions(+), 86 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0c1a4d4..8c947c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ categories = ["hardware-support"] homepage = "https://github.com/newAM/hackrfone" [dependencies] -rusb = "~0.9" +nusb = "0.2.1" [dependencies.num-complex] version = "~0.4" diff --git a/README.md b/README.md index 4720b21..1f4d4b0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ This is a rust API for the [HackRF One] software defined radio. This is not a wrapper around `libhackrf`, this is a re-implementation of -`libhackrf` in rust, using the [rusb] `libusb` wrappers. +`libhackrf` in rust, using the [nusb] user-space rust library. +See [nusb-linux-setup] for setup of user-space USB access. + This is currently in an **experimental** state, and it is incomplete. For full feature support use the official `libhackrf` C library. @@ -16,5 +18,6 @@ For full feature support use the official `libhackrf` C library. This is tested only on Linux, but it will likely work on other platforms where `libhackrf` works. -[rusb]: https://github.com/a1ien/rusb +[nusb]: https://github.com/kevinmehall/nusb +[nusb-linux-setup]: https://docs.rs/nusb/latest/nusb/index.html#linux [HackRF One]: https://greatscottgadgets.com/hackrf/one/ diff --git a/src/lib.rs b/src/lib.rs index 4d18041..cc46a03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,9 +4,12 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![warn(missing_docs)] -pub use rusb; +use std::io; +use std::io::Read; +pub use nusb; -use rusb::{Direction, GlobalContext, Recipient, RequestType, UsbContext, Version, request_type}; +use nusb::{list_devices, Interface, MaybeFuture}; +use nusb::transfer::{ControlIn, ControlOut, ControlType, Recipient, Direction, Bulk, In}; use std::time::Duration; #[cfg(feature = "num-complex")] @@ -65,7 +68,7 @@ impl From for u8 { #[allow(dead_code)] #[repr(u8)] -enum TranscieverMode { +enum TransceiverMode { Off = 0, Receive = 1, Transmit = 2, @@ -74,23 +77,27 @@ enum TranscieverMode { RxSweep = 5, } -impl From for u8 { - fn from(tm: TranscieverMode) -> Self { +impl From for u8 { + fn from(tm: TransceiverMode) -> Self { tm as u8 } } -impl From for u16 { - fn from(tm: TranscieverMode) -> Self { +impl From for u16 { + fn from(tm: TransceiverMode) -> Self { tm as u16 } } /// HackRF One errors. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug)] pub enum Error { /// USB error. - Usb(rusb::Error), + Usb(nusb::Error), + /// USB Connection Errors + UsbTransfer(nusb::transfer::TransferError), + /// IO Error + IO(io::Error), /// Failed to transfer all bytes in a control transfer. CtrlTransfer { /// Control transfer direction. @@ -113,18 +120,56 @@ pub enum Error { Argument, } -impl From for Error { - fn from(e: rusb::Error) -> Self { +impl From for Error { + fn from(e: nusb::Error) -> Self { Error::Usb(e) } } +impl From for Error { + fn from(e: nusb::transfer::TransferError) -> Self { Error::UsbTransfer(e)} +} + +impl From for Error { + fn from(e: io::Error) -> Self { Error::IO(e)} +} + impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{self:?}") } } +#[derive(Clone, Debug)] +/// Version used to denote the parts of BCD +pub struct Version { + /// Major version XX.0.0 + pub major: u8, + /// Minor version 00.X.0 + pub minor: u8, + /// Sub Minor version 00.0.X + pub sub_minor: u8, +} + +impl Version { + fn from_bcd(raw: u16) -> Self { + // 0xJJMN JJ major, M minor, N sub-minor + // Binary Coded Decimal + let major0: u8 = ((raw & 0xF000) >> 12) as u8; + let major1: u8 = ((raw & 0x0F00) >> 8) as u8; + + let minor: u8 = ((raw & 0x00F0) >> 4) as u8 ; + + let sub_minor: u8 = (raw & 0x000F) as u8; + + Self { + major: (major0 * 10) + major1, + minor, + sub_minor, + } + } +} + impl std::error::Error for Error {} /// Typestate for RX mode. @@ -137,11 +182,12 @@ pub struct UnknownMode; /// HackRF One software defined radio. pub struct HackRfOne { - dh: rusb::DeviceHandle, - desc: rusb::DeviceDescriptor, + dh: nusb::Device, + desc: nusb::descriptors::DeviceDescriptor, + interface: Interface, #[allow(dead_code)] mode: MODE, - to: Duration, + timeout: Duration, } impl HackRfOne { @@ -154,27 +200,23 @@ impl HackRfOne { /// /// let mut radio: HackRfOne = HackRfOne::new().unwrap(); /// ``` + #[must_use] pub fn new() -> Option> { - let ctx: GlobalContext = GlobalContext {}; - 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() { + let Ok(devices) = list_devices().wait() else { return None }; + + for device in devices { + if device.vendor_id() == HACKRF_USB_VID && device.product_id() == HACKRF_ONE_USB_PID { + match device.open().wait() { Ok(handle) => { + let Ok(interface) = handle.claim_interface(0).wait() + else { return None }; + return Some(HackRfOne { + desc: handle.device_descriptor(), dh: handle, - desc, + interface, mode: UnknownMode, - to: Duration::from_secs(1), + timeout: Duration::from_secs(1), }); } Err(_) => continue, @@ -193,23 +235,23 @@ impl HackRfOne { value: u16, index: u16, ) -> Result<[u8; N], Error> { - let mut buf: [u8; N] = [0; N]; - let n: usize = self.dh.read_control( - request_type(Direction::In, RequestType::Vendor, Recipient::Device), - request.into(), + let buf = self.interface.control_in(ControlIn { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: request.into(), value, index, - &mut buf, - self.to, - )?; - if n != buf.len() { + length: N as u16, + }, self.timeout).wait()?; + + if N == buf.len() { + Ok(<[u8; N]>::try_from(buf).expect("This should never happen")) + } else { Err(Error::CtrlTransfer { dir: Direction::In, - actual: n, - expected: buf.len(), + actual: buf.len(), + expected: N, }) - } else { - Ok(buf) } } @@ -220,33 +262,26 @@ impl HackRfOne { index: u16, buf: &[u8], ) -> Result<(), Error> { - let n: usize = self.dh.write_control( - request_type(Direction::Out, RequestType::Vendor, Recipient::Device), - request.into(), + self.interface.control_out(ControlOut { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: request.into(), value, index, - buf, - self.to, - )?; - if n != buf.len() { - Err(Error::CtrlTransfer { - dir: Direction::Out, - actual: n, - expected: buf.len(), - }) - } else { - Ok(()) - } + data: &buf, + }, self.timeout).wait()?; + + Ok(()) } fn check_api_version(&self, min: Version) -> Result<(), Error> { - fn version_to_u32(v: Version) -> u32 { - ((v.major() as u32) << 16) | ((v.minor() as u32) << 8) | (v.sub_minor() as u32) + fn version_to_u32(v: &Version) -> u32 { + (u32::from(v.major) << 16) | (u32::from(v.minor) << 8) | u32::from(v.sub_minor) } let v: Version = self.device_version(); - let v_cmp: u32 = version_to_u32(v); - let min_cmp: u32 = version_to_u32(min); + let v_cmp: u32 = version_to_u32(&v); + let min_cmp: u32 = version_to_u32(&min); if v_cmp >= min_cmp { Ok(()) @@ -266,10 +301,10 @@ impl HackRfOne { /// use hackrfone::{HackRfOne, UnknownMode, rusb}; /// /// let mut radio: HackRfOne = HackRfOne::new().unwrap(); - /// assert_eq!(radio.device_version(), rusb::Version(1, 0, 4)); + /// assert_eq!(radio.device_version(), crate::Version(1, 0, 4)); /// ``` pub fn device_version(&self) -> Version { - self.desc.device_version() + Version::from_bcd(self.desc.device_version()) } /// Set the timeout for USB transfers. @@ -286,7 +321,7 @@ impl HackRfOne { /// radio.set_timeout(Duration::from_millis(100)) /// ``` pub fn set_timeout(&mut self, duration: Duration) { - self.to = duration; + self.timeout = duration; } /// Read the board ID. @@ -317,16 +352,16 @@ impl HackRfOne { /// # Ok::<(), hackrfone::Error>(()) /// ``` pub fn version(&self) -> Result { - let mut buf: [u8; 16] = [0; 16]; - let n: usize = self.dh.read_control( - request_type(Direction::In, RequestType::Vendor, Recipient::Device), - Request::VersionStringRead.into(), - 0, - 0, - &mut buf, - self.to, - )?; - Ok(String::from_utf8_lossy(&buf[0..n]).into()) + let buf = self.interface.control_in(ControlIn { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: Request::VersionStringRead.into(), + value: 0, + index: 0, + length: 16, + }, self.timeout).wait()?; + + Ok(String::from_utf8_lossy(&buf[0..16]).into()) } /// Set the center frequency. @@ -547,12 +582,13 @@ impl HackRfOne { Ok(HackRfOne { dh: self.dh, desc: self.desc, + interface: self.interface, mode: UnknownMode, - to: self.to, + timeout: self.timeout, }) } - fn set_transceiver_mode(&mut self, mode: TranscieverMode) -> Result<(), Error> { + fn set_transceiver_mode(&mut self, mode: TransceiverMode) -> Result<(), Error> { self.write_control(Request::SetTransceiverMode, mode.into(), 0, &[]) } @@ -568,13 +604,13 @@ impl HackRfOne { /// # Ok::<(), hackrfone::Error>(()) /// ``` pub fn into_rx_mode(mut self) -> Result, Error> { - self.set_transceiver_mode(TranscieverMode::Receive)?; - self.dh.claim_interface(0)?; + self.set_transceiver_mode(TransceiverMode::Receive)?; Ok(HackRfOne { dh: self.dh, desc: self.desc, + interface: self.interface, mode: RxMode, - to: self.to, + timeout: self.timeout, }) } } @@ -608,7 +644,8 @@ impl HackRfOne { const ENDPOINT: u8 = 0x81; const MTU: usize = 128 * 1024; let mut buf: Vec = vec![0; MTU]; - let n: usize = self.dh.read_bulk(ENDPOINT, &mut buf, self.to)?; + let mut reader = self.interface.endpoint::(ENDPOINT)?.reader(MTU); + let n = reader.read(buf.as_mut_slice())?; buf.truncate(n); Ok(buf) } @@ -627,13 +664,13 @@ impl HackRfOne { /// # Ok::<(), hackrfone::Error>(()) /// ``` pub fn stop_rx(mut self) -> Result, Error> { - self.dh.release_interface(0)?; - self.set_transceiver_mode(TranscieverMode::Off)?; + self.set_transceiver_mode(TransceiverMode::Off)?; Ok(HackRfOne { dh: self.dh, desc: self.desc, + interface: self.interface, mode: UnknownMode, - to: self.to, + timeout: self.timeout, }) } } From 76265c696507ad9637daf84dfbf581a54c1a0ab8 Mon Sep 17 00:00:00 2001 From: nathan Date: Sat, 3 Jan 2026 09:09:32 -0500 Subject: [PATCH 2/3] Fix ci/cd warnings/errors --- src/lib.rs | 144 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 108 insertions(+), 36 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index cc46a03..e453c8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,12 +4,12 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![warn(missing_docs)] +pub use nusb; use std::io; use std::io::Read; -pub use nusb; -use nusb::{list_devices, Interface, MaybeFuture}; -use nusb::transfer::{ControlIn, ControlOut, ControlType, Recipient, Direction, Bulk, In}; +use nusb::transfer::{Bulk, ControlIn, ControlOut, ControlType, Direction, In, Recipient}; +use nusb::{Interface, MaybeFuture, list_devices}; use std::time::Duration; #[cfg(feature = "num-complex")] @@ -127,11 +127,15 @@ impl From for Error { } impl From for Error { - fn from(e: nusb::transfer::TransferError) -> Self { Error::UsbTransfer(e)} + fn from(e: nusb::transfer::TransferError) -> Self { + Error::UsbTransfer(e) + } } impl From for Error { - fn from(e: io::Error) -> Self { Error::IO(e)} + fn from(e: io::Error) -> Self { + Error::IO(e) + } } impl std::fmt::Display for Error { @@ -140,7 +144,7 @@ impl std::fmt::Display for Error { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] /// Version used to denote the parts of BCD pub struct Version { /// Major version XX.0.0 @@ -158,7 +162,7 @@ impl Version { let major0: u8 = ((raw & 0xF000) >> 12) as u8; let major1: u8 = ((raw & 0x0F00) >> 8) as u8; - let minor: u8 = ((raw & 0x00F0) >> 4) as u8 ; + let minor: u8 = ((raw & 0x00F0) >> 4) as u8; let sub_minor: u8 = (raw & 0x000F) as u8; @@ -170,6 +174,47 @@ impl Version { } } +#[cfg(test)] +mod version_bcd { + use super::Version; + + #[test] + fn from_bcd() { + assert_eq!( + Version::from_bcd(0x1234), + Version { + major: 12, + minor: 3, + sub_minor: 4 + } + ); + assert_eq!( + Version::from_bcd(0x4321), + Version { + major: 43, + minor: 2, + sub_minor: 1 + } + ); + assert_eq!( + Version::from_bcd(0x0200), + Version { + major: 2, + minor: 0, + sub_minor: 0 + } + ); + assert_eq!( + Version::from_bcd(0x0110), + Version { + major: 1, + minor: 1, + sub_minor: 0 + } + ); + } +} + impl std::error::Error for Error {} /// Typestate for RX mode. @@ -202,14 +247,17 @@ impl HackRfOne { /// ``` #[must_use] pub fn new() -> Option> { - let Ok(devices) = list_devices().wait() else { return None }; + let Ok(devices) = list_devices().wait() else { + return None; + }; for device in devices { if device.vendor_id() == HACKRF_USB_VID && device.product_id() == HACKRF_ONE_USB_PID { match device.open().wait() { Ok(handle) => { - let Ok(interface) = handle.claim_interface(0).wait() - else { return None }; + let Ok(interface) = handle.claim_interface(0).wait() else { + return None; + }; return Some(HackRfOne { desc: handle.device_descriptor(), @@ -235,14 +283,20 @@ impl HackRfOne { value: u16, index: u16, ) -> Result<[u8; N], Error> { - let buf = self.interface.control_in(ControlIn { - control_type: ControlType::Vendor, - recipient: Recipient::Device, - request: request.into(), - value, - index, - length: N as u16, - }, self.timeout).wait()?; + let buf = self + .interface + .control_in( + ControlIn { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: request.into(), + value, + index, + length: N as u16, + }, + self.timeout, + ) + .wait()?; if N == buf.len() { Ok(<[u8; N]>::try_from(buf).expect("This should never happen")) @@ -262,14 +316,19 @@ impl HackRfOne { index: u16, buf: &[u8], ) -> Result<(), Error> { - self.interface.control_out(ControlOut { - control_type: ControlType::Vendor, - recipient: Recipient::Device, - request: request.into(), - value, - index, - data: &buf, - }, self.timeout).wait()?; + self.interface + .control_out( + ControlOut { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: request.into(), + value, + index, + data: buf, + }, + self.timeout, + ) + .wait()?; Ok(()) } @@ -298,10 +357,17 @@ impl HackRfOne { /// # Example /// /// ```no_run - /// use hackrfone::{HackRfOne, UnknownMode, rusb}; + /// use hackrfone::{HackRfOne, UnknownMode, Version}; /// /// let mut radio: HackRfOne = HackRfOne::new().unwrap(); - /// assert_eq!(radio.device_version(), crate::Version(1, 0, 4)); + /// assert_eq!( + /// radio.device_version(), + /// Version { + /// major: 1, + /// minor: 0, + /// sub_minor: 4 + /// } + /// ); /// ``` pub fn device_version(&self) -> Version { Version::from_bcd(self.desc.device_version()) @@ -352,14 +418,20 @@ impl HackRfOne { /// # Ok::<(), hackrfone::Error>(()) /// ``` pub fn version(&self) -> Result { - let buf = self.interface.control_in(ControlIn { - control_type: ControlType::Vendor, - recipient: Recipient::Device, - request: Request::VersionStringRead.into(), - value: 0, - index: 0, - length: 16, - }, self.timeout).wait()?; + let buf = self + .interface + .control_in( + ControlIn { + control_type: ControlType::Vendor, + recipient: Recipient::Device, + request: Request::VersionStringRead.into(), + value: 0, + index: 0, + length: 16, + }, + self.timeout, + ) + .wait()?; Ok(String::from_utf8_lossy(&buf[0..16]).into()) } From 080bc7aae34ad9a1c1f693d1c8a8b674b2a183c9 Mon Sep 17 00:00:00 2001 From: nathan Date: Sat, 3 Jan 2026 11:28:35 -0500 Subject: [PATCH 3/3] Add note on user-space access and fix issue found during test. --- README.md | 17 +++++++++++++++-- src/lib.rs | 5 ++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1f4d4b0..9f86856 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ This is a rust API for the [HackRF One] software defined radio. This is not a wrapper around `libhackrf`, this is a re-implementation of -`libhackrf` in rust, using the [nusb] user-space rust library. -See [nusb-linux-setup] for setup of user-space USB access. +`libhackrf` in rust, using the [nusb] user-space rust library. This is currently in an **experimental** state, and it is incomplete. @@ -18,6 +17,20 @@ For full feature support use the official `libhackrf` C library. This is tested only on Linux, but it will likely work on other platforms where `libhackrf` works. +## USB user-space access +Adapted from [nusb-linux-setup]. +This is only needed if you want to run as non-root user, although the initial setup does require sudo. +1. ```shell + # Create a `udev` rule + echo 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="6089", MODE="0660", GROUP="hackrfone"' | sudo tee /etc/udev/rules.d/99-hackrfone-usb.rules + ``` +2. ```shell + # Add current user to group + sudo groupadd hackrfone + sudo usermod -aG hackrfone $USER + ``` +3. Best to restart the system now so new group shows up and device is mapped correctly + [nusb]: https://github.com/kevinmehall/nusb [nusb-linux-setup]: https://docs.rs/nusb/latest/nusb/index.html#linux [HackRF One]: https://greatscottgadgets.com/hackrf/one/ diff --git a/src/lib.rs b/src/lib.rs index e453c8b..e3ff897 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -255,7 +255,7 @@ impl HackRfOne { if device.vendor_id() == HACKRF_USB_VID && device.product_id() == HACKRF_ONE_USB_PID { match device.open().wait() { Ok(handle) => { - let Ok(interface) = handle.claim_interface(0).wait() else { + let Ok(interface) = handle.detach_and_claim_interface(0).wait() else { return None; }; @@ -432,8 +432,7 @@ impl HackRfOne { self.timeout, ) .wait()?; - - Ok(String::from_utf8_lossy(&buf[0..16]).into()) + Ok(String::from_utf8_lossy(&buf).into()) } /// Set the center frequency.