waverave_hackrf/
sweep.rs

1use nusb::transfer::{ControlOut, ControlType, Recipient, RequestBuffer};
2
3use crate::{
4    Buffer, Error, HackRf, baseband_filter_bw,
5    consts::{ControlRequest, TransceiverMode},
6    error::StateChangeError,
7};
8
9/// Configuration settings for a receive sweep across multiple frequencies.
10///
11/// The easiest way to configure this is to call
12/// [`SweepParams::init_sample_rate`], then to add the desired
13/// frequency pairs to sweep over. There shouldn't be more than 10 pairs.
14///
15/// The recommended usage adds an offset to the center frequency, such that the
16/// lower edge of the baseband filter aligns with the lower limit of the sweep.
17/// The step width is then 4/3 of the baseband filter.
18///
19/// The `blocks_per_tuning` parameter determines how many [`SweepBuf`] blocks at
20/// a tuned frequency come out in a row. The blocks are *not* consecutive
21/// samples; the HackRF briefly turns off between sample blocks.
22///
23/// It's really best to think of this is a tool for spectrum sensing, not active
24/// demodulation.
25///
26pub struct SweepParams {
27    /// Sample rate to operate at.
28    pub sample_rate_hz: u32,
29    /// List of frequency pairs to sweep over, in MHz. There can be up to 10.
30    pub freq_mhz: Vec<(u16, u16)>,
31    /// Number of blocks to capture per tuning. Each block is 16384 bytes, or
32    /// 8192 samples.
33    pub blocks_per_tuning: u16,
34    /// Width of each tuning step, in Hz. `sample_rate` is a good value, in
35    /// general.
36    pub step_width_hz: u32,
37    /// Frequency offset added to tuned frequencies. `Sample_rate*3/8` is a good
38    /// value for Interleaved sweep mode.
39    pub offset_hz: u32,
40    /// Sweep mode.
41    pub mode: SweepMode,
42}
43
44impl SweepParams {
45    /// Initialize the sweep parameters with some sane defaults, given a sample
46    /// rate.
47    ///
48    /// See the [main `SweepParams` documentation][SweepParams] for more info.
49    pub fn init_sample_rate(sample_rate_hz: u32) -> Self {
50        let filter_bw = baseband_filter_bw(sample_rate_hz * 3 / 4);
51        let offset_hz = filter_bw / 2;
52        let step_width_hz = filter_bw * 4 / 3;
53        Self {
54            sample_rate_hz,
55            freq_mhz: Vec::new(),
56            blocks_per_tuning: 1,
57            step_width_hz,
58            offset_hz,
59            mode: SweepMode::Interleaved,
60        }
61    }
62}
63
64/// A chosen sweep mode.
65///
66/// While linear mode is the easiest to understand, the interleaved mode can
67/// make it easy to discard the portion of the spectrum with the DC mixing spur.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum SweepMode {
70    /// `step_width` is added to the current frequency at each step.
71    Linear,
72    /// Each step is divided into two interleaved sub-steps, allowing the host
73    /// to select the best portions of the FFT of each sub-step and discard the
74    /// rest. The first step adds 1/4 of the step size, and the second step adds
75    /// the remaining 3/4 of the step size. This makes it relatively easy to
76    /// discard the center of the band, where mixer IQ imbalance can create a
77    /// spike in the FFT.
78    Interleaved,
79}
80
81const SWEEP_BUF_SIZE: usize = 16384;
82
83/// A block of samples retrieved for some tuning frequency within the sweep.
84pub struct SweepBuf {
85    freq_hz: u64,
86    buf: Buffer,
87}
88
89impl SweepBuf {
90    /// The frequency tuned to, without the offset added in.
91    pub fn freq_hz(&self) -> u64 {
92        self.freq_hz
93    }
94
95    /// Access the retrieved samples in this tuning block.
96    pub fn samples(&self) -> &[num_complex::Complex<i8>] {
97        // We checked this range would be valid when we made SweepBuf.
98        unsafe { self.buf.samples().get_unchecked(5..) }
99    }
100
101    /// Mutably access the retrieved samples in this tuning block.
102    pub fn samples_mut(&mut self) -> &mut [num_complex::Complex<i8>] {
103        // We checked this range would be valid when we made SweepBuf.
104        unsafe { self.buf.samples_mut().get_unchecked_mut(5..) }
105    }
106
107    fn parse(buf: Buffer) -> Result<Self, Error> {
108        let bytes = buf.bytes();
109        if bytes.len() != SWEEP_BUF_SIZE {
110            return Err(Error::ReturnData);
111        }
112        // SAFETY: We literally just checked the buffer is 16384 bytes. The
113        // first 10 are there for sure.
114        let header: &[u8; 2] = unsafe { &*(bytes.as_ptr() as *const [u8; 2]) };
115        let freq: [u8; 8] = unsafe { *(bytes.as_ptr().add(2) as *const [u8; 8]) };
116        if header != &[0x7f, 0x7f] {
117            return Err(Error::ReturnData);
118        }
119        let freq_hz = u64::from_le_bytes(freq);
120        if !(100_000..=7_100_000_000).contains(&freq_hz) {
121            return Err(Error::ReturnData);
122        }
123        Ok(Self { freq_hz, buf })
124    }
125}
126
127/// A HackRF operating in sweep mode.
128///
129/// A sweep continually retunes the HackRF, grabbing a block of 8187 samples
130/// (8192, but the first 5 are overwritten with an internal header) at each
131/// tuning. The process is:
132///
133/// 1. Get the lower frequency in a range.
134/// 2. Tune to that frequency after adding the frequency offset, then grab the
135///    samples.
136/// 3. If multiple blocks are requested, grab another (non-sequential) block of
137///    samples. Repeat until all requested blocks have been retrieved.
138/// 4. Add the step (ignoring any offset). If using interleaved mode, the first
139///    sub-step is 1/4 of the step size, and the second sub-step is 3/4 of the
140///    step size.
141/// 5. If the new frequency is greater or equal to the upper frequency in the
142///    range, go to the next range and repeat from step 1. Otherwise go to step
143///    2 with the frequency from step 4.
144///
145/// To receive sweeps, first take a HackRF peripheral and call
146/// [`HackRf::start_rx_sweep`], or use [`Sweep::new`] with it.
147///
148/// Next, call [`submit`][Sweep::submit] to queue up requests, stopping when
149/// there are enough pending requests. `libhackrf` queues up 1 MiB of data, or
150/// 64 sweep blocks. You'll probably want something similar.
151///
152/// Actual reception is done with [`next_complete`][Sweep::next_complete],
153/// which will panic if there are no pending requests. The number of pending
154/// requests can always be checked with [`pending`][Sweep::pending].
155///
156/// A sweep requires configuration using [`SweepParams`]. The recommended way to
157/// set this up is with [`SweepParams::init_sample_rate`], which does the following:
158///
159/// 1. Configures for [interleaved][SweepMode::Interleaved] mode.
160/// 2. Finds the actual baseband filter bandwidth for a given sample rate.
161/// 3. Sets the offset to 1/2 of the filter bandwidth, aligning the lower end of
162///    the baseband to the lower frequency.
163/// 4. Sets the step width to be 4/3 of the filter bandwidth.
164///
165/// When processing the retrieved data, if we mark the full sample band as
166/// spanning from -4 to 4:
167///
168/// - -4 to -3: lower band edge, filtered out by baseband filter
169/// - -3 to -1: in-band, low side
170/// - -1 to 1: Too close to DC spur, discard from FFT
171/// - 1 to 3: in-band, upper side
172/// - 3 to 4: upper band edge, filtered out by baseband filter
173///
174/// Note that, in a normal FFT where at least two of the prime factors are 2,
175/// the transition points are also centered on FFT bins. Using an Offset DFT can
176/// fix this, putting the FFT bin transitions at the transition points instead.
177///
178pub struct Sweep {
179    rf: HackRf,
180}
181
182impl Sweep {
183    /// Start a new RX sweep using the provided parameters.
184    ///
185    /// Buffers are reused across sweep operations, provided that
186    /// [`HackRf::start_rx`] isn't used, or is used with a 8192 sample buffer
187    /// size.
188    pub async fn new(rf: HackRf, params: &SweepParams) -> Result<Self, StateChangeError> {
189        if let Err(err) = rf.set_sample_rate(params.sample_rate_hz as f64).await {
190            return Err(StateChangeError { err, rf });
191        }
192
193        Self::new_with_custom_sample_rate(rf, params).await
194    }
195
196    /// Start a new RX sweep without configuring the sample rate or baseband filter.
197    ///
198    /// Buffers are reused across sweep operations, provided that
199    /// [`HackRf::start_rx`] isn't used, or is used with a 8192 sample buffer
200    /// size.
201    pub async fn new_with_custom_sample_rate(
202        rf: HackRf,
203        params: &SweepParams,
204    ) -> Result<Self, StateChangeError> {
205        const MAX_SWEEP_RANGES: usize = 10;
206        const TUNING_BLOCK_BYTES: usize = 16384;
207        if params.freq_mhz.is_empty()
208            || params.freq_mhz.len() > MAX_SWEEP_RANGES
209            || params.blocks_per_tuning < 1
210            || params.step_width_hz < 1
211        {
212            return Err(StateChangeError {
213                rf,
214                err: Error::InvalidParameter("Invalid sweep parameters"),
215            });
216        }
217
218        // Build up the packed struct that we'll send to the HackRF
219        let mut data = Vec::with_capacity(params.freq_mhz.len() * 4 + 9);
220        data.extend_from_slice(&params.step_width_hz.to_le_bytes());
221        data.extend_from_slice(&params.offset_hz.to_be_bytes());
222        data.push(match params.mode {
223            SweepMode::Linear => 0,
224            SweepMode::Interleaved => 1,
225        });
226        for (lo, hi) in params.freq_mhz.iter().copied() {
227            if lo >= hi
228                || lo > (crate::consts::FREQ_MAX_MHZ as u16)
229                || hi > (crate::consts::FREQ_MAX_MHZ as u16)
230            {
231                return Err(StateChangeError {
232                    rf,
233                    err: Error::InvalidParameter("Invalid frequency range"),
234                });
235            }
236            // Force the upper ends of each tuning range to align with the step
237            // size, pushing them upwards if necessary.
238            let lo_hz = lo as u32 * 1_000_000;
239            let hi_hz = hi as u32 * 1_000_000;
240            let steps = (hi_hz - lo_hz).div_ceil(params.step_width_hz);
241            let full_hi = (steps * params.step_width_hz).div_ceil(1_000_000);
242
243            data.extend_from_slice(&lo.to_le_bytes());
244            data.extend_from_slice(&full_hi.to_le_bytes());
245        }
246
247        let num_bytes = (params.blocks_per_tuning as u32) * (TUNING_BLOCK_BYTES as u32);
248
249        // Set up the HackRF
250        if let Err(e) = rf
251            .interface
252            .control_out(ControlOut {
253                control_type: ControlType::Vendor,
254                recipient: Recipient::Device,
255                request: ControlRequest::InitSweep as u8,
256                value: (num_bytes & 0xffff) as u16,
257                index: (num_bytes >> 16) as u16,
258                data: &data,
259            })
260            .await
261            .into_result()
262        {
263            return Err(StateChangeError { rf, err: e.into() });
264        }
265
266        if let Err(err) = rf.set_transceiver_mode(TransceiverMode::RxSweep).await {
267            return Err(StateChangeError { rf, err });
268        }
269        Ok(Self { rf })
270    }
271
272    /// Queue up a sweep transfer.
273    ///
274    /// This will pull from a reusable buffer pool first, and allocate a new
275    /// buffer if none are available in the pool.
276    ///
277    /// The buffer pool will grow so long as completed buffers aren't dropped.
278    pub fn submit(&mut self) {
279        let req = if let Ok(buf) = self.rf.rx.buf_pool.try_recv() {
280            RequestBuffer::reuse(buf, SWEEP_BUF_SIZE)
281        } else {
282            RequestBuffer::new(SWEEP_BUF_SIZE)
283        };
284        self.rf.rx.queue.submit(req);
285    }
286
287    /// Retrieve the next chunk of receive data.
288    ///
289    /// This future is cancel-safe, so feel free to use it alongside a timeout
290    /// or a `select!`-type pattern.
291    pub async fn next_complete(&mut self) -> Result<SweepBuf, Error> {
292        let result = self.rf.rx.queue.next_complete().await;
293        match result.status {
294            Ok(_) => {
295                let buf = Buffer::new(result.data, self.rf.rx.buf_pool_send.clone());
296                SweepBuf::parse(buf)
297            }
298            Err(e) => {
299                // Reuse the buffer even in the event of an error.
300                let _ = self.rf.rx.buf_pool_send.send(result.data);
301                Err(e.into())
302            }
303        }
304    }
305
306    /// Get the number of pending requests.
307    pub fn pending(&self) -> usize {
308        self.rf.rx.queue.pending()
309    }
310
311    /// Halt receiving and return to idle mode.
312    ///
313    /// This attempts to cancel all transfers and then complete whatever is
314    /// left. Transfer errors are ignored.
315    pub async fn stop(mut self) -> Result<HackRf, StateChangeError> {
316        self.rf.rx.queue.cancel_all();
317        while self.pending() > 0 {
318            let _ = self.next_complete().await;
319        }
320        match self.rf.set_transceiver_mode(TransceiverMode::Off).await {
321            Ok(_) => Ok(self.rf),
322            Err(err) => Err(StateChangeError { err, rf: self.rf }),
323        }
324    }
325}