Skip to main content

leodos_protocols/coding/framing/
cltu.rs

1//! Communications Link Transmission Unit (CLTU) Protocol
2//!
3//! Spec: https://ccsds.org/Pubs/131x0b5.pdf
4//!
5//! A CLTU is a highly robust data structure that wraps a TC Transfer Frame.
6//! It adds error-correction coding and special sequences to ensure reliable
7//! reception of commands by the spacecraft.
8
9use crate::physical::{PhysicalWrite};
10
11const START_SEQUENCE: &[u8] = &[0xEB, 0x90];
12const TAIL_SEQUENCE: &[u8] = &[0xC5; 8];
13
14/// An error that can occur during CLTU encoding.
15#[derive(Debug, Copy, Clone, Eq, PartialEq)]
16pub enum CltuError {
17    /// The provided output buffer is too small to hold the encoded CLTU.
18    OutputBufferTooSmall {
19        /// Minimum number of bytes needed for the encoded CLTU.
20        required: usize,
21        /// Actual size of the provided output buffer.
22        provided: usize,
23    },
24}
25
26/// Computes the required buffer size for encoding a TC frame of a given length into a CLTU.
27///
28/// This is a helper function to allow users to allocate a correctly-sized buffer
29/// before calling `encode_cltu`.
30pub fn encoded_cltu_len(tc_frame_len: usize) -> usize {
31    let num_blocks = (tc_frame_len + 6) / 7; // Ceiling division
32    START_SEQUENCE.len() + num_blocks * 8 + TAIL_SEQUENCE.len()
33}
34
35/// Encodes a TC Transfer Frame byte slice into a CLTU in the provided buffer.
36///
37/// This function performs two critical operations for uplink reliability:
38/// 1.  It applies a (63, 56) Bose-Chaudhuri-Hocquhem (BCH) forward error-correction code,
39///     which allows the spacecraft to automatically correct bit errors.
40/// 2.  It wraps the encoded data with a standard Start Sequence and Tail Sequence to
41///     ensure the spacecraft's radio can reliably detect the beginning and end of the command.
42///
43/// The provided `tc_frame_bytes` should typically be randomized before calling this function.
44///
45/// Returns the total number of bytes written to the output buffer.
46pub fn encode_cltu(tc_frame_bytes: &[u8], output_buffer: &mut [u8]) -> Result<usize, CltuError> {
47    let required_len = encoded_cltu_len(tc_frame_bytes.len());
48    if output_buffer.len() < required_len {
49        return Err(CltuError::OutputBufferTooSmall {
50            required: required_len,
51            provided: output_buffer.len(),
52        });
53    }
54
55    let mut writer_idx = 0;
56
57    // Write Start Sequence
58    output_buffer[writer_idx..writer_idx + START_SEQUENCE.len()].copy_from_slice(START_SEQUENCE);
59    writer_idx += START_SEQUENCE.len();
60
61    // Write BCH-encoded blocks
62    for chunk in tc_frame_bytes.chunks(7) {
63        let mut block = [0x55; 7]; // Pad with alternating 01010101 pattern
64        block[..chunk.len()].copy_from_slice(chunk);
65
66        let parity = bch::compute_bch_parity(&block);
67
68        output_buffer[writer_idx..writer_idx + 7].copy_from_slice(&block);
69        writer_idx += 7;
70        output_buffer[writer_idx] = parity;
71        writer_idx += 1;
72    }
73
74    // Write Tail Sequence
75    output_buffer[writer_idx..writer_idx + TAIL_SEQUENCE.len()].copy_from_slice(TAIL_SEQUENCE);
76    writer_idx += TAIL_SEQUENCE.len();
77
78    Ok(writer_idx)
79}
80
81mod bch {
82    /// MSB-first CRC-7 lookup table for the CCSDS BCH(63,56) code.
83    ///
84    /// Generator polynomial: g(x) = x^7 + x^6 + x^2 + 1
85    /// (CCSDS 231.0-B-4 / 131.0-B-5).
86    ///
87    /// The 7-bit CRC is stored left-aligned in each byte (bits [7:1]),
88    /// so the aligned polynomial is 0x45 << 1 = 0x8A.
89    const fn generate_lookup_table() -> [u8; 256] {
90        const POLY_ALIGNED: u8 = 0x8A; // (x^6 + x^2 + 1) << 1
91        let mut table = [0u8; 256];
92        let mut i = 0;
93        while i < 256 {
94            let mut val = i as u8;
95            let mut bit = 0;
96            while bit < 8 {
97                val = if val & 0x80 != 0 {
98                    (val << 1) ^ POLY_ALIGNED
99                } else {
100                    val << 1
101                };
102                bit += 1;
103            }
104            table[i] = val;
105            i += 1;
106        }
107        table
108    }
109
110    static LOOKUP_TABLE: [u8; 256] = generate_lookup_table();
111
112    /// Computes the (63, 56) BCH parity byte as defined in
113    /// CCSDS 231.0-B-4.
114    ///
115    /// Returns an 8-bit parity byte where bits [7:1] hold the
116    /// complemented 7-bit BCH remainder and bit [0] is the
117    /// filler bit (always 0).
118    pub fn compute_bch_parity(bytes: &[u8; 7]) -> u8 {
119        let remainder = bytes
120            .iter()
121            .fold(0u8, |acc, &val| LOOKUP_TABLE[(acc ^ val) as usize]);
122        !remainder & 0xFE
123    }
124}
125
126/// CLTU framer implementing [`Framer`](crate::coding::Framer).
127pub struct CltuFramer;
128
129impl crate::coding::Framer for CltuFramer {
130    type Error = CltuError;
131
132    fn frame(&self, data: &[u8], output: &mut [u8]) -> Result<usize, Self::Error> {
133        encode_cltu(data, output)
134    }
135}
136
137/// Errors that can occur when writing CLTU-encoded frames.
138#[derive(Debug, Clone)]
139pub enum CltuWriterError<E> {
140    /// A CLTU encoding error occurred.
141    Cltu(CltuError),
142    /// The underlying writer returned an error.
143    Writer(E),
144}
145
146impl<E: core::fmt::Display> core::fmt::Display for CltuWriterError<E> {
147    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
148        match self {
149            Self::Cltu(e) => write!(f, "CLTU encoding error: {e:?}"),
150            Self::Writer(e) => write!(f, "writer error: {e}"),
151        }
152    }
153}
154
155impl<E: core::error::Error> core::error::Error for CltuWriterError<E> {}
156
157/// Wraps an [`PhysicalWrite`] to CLTU-encode TC frames
158/// before writing.
159pub struct CltuWriter<W, const BUF: usize> {
160    writer: W,
161    buffer: [u8; BUF],
162}
163
164impl<W, const BUF: usize> CltuWriter<W, BUF> {
165    /// Creates a new CLTU writer wrapping the given writer.
166    pub fn new(writer: W) -> Self {
167        Self {
168            writer,
169            buffer: [0u8; BUF],
170        }
171    }
172
173    /// Consumes this wrapper, returning the inner writer.
174    pub fn into_inner(self) -> W {
175        self.writer
176    }
177}
178
179impl<W: PhysicalWrite, const BUF: usize> CltuWriter<W, BUF> {
180    /// Encodes a TC frame as a CLTU and writes it downstream.
181    pub async fn write_frame(
182        &mut self,
183        tc_frame: &[u8],
184    ) -> Result<(), CltuWriterError<W::Error>> {
185        let required = encoded_cltu_len(tc_frame.len());
186        if required > BUF {
187            return Err(CltuWriterError::Cltu(
188                CltuError::OutputBufferTooSmall {
189                    required,
190                    provided: BUF,
191                },
192            ));
193        }
194
195        let len = encode_cltu(tc_frame, &mut self.buffer)
196            .map_err(CltuWriterError::Cltu)?;
197        self.writer
198            .write(&self.buffer[..len])
199            .await
200            .map_err(CltuWriterError::Writer)
201    }
202}
203
204impl<W: PhysicalWrite, const BUF: usize> PhysicalWrite
205    for CltuWriter<W, BUF>
206{
207    type Error = CltuWriterError<W::Error>;
208
209    async fn write(
210        &mut self,
211        data: &[u8],
212    ) -> Result<(), Self::Error> {
213        self.write_frame(data).await
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn bch_test_vectors() {
223        assert_eq!(
224            bch::compute_bch_parity(&[0x22, 0xF6, 0x00, 0xFF, 0x00, 0x42, 0x1A]),
225            0x12
226        );
227        assert_eq!(
228            bch::compute_bch_parity(&[0x8C, 0xC0, 0x0E, 0x01, 0x0D, 0x19, 0x06]),
229            0x5A
230        );
231    }
232
233    #[test]
234    fn cltu_encoding() {
235        let tc_frame: &[u8] = &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
236        let mut cltu_buffer = [0u8; 34]; // 2 (start) + 2*8 (blocks) + 8 (tail) = 26 bytes needed
237
238        let len = encode_cltu(tc_frame, &mut cltu_buffer).unwrap();
239        assert_eq!(len, 26);
240
241        let expected_start = &cltu_buffer[0..2];
242        assert_eq!(expected_start, &[0xEB, 0x90]);
243
244        // First block: 01,02,03,04,05,06,07 -> Parity 0x70
245        assert_eq!(
246            cltu_buffer[2..9],
247            [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]
248        );
249        assert_eq!(cltu_buffer[9], 0x70);
250
251        // Second block: 08,55,55,55,55,55,55 -> Parity 0x90
252        assert_eq!(
253            cltu_buffer[10..17],
254            [0x08, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55]
255        );
256        assert_eq!(cltu_buffer[17], 0x90);
257
258        let expected_tail = &cltu_buffer[18..26];
259        assert_eq!(expected_tail, &[0xC5; 8]);
260    }
261}