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 receive-side `#TP` frame parser (RX side) - allocation-free.
//! Synchs on `#tp\0` header, parses msgid/dataLen/data/checkSum, stream-tolerant.

pub const ARLINK_USR_DATA_MAX_LEN: usize = 16384;
pub const HEADER_STREAM: [u8; 4] = [0x23, 0x74, 0x70, 0x00];

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RxFrameRef<'a> {
    pub msgid: u8,
    pub data_len: usize,
    pub data: &'a [u8],
    pub checksum: u8,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State { Header, MsgId, DataLen, Data, CheckSum }

#[derive(Debug, Clone, Copy)]
struct PendingFrame { data_offset: usize, msgid: u8, data_len: usize, checksum: u8 }

pub struct RxDecoder<const CAP: usize> {
    buf: [u8; CAP],
    len: usize,
    state: State,
    header_idx: usize,
    data_len: usize,
    pending: Option<PendingFrame>,
}

impl<const CAP: usize> Default for RxDecoder<CAP> {
    fn default() -> Self { Self::new() }
}

impl<const CAP: usize> RxDecoder<CAP> {
    pub fn new() -> Self {
        Self { buf: [0u8; CAP], len: 0, state: State::Header, header_idx: 0, data_len: 0, pending: None }
    }

    pub fn has_frame(&self) -> bool { self.pending.is_some() }

    pub fn feed(&mut self, chunk: &[u8]) {
        for &b in chunk { self.feed_byte(b); }
    }

    pub fn feed_byte(&mut self, byte: u8) {
        match self.state {
            State::Header => {
                if self.header_idx < CAP && byte == HEADER_STREAM[self.header_idx] {
                    self.buf[self.header_idx] = byte;
                    self.header_idx += 1;
                    if self.header_idx == HEADER_STREAM.len() {
                        self.state = State::MsgId;
                        self.len = HEADER_STREAM.len();
                    }
                } else if self.header_idx > 0 {
                    self.header_idx = 0;
                    if byte == HEADER_STREAM[0] { self.buf[0] = byte; self.header_idx = 1; }
                } else {
                    self.header_idx = if byte == HEADER_STREAM[0] { 1 } else { 0 };
                }
            }
            State::MsgId => {
                if self.len >= CAP { self.reset(); return; }
                self.buf[self.len] = byte;
                self.len += 1;
                self.state = State::DataLen;
            }
            State::DataLen => {
                let dl = byte as usize;
                if self.len + dl + 1 > CAP {
                    self.reset();
                    if byte == HEADER_STREAM[0] { self.buf[0] = byte; self.header_idx = 1; }
                    return;
                }
                self.buf[self.len] = byte;
                self.len += 1;
                self.data_len = dl;
                self.state = if dl == 0 { State::CheckSum } else { State::Data };
            }
            State::Data => {
                if self.len + 1 >= CAP { self.reset(); return; }
                self.buf[self.len] = byte;
                self.len += 1;
                if self.len == HEADER_STREAM.len() + 2 + self.data_len {
                    self.state = State::CheckSum;
                }
            }
            State::CheckSum => {
                let calc = self.buf[..self.len].iter().fold(0u8, |a, b| a.wrapping_add(*b));
                let valid = calc == byte;
                let pending = PendingFrame {
                    data_offset: HEADER_STREAM.len() + 2,
                    msgid: self.buf[HEADER_STREAM.len()],
                    data_len: self.data_len,
                    checksum: byte,
                };
                self.reset();
                if valid { self.pending = Some(pending); }
            }
        }
    }

    pub fn take_ref(&mut self) -> Option<RxFrameRef<'_>> {
        let p = self.pending.take()?;
        Some(RxFrameRef {
            msgid: p.msgid,
            data_len: p.data_len,
            data: &self.buf[p.data_offset..p.data_offset + p.data_len],
            checksum: p.checksum,
        })
    }

    pub fn take_packet(&mut self, out: &mut [u8]) -> Option<usize> {
        let p = self.pending.take()?;
        let total = HEADER_STREAM.len() + 2 + p.data_len + 1;
        if out.len() < total { self.pending = Some(p); return None; }
        out[..HEADER_STREAM.len()].copy_from_slice(&HEADER_STREAM);
        out[HEADER_STREAM.len()] = p.msgid;
        out[HEADER_STREAM.len() + 1] = p.data_len as u8;
        out[HEADER_STREAM.len() + 2..HEADER_STREAM.len() + 2 + p.data_len]
            .copy_from_slice(&self.buf[p.data_offset..p.data_offset + p.data_len]);
        out[total - 1] = p.checksum;
        Some(total)
    }

    fn reset(&mut self) {
        self.state = State::Header;
        self.header_idx = 0;
        self.len = 0;
        self.data_len = 0;
        self.pending = None;
    }
}

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

    fn make_frame(msgid: u8, payload: &[u8], out: &mut [u8]) -> usize {
        let mut n = 0;
        out[n..n+4].copy_from_slice(&HEADER_STREAM); n += 4;
        out[n] = msgid; n += 1;
        out[n] = payload.len() as u8; n += 1;
        out[n..n+payload.len()].copy_from_slice(payload); n += payload.len();
        let sum = out[..n].iter().fold(0u8, |a, b| a.wrapping_add(*b));
        out[n] = sum; n + 1
    }

    #[test]
    fn parses_single_frame() {
        let mut f = [0u8; 64];
        let n = make_frame(1, b"hi", &mut f);
        let mut d = RxDecoder::<64>::new();
        d.feed(&f[..n]);
        assert!(d.has_frame());
        let fr = d.take_ref().unwrap();
        assert_eq!(fr.msgid, 1);
        assert_eq!(fr.data, b"hi");
    }

    #[test]
    fn tolerates_partial_chunks() {
        let mut f = [0u8; 64];
        let n = make_frame(7, b"payload-123456", &mut f);
        let mut d = RxDecoder::<64>::new();
        for &b in &f[..n] { d.feed_byte(b); }
        let fr = d.take_ref().unwrap();
        assert_eq!(fr.data, b"payload-123456");
    }

    #[test]
    fn rejects_checksum_mismatch() {
        let mut f = [0u8; 64];
        let n = make_frame(1, b"abc", &mut f);
        f[n-1] = f[n-1].wrapping_add(1);
        let mut d = RxDecoder::<64>::new();
        d.feed(&f[..n]);
        assert!(!d.has_frame());
        assert!(d.take_ref().is_none());
    }

    #[test]
    fn rescans_after_garbage() {
        let mut f = [0u8; 64];
        let mut n = 0;
        f[n..n+8].copy_from_slice(b"garbage!"); n += 8;
        n += make_frame(3, b"ok", &mut f[n..]);
        let mut d = RxDecoder::<64>::new();
        d.feed(&f[..n]);
        let fr = d.take_ref().unwrap();
        assert_eq!(fr.msgid, 3);
        assert_eq!(fr.data, b"ok");
    }

    #[test]
    fn take_packet_writes_full_packet() {
        let mut f = [0u8; 64];
        let n = make_frame(9, b"xyz", &mut f);
        let mut d = RxDecoder::<64>::new();
        d.feed(&f[..n]);
        let mut out = [0u8; 64];
        let total = d.take_packet(&mut out).unwrap();
        assert_eq!(total, 4+1+1+3+1);
        assert_eq!(&out[..4], &HEADER_STREAM);
        assert_eq!(out[4], 9);
        assert_eq!(&out[6..9], b"xyz");
    }

    #[test]
    fn rejects_frame_larger_than_cap() {
        let mut f = [0u8; 64];
        let mut n = 0;
        f[n..n+4].copy_from_slice(&HEADER_STREAM); n += 4;
        f[n] = 1; n += 1;
        f[n] = 40; n += 1;
        for i in 0..40u8 { f[n] = i; n += 1; }
        let sum = f[..n].iter().fold(0u8, |a, b| a.wrapping_add(*b));
        f[n] = sum; n += 1;
        let mut d = RxDecoder::<16>::new();
        d.feed(&f[..n]);
        assert!(!d.has_frame());
    }
}