Skip to main content

pamoja_mavlink/drivers/
udp.rs

1//! A UDP [`ByteLink`], the transport PX4 SITL uses and a common ground-station link.
2//!
3//! MAVLink over UDP is connectionless: a ground station binds a port and learns the vehicle's
4//! address from the first datagram it receives, then replies to it. [`UdpLink`] supports that
5//! bind-and-learn pattern with [`bind`](UdpLink::bind), and the send-first pattern (talking to a
6//! vehicle at a known address, as with PX4's offboard port) with [`connect`](UdpLink::connect).
7//! Each MAVLink frame is written as one datagram, which stays well under the MTU.
8
9use std::io;
10use std::net::SocketAddr;
11
12use tokio::net::{ToSocketAddrs, UdpSocket};
13
14use super::link_fault;
15use crate::error::{MavlinkError, Result};
16use crate::link::ByteLink;
17
18/// A UDP socket presented as a [`ByteLink`].
19///
20/// The peer is either learned from the first datagram received (in [`bind`](UdpLink::bind)
21/// mode) or set up front (in [`connect`](UdpLink::connect) mode), and every received datagram
22/// refreshes it, so a vehicle that moves ports is followed.
23pub struct UdpLink {
24    socket: UdpSocket,
25    peer: Option<SocketAddr>,
26}
27
28impl UdpLink {
29    /// Binds a local address and learns the peer from the first datagram received.
30    ///
31    /// Read before writing in this mode: a write before any datagram has arrived has no peer to
32    /// send to. This suits a vehicle configured to send its telemetry to this port.
33    ///
34    /// # Arguments
35    ///
36    /// * `local` - the local address to bind, such as `"0.0.0.0:14550"`.
37    ///
38    /// # Returns
39    ///
40    /// The bound link, with no peer yet.
41    ///
42    /// # Errors
43    ///
44    /// Returns an [`io::Error`] if the address cannot be bound.
45    pub async fn bind(local: impl ToSocketAddrs) -> io::Result<Self> {
46        let socket = UdpSocket::bind(local).await?;
47        Ok(UdpLink { socket, peer: None })
48    }
49
50    /// Binds a local address and sets a fixed peer to send to.
51    ///
52    /// This suits talking to a vehicle at a known address, such as PX4's offboard UDP port.
53    ///
54    /// # Arguments
55    ///
56    /// * `local` - the local address to bind, such as `"0.0.0.0:0"`.
57    /// * `remote` - the vehicle's address to send to.
58    ///
59    /// # Returns
60    ///
61    /// The link, ready to send.
62    ///
63    /// # Errors
64    ///
65    /// Returns an [`io::Error`] if the address cannot be bound.
66    pub async fn connect(local: impl ToSocketAddrs, remote: SocketAddr) -> io::Result<Self> {
67        let socket = UdpSocket::bind(local).await?;
68        Ok(UdpLink {
69            socket,
70            peer: Some(remote),
71        })
72    }
73
74    /// Returns the local address the socket is bound to.
75    ///
76    /// # Returns
77    ///
78    /// The bound local address.
79    ///
80    /// # Errors
81    ///
82    /// Returns an [`io::Error`] if the address cannot be read.
83    pub fn local_addr(&self) -> io::Result<SocketAddr> {
84        self.socket.local_addr()
85    }
86
87    /// Returns the peer address, once one is known.
88    ///
89    /// # Returns
90    ///
91    /// The peer address, or [`None`] before any datagram has been received in bind mode.
92    pub fn peer(&self) -> Option<SocketAddr> {
93        self.peer
94    }
95}
96
97impl ByteLink for UdpLink {
98    async fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
99        let (n, from) = self.socket.recv_from(buf).await.map_err(link_fault)?;
100        self.peer = Some(from);
101        Ok(n)
102    }
103
104    async fn write_all(&mut self, data: &[u8]) -> Result<()> {
105        let peer = self.peer.ok_or(MavlinkError::Closed)?;
106        let sent = self.socket.send_to(data, peer).await.map_err(link_fault)?;
107        if sent != data.len() {
108            return Err(MavlinkError::Closed);
109        }
110        Ok(())
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::dialect::{Heartbeat, Message};
118    use crate::link::Connection;
119
120    #[tokio::test]
121    async fn a_frame_crosses_a_real_udp_socket_pair() {
122        // A vehicle end binds and a ground-station end sends to it; the frame crosses a real
123        // localhost UDP socket, exercising the driver rather than an in-memory pipe.
124        let vehicle = UdpLink::bind("127.0.0.1:0").await.unwrap();
125        let vehicle_addr = vehicle.local_addr().unwrap();
126        let gcs = UdpLink::connect("127.0.0.1:0", vehicle_addr).await.unwrap();
127
128        let mut vehicle = Connection::new(vehicle, 1, 1);
129        let mut gcs = Connection::new(gcs, 255, 190);
130
131        let heartbeat = Heartbeat {
132            custom_mode: 0,
133            type_: 2,
134            autopilot: 3,
135            base_mode: 0,
136            system_status: 4,
137            mavlink_version: 3,
138        };
139        gcs.send(&heartbeat).await.unwrap();
140        let frame = vehicle.recv().await.unwrap();
141        assert_eq!(frame.message_id(), Heartbeat::ID);
142
143        // The vehicle now knows the ground station's address and can reply.
144        vehicle.send(&heartbeat).await.unwrap();
145        let reply = gcs.recv().await.unwrap();
146        assert_eq!(reply.message_id(), Heartbeat::ID);
147    }
148}