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(¶ms.step_width_hz.to_le_bytes());
221 data.extend_from_slice(¶ms.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}