//! MAVLink frame encoding and UART transmission API. use kaptan_dma::{DmaTxBufferFactory, DmaTxEngine, TxCompletion}; use kaptan_tx::{PendingFrame, TransmitQueue}; use crate::MavlinkFrame; /// Delivery importance used for bounded MAVLink transmission admission. pub use kaptan_tx::FrameClass; /// Failure while preparing or starting a MAVLink frame transfer. #[derive(Debug)] pub enum MavlinkTransmitFault { /// DMA-visible frame preparation failed. Memory(MemoryError), /// The UART DMA route rejected a transfer start. Engine(EngineError), } /// Owns MAVLink frame admission and UART DMA lifecycle. /// /// Exactly one worker invokes this type. It accepts local encoded frames and /// received frames alike; the caller owns the policy that decides which frames /// are transmitted. pub struct MavlinkTransmitter { transmit: TransmitQueue, memory: Memory, engine: Engine, } impl MavlinkTransmitter { /// Creates an empty MAVLink transmitter. pub const fn new(memory: Memory, engine: Engine) -> Self { Self { transmit: TransmitQueue::new(), memory, engine, } } } impl MavlinkTransmitter where Memory: DmaTxBufferFactory, Engine: DmaTxEngine, { /// Admits an already-serialized MAVLink frame for UART transmission. pub fn send_frame( &mut self, class: FrameClass, frame: MavlinkFrame, ) -> Result<(), MavlinkTransmitFault> { let _ = self.transmit.enqueue(PendingFrame { class, frame }); self.start_next() } /// Releases a terminal DMA frame and opportunistically starts the next. pub fn on_dma_complete(&mut self, completion: TxCompletion) { match completion { TxCompletion::NotPending => {} TxCompletion::Complete => { let _ = self.transmit.complete(); let _ = self.start_next(); } TxCompletion::TransferFault | TxCompletion::Halted => { let _ = self.transmit.fail(); } } } fn start_next( &mut self, ) -> Result<(), MavlinkTransmitFault> { let (transmit, memory, engine) = (&mut self.transmit, &mut self.memory, &mut self.engine); transmit .start_with(|frame| { let buffer = memory .prepare(frame.raw_bytes()) .map_err(MavlinkTransmitFault::Memory)?; engine.start(buffer).map_err(MavlinkTransmitFault::Engine) }) .map(|_| ()) } } #[cfg(test)] mod tests { use super::{FrameClass, MavlinkTransmitter}; use crate::{encoder::MavlinkEncoder, message::MavState}; use kaptan_dma::{DmaReadableBuffer, DmaTxBufferFactory, DmaTxEngine, TxCompletion}; struct Memory; impl DmaTxBufferFactory for Memory { type Error = core::convert::Infallible; fn prepare<'a>(&mut self, bytes: &'a [u8]) -> Result, Self::Error> { // SAFETY: this host-only test keeps the queue's active frame alive. Ok(unsafe { DmaReadableBuffer::new_unchecked(bytes) }) } } struct Engine; impl DmaTxEngine for Engine { type StartError = core::convert::Infallible; fn start(&mut self, _: DmaReadableBuffer<'_>) -> Result<(), Self::StartError> { Ok(()) } } #[test] fn transfer_fault_drops_only_the_active_frame() { let mut encoder = MavlinkEncoder::new(1, 1); let mut transmitter = MavlinkTransmitter::::new(Memory, Engine); transmitter .send_frame( FrameClass::BestEffort, encoder.heartbeat(MavState::MAV_STATE_STANDBY), ) .unwrap(); transmitter .send_frame( FrameClass::BestEffort, encoder.heartbeat(MavState::MAV_STATE_STANDBY), ) .unwrap(); assert!(transmitter.transmit.is_active()); transmitter.on_dma_complete(TxCompletion::TransferFault); assert!(!transmitter.transmit.is_active()); } }