skydroid-protocol 0.1.0

no_std, allocation-free Rust implementation of the SkydroidCameraFPV control protocol (#TP and AT+ command families). Build and parse camera control packets for embedded and OS projects.
Documentation
//! Optional OS socket transport, enabled with the `transport` (or `std`) feature.
//! The ONLY module that uses std::net. Default control port is 9002.

use crate::framing::RxDecoder;
use crate::packet;

/// Default UDP control port used by the Skydroid app.
pub const DEFAULT_UDP_PORT: u16 = 9002;

/// A thin UDP wrapper for sending commands and receiving framed `#tp` packets.
pub struct UdpClient<const CAP: usize> {
    sock: std::net::UdpSocket,
    decoder: RxDecoder<CAP>,
}

impl<const CAP: usize> UdpClient<CAP> {
    /// Bind to any local address and connect to the camera at `addr`
    /// (e.g. `192.168.144.108:9002`).
    pub fn connect(addr: &str) -> std::io::Result<Self> {
        let sock = std::net::UdpSocket::bind("0.0.0.0:0")?;
        sock.connect(addr)?;
        Ok(Self {
            sock,
            decoder: RxDecoder::new(),
        })
    }

    /// Send a pre-built `#TP` wire packet.
    pub fn send_packet(&self, packet: &[u8]) -> std::io::Result<usize> {
        self.sock.send(packet)
    }

    /// Send a raw `#TP` command string (appends the checksum).
    pub fn send_command(&self, cmd: &[u8], scratch: &mut [u8]) -> std::io::Result<usize> {
        let n = packet::build(cmd, scratch).ok_or_else(|| {
            std::io::Error::new(std::io::ErrorKind::InvalidInput, "scratch too small")
        })?;
        self.sock.send(&scratch[..n])
    }

    /// Block until a datagram is available; feed it through the `#tp` parser.
    pub fn recv(&mut self) -> std::io::Result<RecvStatus> {
        let mut buf = [0u8; 2048];
        let n = self.sock.recv(&mut buf)?;
        self.decoder.feed(&buf[..n]);
        Ok(RecvStatus {
            datagram_len: n,
            has_frame: self.decoder.has_frame(),
        })
    }

    /// Take a validated frame as a zero-copy borrow (valid until next `recv`).
    pub fn take_ref(&mut self) -> Option<crate::framing::RxFrameRef<'_>> {
        self.decoder.take_ref()
    }

    /// Copy the next validated full packet into `out`.
    pub fn take_packet(&mut self, out: &mut [u8]) -> Option<usize> {
        self.decoder.take_packet(out)
    }
}

/// Result of a [`UdpClient::recv`] datagram.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecvStatus {
    /// Number of bytes received in the datagram.
    pub datagram_len: usize,
    /// Whether at least one complete, validated `#tp` frame is now ready.
    pub has_frame: bool,
}

/// One-shot: send a command to an address and return bytes sent.
pub fn send_command_to(addr: &str, cmd: &[u8], scratch: &mut [u8]) -> std::io::Result<usize> {
    let sock = std::net::UdpSocket::bind("0.0.0.0:0")?;
    sock.connect(addr)?;
    let n = packet::build(cmd, scratch).ok_or_else(|| {
        std::io::Error::new(std::io::ErrorKind::InvalidInput, "scratch too small")
    })?;
    sock.send(&scratch[..n])
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::framing::HEADER_STREAM;

    #[test]
    fn packet_scratch_roundtrip() {
        let mut scratch = [0u8; 64];
        let n = packet::build(b"#TPUD2wCAP01", &mut scratch).unwrap();
        assert!(packet::verify(&scratch[..n]));
        assert_eq!(HEADER_STREAM.len(), 4);
    }
}