Skip to main content

hackrf_nusb/
high_level.rs

1//! High-level owned HackRF device and explicit half-duplex streams.
2
3use std::future::{Future, IntoFuture, poll_fn};
4use std::pin::Pin;
5use std::task::{Context, Poll};
6use std::time::Duration;
7
8use nusb::MaybeFuture;
9
10use crate::Complex32;
11use crate::config::{
12    Config, ConfigBuilder, validate_frequency, validate_lna_gain, validate_sample_rate,
13    validate_tx_vga_gain, validate_vga_gain,
14};
15use crate::device::{HackRf, shutdown_hardware};
16use crate::errors::{Error, Result};
17use crate::maybe_future::{MaybeFutureExt, ready};
18use crate::radio::{
19    Direction, ExecutionStyle, LifecycleController, StreamClaim, TxStreamingStats, TxTransport,
20};
21use crate::streaming::{AsyncDirectRxStream, AsyncStreamingBackend, StreamingStats};
22#[cfg(not(target_arch = "wasm32"))]
23use crate::streaming::{DirectRxStream, StreamingBackend};
24use crate::types::DeviceInfo;
25use crate::usb::{ControlBackend, NusbControl};
26
27#[cfg(not(target_arch = "wasm32"))]
28type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
29#[cfg(target_arch = "wasm32")]
30type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
31
32#[derive(Debug)]
33struct DeviceInner {
34    direct: HackRf<NusbControl>,
35    info: DeviceInfo,
36    #[cfg(not(target_arch = "wasm32"))]
37    shutdown_on_drop: bool,
38}
39
40/// Ensures a partially initialized device is returned to off mode.
41///
42/// It owns a second handle to the same control interface, leaving the primary
43/// handle available to the opening operation. A completed open consumes this
44/// guard; an error or cancelled future drops it and triggers best-effort
45/// cleanup instead.
46struct OpeningCleanup {
47    direct: Option<HackRf<NusbControl>>,
48}
49
50impl OpeningCleanup {
51    fn new(direct: HackRf<NusbControl>) -> Self {
52        Self {
53            direct: Some(direct),
54        }
55    }
56
57    fn disarm(mut self) {
58        self.direct = None;
59    }
60}
61
62#[cfg(not(target_arch = "wasm32"))]
63impl Drop for OpeningCleanup {
64    fn drop(&mut self) {
65        if let Some(direct) = self.direct.as_ref() {
66            let _ = shutdown_hardware(direct).wait();
67        }
68    }
69}
70
71#[cfg(target_arch = "wasm32")]
72impl Drop for OpeningCleanup {
73    fn drop(&mut self) {
74        if let Some(direct) = self.direct.take() {
75            wasm_bindgen_futures::spawn_local(async move {
76                let _ = shutdown_hardware(&direct).await;
77            });
78        }
79    }
80}
81
82#[cfg(not(target_arch = "wasm32"))]
83impl Drop for DeviceInner {
84    fn drop(&mut self) {
85        if self.shutdown_on_drop {
86            let _ = shutdown_hardware(&self.direct).wait();
87        }
88    }
89}
90
91/// High-level owned HackRF device handle.
92#[derive(Debug)]
93pub struct Device {
94    inner: DeviceInner,
95    config: Config,
96    radio: std::sync::Arc<LifecycleController>,
97    #[cfg(target_arch = "wasm32")]
98    shutdown_complete: bool,
99}
100
101#[derive(Clone, Debug)]
102enum ConfigOperationKind {
103    Configure(Config),
104    Frequency(u64),
105    SampleRate(u32),
106    LnaGain(u8),
107    VgaGain(u8),
108    TxVgaGain(u8),
109    Amp(bool),
110    BiasTee(bool),
111}
112
113struct ConfigOperation<'a> {
114    device: &'a mut Device,
115    kind: ConfigOperationKind,
116}
117
118impl ConfigOperation<'_> {
119    fn begin(&self, style: ExecutionStyle) -> Result<crate::radio::ReconfigureGuard> {
120        match self.kind {
121            ConfigOperationKind::Configure(_)
122            | ConfigOperationKind::Amp(_)
123            | ConfigOperationKind::BiasTee(_) => {}
124            ConfigOperationKind::Frequency(value) => validate_frequency(value)?,
125            ConfigOperationKind::SampleRate(value) => validate_sample_rate(value)?,
126            ConfigOperationKind::LnaGain(value) => validate_lna_gain(value)?,
127            ConfigOperationKind::VgaGain(value) => validate_vga_gain(value)?,
128            ConfigOperationKind::TxVgaGain(value) => validate_tx_vga_gain(value)?,
129        }
130        self.device.radio.begin_reconfigure(style)
131    }
132
133    fn record_success(&mut self) {
134        match &self.kind {
135            ConfigOperationKind::Configure(config) => {
136                self.device.config = config.clone();
137                self.device
138                    .radio
139                    .set_bias_tee(self.device.config.bias_tee_enabled());
140            }
141            ConfigOperationKind::Frequency(value) => {
142                self.device.config.set_frequency_hz_internal(*value)
143            }
144            ConfigOperationKind::SampleRate(value) => {
145                self.device.config.set_sample_rate_hz_internal(*value)
146            }
147            ConfigOperationKind::LnaGain(value) => {
148                self.device.config.set_lna_gain_db_internal(*value)
149            }
150            ConfigOperationKind::VgaGain(value) => {
151                self.device.config.set_vga_gain_db_internal(*value)
152            }
153            ConfigOperationKind::TxVgaGain(value) => {
154                self.device.config.set_tx_vga_gain_db_internal(*value)
155            }
156            ConfigOperationKind::Amp(value) => self.device.config.set_amp_enabled_internal(*value),
157            ConfigOperationKind::BiasTee(value) => {
158                self.device.config.set_bias_tee_enabled_internal(*value);
159                self.device.radio.set_bias_tee(*value);
160            }
161        }
162    }
163
164    #[cfg(not(target_arch = "wasm32"))]
165    fn wait(mut self) -> Result<()> {
166        let _guard = self.begin(ExecutionStyle::Blocking)?;
167        let result = match &self.kind {
168            ConfigOperationKind::Configure(config) => {
169                self.device.inner.direct.configure(config).wait()
170            }
171            ConfigOperationKind::Frequency(value) => {
172                self.device.inner.direct.set_frequency(*value).wait()
173            }
174            ConfigOperationKind::SampleRate(value) => {
175                self.device.inner.direct.set_sample_rate(*value).wait()
176            }
177            ConfigOperationKind::LnaGain(value) => {
178                self.device.inner.direct.set_lna_gain(*value).wait()
179            }
180            ConfigOperationKind::VgaGain(value) => {
181                self.device.inner.direct.set_vga_gain(*value).wait()
182            }
183            ConfigOperationKind::TxVgaGain(value) => {
184                self.device.inner.direct.set_tx_vga_gain(*value).wait()
185            }
186            ConfigOperationKind::Amp(value) => self.device.inner.direct.set_amp(*value).wait(),
187            ConfigOperationKind::BiasTee(value) => {
188                self.device.inner.direct.set_bias_tee(*value).wait()
189            }
190        };
191        if result.is_ok() {
192            self.record_success();
193        }
194        result
195    }
196
197    async fn run_async(mut self) -> Result<()> {
198        let _guard = self.begin(ExecutionStyle::Async)?;
199        let result = match &self.kind {
200            ConfigOperationKind::Configure(config) => {
201                self.device.inner.direct.configure(config).await
202            }
203            ConfigOperationKind::Frequency(value) => {
204                self.device.inner.direct.set_frequency(*value).await
205            }
206            ConfigOperationKind::SampleRate(value) => {
207                self.device.inner.direct.set_sample_rate(*value).await
208            }
209            ConfigOperationKind::LnaGain(value) => {
210                self.device.inner.direct.set_lna_gain(*value).await
211            }
212            ConfigOperationKind::VgaGain(value) => {
213                self.device.inner.direct.set_vga_gain(*value).await
214            }
215            ConfigOperationKind::TxVgaGain(value) => {
216                self.device.inner.direct.set_tx_vga_gain(*value).await
217            }
218            ConfigOperationKind::Amp(value) => self.device.inner.direct.set_amp(*value).await,
219            ConfigOperationKind::BiasTee(value) => {
220                self.device.inner.direct.set_bias_tee(*value).await
221            }
222        };
223        if result.is_ok() {
224            self.record_success();
225        }
226        result
227    }
228}
229
230impl<'a> IntoFuture for ConfigOperation<'a> {
231    type Output = Result<()>;
232    type IntoFuture = BoxFuture<'a, Result<()>>;
233
234    fn into_future(self) -> Self::IntoFuture {
235        Box::pin(async move { self.run_async().await })
236    }
237}
238
239impl MaybeFuture for ConfigOperation<'_> {
240    #[cfg(not(target_arch = "wasm32"))]
241    fn wait(self) -> Self::Output {
242        self.wait()
243    }
244}
245
246impl Device {
247    /// List visible normal-mode HackRF devices without claiming them.
248    pub fn list() -> impl MaybeFuture<Output = Result<Vec<crate::DeviceDescriptor>>> {
249        crate::discovery::list_devices()
250    }
251
252    /// Start building and opening a HackRF.
253    pub fn builder() -> DeviceBuilder {
254        DeviceBuilder::default()
255    }
256
257    /// Open and configure the first visible HackRF.
258    pub fn open() -> impl MaybeFuture<Output = Result<Self>> {
259        Self::builder().open()
260    }
261
262    /// Open and configure a HackRF by its exact 128-bit serial.
263    pub fn open_serial(serial: u128) -> impl MaybeFuture<Output = Result<Self>> {
264        Self::builder().serial(serial).open()
265    }
266
267    /// Ask a browser user to grant WebUSB access to any supported HackRF.
268    ///
269    /// This must be called from a browser-window user gesture.
270    #[cfg(target_arch = "wasm32")]
271    pub async fn request_permission() -> Result<()> {
272        Self::builder().request_permission().await
273    }
274
275    /// Return metadata collected while opening the device.
276    pub fn info(&self) -> &DeviceInfo {
277        &self.inner.info
278    }
279
280    /// Return the configuration last successfully applied by this driver.
281    pub fn config(&self) -> &Config {
282        &self.config
283    }
284
285    /// Apply a complete validated transceiver configuration while off or streaming.
286    ///
287    /// A live configuration change has no sample-accurate application boundary.
288    pub fn configure(&mut self, config: &Config) -> impl MaybeFuture<Output = Result<()>> + '_ {
289        ConfigOperation {
290            device: self,
291            kind: ConfigOperationKind::Configure(config.clone()),
292        }
293    }
294
295    /// Change the center frequency while the radio is off or streaming.
296    ///
297    /// A live retune has no sample-accurate application boundary.
298    pub fn set_frequency_hz(
299        &mut self,
300        frequency_hz: u64,
301    ) -> impl MaybeFuture<Output = Result<()>> + '_ {
302        ConfigOperation {
303            device: self,
304            kind: ConfigOperationKind::Frequency(frequency_hz),
305        }
306    }
307
308    /// Change the complex sample rate while the radio is off or streaming.
309    ///
310    /// The driver also requests a baseband filter bandwidth equal to this rate.
311    /// A live rate change has no sample-accurate application boundary.
312    pub fn set_sample_rate_hz(
313        &mut self,
314        sample_rate_hz: u32,
315    ) -> impl MaybeFuture<Output = Result<()>> + '_ {
316        ConfigOperation {
317            device: self,
318            kind: ConfigOperationKind::SampleRate(sample_rate_hz),
319        }
320    }
321
322    /// Set RX IF/LNA gain in dB while the radio is off or streaming.
323    ///
324    /// A live gain change has no sample-accurate application boundary.
325    pub fn set_lna_gain_db(&mut self, gain_db: u8) -> impl MaybeFuture<Output = Result<()>> + '_ {
326        ConfigOperation {
327            device: self,
328            kind: ConfigOperationKind::LnaGain(gain_db),
329        }
330    }
331
332    /// Set baseband/VGA gain in dB while the radio is off or streaming.
333    ///
334    /// A live gain change has no sample-accurate application boundary.
335    pub fn set_vga_gain_db(&mut self, gain_db: u8) -> impl MaybeFuture<Output = Result<()>> + '_ {
336        ConfigOperation {
337            device: self,
338            kind: ConfigOperationKind::VgaGain(gain_db),
339        }
340    }
341
342    /// Set TX VGA gain in dB while the radio is off or streaming.
343    ///
344    /// A live gain change has no sample-accurate application boundary.
345    pub fn set_tx_vga_gain_db(
346        &mut self,
347        gain_db: u8,
348    ) -> impl MaybeFuture<Output = Result<()>> + '_ {
349        ConfigOperation {
350            device: self,
351            kind: ConfigOperationKind::TxVgaGain(gain_db),
352        }
353    }
354
355    /// Enable or disable the RF amplifier while the radio is off or streaming.
356    ///
357    /// A live amplifier change has no sample-accurate application boundary.
358    pub fn set_amp_enable(&mut self, enabled: bool) -> impl MaybeFuture<Output = Result<()>> + '_ {
359        ConfigOperation {
360            device: self,
361            kind: ConfigOperationKind::Amp(enabled),
362        }
363    }
364
365    /// Enable or disable antenna-port bias power while the radio is off or streaming.
366    ///
367    /// A live bias-power change has no sample-accurate application boundary.
368    pub fn set_bias_tee(&mut self, enabled: bool) -> impl MaybeFuture<Output = Result<()>> + '_ {
369        ConfigOperation {
370            device: self,
371            kind: ConfigOperationKind::BiasTee(enabled),
372        }
373    }
374
375    /// Claim the device's single owned receive stream.
376    ///
377    /// The stream is initially stopped. Call [`RxStream::start`] before
378    /// reading it and [`RxStream::stop`] before starting TX.
379    pub fn rx_stream(&self) -> Result<RxStream> {
380        let claim = self.radio.claim_stream(Direction::Rx)?;
381        Ok(RxStream::new(
382            self.inner.direct.stream_handle(),
383            std::sync::Arc::clone(&self.radio),
384            claim,
385        ))
386    }
387
388    /// Claim the device's single owned transmit stream.
389    ///
390    /// The stream is initially stopped. Call [`TxStream::start`] before
391    /// writing it and [`TxStream::stop`] before starting RX.
392    pub fn tx_stream(&self) -> Result<TxStream> {
393        let claim = self.radio.claim_stream(Direction::Tx)?;
394        Ok(TxStream::new(std::sync::Arc::clone(&self.radio), claim))
395    }
396
397    /// Put the transceiver in off mode and close the logical device session.
398    ///
399    /// Returns [`Error::Busy`] unless both directions have been stopped. Once
400    /// shutdown starts, streams can no longer be started. The session remains
401    /// closed if the hardware request fails or its future is cancelled; drop
402    /// this handle and reopen the device to try again. Dropping the handle
403    /// performs best-effort cleanup on supported targets.
404    #[must_use = "shutdown must be waited or awaited to observe hardware cleanup"]
405    pub fn shutdown(&mut self) -> impl MaybeFuture<Output = Result<()>> + '_ {
406        DeviceShutdownOperation { device: self }
407    }
408
409    #[cfg(not(target_arch = "wasm32"))]
410    fn shutdown_blocking(&mut self) -> Result<()> {
411        self.radio.shutdown_blocking()?;
412        self.inner.shutdown_on_drop = false;
413        Ok(())
414    }
415
416    async fn shutdown_async(&mut self) -> Result<()> {
417        self.radio.shutdown_async().await?;
418        #[cfg(not(target_arch = "wasm32"))]
419        {
420            self.inner.shutdown_on_drop = false;
421        }
422        #[cfg(target_arch = "wasm32")]
423        {
424            self.shutdown_complete = true;
425        }
426        Ok(())
427    }
428}
429
430impl Drop for Device {
431    fn drop(&mut self) {
432        self.radio.close_on_drop();
433        #[cfg(not(target_arch = "wasm32"))]
434        {
435            // Device drop is deliberately best effort. Streams surviving it
436            // are unsupported and no cleanup outcome is observable.
437        }
438        #[cfg(target_arch = "wasm32")]
439        if !self.shutdown_complete {
440            let direct = self.inner.direct.stream_handle();
441            wasm_bindgen_futures::spawn_local(async move {
442                let _ = shutdown_hardware(&direct).await;
443            });
444        }
445    }
446}
447
448/// Builder for opening and initially configuring a [`Device`].
449#[derive(Clone, Debug, Default)]
450pub struct DeviceBuilder {
451    serial: Option<u128>,
452    config: ConfigBuilder,
453}
454
455impl DeviceBuilder {
456    /// Select a device by its exact full 128-bit serial.
457    pub fn serial(mut self, serial: u128) -> Self {
458        self.serial = Some(serial);
459        self
460    }
461
462    /// Set the initial center frequency.
463    pub fn frequency_hz(mut self, value: u64) -> Self {
464        self.config = self.config.frequency_hz(value);
465        self
466    }
467
468    /// Set the initial complex sample rate.
469    pub fn sample_rate_hz(mut self, value: u32) -> Self {
470        self.config = self.config.sample_rate_hz(value);
471        self
472    }
473
474    /// Set the initial RX IF/LNA gain.
475    pub fn lna_gain_db(mut self, value: u8) -> Self {
476        self.config = self.config.lna_gain_db(value);
477        self
478    }
479
480    /// Set the initial baseband/VGA gain.
481    pub fn vga_gain_db(mut self, value: u8) -> Self {
482        self.config = self.config.vga_gain_db(value);
483        self
484    }
485
486    /// Set the initial TX VGA gain.
487    pub fn tx_vga_gain_db(mut self, value: u8) -> Self {
488        self.config = self.config.tx_vga_gain_db(value);
489        self
490    }
491
492    /// Set the initial RF amplifier state.
493    pub fn amp_enable(mut self, value: bool) -> Self {
494        self.config = self.config.amp_enable(value);
495        self
496    }
497
498    /// Set the initial antenna-port bias-power state.
499    pub fn bias_tee(mut self, value: bool) -> Self {
500        self.config = self.config.bias_tee(value);
501        self
502    }
503
504    /// Open the selected device, query metadata, and apply the configuration.
505    pub fn open(self) -> impl MaybeFuture<Output = Result<Device>> {
506        let config = self.config.build();
507        let serial = self.serial;
508        ready(config).and_then(move |config| {
509            HackRf::open(serial).and_then(move |(direct, usb_api_version)| {
510                let cleanup = OpeningCleanup::new(direct.stream_handle());
511                let info = direct.fetch_device_info(usb_api_version);
512                info.and_then(move |info| {
513                    let flush_size = direct.tx_flush_size(info.usb_api_version);
514                    direct
515                        .configure(&config)
516                        .and_then(move |()| flush_size)
517                        .map(move |flush_size| {
518                            let flush_size = flush_size?;
519                            let tx_endpoint = direct.bulk_out()?;
520                            cleanup.disarm();
521                            Ok(Device {
522                                inner: DeviceInner {
523                                    direct: direct.stream_handle(),
524                                    info,
525                                    #[cfg(not(target_arch = "wasm32"))]
526                                    shutdown_on_drop: true,
527                                },
528                                radio: LifecycleController::new(
529                                    direct,
530                                    tx_endpoint,
531                                    flush_size,
532                                    config.bias_tee_enabled(),
533                                ),
534                                config,
535                                #[cfg(target_arch = "wasm32")]
536                                shutdown_complete: false,
537                            })
538                        })
539                })
540            })
541        })
542    }
543
544    /// Ask a browser user to grant WebUSB access to this builder's selector.
545    #[cfg(target_arch = "wasm32")]
546    pub async fn request_permission(&self) -> Result<()> {
547        crate::discovery::request_device_permission(self.serial).await
548    }
549}
550
551#[derive(Clone, Copy, Debug, Eq, PartialEq)]
552enum ReceiverState {
553    Stopped,
554    CleanupRequired,
555    Paused,
556    Running,
557}
558
559#[cfg(not(target_arch = "wasm32"))]
560struct BlockingRxInner<C: ControlBackend + StreamingBackend> {
561    direct: HackRf<C>,
562    stream: Option<DirectRxStream<C::BulkIn>>,
563    stats: StreamingStats,
564    state: ReceiverState,
565}
566
567#[cfg(not(target_arch = "wasm32"))]
568impl<C> BlockingRxInner<C>
569where
570    C: ControlBackend + StreamingBackend,
571{
572    fn new(direct: HackRf<C>) -> Self {
573        Self {
574            direct,
575            stream: None,
576            stats: StreamingStats::default(),
577            state: ReceiverState::Stopped,
578        }
579    }
580
581    fn open_queue(&mut self) -> Result<()> {
582        match self.state {
583            ReceiverState::Running => return Ok(()),
584            ReceiverState::CleanupRequired => return Err(Error::Busy),
585            ReceiverState::Paused => {
586                self.stats = StreamingStats::default();
587                self.stream
588                    .as_mut()
589                    .ok_or(Error::stream_closed("RX stream has no USB queue"))?
590                    .reset_stats();
591                return Ok(());
592            }
593            ReceiverState::Stopped => {}
594        }
595        self.stats = StreamingStats::default();
596        self.state = ReceiverState::CleanupRequired;
597        self.stream = Some(self.direct.start_rx_blocking()?);
598        Ok(())
599    }
600
601    fn commit_start(&mut self) {
602        debug_assert!(self.stream.is_some());
603        self.state = ReceiverState::Running;
604    }
605
606    fn read(&mut self, out: &mut [Complex32], timeout: Duration) -> Result<usize> {
607        if self.state != ReceiverState::Running {
608            return Err(Error::stream_closed("RX stream is stopped"));
609        }
610        let result = self
611            .stream
612            .as_mut()
613            .ok_or(Error::stream_closed("RX stream has no USB queue"))?
614            .read_complex(out, timeout);
615        if result.is_err() {
616            self.state = ReceiverState::CleanupRequired;
617        }
618        result
619    }
620
621    fn close_queue(&mut self) -> StreamingStats {
622        if self.state == ReceiverState::Stopped {
623            return self.stats;
624        }
625        if let Some(mut stream) = self.stream.take() {
626            self.stats = stream.close();
627        }
628        self.state = ReceiverState::Stopped;
629        self.stats
630    }
631
632    fn pause_queue(&mut self) -> StreamingStats {
633        if self.state == ReceiverState::Running {
634            if let Some(stream) = self.stream.as_mut() {
635                self.stats = stream.stats();
636            }
637            self.state = ReceiverState::Paused;
638        }
639        self.stats
640    }
641
642    fn stop(&mut self) -> StreamingStats {
643        if self.state == ReceiverState::CleanupRequired {
644            self.close_queue()
645        } else {
646            self.pause_queue()
647        }
648    }
649}
650
651struct AsyncRxInner<C: ControlBackend + AsyncStreamingBackend> {
652    direct: HackRf<C>,
653    stream: Option<AsyncDirectRxStream<C::BulkIn>>,
654    stats: StreamingStats,
655    state: ReceiverState,
656}
657
658impl<C> AsyncRxInner<C>
659where
660    C: ControlBackend + AsyncStreamingBackend,
661{
662    fn new(direct: HackRf<C>) -> Self {
663        Self {
664            direct,
665            stream: None,
666            stats: StreamingStats::default(),
667            state: ReceiverState::Stopped,
668        }
669    }
670
671    async fn open_queue(&mut self) -> Result<()> {
672        match self.state {
673            ReceiverState::Running => return Ok(()),
674            ReceiverState::CleanupRequired => return Err(Error::Busy),
675            ReceiverState::Paused => {
676                self.stats = StreamingStats::default();
677                self.stream
678                    .as_mut()
679                    .ok_or(Error::stream_closed("async RX stream has no USB queue"))?
680                    .reset_stats();
681                return Ok(());
682            }
683            ReceiverState::Stopped => {}
684        }
685        self.stats = StreamingStats::default();
686        self.state = ReceiverState::CleanupRequired;
687        self.stream = Some(self.direct.start_rx_async().await?);
688        Ok(())
689    }
690
691    fn commit_start(&mut self) {
692        debug_assert!(self.stream.is_some());
693        self.state = ReceiverState::Running;
694    }
695
696    fn poll_read(&mut self, out: &mut [Complex32], cx: &mut Context<'_>) -> Poll<Result<usize>> {
697        if self.state != ReceiverState::Running {
698            return Poll::Ready(Err(Error::stream_closed("async RX stream is stopped")));
699        }
700        let result = match self.stream.as_mut() {
701            Some(stream) => stream.poll_read_complex(out, cx),
702            None => Poll::Ready(Err(Error::stream_closed(
703                "async RX stream has no USB queue",
704            ))),
705        };
706        if matches!(result, Poll::Ready(Err(_))) {
707            self.state = ReceiverState::CleanupRequired;
708        }
709        result
710    }
711
712    fn close_queue(&mut self) -> StreamingStats {
713        if self.state == ReceiverState::Stopped {
714            return self.stats;
715        }
716        if let Some(mut stream) = self.stream.take() {
717            self.stats = stream.close();
718        }
719        self.state = ReceiverState::Stopped;
720        self.stats
721    }
722
723    fn pause_queue(&mut self) -> StreamingStats {
724        if self.state == ReceiverState::Running {
725            if let Some(stream) = self.stream.as_mut() {
726                self.stats = stream.stats();
727            }
728            self.state = ReceiverState::Paused;
729        }
730        self.stats
731    }
732
733    fn stop(&mut self) -> StreamingStats {
734        if self.state == ReceiverState::CleanupRequired {
735            self.close_queue()
736        } else {
737            self.pause_queue()
738        }
739    }
740}
741
742enum RxStreamState {
743    Dormant(HackRf<NusbControl>),
744    Poisoned,
745    #[cfg(not(target_arch = "wasm32"))]
746    Blocking(BlockingRxInner<NusbControl>),
747    Async(AsyncRxInner<NusbControl>),
748}
749
750/// Owned HackRF receive stream.
751///
752/// `start` selects RX while the radio is off. `read` only consumes the owned
753/// RX queue and never switches direction or takes the lifecycle mutex.
754#[must_use = "RX streams retain the device's exclusive RX stream claim until dropped"]
755pub struct RxStream {
756    state: RxStreamState,
757    radio: std::sync::Arc<LifecycleController>,
758    failed: bool,
759    #[cfg(target_arch = "wasm32")]
760    queue_started: bool,
761    _claim: StreamClaim,
762}
763
764impl RxStream {
765    fn new(
766        direct: HackRf<NusbControl>,
767        radio: std::sync::Arc<LifecycleController>,
768        claim: StreamClaim,
769    ) -> Self {
770        Self {
771            state: RxStreamState::Dormant(direct),
772            radio,
773            failed: false,
774            #[cfg(target_arch = "wasm32")]
775            queue_started: false,
776            _claim: claim,
777        }
778    }
779
780    /// Start or resume RX while the radio is off.
781    ///
782    /// Returns [`Error::Busy`] when TX is active or any control transition is
783    /// in progress. Calling `start` on an already-running RX stream succeeds
784    /// without changing hardware state.
785    pub fn start(&mut self) -> impl MaybeFuture<Output = Result<()>> + '_ {
786        RxStartOperation { stream: self }
787    }
788
789    /// Read normalized complex IQ samples from a started RX stream.
790    ///
791    /// Blocking operation fills the output until its total timeout expires;
792    /// `None` waits indefinitely. Awaited operation ignores the timeout and
793    /// consumes buffered data or one new USB completion.
794    pub fn read<'a>(
795        &'a mut self,
796        out: &'a mut [Complex32],
797        timeout: Option<Duration>,
798    ) -> impl MaybeFuture<Output = Result<usize>> + 'a {
799        ReadOperation {
800            stream: self,
801            out,
802            timeout,
803        }
804    }
805
806    /// Stop RX, turn the radio off, and return counters for this run.
807    ///
808    /// A successful stop retains the RX queue so a later [`Self::start`] can
809    /// resume it without submitting a second queue. Drop the stream for final
810    /// best-effort queue cleanup. A stream I/O failure turns the radio off,
811    /// closes the queue, and permanently invalidates this stream; drop it and
812    /// create a new RX stream before starting again. On WebUSB, dropping an
813    /// RX stream whose queue has started is terminal for RX on this device
814    /// session; drop and reopen the device instead.
815    pub fn stop(&mut self) -> impl MaybeFuture<Output = Result<StreamingStats>> + '_ {
816        RxStopOperation { stream: self }
817    }
818
819    fn receiver_state(&self) -> ReceiverState {
820        match &self.state {
821            RxStreamState::Dormant(_) => ReceiverState::Stopped,
822            RxStreamState::Poisoned => ReceiverState::CleanupRequired,
823            #[cfg(not(target_arch = "wasm32"))]
824            RxStreamState::Blocking(stream) => stream.state,
825            RxStreamState::Async(stream) => stream.state,
826        }
827    }
828
829    #[cfg(not(target_arch = "wasm32"))]
830    fn initialize_blocking(&mut self) -> Result<()> {
831        if matches!(self.state, RxStreamState::Blocking(_)) {
832            return Ok(());
833        }
834        if matches!(self.state, RxStreamState::Async(_)) {
835            return Err(Error::Busy);
836        }
837        let state = core::mem::replace(&mut self.state, RxStreamState::Poisoned);
838        match state {
839            RxStreamState::Dormant(direct) => {
840                self.state = RxStreamState::Blocking(BlockingRxInner::new(direct));
841                Ok(())
842            }
843            other => {
844                self.state = other;
845                Err(Error::stream_closed("RX stream has no device"))
846            }
847        }
848    }
849
850    fn initialize_async(&mut self) -> Result<()> {
851        if matches!(self.state, RxStreamState::Async(_)) {
852            return Ok(());
853        }
854        #[cfg(not(target_arch = "wasm32"))]
855        if matches!(self.state, RxStreamState::Blocking(_)) {
856            return Err(Error::Busy);
857        }
858        let state = core::mem::replace(&mut self.state, RxStreamState::Poisoned);
859        match state {
860            RxStreamState::Dormant(direct) => {
861                self.state = RxStreamState::Async(AsyncRxInner::new(direct));
862                Ok(())
863            }
864            other => {
865                self.state = other;
866                Err(Error::stream_closed("RX stream has no device"))
867            }
868        }
869    }
870
871    #[cfg(not(target_arch = "wasm32"))]
872    fn start_blocking(&mut self) -> Result<()> {
873        if self.failed {
874            return Err(self.failed_error());
875        }
876        match self.receiver_state() {
877            ReceiverState::Running => return Ok(()),
878            ReceiverState::CleanupRequired => return Err(Error::Busy),
879            ReceiverState::Stopped | ReceiverState::Paused => {}
880        }
881        let mut transition = self.radio.begin_rx_start_blocking()?;
882        self.initialize_blocking()?;
883        match &mut self.state {
884            RxStreamState::Blocking(stream) => stream.open_queue()?,
885            _ => return Err(Error::Busy),
886        }
887        transition.finish_blocking()?;
888        if let RxStreamState::Blocking(stream) = &mut self.state {
889            stream.commit_start();
890        }
891        Ok(())
892    }
893
894    async fn start_async(&mut self) -> Result<()> {
895        if self.failed {
896            return Err(self.failed_error());
897        }
898        match self.receiver_state() {
899            ReceiverState::Running => return Ok(()),
900            ReceiverState::CleanupRequired => return Err(Error::Busy),
901            ReceiverState::Stopped | ReceiverState::Paused => {}
902        }
903        let mut transition = self.radio.begin_rx_start_async().await?;
904        self.initialize_async()?;
905        match &mut self.state {
906            RxStreamState::Async(stream) => stream.open_queue().await?,
907            _ => return Err(Error::Busy),
908        }
909        #[cfg(target_arch = "wasm32")]
910        {
911            // `open_queue` submits every browser transfer synchronously after
912            // its final await, so a successful return proves this stream owns
913            // the non-cancellable WebUSB queue.
914            self.queue_started = true;
915        }
916        transition.finish_async().await?;
917        if let RxStreamState::Async(stream) = &mut self.state {
918            stream.commit_start();
919        }
920        Ok(())
921    }
922
923    #[cfg(not(target_arch = "wasm32"))]
924    fn read_blocking(&mut self, out: &mut [Complex32], timeout: Duration) -> Result<usize> {
925        if self.receiver_state() != ReceiverState::Running {
926            return Err(Error::stream_closed("RX stream is stopped"));
927        }
928        match &mut self.state {
929            RxStreamState::Blocking(stream) => {
930                let result = stream.read(out, timeout);
931                if let Err(error) = result {
932                    self.failed = true;
933                    self.radio.mark_stream_failed(Direction::Rx);
934                    return match self.stop_blocking() {
935                        Ok(_) => Err(error),
936                        Err(cleanup_error) => {
937                            Err(cleanup_error.at("turn radio off after RX failure"))
938                        }
939                    };
940                }
941                result
942            }
943            RxStreamState::Async(_) => Err(Error::Busy),
944            RxStreamState::Dormant(_) => Err(Error::stream_closed("RX stream is not started")),
945            RxStreamState::Poisoned => Err(Error::stream_closed("RX stream requires stop")),
946        }
947    }
948
949    async fn read_async(&mut self, out: &mut [Complex32]) -> Result<usize> {
950        if self.receiver_state() != ReceiverState::Running {
951            return Err(Error::stream_closed("RX stream is stopped"));
952        }
953        match &mut self.state {
954            RxStreamState::Async(stream) => {
955                let result = poll_fn(|cx| stream.poll_read(out, cx)).await;
956                if let Err(error) = result {
957                    self.failed = true;
958                    self.radio.mark_stream_failed(Direction::Rx);
959                    return match self.stop_async().await {
960                        Ok(_) => Err(error),
961                        Err(cleanup_error) => {
962                            Err(cleanup_error.at("turn radio off after RX failure"))
963                        }
964                    };
965                }
966                result
967            }
968            #[cfg(not(target_arch = "wasm32"))]
969            RxStreamState::Blocking(_) => Err(Error::Busy),
970            RxStreamState::Dormant(_) => Err(Error::stream_closed("RX stream is not started")),
971            RxStreamState::Poisoned => Err(Error::stream_closed("RX stream requires stop")),
972        }
973    }
974
975    #[cfg(not(target_arch = "wasm32"))]
976    fn stop_blocking(&mut self) -> Result<StreamingStats> {
977        if matches!(self.state, RxStreamState::Async(_)) {
978            return Err(Error::Busy);
979        }
980        let transition = self.radio.begin_stop_blocking(Direction::Rx)?;
981        let stats = match &mut self.state {
982            RxStreamState::Dormant(_) | RxStreamState::Poisoned => StreamingStats::default(),
983            RxStreamState::Blocking(stream) => stream.stop(),
984            RxStreamState::Async(_) => return Err(Error::Busy),
985        };
986        if let Some(mut transition) = transition {
987            transition.finish_blocking()?;
988        }
989        Ok(stats)
990    }
991
992    async fn stop_async(&mut self) -> Result<StreamingStats> {
993        #[cfg(not(target_arch = "wasm32"))]
994        if matches!(self.state, RxStreamState::Blocking(_)) {
995            return Err(Error::Busy);
996        }
997        let transition = self.radio.begin_stop_async(Direction::Rx).await?;
998        #[cfg(target_arch = "wasm32")]
999        if self.receiver_state() == ReceiverState::CleanupRequired {
1000            // Closing the queue drops browser-owned transfer promises that
1001            // WebUSB cannot cancel. Do this before the close so the same
1002            // stream cannot create a replacement queue after recovery.
1003            self.abandon_webusb_queue();
1004        }
1005        let stats = match &mut self.state {
1006            RxStreamState::Dormant(_) | RxStreamState::Poisoned => StreamingStats::default(),
1007            RxStreamState::Async(stream) => stream.stop(),
1008            #[cfg(not(target_arch = "wasm32"))]
1009            RxStreamState::Blocking(_) => return Err(Error::Busy),
1010        };
1011        if let Some(mut transition) = transition {
1012            transition.finish_async().await?;
1013        }
1014        Ok(stats)
1015    }
1016
1017    fn failed_error(&self) -> Error {
1018        #[cfg(not(target_arch = "wasm32"))]
1019        {
1020            Error::stream_closed("RX stream failed; drop it and create a new RX stream")
1021        }
1022        #[cfg(target_arch = "wasm32")]
1023        {
1024            Error::stream_closed("RX stream failed; drop and reopen the WebUSB device")
1025        }
1026    }
1027
1028    #[cfg(target_arch = "wasm32")]
1029    fn abandon_webusb_queue(&mut self) {
1030        if self.queue_started {
1031            self.failed = true;
1032            self.radio.mark_rx_queue_abandoned();
1033        }
1034    }
1035
1036    #[cfg(not(target_arch = "wasm32"))]
1037    fn close_queue_on_drop(&mut self) {
1038        match &mut self.state {
1039            RxStreamState::Dormant(_) | RxStreamState::Poisoned => {}
1040            RxStreamState::Blocking(stream) => {
1041                stream.close_queue();
1042            }
1043            RxStreamState::Async(stream) => {
1044                stream.close_queue();
1045            }
1046        }
1047    }
1048}
1049
1050impl Drop for RxStream {
1051    fn drop(&mut self) {
1052        #[cfg(not(target_arch = "wasm32"))]
1053        if let Ok(Some(mut transition)) = self.radio.begin_stop_on_drop_blocking(Direction::Rx) {
1054            self.close_queue_on_drop();
1055            let _ = transition.finish_blocking();
1056        }
1057        #[cfg(target_arch = "wasm32")]
1058        {
1059            self.abandon_webusb_queue();
1060            let radio = std::sync::Arc::clone(&self.radio);
1061            wasm_bindgen_futures::spawn_local(async move {
1062                if let Ok(Some(mut transition)) = radio.begin_stop_async(Direction::Rx).await {
1063                    let _ = transition.finish_async().await;
1064                }
1065            });
1066        }
1067    }
1068}
1069
1070/// Owned HackRF transmit stream.
1071///
1072/// `start` selects TX while the radio is off. `write` owns the TX transport
1073/// directly and never changes direction or takes the lifecycle mutex.
1074#[must_use = "TX streams retain the device's exclusive TX stream claim until dropped"]
1075pub struct TxStream {
1076    radio: std::sync::Arc<LifecycleController>,
1077    transport: Option<TxTransport>,
1078    style: Option<ExecutionStyle>,
1079    state: TxStreamState,
1080    failed: bool,
1081    _claim: StreamClaim,
1082}
1083
1084#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1085enum TxStreamState {
1086    Dormant,
1087    Active,
1088    Recovery,
1089}
1090
1091impl TxStream {
1092    fn new(radio: std::sync::Arc<LifecycleController>, claim: StreamClaim) -> Self {
1093        Self {
1094            radio,
1095            transport: None,
1096            style: None,
1097            state: TxStreamState::Dormant,
1098            failed: false,
1099            _claim: claim,
1100        }
1101    }
1102
1103    /// Start TX while the radio is off.
1104    ///
1105    /// Returns [`Error::Busy`] when RX is active or any control transition is
1106    /// in progress. Calling `start` on an already-running TX stream succeeds
1107    /// without changing hardware state.
1108    pub fn start(&mut self) -> impl MaybeFuture<Output = Result<()>> + '_ {
1109        TxStartOperation { stream: self }
1110    }
1111
1112    /// Queue normalized complex IQ samples on a started TX stream.
1113    ///
1114    /// Blocking calls honor `timeout`; awaited calls use asynchronous USB and
1115    /// ignore it. `end_burst` appends the terminal zero-filled flush used by
1116    /// libhackrf. If a timeout leaves that flush pending, the next write or
1117    /// `stop` submits it before accepting a later burst.
1118    pub fn write<'a>(
1119        &'a mut self,
1120        samples: &'a [Complex32],
1121        timeout: Option<Duration>,
1122        end_burst: bool,
1123    ) -> impl MaybeFuture<Output = Result<usize>> + 'a {
1124        TxWriteOperation {
1125            stream: self,
1126            samples,
1127            timeout,
1128            end_burst,
1129        }
1130    }
1131
1132    /// Flush and drain accepted samples, then turn the radio off.
1133    ///
1134    /// If the normal 16-transfer TX backpressure window is full, the terminal
1135    /// flush may temporarily submit one additional transfer before draining.
1136    /// This deliberately preserves the requested end-of-burst boundary.
1137    ///
1138    /// A successful stop leaves the stream reusable. After a stream I/O
1139    /// failure, `stop` still attempts to turn the radio off and permanently
1140    /// invalidates this stream. If draining the endpoint fails, reopen the
1141    /// device before transmitting again because its pending transfers cannot
1142    /// be safely reused.
1143    pub fn stop(&mut self) -> impl MaybeFuture<Output = Result<TxStreamingStats>> + '_ {
1144        TxStopOperation { stream: self }
1145    }
1146
1147    #[cfg(not(target_arch = "wasm32"))]
1148    fn start_blocking(&mut self) -> Result<()> {
1149        if self.failed {
1150            return Err(Error::stream_closed(
1151                "TX stream failed; drop it and create a new TX stream",
1152            ));
1153        }
1154        match self.state {
1155            TxStreamState::Active => return Ok(()),
1156            TxStreamState::Recovery => return Err(Error::Busy),
1157            TxStreamState::Dormant => {}
1158        }
1159        let mut transition = self.radio.begin_tx_start_blocking()?;
1160        let transport = transition.finish_blocking()?;
1161        self.transport = Some(transport);
1162        self.style = Some(ExecutionStyle::Blocking);
1163        self.state = TxStreamState::Active;
1164        Ok(())
1165    }
1166
1167    async fn start_async(&mut self) -> Result<()> {
1168        if self.failed {
1169            return Err(Error::stream_closed(
1170                "TX stream failed; drop it and create a new TX stream",
1171            ));
1172        }
1173        match self.state {
1174            TxStreamState::Active => return Ok(()),
1175            TxStreamState::Recovery => return Err(Error::Busy),
1176            TxStreamState::Dormant => {}
1177        }
1178        let mut transition = self.radio.begin_tx_start_async().await?;
1179        let transport = transition.finish_async().await?;
1180        self.transport = Some(transport);
1181        self.style = Some(ExecutionStyle::Async);
1182        self.state = TxStreamState::Active;
1183        Ok(())
1184    }
1185
1186    #[cfg(not(target_arch = "wasm32"))]
1187    fn write_blocking(
1188        &mut self,
1189        samples: &[Complex32],
1190        timeout: Duration,
1191        end_burst: bool,
1192    ) -> Result<usize> {
1193        match self.state {
1194            TxStreamState::Active => {}
1195            TxStreamState::Dormant | TxStreamState::Recovery => {
1196                return Err(Error::stream_closed("TX stream is not started"));
1197            }
1198        }
1199        if self.style != Some(ExecutionStyle::Blocking) {
1200            return Err(Error::Busy);
1201        }
1202        let result = self
1203            .transport
1204            .as_mut()
1205            .ok_or(Error::stream_closed("TX stream has no transport"))?
1206            .write_blocking(samples, timeout, end_burst);
1207        if let Err(error) = result {
1208            self.failed = true;
1209            self.state = TxStreamState::Recovery;
1210            self.radio.mark_stream_failed(Direction::Tx);
1211            return match self.stop_blocking() {
1212                Ok(_) => Err(error),
1213                Err(cleanup_error) => Err(cleanup_error.at("turn radio off after TX failure")),
1214            };
1215        }
1216        result
1217    }
1218
1219    async fn write_async(&mut self, samples: &[Complex32], end_burst: bool) -> Result<usize> {
1220        match self.state {
1221            TxStreamState::Active => {}
1222            TxStreamState::Dormant | TxStreamState::Recovery => {
1223                return Err(Error::stream_closed("TX stream is not started"));
1224            }
1225        }
1226        if self.style != Some(ExecutionStyle::Async) {
1227            return Err(Error::Busy);
1228        }
1229        let result = match self.transport.as_mut() {
1230            Some(transport) => transport.write_async(samples, end_burst).await,
1231            None => Err(Error::stream_closed("TX stream has no transport")),
1232        };
1233        if let Err(error) = result {
1234            self.failed = true;
1235            self.state = TxStreamState::Recovery;
1236            self.radio.mark_stream_failed(Direction::Tx);
1237            return match self.stop_async().await {
1238                Ok(_) => Err(error),
1239                Err(cleanup_error) => Err(cleanup_error.at("turn radio off after TX failure")),
1240            };
1241        }
1242        result
1243    }
1244
1245    #[cfg(not(target_arch = "wasm32"))]
1246    fn stop_blocking(&mut self) -> Result<TxStreamingStats> {
1247        let transition = self.radio.begin_stop_blocking(Direction::Tx)?;
1248        self.state = TxStreamState::Recovery;
1249        let (stats, drain_error) = if let Some(transport) = self.transport.as_mut() {
1250            match transport.flush_and_drain_blocking() {
1251                Ok(()) => (transport.stats(), None),
1252                Err(error) => {
1253                    self.failed = true;
1254                    transport.mark_unusable();
1255                    (transport.stats(), Some(error))
1256                }
1257            }
1258        } else {
1259            (TxStreamingStats::default(), None)
1260        };
1261        if let Some(mut transition) = transition
1262            && let Err(error) = transition.finish_blocking()
1263        {
1264            self.state = TxStreamState::Recovery;
1265            return Err(match drain_error {
1266                Some(_) => error.at("turn radio off after TX drain failure"),
1267                None => error,
1268            });
1269        }
1270        if let Some(transport) = self.transport.take()
1271            && transport.is_reusable()
1272        {
1273            self.radio.return_tx_transport(transport);
1274        }
1275        self.state = TxStreamState::Dormant;
1276        drain_error.map_or(Ok(stats), Err)
1277    }
1278
1279    async fn stop_async(&mut self) -> Result<TxStreamingStats> {
1280        let transition = self.radio.begin_stop_async(Direction::Tx).await?;
1281        self.state = TxStreamState::Recovery;
1282        let (stats, drain_error) = if let Some(transport) = self.transport.as_mut() {
1283            match transport.flush_and_drain_async().await {
1284                Ok(()) => (transport.stats(), None),
1285                Err(error) => {
1286                    self.failed = true;
1287                    transport.mark_unusable();
1288                    (transport.stats(), Some(error))
1289                }
1290            }
1291        } else {
1292            (TxStreamingStats::default(), None)
1293        };
1294        if let Some(mut transition) = transition
1295            && let Err(error) = transition.finish_async().await
1296        {
1297            self.state = TxStreamState::Recovery;
1298            return Err(match drain_error {
1299                Some(_) => error.at("turn radio off after TX drain failure"),
1300                None => error,
1301            });
1302        }
1303        if let Some(transport) = self.transport.take()
1304            && transport.is_reusable()
1305        {
1306            self.radio.return_tx_transport(transport);
1307        }
1308        self.state = TxStreamState::Dormant;
1309        drain_error.map_or(Ok(stats), Err)
1310    }
1311
1312    #[cfg(not(target_arch = "wasm32"))]
1313    fn stop_on_drop_blocking(&mut self) {
1314        let Ok(Some(mut transition)) = self.radio.begin_stop_on_drop_blocking(Direction::Tx) else {
1315            return;
1316        };
1317        let drain_succeeded = self
1318            .transport
1319            .as_mut()
1320            .map(|transport| transport.flush_and_drain_blocking().is_ok())
1321            .unwrap_or(true);
1322        if transition.finish_blocking().is_ok()
1323            && drain_succeeded
1324            && let Some(transport) = self.transport.take()
1325            && transport.is_reusable()
1326        {
1327            self.radio.return_tx_transport(transport);
1328        }
1329    }
1330}
1331
1332impl Drop for TxStream {
1333    fn drop(&mut self) {
1334        #[cfg(not(target_arch = "wasm32"))]
1335        self.stop_on_drop_blocking();
1336        #[cfg(target_arch = "wasm32")]
1337        {
1338            let radio = std::sync::Arc::clone(&self.radio);
1339            let mut transport = self.transport.take();
1340            wasm_bindgen_futures::spawn_local(async move {
1341                if let Ok(Some(mut transition)) = radio.begin_stop_async(Direction::Tx).await {
1342                    let drain_succeeded = match transport.as_mut() {
1343                        Some(transport) => transport.flush_and_drain_async().await.is_ok(),
1344                        None => true,
1345                    };
1346                    if transition.finish_async().await.is_ok()
1347                        && drain_succeeded
1348                        && let Some(transport) = transport.take()
1349                        && transport.is_reusable()
1350                    {
1351                        radio.return_tx_transport(transport);
1352                    }
1353                }
1354            });
1355        }
1356    }
1357}
1358
1359struct RxStartOperation<'a> {
1360    stream: &'a mut RxStream,
1361}
1362
1363impl<'a> IntoFuture for RxStartOperation<'a> {
1364    type Output = Result<()>;
1365    type IntoFuture = BoxFuture<'a, Result<()>>;
1366
1367    fn into_future(self) -> Self::IntoFuture {
1368        Box::pin(async move { self.stream.start_async().await })
1369    }
1370}
1371
1372impl MaybeFuture for RxStartOperation<'_> {
1373    #[cfg(not(target_arch = "wasm32"))]
1374    fn wait(self) -> Self::Output {
1375        self.stream.start_blocking()
1376    }
1377}
1378
1379struct RxStopOperation<'a> {
1380    stream: &'a mut RxStream,
1381}
1382
1383impl<'a> IntoFuture for RxStopOperation<'a> {
1384    type Output = Result<StreamingStats>;
1385    type IntoFuture = BoxFuture<'a, Result<StreamingStats>>;
1386
1387    fn into_future(self) -> Self::IntoFuture {
1388        Box::pin(async move { self.stream.stop_async().await })
1389    }
1390}
1391
1392impl MaybeFuture for RxStopOperation<'_> {
1393    #[cfg(not(target_arch = "wasm32"))]
1394    fn wait(self) -> Self::Output {
1395        self.stream.stop_blocking()
1396    }
1397}
1398
1399struct TxStartOperation<'a> {
1400    stream: &'a mut TxStream,
1401}
1402
1403impl<'a> IntoFuture for TxStartOperation<'a> {
1404    type Output = Result<()>;
1405    type IntoFuture = BoxFuture<'a, Result<()>>;
1406
1407    fn into_future(self) -> Self::IntoFuture {
1408        Box::pin(async move { self.stream.start_async().await })
1409    }
1410}
1411
1412impl MaybeFuture for TxStartOperation<'_> {
1413    #[cfg(not(target_arch = "wasm32"))]
1414    fn wait(self) -> Self::Output {
1415        self.stream.start_blocking()
1416    }
1417}
1418
1419struct TxWriteOperation<'a> {
1420    stream: &'a mut TxStream,
1421    samples: &'a [Complex32],
1422    timeout: Option<Duration>,
1423    end_burst: bool,
1424}
1425
1426impl<'a> IntoFuture for TxWriteOperation<'a> {
1427    type Output = Result<usize>;
1428    type IntoFuture = BoxFuture<'a, Result<usize>>;
1429
1430    fn into_future(self) -> Self::IntoFuture {
1431        let _ = self.timeout;
1432        Box::pin(async move { self.stream.write_async(self.samples, self.end_burst).await })
1433    }
1434}
1435
1436impl MaybeFuture for TxWriteOperation<'_> {
1437    #[cfg(not(target_arch = "wasm32"))]
1438    fn wait(self) -> Self::Output {
1439        self.stream.write_blocking(
1440            self.samples,
1441            self.timeout.unwrap_or(Duration::MAX),
1442            self.end_burst,
1443        )
1444    }
1445}
1446
1447struct TxStopOperation<'a> {
1448    stream: &'a mut TxStream,
1449}
1450
1451impl<'a> IntoFuture for TxStopOperation<'a> {
1452    type Output = Result<TxStreamingStats>;
1453    type IntoFuture = BoxFuture<'a, Result<TxStreamingStats>>;
1454
1455    fn into_future(self) -> Self::IntoFuture {
1456        Box::pin(async move { self.stream.stop_async().await })
1457    }
1458}
1459
1460impl MaybeFuture for TxStopOperation<'_> {
1461    #[cfg(not(target_arch = "wasm32"))]
1462    fn wait(self) -> Self::Output {
1463        self.stream.stop_blocking()
1464    }
1465}
1466
1467struct ReadOperation<'a> {
1468    stream: &'a mut RxStream,
1469    out: &'a mut [Complex32],
1470    timeout: Option<Duration>,
1471}
1472
1473impl<'a> IntoFuture for ReadOperation<'a> {
1474    type Output = Result<usize>;
1475    type IntoFuture = BoxFuture<'a, Result<usize>>;
1476
1477    fn into_future(self) -> Self::IntoFuture {
1478        let _ = self.timeout;
1479        Box::pin(async move { self.stream.read_async(self.out).await })
1480    }
1481}
1482
1483impl MaybeFuture for ReadOperation<'_> {
1484    #[cfg(not(target_arch = "wasm32"))]
1485    fn wait(self) -> Self::Output {
1486        self.stream
1487            .read_blocking(self.out, self.timeout.unwrap_or(Duration::MAX))
1488    }
1489}
1490
1491struct DeviceShutdownOperation<'a> {
1492    device: &'a mut Device,
1493}
1494
1495impl<'a> IntoFuture for DeviceShutdownOperation<'a> {
1496    type Output = Result<()>;
1497    type IntoFuture = BoxFuture<'a, Result<()>>;
1498
1499    fn into_future(self) -> Self::IntoFuture {
1500        Box::pin(async move { self.device.shutdown_async().await })
1501    }
1502}
1503
1504impl MaybeFuture for DeviceShutdownOperation<'_> {
1505    #[cfg(not(target_arch = "wasm32"))]
1506    fn wait(self) -> Self::Output {
1507        self.device.shutdown_blocking()
1508    }
1509}