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
//! The transmit-side packet builder — **allocation-free**.
//!
//! Builds the full wire packet: `command + checksum`, encoded as UTF-8 bytes,
//! written into a caller-provided buffer. The checksum is a bytewise sum mod
//! 256, formatted as 2 uppercase hex chars — exactly the app's
//! `SkydroidControl.getCrc`.

use crate::command::Command;
use crate::hex::byte2hex;

/// Compute the checksum byte for a command: `(sum of all bytes) mod 256`.
#[inline]
pub fn checksum_byte(cmd: &[u8]) -> u8 {
    cmd.iter().fold(0u8, |acc, &b| acc.wrapping_add(b))
}

/// Compute the 2-character uppercase-hex checksum string as ASCII bytes.
#[inline]
pub fn checksum_hex(cmd: &[u8]) -> [u8; 2] {
    byte2hex(checksum_byte(cmd))
}

/// Build the full wire packet from raw command bytes: `command + checksum`,
/// written into `out`. Returns the total bytes written, or `None` if `out`
/// is too small (`cmd.len() + 2` needed).
///
/// ```
/// use skydroid_protocol::packet::build;
/// let mut out = [0u8; 32];
/// let n = build(b"#TPUD2wCAP01", &mut out).unwrap();
/// assert_eq!(n, 14);
/// assert_eq!(&out[..12], b"#TPUD2wCAP01");
/// ```
pub fn build(cmd: &[u8], out: &mut [u8]) -> Option<usize> {
    let n = cmd.len() + 2;
    if out.len() < n {
        return None;
    }
    out[..cmd.len()].copy_from_slice(cmd);
    let h = checksum_hex(cmd);
    out[cmd.len()] = h[0];
    out[cmd.len() + 1] = h[1];
    Some(n)
}

/// Build a wire packet from any [`Command`] implementor, writing into `out`.
///
/// ```
/// use skydroid_protocol::packet::build_command;
/// use skydroid_protocol::command::Record;
/// let mut out = [0u8; 32];
/// let n = build_command(&Record::Start, &mut out).unwrap();
/// assert_eq!(n, 14);
/// assert_eq!(&out[..12], b"#TPUD2wREC01");
/// ```
pub fn build_command<C: Command>(cmd: &C, out: &mut [u8]) -> Option<usize> {
    let cmd_len = cmd.len();
    let n = cmd_len + 2;
    if out.len() < n {
        return None;
    }
    cmd.write(&mut out[..cmd_len])?;
    let h = checksum_hex(&out[..cmd_len]);
    out[cmd_len] = h[0];
    out[cmd_len + 1] = h[1];
    Some(n)
}

/// Given a full packet (`cmd + 2-char checksum`), verify the checksum matches.
pub fn verify(packet: &[u8]) -> bool {
    if packet.len() < 3 {
        return false;
    }
    let (body, tail) = packet.split_at(packet.len() - 2);
    let h = checksum_hex(body);
    tail == &h
}

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

    #[test]
    fn checksum_byte_is_sum_mod_256() {
        assert_eq!(checksum_byte(b""), 0);
        assert_eq!(checksum_byte(b"A"), 0x41);
        assert_eq!(checksum_byte(b"AB"), 0x41 + 0x42);
        // wrap around 256
        assert_eq!(checksum_byte(&[0xFF, 0x01]), 0x00);
    }

    #[test]
    fn checksum_hex_is_two_hex_chars() {
        let h = checksum_hex(b"#TPUG2wPTZ02");
        assert_eq!(h.len(), 2);
        // Hex digits: 0-9 or A-F; uppercase letters if a letter.
        assert!(h.iter().all(|b| b.is_ascii_hexdigit()));
        assert!(h
            .iter()
            .all(|b| b.is_ascii_digit() || b.is_ascii_uppercase()));
    }

    #[test]
    fn build_appends_two_chars() {
        let mut out = [0u8; 32];
        let n = build(b"#TPUD2wCAP01", &mut out).unwrap();
        assert_eq!(n, 14);
        assert_eq!(&out[..12], b"#TPUD2wCAP01");
        let h = checksum_hex(b"#TPUD2wCAP01");
        assert_eq!(&out[12..14], &h);
    }

    #[test]
    fn build_returns_none_if_small() {
        let mut out = [0u8; 4];
        assert_eq!(build(b"#TPUD2wCAP01", &mut out), None);
    }

    #[test]
    fn build_command_uses_command_trait() {
        let mut out = [0u8; 32];
        let n = build_command(&Record::Start, &mut out).unwrap();
        assert_eq!(n, 14);
        assert_eq!(&out[..12], b"#TPUD2wREC01");
    }

    #[test]
    fn verify_roundtrip() {
        let mut out = [0u8; 32];
        let n = build(b"#TPUM2wZMC01", &mut out).unwrap();
        assert!(verify(&out[..n]));
        // corrupt a checksum char
        let mut bad = out;
        bad[n - 1] ^= 0xFF;
        assert!(!verify(&bad[..n]));
    }
}