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 7486176..13acac9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +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"] aaronia = ["dep:aaronia-rtsa"] aaronia_http = ["dep:ureq"] soapy = ["dep:soapysdr"] @@ -32,6 +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/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/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/device.rs b/src/device.rs index 95e8538..8938767 100644 --- a/src/device.rs +++ b/src/device.rs @@ -301,6 +301,25 @@ 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 new file mode 100644 index 0000000..c219865 --- /dev/null +++ b/src/impls/hackrfone.rs @@ -0,0 +1,526 @@ +use std::{ + os::fd::{FromRawFd, OwnedFd}, + sync::{Arc, Mutex}, +}; + +use num_complex::Complex32; +use seify_hackrfone::Config; + +use crate::{Args, Direction, Error, Range, RangeItem}; + +pub struct HackRfOne { + inner: Arc, +} + +const MTU: usize = 64 * 1024; + +impl HackRfOne { + pub fn probe(_args: &Args) -> Result, Error> { + let mut devs = vec![]; + for (bus_number, address) in seify_hackrfone::HackRf::scan()? { + log::debug!("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))?; + + if let Ok(fd) = args.get::("fd") { + let fd = unsafe { OwnedFd::from_raw_fd(fd) }; + + return Ok(Self { + inner: Arc::new(HackRfInner { + dev: seify_hackrfone::HackRf::from_fd(fd)?, + 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"); + let dev = match (bus_number, address) { + (Ok(bus_number), Ok(address)) => { + seify_hackrfone::HackRf::open_bus(bus_number, address)? + } + (Err(Error::NotFound), Err(Error::NotFound)) => { + log::debug!("Opening first hackrf device"); + seify_hackrfone::HackRf::open_first()? + } + (bus_number, address) => { + log::warn!("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, + stream: Option, +} + +impl RxStreamer { + fn new(inner: Arc) -> Self { + Self { + inner, + stream: None, + } + } +} + +impl crate::RxStreamer for RxStreamer { + fn mtu(&self) -> Result { + Ok(MTU) + } + + fn activate_at(&mut self, _time_ns: Option) -> Result<(), Error> { + // TODO: sleep precisely for `time_ns` + 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: sleep precisely for `time_ns` + + let _ = self.stream.take().unwrap(); + self.inner.dev.stop_rx()?; + Ok(()) + } + + fn read( + &mut self, + buffers: &mut [&mut [num_complex::Complex32]], + _timeout_us: i64, + ) -> Result { + debug_assert_eq!(buffers.len(), 1); + + if buffers[0].is_empty() { + return Ok(0); + } + let buf = self.stream.as_mut().unwrap().read_sync(buffers[0].len())?; + + let samples = buf.len() / 2; + for i in 0..samples { + buffers[0][i] = Complex32::new( + (buf[i * 2] as f32 - 127.0) / 128.0, + (buf[i * 2 + 1] as f32 - 127.0) / 128.0, + ); + } + Ok(samples) + } +} + +pub struct TxStreamer { + inner: Arc, +} + +impl TxStreamer { + fn new(inner: Arc) -> Self { + Self { inner } + } +} + +impl crate::TxStreamer for TxStreamer { + fn mtu(&self) -> Result { + Ok(MTU) + } + + fn activate_at(&mut self, _time_ns: Option) -> Result<(), Error> { + // TODO: sleep precisely for `time_ns` + + 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> { + // TODO: 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, + ) -> Result { + debug_assert_eq!(buffers.len(), 1); + todo!(); + + // self.inner.dev.write(samples) + } + + fn write_all( + &mut self, + buffers: &[&[num_complex::Complex32]], + _at_ns: Option, + _end_burst: bool, + _timeout_us: i64, + ) -> Result<(), Error> { + 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 crate::DeviceTrait for HackRfOne { + type RxStreamer = RxStreamer; + + type TxStreamer = TxStreamer; + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn driver(&self) -> crate::Driver { + crate::Driver::HackRf + } + + fn id(&self) -> Result { + Ok(self.inner.dev.board_id()?.to_string()) + } + + fn info(&self) -> Result { + let mut args = crate::Args::default(); + args.set("firmware version", self.inner.dev.version()?); + Ok(args) + } + + fn num_channels(&self, _: crate::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(Arc::clone(&self.inner))) + } + } + + 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> { + self.antenna(direction, channel).map(|a| vec![a]) + } + + fn antenna(&self, direction: Direction, channel: usize) -> Result { + if channel == 0 { + Ok(match direction { + Direction::Rx => "RX".to_string(), + Direction::Tx => "TX".to_string(), + }) + } else { + Err(Error::ValueError) + } + } + + fn set_antenna(&self, direction: Direction, channel: usize, name: &str) -> Result<(), Error> { + if channel == 0 { + if direction == Direction::Rx && name == "RX" + || direction == Direction::Tx && name == "TX" + { + Ok(()) + } else { + Err(Error::NotSupported) + } + } else { + Err(Error::ValueError) + } + } + + fn gain_elements(&self, direction: Direction, channel: usize) -> Result, Error> { + if channel == 0 { + // 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()]), + // TODO: add rest + Direction::Rx => Ok(vec!["IF".into()]), + } + } else { + Err(Error::ValueError) + } + } + + fn supports_agc(&self, _direction: Direction, channel: usize) -> Result { + if channel == 0 { + Ok(false) + } else { + Err(Error::ValueError) + } + } + + 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 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, "IF", gain) + } + + fn gain(&self, direction: Direction, channel: usize) -> Result, Error> { + self.gain_element(direction, channel, "IF") + } + + fn gain_range(&self, direction: Direction, channel: usize) -> Result { + self.gain_element_range(direction, channel, "IF") + } + + 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 == "IF" { + match direction { + Direction::Tx => todo!(), + Direction::Rx => { + let mut config = self.inner.rx_config.lock().unwrap(); + config.lna_db = gain as u16; + Ok(()) + } + } + } 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 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 { + Err(Error::ValueError) + } + } + + fn gain_element_range( + &self, + direction: Direction, + channel: usize, + name: &str, + ) -> Result { + // 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)])), + Direction::Rx => Ok(Range::new(vec![RangeItem::Step(0.0, 40.0, 8.0)])), + } + } else { + Err(Error::ValueError) + } + } + + 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 channel == 0 { + Ok(vec!["TUNER".to_string()]) + } else { + Err(Error::ValueError) + } + } + + fn component_frequency_range( + &self, + _direction: Direction, + channel: usize, + name: &str, + ) -> Result { + 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::ValueError) + } + } + + fn component_frequency( + &self, + direction: Direction, + channel: usize, + name: &str, + ) -> Result { + if channel == 0 && name == "TUNER" { + self.with_config(direction, |config| Ok(config.frequency_hz as f64)) + } else { + Err(Error::ValueError) + } + } + + fn set_component_frequency( + &self, + direction: Direction, + channel: usize, + name: &str, + frequency: f64, + ) -> Result<(), Error> { + if channel == 0 + && self + .frequency_range(direction, channel)? + .contains(frequency) + && name == "TUNER" + { + self.with_config(direction, |config| { + config.frequency_hz = frequency as u64; + self.inner.dev.set_freq(frequency as u64)?; + Ok(()) + }) + } else { + Err(Error::ValueError) + } + } + + 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 we're not full duplex + if channel == 0 { + self.with_config(direction, |config| Ok(config.sample_rate_hz as f64)) + } else { + Err(Error::ValueError) + } + } + + fn set_sample_rate( + &self, + direction: Direction, + channel: usize, + rate: f64, + ) -> Result<(), Error> { + if channel == 0 + && self + .get_sample_rate_range(direction, channel)? + .contains(rate) + { + 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::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::ValueError) + } + } +} 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..10279a3 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 = "hackrfone", not(target_arch = "wasm32")))] + #[error("Hackrf ({0})")] + HackRfOne(#[from] seify_hackrfone::Error), } #[cfg(all(feature = "aaronia_http", not(target_arch = "wasm32")))] @@ -70,6 +73,7 @@ impl From for Error { pub enum Driver { Aaronia, AaroniaHttp, + HackRf, RtlSdr, Soapy, } @@ -91,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) } } @@ -180,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) }