File
Blob: archive/fft-benchmark/firmware/crates/esp32-radio/src/platform/peer.rs
| 1 | //! Task-affine peer. Native callbacks terminate in C-owned bounded queues. |
| 2 | use super::ffi; |
| 3 | use crate::error::{Error, Result, check}; |
| 4 | use std::{ffi::c_void, marker::PhantomData, ptr::NonNull, rc::Rc}; |
| 5 | |
| 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 7 | pub(crate) enum PeerState { |
| 8 | Connecting, |
| 9 | Connected, |
| 10 | Lost, |
| 11 | } |
| 12 | |
| 13 | /// C owns this single peer until reboot. Intentionally no close-on-Drop: vendor |
| 14 | /// teardown currently hangs. The private raw handle and Rc marker forbid sharing |
| 15 | /// or moving it to another task; no Rust allocation is retained by its callbacks. |
| 16 | #[derive(Debug)] |
| 17 | pub(crate) struct Peer { |
| 18 | handle: NonNull<c_void>, |
| 19 | _task: PhantomData<Rc<()>>, |
| 20 | } |
| 21 | |
| 22 | impl Peer { |
| 23 | pub(crate) fn open() -> Result<Self> { |
| 24 | // SAFETY: C enforces single creation, owns all callback contexts and |
| 25 | // configuration until reboot, and returns either null or a live handle. |
| 26 | let handle = NonNull::new(unsafe { ffi::radio_peer_open() }) |
| 27 | .ok_or(Error::new("peer initialization failed"))?; |
| 28 | Ok(Self { |
| 29 | handle, |
| 30 | _task: PhantomData, |
| 31 | }) |
| 32 | } |
| 33 | pub(crate) fn poll(&mut self) { |
| 34 | // SAFETY: private live handle, same task; callbacks access C queues only. |
| 35 | unsafe { ffi::radio_peer_poll(self.handle.as_ptr()) }; |
| 36 | } |
| 37 | pub(crate) fn state(&self) -> PeerState { |
| 38 | // SAFETY: C performs an atomic read on its live callback state. |
| 39 | match unsafe { ffi::radio_peer_state(self.handle.as_ptr()) } { |
| 40 | 1 => PeerState::Connected, |
| 41 | -1 => PeerState::Lost, |
| 42 | _ => PeerState::Connecting, |
| 43 | } |
| 44 | } |
| 45 | pub(crate) fn offer(&mut self, out: &mut [u8]) -> Result<usize> { |
| 46 | // SAFETY: C copies at most out.len() bytes while holding its offer lock; |
| 47 | // it retains no pointer to out. The handle remains on its creating task. |
| 48 | let length = |
| 49 | unsafe { ffi::radio_peer_offer(self.handle.as_ptr(), out.as_mut_ptr(), out.len()) }; |
| 50 | bounded_length(length, out.len()) |
| 51 | } |
| 52 | pub(crate) fn answer(&mut self, sdp: &str) -> Result<()> { |
| 53 | check( |
| 54 | // SAFETY: C copies the bytes into its persistent answer storage before |
| 55 | // invoking the vendor. It rejects oversized and repeated answers. |
| 56 | unsafe { ffi::radio_peer_answer(self.handle.as_ptr(), sdp.as_ptr(), sdp.len()) }, |
| 57 | "peer rejected answer", |
| 58 | ) |
| 59 | } |
| 60 | pub(crate) fn create_channels(&mut self) -> Result<()> { |
| 61 | check( |
| 62 | // SAFETY: sole task owner, C validates state and stores configs for life. |
| 63 | unsafe { ffi::radio_peer_channels(self.handle.as_ptr()) }, |
| 64 | "channel creation failed", |
| 65 | ) |
| 66 | } |
| 67 | pub(crate) fn command(&mut self, out: &mut [u8]) -> Result<usize> { |
| 68 | // SAFETY: C copies a single bounded queue item into caller-owned output |
| 69 | // and never retains out. Callback and consumer synchronize via the queue. |
| 70 | let length = |
| 71 | unsafe { ffi::radio_peer_command(self.handle.as_ptr(), out.as_mut_ptr(), out.len()) }; |
| 72 | bounded_length(length, out.len()) |
| 73 | } |
| 74 | pub(crate) fn dropped(&self) -> u32 { |
| 75 | // SAFETY: atomic read from a live, process-lifetime C context. |
| 76 | unsafe { ffi::radio_peer_dropped(self.handle.as_ptr()) } |
| 77 | } |
| 78 | pub(crate) fn audio(&mut self, pts: u32, bytes: &[u8]) -> Result<()> { |
| 79 | check( |
| 80 | // SAFETY: the shim bounds-checks and copies input to C-owned storage. |
| 81 | // No vendor code receives a pointer into this Rust slice; see FFI contract |
| 82 | // for the pinned encoder's synchronous packet/cache copy behavior. |
| 83 | unsafe { |
| 84 | ffi::radio_peer_audio(self.handle.as_ptr(), pts, bytes.as_ptr(), bytes.len()) |
| 85 | }, |
| 86 | "audio send failed", |
| 87 | ) |
| 88 | } |
| 89 | pub(crate) fn data(&mut self, stream: u16, binary: bool, bytes: &[u8]) -> Result<()> { |
| 90 | check( |
| 91 | // SAFETY: the shim validates stream/length and copies to C-owned storage; |
| 92 | // SCTP copies into its bounded cache before returning. No Rust pointer is retained. |
| 93 | unsafe { |
| 94 | ffi::radio_peer_data( |
| 95 | self.handle.as_ptr(), |
| 96 | stream, |
| 97 | i32::from(binary), |
| 98 | bytes.as_ptr(), |
| 99 | bytes.len(), |
| 100 | ) |
| 101 | }, |
| 102 | "data send failed", |
| 103 | ) |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | fn bounded_length(length: i32, capacity: usize) -> Result<usize> { |
| 108 | let length = usize::try_from(length).map_err(|_| Error::new("invalid peer response length"))?; |
| 109 | if length > capacity { |
| 110 | return Err(Error::new("oversized peer response")); |
| 111 | } |
| 112 | Ok(length) |
| 113 | } |