1use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
5#[cfg(not(target_arch = "wasm32"))]
6use std::time::{Duration, Instant};
7
8use crate::Complex32;
9use crate::commands::TransceiverMode;
10use crate::constants::{TRANSFER_COUNT, TRANSFER_SIZE};
11use crate::device::HackRf;
12use crate::errors::{Error, Result};
13use crate::usb::{NusbBulkOut, NusbControl};
14#[cfg(not(target_arch = "wasm32"))]
15use nusb::MaybeFuture;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub(crate) enum Direction {
20 Rx,
22 Tx,
24}
25
26impl Direction {
27 fn transceiver_mode(self) -> TransceiverMode {
28 match self {
29 Self::Rx => TransceiverMode::Receive,
30 Self::Tx => TransceiverMode::Transmit,
31 }
32 }
33}
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36enum DeviceState {
37 Open,
38 Closed,
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42enum Phase {
43 Off,
44 Starting(Direction),
45 Active(Direction),
46 Stopping(Direction),
47 Recovery(Direction),
48}
49
50#[derive(Debug)]
51struct ControllerState {
52 device: DeviceState,
53 phase: Phase,
54 control_in_progress: bool,
55 style: Option<ExecutionStyle>,
56 rx_stream_claimed: bool,
57 tx_stream_claimed: bool,
58 #[cfg(target_arch = "wasm32")]
62 rx_queue_abandoned: bool,
63}
64
65impl Default for ControllerState {
66 fn default() -> Self {
67 Self {
68 device: DeviceState::Open,
69 phase: Phase::Off,
70 control_in_progress: false,
71 style: None,
72 rx_stream_claimed: false,
73 tx_stream_claimed: false,
74 #[cfg(target_arch = "wasm32")]
75 rx_queue_abandoned: false,
76 }
77 }
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub(crate) enum ExecutionStyle {
82 #[cfg(not(target_arch = "wasm32"))]
83 Blocking,
84 Async,
85}
86
87#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
89pub struct TxStreamingStats {
90 pub samples_accepted: u64,
92 pub buffers_submitted: u64,
94 pub buffers_completed: u64,
96 pub flush_buffers: u64,
101}
102
103#[derive(Debug, Default)]
104struct TxTail {
105 flush_required: bool,
106 terminal_flush_pending: bool,
107}
108
109impl TxTail {
110 fn submitted_samples(&mut self) {
111 self.flush_required = true;
112 }
113
114 fn submitted_flush(&mut self) {
115 self.flush_required = false;
116 self.terminal_flush_pending = false;
117 }
118
119 fn flush_required(&self) -> bool {
120 self.flush_required
121 }
122
123 fn require_terminal_flush(&mut self) {
124 self.flush_required = true;
125 self.terminal_flush_pending = true;
126 }
127
128 fn terminal_flush_pending(&self) -> bool {
129 self.terminal_flush_pending
130 }
131}
132
133#[derive(Debug)]
138pub(crate) struct TxTransport {
139 endpoint: NusbBulkOut,
140 flush_size: usize,
141 stats: TxStreamingStats,
142 tail: TxTail,
143 unusable: bool,
144}
145
146impl TxTransport {
147 fn new(endpoint: NusbBulkOut, flush_size: usize) -> Self {
148 Self {
149 endpoint,
150 flush_size,
151 stats: TxStreamingStats::default(),
152 tail: TxTail::default(),
153 unusable: false,
154 }
155 }
156
157 fn reset(&mut self) {
158 self.stats = TxStreamingStats::default();
159 self.tail = TxTail::default();
160 }
161
162 pub(crate) fn stats(&self) -> TxStreamingStats {
163 self.stats
164 }
165
166 pub(crate) fn mark_unusable(&mut self) {
169 self.unusable = true;
170 }
171
172 pub(crate) fn is_reusable(&self) -> bool {
173 !self.unusable
174 }
175
176 fn submit_samples(&mut self, samples: &[Complex32]) {
177 let mut bytes = Vec::with_capacity((samples.len() * 2).next_multiple_of(512));
178 for sample in samples {
179 bytes.push(f32_to_cs8(sample.re) as u8);
180 bytes.push(f32_to_cs8(sample.im) as u8);
181 }
182 bytes.resize(bytes.len().next_multiple_of(512), 0);
183 self.endpoint.submit(bytes);
184 self.tail.submitted_samples();
185 self.stats.samples_accepted += samples.len() as u64;
186 self.stats.buffers_submitted += 1;
187 }
188
189 fn submit_flush(&mut self) {
190 self.endpoint.submit(vec![0; self.flush_size]);
191 self.tail.submitted_flush();
192 self.stats.buffers_submitted += 1;
193 self.stats.flush_buffers += 1;
194 }
195
196 fn flush_if_needed(&mut self) {
197 if self.tail.flush_required() {
198 self.submit_flush();
199 }
200 }
201
202 #[cfg(not(target_arch = "wasm32"))]
203 fn complete_one_blocking(&mut self, timeout: Duration) -> Result<bool> {
204 match self.endpoint.wait_next_complete(timeout) {
205 Some(result) => {
206 result?;
207 self.stats.buffers_completed += 1;
208 Ok(true)
209 }
210 None => Ok(false),
211 }
212 }
213
214 async fn complete_one_async(&mut self) -> Result<()> {
215 self.endpoint.next_complete().await?;
216 self.stats.buffers_completed += 1;
217 Ok(())
218 }
219
220 #[cfg(not(target_arch = "wasm32"))]
221 pub(crate) fn clear_halt_blocking(&mut self) -> Result<()> {
222 self.endpoint.clear_halt_blocking()
223 }
224
225 pub(crate) async fn clear_halt_async(&mut self) -> Result<()> {
226 self.endpoint.clear_halt_async().await
227 }
228
229 #[cfg(not(target_arch = "wasm32"))]
230 pub(crate) fn write_blocking(
231 &mut self,
232 samples: &[Complex32],
233 timeout: Duration,
234 end_burst: bool,
235 ) -> Result<usize> {
236 let deadline = (timeout != Duration::MAX).then(|| Instant::now() + timeout);
237 let had_pending_terminal_flush = self.tail.terminal_flush_pending();
238 if had_pending_terminal_flush {
239 while self.endpoint.pending() >= TRANSFER_COUNT {
240 if !self.complete_one_blocking(remaining(deadline))? {
241 return Ok(0);
242 }
243 }
244 self.submit_flush();
245 }
246 if samples.is_empty() && had_pending_terminal_flush {
247 return Ok(0);
248 }
249
250 let mut accepted = 0;
251 for chunk in samples.chunks(TRANSFER_SIZE / 2) {
252 while self.endpoint.pending() >= TRANSFER_COUNT {
253 if !self.complete_one_blocking(remaining(deadline))? {
254 if end_burst && accepted != 0 {
255 self.tail.require_terminal_flush();
256 }
257 return Ok(accepted);
258 }
259 }
260 self.submit_samples(chunk);
261 accepted += chunk.len();
262 }
263 if end_burst && (accepted != 0 || samples.is_empty()) {
264 self.tail.require_terminal_flush();
265 while self.endpoint.pending() >= TRANSFER_COUNT {
266 if !self.complete_one_blocking(remaining(deadline))? {
267 return Ok(accepted);
268 }
269 }
270 self.submit_flush();
271 }
272 Ok(accepted)
273 }
274
275 pub(crate) async fn write_async(
276 &mut self,
277 samples: &[Complex32],
278 end_burst: bool,
279 ) -> Result<usize> {
280 let had_pending_terminal_flush = self.tail.terminal_flush_pending();
281 if had_pending_terminal_flush {
282 while self.endpoint.pending() >= TRANSFER_COUNT {
283 self.complete_one_async().await?;
284 }
285 self.submit_flush();
286 }
287 if samples.is_empty() && had_pending_terminal_flush {
288 return Ok(0);
289 }
290
291 let mut accepted = 0;
292 for chunk in samples.chunks(TRANSFER_SIZE / 2) {
293 while self.endpoint.pending() >= TRANSFER_COUNT {
294 self.complete_one_async().await?;
295 }
296 self.submit_samples(chunk);
297 accepted += chunk.len();
298 if end_burst && accepted == chunk.len() {
303 self.tail.require_terminal_flush();
304 }
305 }
306 if end_burst && (accepted != 0 || samples.is_empty()) {
307 if samples.is_empty() {
310 self.tail.require_terminal_flush();
311 }
312 while self.endpoint.pending() >= TRANSFER_COUNT {
313 self.complete_one_async().await?;
314 }
315 self.submit_flush();
316 }
317 Ok(accepted)
318 }
319
320 #[cfg(not(target_arch = "wasm32"))]
321 pub(crate) fn flush_and_drain_blocking(&mut self) -> Result<()> {
322 self.flush_if_needed();
326 while self.endpoint.pending() != 0 {
327 self.complete_one_blocking(Duration::MAX)?;
328 }
329 Ok(())
330 }
331
332 pub(crate) async fn flush_and_drain_async(&mut self) -> Result<()> {
333 self.flush_if_needed();
336 while self.endpoint.pending() != 0 {
337 self.complete_one_async().await?;
338 }
339 Ok(())
340 }
341}
342
343fn f32_to_cs8(value: f32) -> i8 {
344 if !value.is_finite() {
345 0
346 } else {
347 (value * 128.0).round().clamp(-128.0, 127.0) as i8
348 }
349}
350
351#[cfg(not(target_arch = "wasm32"))]
352fn remaining(deadline: Option<Instant>) -> Duration {
353 deadline.map_or(Duration::MAX, |value| {
354 value.saturating_duration_since(Instant::now())
355 })
356}
357
358#[derive(Debug)]
364pub(crate) struct LifecycleController {
365 direct: HackRf<NusbControl>,
366 bias_tee: AtomicBool,
367 state: Mutex<ControllerState>,
368 transport: Mutex<Option<TxTransport>>,
369}
370
371#[derive(Debug)]
373pub(crate) struct StreamClaim {
374 radio: Arc<LifecycleController>,
375 direction: Direction,
376}
377
378impl Drop for StreamClaim {
379 fn drop(&mut self) {
380 let mut state = lock(&self.radio.state);
381 match self.direction {
382 Direction::Rx => state.rx_stream_claimed = false,
383 Direction::Tx => state.tx_stream_claimed = false,
384 }
385 }
386}
387
388#[derive(Debug)]
391pub(crate) struct StartTransition {
392 radio: Arc<LifecycleController>,
393 direction: Direction,
394 complete: bool,
395}
396
397impl StartTransition {
398 #[cfg(not(target_arch = "wasm32"))]
399 pub(crate) fn finish_blocking(&mut self) -> Result<()> {
400 self.radio
401 .direct
402 .set_transceiver_mode(self.direction.transceiver_mode())
403 .wait()?;
404 self.radio
405 .direct
406 .set_bias_tee(self.radio.bias_tee.load(Ordering::Acquire))
407 .wait()?;
408 self.finish_success()
409 }
410
411 pub(crate) async fn finish_async(&mut self) -> Result<()> {
412 self.radio
413 .direct
414 .set_transceiver_mode(self.direction.transceiver_mode())
415 .await?;
416 self.radio
417 .direct
418 .set_bias_tee(self.radio.bias_tee.load(Ordering::Acquire))
419 .await?;
420 self.finish_success()
421 }
422
423 fn finish_success(&mut self) -> Result<()> {
424 let mut state = lock(&self.radio.state);
425 if state.phase != Phase::Starting(self.direction) {
426 return Err(Error::Busy);
427 }
428 state.phase = Phase::Active(self.direction);
429 self.complete = true;
430 Ok(())
431 }
432}
433
434impl Drop for StartTransition {
435 fn drop(&mut self) {
436 if !self.complete {
437 let mut state = lock(&self.radio.state);
438 if state.phase == Phase::Starting(self.direction) {
439 state.phase = Phase::Recovery(self.direction);
440 }
441 }
442 }
443}
444
445#[derive(Debug)]
447pub(crate) struct StopTransition {
448 radio: Arc<LifecycleController>,
449 direction: Direction,
450 complete: bool,
451}
452
453impl StopTransition {
454 #[cfg(not(target_arch = "wasm32"))]
455 pub(crate) fn finish_blocking(&mut self) -> Result<()> {
456 self.radio
457 .direct
458 .set_transceiver_mode(TransceiverMode::Off)
459 .wait()?;
460 self.finish_success()
461 }
462
463 pub(crate) async fn finish_async(&mut self) -> Result<()> {
464 self.radio
465 .direct
466 .set_transceiver_mode(TransceiverMode::Off)
467 .await?;
468 self.finish_success()
469 }
470
471 fn finish_success(&mut self) -> Result<()> {
472 let mut state = lock(&self.radio.state);
473 if state.phase != Phase::Stopping(self.direction) {
474 return Err(Error::Busy);
475 }
476 state.phase = Phase::Off;
477 self.complete = true;
478 Ok(())
479 }
480}
481
482impl Drop for StopTransition {
483 fn drop(&mut self) {
484 if !self.complete {
485 let mut state = lock(&self.radio.state);
486 if state.phase == Phase::Stopping(self.direction) {
487 state.phase = Phase::Recovery(self.direction);
488 }
489 }
490 }
491}
492
493#[derive(Debug)]
495pub(crate) struct TxStartTransition {
496 start: StartTransition,
497 transport: Option<TxTransport>,
498}
499
500impl TxStartTransition {
501 #[cfg(not(target_arch = "wasm32"))]
502 pub(crate) fn finish_blocking(&mut self) -> Result<TxTransport> {
503 self.transport
504 .as_mut()
505 .expect("TX start owns transport")
506 .clear_halt_blocking()?;
507 self.start.finish_blocking()?;
508 let transport = self.transport.as_mut().expect("TX start owns transport");
509 transport.reset();
510 Ok(self.transport.take().expect("TX start owns transport"))
511 }
512
513 pub(crate) async fn finish_async(&mut self) -> Result<TxTransport> {
514 self.transport
515 .as_mut()
516 .expect("TX start owns transport")
517 .clear_halt_async()
518 .await?;
519 self.start.finish_async().await?;
520 let transport = self.transport.as_mut().expect("TX start owns transport");
521 transport.reset();
522 Ok(self.transport.take().expect("TX start owns transport"))
523 }
524}
525
526impl Drop for TxStartTransition {
527 fn drop(&mut self) {
528 if let Some(transport) = self.transport.take() {
529 self.start.radio.put_transport(transport);
530 }
531 }
532}
533
534#[derive(Debug)]
536pub(crate) struct ReconfigureGuard {
537 radio: Arc<LifecycleController>,
538}
539
540impl Drop for ReconfigureGuard {
541 fn drop(&mut self) {
542 let mut state = lock(&self.radio.state);
543 state.control_in_progress = false;
547 }
548}
549
550impl LifecycleController {
551 #[cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
552 pub(crate) fn new(
553 direct: HackRf<NusbControl>,
554 endpoint: NusbBulkOut,
555 flush_size: usize,
556 bias_tee: bool,
557 ) -> Arc<Self> {
558 Arc::new(Self {
559 direct,
560 bias_tee: AtomicBool::new(bias_tee),
561 state: Mutex::new(ControllerState::default()),
562 transport: Mutex::new(Some(TxTransport::new(endpoint, flush_size))),
563 })
564 }
565
566 pub(crate) fn set_bias_tee(&self, enabled: bool) {
567 self.bias_tee.store(enabled, Ordering::Release);
568 }
569
570 pub(crate) fn claim_stream(self: &Arc<Self>, direction: Direction) -> Result<StreamClaim> {
571 let mut state = lock(&self.state);
572 if state.device != DeviceState::Open {
573 return Err(Error::DeviceClosed);
574 }
575 #[cfg(target_arch = "wasm32")]
576 if direction == Direction::Rx && state.rx_queue_abandoned {
577 return Err(Error::stream_closed(
578 "WebUSB RX queue was dropped; reopen the device before creating another RX stream",
579 ));
580 }
581 let claimed = match direction {
582 Direction::Rx => &mut state.rx_stream_claimed,
583 Direction::Tx => &mut state.tx_stream_claimed,
584 };
585 if *claimed {
586 return Err(Error::Busy);
587 }
588 *claimed = true;
589 Ok(StreamClaim {
590 radio: Arc::clone(self),
591 direction,
592 })
593 }
594
595 #[cfg(not(target_arch = "wasm32"))]
596 pub(crate) fn begin_rx_start_blocking(self: &Arc<Self>) -> Result<StartTransition> {
597 self.begin_start(Direction::Rx, ExecutionStyle::Blocking)
598 }
599
600 pub(crate) async fn begin_rx_start_async(self: &Arc<Self>) -> Result<StartTransition> {
601 self.begin_start(Direction::Rx, ExecutionStyle::Async)
602 }
603
604 #[cfg(not(target_arch = "wasm32"))]
605 pub(crate) fn begin_tx_start_blocking(self: &Arc<Self>) -> Result<TxStartTransition> {
606 self.begin_tx_start(ExecutionStyle::Blocking)
607 }
608
609 pub(crate) async fn begin_tx_start_async(self: &Arc<Self>) -> Result<TxStartTransition> {
610 self.begin_tx_start(ExecutionStyle::Async)
611 }
612
613 fn begin_tx_start(self: &Arc<Self>, style: ExecutionStyle) -> Result<TxStartTransition> {
614 let transport = lock(&self.transport).take().ok_or(Error::stream_closed(
615 "TX transport has unresolved transfers; reopen the device",
616 ))?;
617 let start = match self.begin_start(Direction::Tx, style) {
618 Ok(start) => start,
619 Err(error) => {
620 self.put_transport(transport);
621 return Err(error);
622 }
623 };
624 Ok(TxStartTransition {
625 start,
626 transport: Some(transport),
627 })
628 }
629
630 fn begin_start(
631 self: &Arc<Self>,
632 direction: Direction,
633 style: ExecutionStyle,
634 ) -> Result<StartTransition> {
635 let mut state = lock(&self.state);
636 if state.device != DeviceState::Open {
637 return Err(Error::DeviceClosed);
638 }
639 if state.control_in_progress || state.phase != Phase::Off {
640 return Err(Error::Busy);
641 }
642 match state.style {
643 Some(selected) if selected != style => return Err(Error::Busy),
644 Some(_) => {}
645 None => state.style = Some(style),
646 }
647 state.phase = Phase::Starting(direction);
648 Ok(StartTransition {
649 radio: Arc::clone(self),
650 direction,
651 complete: false,
652 })
653 }
654
655 #[cfg(not(target_arch = "wasm32"))]
656 pub(crate) fn begin_stop_blocking(
657 self: &Arc<Self>,
658 direction: Direction,
659 ) -> Result<Option<StopTransition>> {
660 self.begin_stop(direction, Some(ExecutionStyle::Blocking), false)
661 }
662
663 pub(crate) async fn begin_stop_async(
664 self: &Arc<Self>,
665 direction: Direction,
666 ) -> Result<Option<StopTransition>> {
667 self.begin_stop(direction, Some(ExecutionStyle::Async), false)
668 }
669
670 #[cfg(not(target_arch = "wasm32"))]
671 pub(crate) fn begin_stop_on_drop_blocking(
672 self: &Arc<Self>,
673 direction: Direction,
674 ) -> Result<Option<StopTransition>> {
675 self.begin_stop(direction, None, true)
676 }
677
678 fn begin_stop(
679 self: &Arc<Self>,
680 direction: Direction,
681 style: Option<ExecutionStyle>,
682 allow_closed_device: bool,
683 ) -> Result<Option<StopTransition>> {
684 let mut state = lock(&self.state);
685 if !allow_closed_device && state.device != DeviceState::Open {
686 return Err(Error::DeviceClosed);
687 }
688 if let Some(style) = style {
689 match state.style {
690 Some(selected) if selected != style => return Err(Error::Busy),
691 Some(_) | None => {}
692 }
693 }
694 match state.phase {
695 Phase::Off => Ok(None),
696 Phase::Active(active) if active == direction => {
697 state.phase = Phase::Stopping(direction);
698 Ok(Some(StopTransition {
699 radio: Arc::clone(self),
700 direction,
701 complete: false,
702 }))
703 }
704 Phase::Recovery(owner) if owner == direction => {
705 state.phase = Phase::Stopping(direction);
706 Ok(Some(StopTransition {
707 radio: Arc::clone(self),
708 direction,
709 complete: false,
710 }))
711 }
712 _ => Err(Error::Busy),
713 }
714 }
715
716 pub(crate) fn mark_stream_failed(&self, direction: Direction) {
717 let mut state = lock(&self.state);
718 if state.phase == Phase::Active(direction) {
719 state.phase = Phase::Recovery(direction);
720 }
721 }
722
723 #[cfg(target_arch = "wasm32")]
727 pub(crate) fn mark_rx_queue_abandoned(&self) {
728 lock(&self.state).rx_queue_abandoned = true;
729 }
730
731 pub(crate) fn begin_reconfigure(
732 self: &Arc<Self>,
733 style: ExecutionStyle,
734 ) -> Result<ReconfigureGuard> {
735 let mut state = lock(&self.state);
736 if state.device != DeviceState::Open {
737 return Err(Error::DeviceClosed);
738 }
739 if state.control_in_progress || !matches!(state.phase, Phase::Off | Phase::Active(_)) {
740 return Err(Error::Busy);
741 }
742 match state.style {
743 Some(selected) if selected != style => return Err(Error::Busy),
744 Some(_) | None => {}
745 }
746 state.control_in_progress = true;
747 Ok(ReconfigureGuard {
748 radio: Arc::clone(self),
749 })
750 }
751
752 #[cfg(not(target_arch = "wasm32"))]
753 pub(crate) fn shutdown_blocking(&self) -> Result<()> {
754 self.begin_device_shutdown()?;
755 self.direct
756 .set_transceiver_mode(TransceiverMode::Off)
757 .wait()
758 }
759
760 pub(crate) async fn shutdown_async(&self) -> Result<()> {
761 self.begin_device_shutdown()?;
762 self.direct.set_transceiver_mode(TransceiverMode::Off).await
763 }
764
765 fn begin_device_shutdown(&self) -> Result<()> {
766 let mut state = lock(&self.state);
767 if state.device == DeviceState::Closed {
768 return Ok(());
769 }
770 if state.control_in_progress || state.phase != Phase::Off {
771 return Err(Error::Busy);
772 }
773 state.device = DeviceState::Closed;
774 Ok(())
775 }
776
777 pub(crate) fn close_on_drop(&self) {
778 lock(&self.state).device = DeviceState::Closed;
779 }
780
781 fn put_transport(&self, transport: TxTransport) {
782 let previous = lock(&self.transport).replace(transport);
783 debug_assert!(previous.is_none());
784 }
785
786 pub(crate) fn return_tx_transport(&self, transport: TxTransport) {
787 self.put_transport(transport);
788 }
789}
790
791fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
792 mutex.lock().unwrap_or_else(PoisonError::into_inner)
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798
799 #[test]
800 fn tx_tail_requires_one_flush_after_samples() {
801 let mut tail = TxTail::default();
802 tail.submitted_samples();
803 assert!(tail.flush_required());
804 tail.require_terminal_flush();
805 assert!(tail.terminal_flush_pending());
806 tail.submitted_flush();
807 assert!(!tail.flush_required());
808 assert!(!tail.terminal_flush_pending());
809 }
810
811 #[test]
812 fn directions_are_distinct() {
813 assert_ne!(Direction::Rx, Direction::Tx);
814 assert_ne!(Phase::Active(Direction::Rx), Phase::Active(Direction::Tx));
815 assert_ne!(
816 Phase::Recovery(Direction::Rx),
817 Phase::Recovery(Direction::Tx)
818 );
819 }
820}