Skip to main content

leodos_protocols/coding/framing/
cadu.rs

1//! Channel Access Data Unit (CADU) — TM Synchronization and Channel Coding
2//!
3//! Spec: CCSDS 131.0-B-5 (TM Synchronization and Channel Coding)
4//!
5//! A CADU is the unit produced by the Coding & Synchronization sublayer
6//! on the downlink (TM/AOS direction). It consists of:
7//!
8//! ```text
9//! ┌──────────┬────────────────────────┐
10//! │  ASM     │  Transfer Frame        │
11//! │ (4 bytes)│  (fixed-length)        │
12//! └──────────┴────────────────────────┘
13//! ```
14//!
15//! The **Attached Sync Marker (ASM)** is a fixed 32-bit pattern that
16//! the receiver uses to locate frame boundaries in the continuous
17//! bitstream. The standard ASM for TM/AOS is `0x1ACFFC1D`.
18//!
19//! Proximity-1 uses a 24-bit ASM (`0xFAF320`) as defined in
20//! CCSDS 211.2-B-3.
21//!
22//! This module provides:
23//! - ASM constants for TM, AOS, and Proximity-1
24//! - CADU encoding (prepend ASM to a frame)
25//! - Frame synchronization (find ASM in a bitstream)
26
27use crate::physical::PhysicalRead;
28use crate::physical::PhysicalWrite;
29
30/// Standard 32-bit ASM for TM and AOS frames (CCSDS 131.0-B-5).
31pub const ASM_TM: [u8; 4] = [0x1A, 0xCF, 0xFC, 0x1D];
32
33/// Inverted 32-bit ASM used for the odd frames when Convolutional
34/// coding with ambiguity resolution is employed.
35pub const ASM_TM_INVERTED: [u8; 4] = [0xE5, 0x30, 0x03, 0xE2];
36
37/// 24-bit ASM for Proximity-1 links (CCSDS 211.2-B-3).
38pub const ASM_PROXIMITY1: [u8; 3] = [0xFA, 0xF3, 0x20];
39
40/// Errors that can occur during CADU operations.
41#[derive(Debug, Copy, Clone, Eq, PartialEq)]
42pub enum CaduError {
43    /// The output buffer is too small for the CADU.
44    BufferTooSmall {
45        /// Minimum number of bytes needed.
46        required: usize,
47        /// Actual buffer size provided.
48        provided: usize,
49    },
50    /// The input is too short to contain an ASM and frame.
51    InputTooShort,
52    /// The expected ASM was not found at the start of the data.
53    AsmMismatch,
54}
55
56impl core::fmt::Display for CaduError {
57    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58        match self {
59            Self::BufferTooSmall { required, provided } => {
60                write!(f, "buffer too small: need {required}, have {provided}")
61            }
62            Self::InputTooShort => write!(f, "input too short"),
63            Self::AsmMismatch => write!(f, "ASM mismatch"),
64        }
65    }
66}
67
68impl core::error::Error for CaduError {}
69
70/// Encodes a transfer frame into a CADU by prepending the ASM.
71///
72/// Writes `asm | frame` into `output` and returns the total bytes
73/// written.
74pub fn encode_cadu(asm: &[u8], frame: &[u8], output: &mut [u8]) -> Result<usize, CaduError> {
75    let total = asm.len() + frame.len();
76    if output.len() < total {
77        return Err(CaduError::BufferTooSmall {
78            required: total,
79            provided: output.len(),
80        });
81    }
82    output[..asm.len()].copy_from_slice(asm);
83    output[asm.len()..total].copy_from_slice(frame);
84    Ok(total)
85}
86
87/// Strips the ASM from a CADU and returns the frame payload.
88///
89/// Verifies that the leading bytes match the expected `asm` pattern.
90pub fn decode_cadu<'a>(asm: &[u8], cadu: &'a [u8]) -> Result<&'a [u8], CaduError> {
91    if cadu.len() < asm.len() {
92        return Err(CaduError::InputTooShort);
93    }
94    if &cadu[..asm.len()] != asm {
95        return Err(CaduError::AsmMismatch);
96    }
97    Ok(&cadu[asm.len()..])
98}
99
100/// A frame synchronizer that searches for ASM patterns in a byte
101/// stream to locate frame boundaries.
102///
103/// This implements a simple byte-aligned ASM search suitable for
104/// simulation. A real receiver would do bit-level correlation with
105/// an allowable bit-error threshold.
106pub struct FrameSync<'a> {
107    asm: &'a [u8],
108    frame_len: usize,
109}
110
111impl<'a> FrameSync<'a> {
112    /// Creates a new frame synchronizer.
113    ///
114    /// - `asm`: the sync marker pattern to search for
115    /// - `frame_len`: expected frame length *excluding* the ASM
116    pub fn new(asm: &'a [u8], frame_len: usize) -> Self {
117        Self { asm, frame_len }
118    }
119
120    /// Returns the total CADU length (ASM + frame).
121    pub fn cadu_len(&self) -> usize {
122        self.asm.len() + self.frame_len
123    }
124
125    /// Searches `data` for the next ASM-aligned frame.
126    ///
127    /// Returns `Some((offset, frame))` where `offset` is the byte
128    /// position of the ASM in `data` and `frame` is the frame
129    /// payload (after the ASM). Returns `None` if no complete frame
130    /// is found.
131    pub fn find_frame<'b>(&self, data: &'b [u8]) -> Option<(usize, &'b [u8])> {
132        let cadu_len = self.cadu_len();
133        if data.len() < cadu_len {
134            return None;
135        }
136
137        let search_end = data.len() - cadu_len + 1;
138        for offset in 0..search_end {
139            if &data[offset..offset + self.asm.len()] == self.asm {
140                let frame_start = offset + self.asm.len();
141                let frame_end = frame_start + self.frame_len;
142                return Some((offset, &data[frame_start..frame_end]));
143            }
144        }
145        None
146    }
147
148    /// Finds all ASM-aligned frames in `data`.
149    ///
150    /// Returns an iterator of `(offset, frame_slice)` pairs.
151    /// Frames may overlap if the data contains spurious ASM
152    /// matches; callers should validate frame contents.
153    pub fn find_all_frames<'b>(&'b self, data: &'b [u8]) -> FrameIter<'b> {
154        FrameIter {
155            sync: self,
156            data,
157            pos: 0,
158        }
159    }
160}
161
162/// Iterator over frames found by [`FrameSync::find_all_frames`].
163pub struct FrameIter<'a> {
164    sync: &'a FrameSync<'a>,
165    data: &'a [u8],
166    pos: usize,
167}
168
169impl<'a> Iterator for FrameIter<'a> {
170    type Item = (usize, &'a [u8]);
171
172    fn next(&mut self) -> Option<Self::Item> {
173        let remaining = &self.data[self.pos..];
174        let result = self.sync.find_frame(remaining);
175        if let Some((rel_offset, frame)) = result {
176            let abs_offset = self.pos + rel_offset;
177            // Advance past this ASM to avoid finding it again
178            self.pos = abs_offset + self.sync.asm.len();
179            Some((abs_offset, frame))
180        } else {
181            None
182        }
183    }
184}
185
186/// ASM framer implementing [`Framer`](crate::coding::Framer).
187pub struct AsmFramer {
188    asm: &'static [u8],
189}
190
191impl AsmFramer {
192    /// Creates a TM/AOS ASM framer.
193    pub fn tm() -> Self {
194        Self { asm: &ASM_TM }
195    }
196
197    /// Creates a Proximity-1 ASM framer.
198    pub fn proximity1() -> Self {
199        Self {
200            asm: &ASM_PROXIMITY1,
201        }
202    }
203}
204
205impl crate::coding::Framer for AsmFramer {
206    type Error = CaduError;
207
208    fn frame(&self, data: &[u8], output: &mut [u8]) -> Result<usize, Self::Error> {
209        encode_cadu(self.asm, data, output)
210    }
211}
212
213/// ASM deframer implementing [`Deframer`](crate::coding::Deframer).
214pub struct AsmDeframer {
215    asm: &'static [u8],
216    frame_len: usize,
217}
218
219impl AsmDeframer {
220    /// Creates a TM/AOS ASM deframer for the given frame length.
221    pub fn tm(frame_len: usize) -> Self {
222        Self {
223            asm: &ASM_TM,
224            frame_len,
225        }
226    }
227
228    /// Creates a Proximity-1 ASM deframer for the given frame length.
229    pub fn proximity1(frame_len: usize) -> Self {
230        Self {
231            asm: &ASM_PROXIMITY1,
232            frame_len,
233        }
234    }
235}
236
237impl crate::coding::Deframer for AsmDeframer {
238    type Error = CaduError;
239
240    fn deframe<'a>(&self, data: &'a [u8], output: &mut [u8]) -> Result<usize, Self::Error> {
241        let sync = FrameSync::new(self.asm, self.frame_len);
242        let (_offset, frame) = sync.find_frame(data).ok_or(CaduError::AsmMismatch)?;
243        let len = frame.len().min(output.len());
244        output[..len].copy_from_slice(&frame[..len]);
245        Ok(len)
246    }
247}
248
249/// Wraps an [`PhysicalWrite`] to prepend an ASM before writing.
250pub struct AsmWriter<W, const BUF: usize> {
251    writer: W,
252    asm: &'static [u8],
253    buffer: [u8; BUF],
254}
255
256/// Errors from ASM writer operations.
257#[derive(Debug, Clone)]
258pub enum AsmWriterError<E> {
259    /// CADU encoding error.
260    Cadu(CaduError),
261    /// The underlying writer returned an error.
262    Writer(E),
263}
264
265impl<E: core::fmt::Display> core::fmt::Display for AsmWriterError<E> {
266    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
267        match self {
268            Self::Cadu(e) => write!(f, "ASM: {e:?}"),
269            Self::Writer(e) => write!(f, "writer: {e}"),
270        }
271    }
272}
273
274impl<E: core::error::Error> core::error::Error for AsmWriterError<E> {}
275
276impl<W, const BUF: usize> AsmWriter<W, BUF> {
277    /// Creates a writer that prepends the TM ASM (`1ACFFC1D`).
278    pub fn tm(writer: W) -> Self {
279        Self {
280            writer,
281            asm: &ASM_TM,
282            buffer: [0u8; BUF],
283        }
284    }
285
286    /// Creates a writer that prepends the Proximity-1 ASM.
287    pub fn proximity1(writer: W) -> Self {
288        Self {
289            writer,
290            asm: &ASM_PROXIMITY1,
291            buffer: [0u8; BUF],
292        }
293    }
294}
295
296impl<W: PhysicalWrite, const BUF: usize> PhysicalWrite for AsmWriter<W, BUF> {
297    type Error = AsmWriterError<W::Error>;
298
299    async fn write(&mut self, data: &[u8]) -> Result<(), Self::Error> {
300        let len = encode_cadu(self.asm, data, &mut self.buffer).map_err(AsmWriterError::Cadu)?;
301        self.writer
302            .write(&self.buffer[..len])
303            .await
304            .map_err(AsmWriterError::Writer)
305    }
306}
307
308/// Wraps an [`PhysicalRead`] to find and strip ASM from
309/// incoming data.
310pub struct FrameSyncReader<R, const BUF: usize> {
311    reader: R,
312    asm: &'static [u8],
313    frame_len: usize,
314    buffer: [u8; BUF],
315}
316
317/// Errors from frame sync reader operations.
318#[derive(Debug, Clone)]
319pub enum FrameSyncReaderError<E> {
320    /// No valid frame found in received data.
321    NoFrame,
322    /// The underlying reader returned an error.
323    Reader(E),
324}
325
326impl<E: core::fmt::Display> core::fmt::Display for FrameSyncReaderError<E> {
327    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
328        match self {
329            Self::NoFrame => write!(f, "no ASM-aligned frame found"),
330            Self::Reader(e) => write!(f, "reader: {e}"),
331        }
332    }
333}
334
335impl<E: core::error::Error> core::error::Error for FrameSyncReaderError<E> {}
336
337impl<R, const BUF: usize> FrameSyncReader<R, BUF> {
338    /// Creates a reader that searches for TM ASM and extracts
339    /// frames of the given length.
340    pub fn tm(reader: R, frame_len: usize) -> Self {
341        Self {
342            reader,
343            asm: &ASM_TM,
344            frame_len,
345            buffer: [0u8; BUF],
346        }
347    }
348
349    /// Creates a reader for Proximity-1 ASM.
350    pub fn proximity1(reader: R, frame_len: usize) -> Self {
351        Self {
352            reader,
353            asm: &ASM_PROXIMITY1,
354            frame_len,
355            buffer: [0u8; BUF],
356        }
357    }
358}
359
360impl<R: PhysicalRead, const BUF: usize> PhysicalRead for FrameSyncReader<R, BUF> {
361    type Error = FrameSyncReaderError<R::Error>;
362
363    async fn read(&mut self, output: &mut [u8]) -> Result<usize, Self::Error> {
364        let len = self
365            .reader
366            .read(&mut self.buffer)
367            .await
368            .map_err(FrameSyncReaderError::Reader)?;
369
370        let sync = FrameSync::new(self.asm, self.frame_len);
371        let Some((_offset, frame)) = sync.find_frame(&self.buffer[..len]) else {
372            return Err(FrameSyncReaderError::NoFrame);
373        };
374
375        let copy_len = frame.len().min(output.len());
376        output[..copy_len].copy_from_slice(&frame[..copy_len]);
377        Ok(copy_len)
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn encode_tm_cadu() {
387        let frame = [0xAA; 16];
388        let mut buf = [0u8; 20];
389        let len = encode_cadu(&ASM_TM, &frame, &mut buf).unwrap();
390
391        assert_eq!(len, 20);
392        assert_eq!(&buf[..4], &ASM_TM);
393        assert_eq!(&buf[4..20], &frame);
394    }
395
396    #[test]
397    fn encode_proximity1_cadu() {
398        let frame = [0xBB; 10];
399        let mut buf = [0u8; 13];
400        let len = encode_cadu(&ASM_PROXIMITY1, &frame, &mut buf).unwrap();
401
402        assert_eq!(len, 13);
403        assert_eq!(&buf[..3], &ASM_PROXIMITY1);
404        assert_eq!(&buf[3..13], &frame);
405    }
406
407    #[test]
408    fn encode_buffer_too_small() {
409        let frame = [0u8; 16];
410        let mut buf = [0u8; 10]; // need 20
411        let err = encode_cadu(&ASM_TM, &frame, &mut buf);
412        assert!(matches!(
413            err,
414            Err(CaduError::BufferTooSmall {
415                required: 20,
416                provided: 10,
417            })
418        ));
419    }
420
421    #[test]
422    fn decode_tm_cadu() {
423        let mut cadu = [0u8; 20];
424        cadu[..4].copy_from_slice(&ASM_TM);
425        cadu[4..].fill(0xCC);
426
427        let frame = decode_cadu(&ASM_TM, &cadu).unwrap();
428        assert_eq!(frame.len(), 16);
429        assert!(frame.iter().all(|&b| b == 0xCC));
430    }
431
432    #[test]
433    fn decode_asm_mismatch() {
434        let cadu = [0u8; 20]; // all zeros, not ASM_TM
435        let err = decode_cadu(&ASM_TM, &cadu);
436        assert!(matches!(err, Err(CaduError::AsmMismatch)));
437    }
438
439    #[test]
440    fn decode_input_too_short() {
441        let cadu = [0x1A, 0xCF]; // only 2 bytes
442        let err = decode_cadu(&ASM_TM, &cadu);
443        assert!(matches!(err, Err(CaduError::InputTooShort)));
444    }
445
446    #[test]
447    fn frame_sync_find_single() {
448        let frame_len = 8;
449        let sync = FrameSync::new(&ASM_TM, frame_len);
450
451        // Build: garbage + ASM + frame data
452        let mut data = [0u8; 32];
453        data[5..9].copy_from_slice(&ASM_TM);
454        data[9..17].fill(0xDD);
455
456        let (offset, frame) = sync.find_frame(&data).unwrap();
457        assert_eq!(offset, 5);
458        assert_eq!(frame.len(), 8);
459        assert!(frame.iter().all(|&b| b == 0xDD));
460    }
461
462    #[test]
463    fn frame_sync_find_multiple() {
464        let frame_len = 4;
465        let sync = FrameSync::new(&ASM_TM, frame_len);
466        let cadu_len = sync.cadu_len(); // 8
467
468        // Two back-to-back CADUs
469        let mut data = [0u8; 16];
470        // First CADU at offset 0
471        data[0..4].copy_from_slice(&ASM_TM);
472        data[4..8].fill(0x11);
473        // Second CADU at offset 8
474        data[8..12].copy_from_slice(&ASM_TM);
475        data[12..16].fill(0x22);
476
477        let frames: heapless::Vec<(usize, &[u8]), 4> = sync.find_all_frames(&data).collect();
478
479        assert_eq!(frames.len(), 2);
480        assert_eq!(frames[0].0, 0);
481        assert!(frames[0].1.iter().all(|&b| b == 0x11));
482        assert_eq!(frames[1].0, cadu_len);
483        assert!(frames[1].1.iter().all(|&b| b == 0x22));
484    }
485
486    #[test]
487    fn frame_sync_no_match() {
488        let sync = FrameSync::new(&ASM_TM, 8);
489        let data = [0u8; 32]; // no ASM present
490        assert!(sync.find_frame(&data).is_none());
491    }
492
493    #[test]
494    fn frame_sync_incomplete_frame() {
495        let sync = FrameSync::new(&ASM_TM, 100);
496        // ASM present but not enough data for full frame
497        let mut data = [0u8; 20];
498        data[0..4].copy_from_slice(&ASM_TM);
499        assert!(sync.find_frame(&data).is_none());
500    }
501
502    #[test]
503    fn roundtrip_encode_decode() {
504        let frame = [0x42; 64];
505        let mut cadu_buf = [0u8; 68];
506
507        let len = encode_cadu(&ASM_TM, &frame, &mut cadu_buf).unwrap();
508        let decoded = decode_cadu(&ASM_TM, &cadu_buf[..len]).unwrap();
509
510        assert_eq!(decoded, &frame);
511    }
512
513    #[test]
514    fn proximity1_roundtrip() {
515        let frame = [0x77; 32];
516        let mut cadu_buf = [0u8; 35];
517
518        let len = encode_cadu(&ASM_PROXIMITY1, &frame, &mut cadu_buf).unwrap();
519        let decoded = decode_cadu(&ASM_PROXIMITY1, &cadu_buf[..len]).unwrap();
520
521        assert_eq!(decoded, &frame);
522    }
523}