Skip to main content

leodos_protocols/datalink/framing/sdlp/
aos.rs

1//! Advanced Orbiting Systems Space Datalink Protocol (AOS-SDLP)
2//!
3//! Spec: https://ccsds.org/Pubs/732x0b4.pdf
4//!
5//! AOS Frames are fixed-length frames optimized for high-speed, mixed-media data
6//! (e.g., audio, video, and packets interleaved). They are the standard for
7//! Space Station (ISS) and modern constellation Inter-Satellite Links.
8
9use bon::bon;
10use zerocopy::FromBytes;
11use zerocopy::Immutable;
12use zerocopy::IntoBytes;
13use zerocopy::KnownLayout;
14use zerocopy::Unaligned;
15use zerocopy::byteorder::network_endian::U16;
16
17use crate::coding::randomizer::Randomizer;
18use crate::utils::get_bits_u8;
19use crate::utils::get_bits_u16;
20use crate::utils::set_bits_u8;
21use crate::utils::set_bits_u16;
22
23/// A zero-copy view over a CCSDS AOS Transfer Frame.
24///
25/// # Layout
26///
27/// ```text
28/// +------------------------------------+----------+
29/// | Field Name                         | Size     |
30/// +------------------------------------+----------+
31/// | -- Primary Header (6 bytes) ------ |          |
32/// | Version Number (01)                | 2 bits   |
33/// | Spacecraft ID                      | 8 bits   |
34/// | Virtual Channel ID                 | 6 bits   |
35/// | Virtual Channel Frame Count        | 24 bits  |
36/// | Replay Flag                        | 1 bit    |
37/// | Usage Flag (Spare)                 | 1 bit    |
38/// | Spare                              | 6 bits   |
39/// |                                    |          |
40/// | -- Insert Zone (Optional) -------- | Variable |
41/// | -- Data Field -------------------- | Variable |
42/// | -- Trailer (Optional) ------------ |          |
43/// | Frame Error Control (CRC)          | 2 bytes  |
44/// +------------------------------------+----------+
45/// ```
46#[repr(C, packed)]
47#[derive(IntoBytes, FromBytes, Unaligned, KnownLayout, Immutable)]
48pub struct AosTransferFrame {
49    /// The 6-byte primary header containing routing and sequencing fields.
50    pub header: AosPrimaryHeader,
51    /// The variable-length data field carrying the frame payload.
52    pub data_field: [u8],
53}
54
55/// The 6-byte Primary Header of an AOS Frame.
56#[repr(C)]
57#[derive(FromBytes, IntoBytes, KnownLayout, Unaligned, Immutable, Debug, Copy, Clone)]
58pub struct AosPrimaryHeader {
59    version_scid_vcid_field: U16,
60    vc_frame_count: [u8; 3],
61    replay_usage_spare_field: u8,
62}
63
64/// An error that can occur during AOS Transfer Frame construction.
65#[derive(Debug, Copy, Clone, Eq, PartialEq)]
66pub enum BuildError {
67    /// The provided Spacecraft ID is outside the valid 8-bit range.
68    InvalidScid(Scid),
69    /// The provided Virtual Channel ID is outside the valid 6-bit range.
70    InvalidVcid(Vcid),
71    /// The provided buffer is too small to hold the requested frame.
72    BufferTooSmall {
73        /// Minimum number of bytes needed for the frame.
74        required: usize,
75        /// Actual buffer size provided.
76        provided: usize,
77    },
78}
79
80/// An error that can occur during AOS Transfer Frame parsing.
81#[derive(Debug, Copy, Clone, Eq, PartialEq)]
82pub enum ParseError {
83    /// The provided slice is shorter than the 6-byte primary header.
84    TooShortForHeader,
85    /// The header version field does not match the expected AOS version.
86    InvalidVersion(u8),
87}
88
89/// Bitmasks for AOS Transfer Frame header fields.
90#[rustfmt::skip]
91pub mod bitmasks {
92    /// Bitmask for the 2-bit version number field.
93    pub const VERSION_MASK: u16    = 0b_11000000_00000000;
94    /// Bitmask for the 8-bit Spacecraft ID field.
95    pub const SCID_MASK: u16       = 0b_00111111_11000000;
96    /// Bitmask for the 6-bit Virtual Channel ID field.
97    pub const VCID_MASK: u16       = 0b_00000000_00111111;
98
99    /// Bitmask for the 1-bit replay flag.
100    pub const REPLAY_FLAG_MASK: u8 = 0b_10000000;
101    /// Bitmask for the 1-bit usage/spare flag.
102    pub const USAGE_FLAG_MASK: u8  = 0b_01000000;
103    /// Bitmask for the 6-bit spare field.
104    pub const _SPARE_MASK: u8      = 0b_00111111;
105}
106
107use bitmasks::*;
108use crate::ids::{Scid, Vcid};
109
110#[bon]
111impl AosTransferFrame {
112    /// The AOS Transfer Frame version number (01 binary).
113    pub const AOS_VERSION: u8 = 0b01;
114
115    /// Parses a raw byte slice into a zero-copy AOS Frame view.
116    ///
117    /// Optionally applies de-randomization if a randomizer is provided.
118    pub fn parse<'a>(
119        bytes: &[u8],
120        output_buffer: &'a mut [u8],
121        randomizer: Option<&impl Randomizer>,
122    ) -> Result<&'a AosTransferFrame, ParseError> {
123        if bytes.len() < size_of::<AosPrimaryHeader>() {
124            return Err(ParseError::TooShortForHeader);
125        }
126        if output_buffer.len() < bytes.len() {
127            // In a real implementation, handle this gracefully
128            return Err(ParseError::TooShortForHeader);
129        }
130
131        // Copy input to output
132        let frame_buf = &mut output_buffer[..bytes.len()];
133        frame_buf.copy_from_slice(bytes);
134
135        // De-randomize in place if needed
136        if let Some(r) = randomizer {
137            r.apply(frame_buf);
138        }
139
140        // Cast
141        let frame = AosTransferFrame::ref_from_bytes(frame_buf)
142            .map_err(|_| ParseError::TooShortForHeader)?;
143
144        // Validate Version
145        if frame.header.version() != Self::AOS_VERSION {
146            return Err(ParseError::InvalidVersion(frame.header.version()));
147        }
148
149        Ok(frame)
150    }
151
152    /// Constructs a new AOS Transfer Frame in the provided buffer.
153    #[builder]
154    pub fn new<'a>(
155        buffer: &'a mut [u8],
156        scid: Scid,
157        vcid: Vcid,
158        vc_frame_count: u32,
159        replay_flag: bool,
160        usage_flag: bool,
161        payload: &'a [u8],
162    ) -> Result<&'a mut Self, BuildError> {
163        let total_len = size_of::<AosPrimaryHeader>() + payload.len();
164        if buffer.len() < total_len {
165            return Err(BuildError::BufferTooSmall {
166                required: total_len,
167                provided: buffer.len(),
168            });
169        }
170        if vcid.num_bits() > 6 {
171            return Err(BuildError::InvalidVcid(vcid));
172        }
173
174        let frame = AosTransferFrame::mut_from_bytes(&mut buffer[..total_len]).unwrap();
175
176        frame.header.set_version(Self::AOS_VERSION);
177        frame.header.set_scid(scid);
178        frame.header.set_vcid(vcid);
179        frame.header.set_vc_frame_count(vc_frame_count);
180        frame.header.set_replay(replay_flag);
181        frame.header.set_usage_flag(usage_flag);
182        frame.data_field.copy_from_slice(payload);
183
184        Ok(frame)
185    }
186
187    /// Returns a reference to the frame's primary header.
188    pub fn header(&self) -> &AosPrimaryHeader {
189        &self.header
190    }
191
192    /// Returns a reference to the frame's data field.
193    pub fn data(&self) -> &[u8] {
194        &self.data_field
195    }
196}
197
198impl AosPrimaryHeader {
199    /// Returns the 2-bit Transfer Frame Version Number.
200    pub fn version(&self) -> u8 {
201        get_bits_u16(self.version_scid_vcid_field, VERSION_MASK) as u8
202    }
203    /// Sets the 2-bit Transfer Frame Version Number.
204    pub fn set_version(&mut self, version: u8) {
205        set_bits_u16(
206            &mut self.version_scid_vcid_field,
207            VERSION_MASK,
208            version as u16,
209        );
210    }
211
212    /// Returns the 8-bit Spacecraft ID.
213    pub fn scid(&self) -> Scid {
214        Scid::new(get_bits_u16(self.version_scid_vcid_field, SCID_MASK) as u32)
215    }
216    /// Sets the 8-bit Spacecraft ID.
217    pub fn set_scid(&mut self, scid: Scid) {
218        set_bits_u16(&mut self.version_scid_vcid_field, SCID_MASK, scid.get() as u16);
219    }
220
221    /// Returns the 6-bit Virtual Channel ID.
222    pub fn vcid(&self) -> Vcid {
223        Vcid::new(get_bits_u16(self.version_scid_vcid_field, VCID_MASK) as u32)
224    }
225    /// Sets the 6-bit Virtual Channel ID.
226    pub fn set_vcid(&mut self, vcid: Vcid) {
227        set_bits_u16(&mut self.version_scid_vcid_field, VCID_MASK, vcid.get() as u16);
228    }
229
230    /// Returns the 24-bit Virtual Channel Frame Count.
231    pub fn vc_frame_count(&self) -> u32 {
232        let b = self.vc_frame_count;
233        u32::from_be_bytes([0, b[0], b[1], b[2]])
234    }
235    /// Sets the 24-bit Virtual Channel Frame Count.
236    pub fn set_vc_frame_count(&mut self, count: u32) {
237        let bytes = count.to_be_bytes();
238        self.vc_frame_count.copy_from_slice(&bytes[1..4]);
239    }
240
241    /// Returns true if the replay flag is set.
242    pub fn is_replay(&self) -> bool {
243        get_bits_u8(self.replay_usage_spare_field, REPLAY_FLAG_MASK) != 0
244    }
245    /// Sets the replay flag.
246    pub fn set_replay(&mut self, replay: bool) {
247        set_bits_u8(
248            &mut self.replay_usage_spare_field,
249            REPLAY_FLAG_MASK,
250            if replay { 1 } else { 0 },
251        );
252    }
253
254    /// Returns the usage/spare flag value.
255    pub fn usage_flag(&self) -> bool {
256        get_bits_u8(self.replay_usage_spare_field, USAGE_FLAG_MASK) != 0
257    }
258    /// Sets the usage/spare flag value.
259    pub fn set_usage_flag(&mut self, usage: bool) {
260        set_bits_u8(
261            &mut self.replay_usage_spare_field,
262            USAGE_FLAG_MASK,
263            if usage { 1 } else { 0 },
264        );
265    }
266}
267
268impl AosTransferFrame {
269    /// The size of the AOS Transfer Frame primary header in bytes.
270    pub const HEADER_SIZE: usize = 6;
271
272    /// Parses a transfer frame without applying derandomization.
273    ///
274    /// Use this when the coding pipeline has already handled
275    /// derandomization.
276    pub fn parse_raw(bytes: &[u8]) -> Result<&AosTransferFrame, ParseError> {
277        if bytes.len() < Self::HEADER_SIZE {
278            return Err(ParseError::TooShortForHeader);
279        }
280        let frame = AosTransferFrame::ref_from_bytes(bytes)
281            .map_err(|_| ParseError::TooShortForHeader)?;
282        if frame.header.version() != Self::AOS_VERSION {
283            return Err(ParseError::InvalidVersion(frame.header.version()));
284        }
285        Ok(frame)
286    }
287}
288
289// ── FrameWrite / FrameRead implementations ──
290
291use super::super::{FrameRead, FrameWrite, PushError};
292
293/// Configuration for building AOS transfer frames.
294#[derive(Debug, Clone)]
295pub struct AosFrameWriterConfig {
296    /// Spacecraft ID (8-bit).
297    pub scid: Scid,
298    /// Virtual Channel ID (6-bit).
299    pub vcid: Vcid,
300    /// Maximum data field length in bytes.
301    pub max_data_field_len: usize,
302}
303
304/// Accumulates packets into AOS transfer frames.
305///
306/// Owns its frame buffer internally (sized by `BUF`). Packets
307/// are pushed directly into the buffer at the correct offset.
308/// [`finish()`](FrameWrite::finish) stamps the header and
309/// returns a borrow of the completed frame.
310pub struct AosFrameWriter<const BUF: usize> {
311    config: AosFrameWriterConfig,
312    vc_frame_count: u32,
313    data_len: usize,
314    buf: [u8; BUF],
315}
316
317impl<const BUF: usize> AosFrameWriter<BUF> {
318    /// Creates a new AOS frame writer.
319    pub fn new(config: AosFrameWriterConfig) -> Self {
320        Self {
321            config,
322            vc_frame_count: 0,
323            data_len: 0,
324            buf: [0u8; BUF],
325        }
326    }
327}
328
329impl<const BUF: usize> AosFrameWriter<BUF> {
330    fn remaining(&self) -> usize {
331        self.config
332            .max_data_field_len
333            .saturating_sub(self.data_len)
334    }
335}
336
337impl<const BUF: usize> FrameWrite for AosFrameWriter<BUF> {
338    type Error = BuildError;
339
340    fn is_empty(&self) -> bool {
341        self.data_len == 0
342    }
343
344    fn push(&mut self, data: &[u8]) -> Result<(), PushError> {
345        if data.len() > self.config.max_data_field_len {
346            return Err(PushError::TooLarge);
347        }
348        if data.len() > self.remaining() {
349            return Err(PushError::Full);
350        }
351        let off =
352            AosTransferFrame::HEADER_SIZE + self.data_len;
353        self.buf[off..off + data.len()].copy_from_slice(data);
354        self.data_len += data.len();
355        Ok(())
356    }
357
358    fn finish(&mut self) -> Result<&[u8], BuildError> {
359        let total =
360            AosTransferFrame::HEADER_SIZE + self.data_len;
361        let count = self.vc_frame_count;
362        self.vc_frame_count =
363            self.vc_frame_count.wrapping_add(1) & 0xFF_FFFF;
364
365        let buf_len = self.buf.len();
366        let frame =
367            AosTransferFrame::mut_from_bytes(&mut self.buf[..total])
368                .map_err(|_| BuildError::BufferTooSmall {
369                    required: total,
370                    provided: buf_len,
371                })?;
372
373        frame.header.set_version(AosTransferFrame::AOS_VERSION);
374        frame.header.set_scid(self.config.scid);
375        frame.header.set_vcid(self.config.vcid);
376        frame.header.set_vc_frame_count(count);
377        frame.header.set_replay(false);
378        frame.header.set_usage_flag(false);
379
380        self.data_len = 0;
381        Ok(&self.buf[..total])
382    }
383}
384
385/// Extracts packets from a received AOS transfer frame.
386///
387/// Owns its frame buffer internally (sized by `BUF`). The
388/// coding layer writes into
389/// [`buffer_mut()`](FrameRead::buffer_mut),
390/// [`feed()`](FrameRead::feed) validates the header, and
391/// [`next()`](FrameRead::next) returns zero-copy sub-slices.
392pub struct AosFrameReader<const BUF: usize> {
393    buf: [u8; BUF],
394    data_start: usize,
395    data_end: usize,
396}
397
398impl<const BUF: usize> AosFrameReader<BUF> {
399    /// Creates a new AOS frame reader.
400    pub fn new() -> Self {
401        Self {
402            buf: [0u8; BUF],
403            data_start: 0,
404            data_end: 0,
405        }
406    }
407}
408
409impl<const BUF: usize> FrameRead for AosFrameReader<BUF> {
410    type Error = ParseError;
411
412    fn buffer_mut(&mut self) -> &mut [u8] {
413        &mut self.buf
414    }
415
416    fn feed(&mut self, len: usize) -> Result<(), ParseError> {
417        let parsed =
418            AosTransferFrame::parse_raw(&self.buf[..len])?;
419        let data = parsed.data();
420        self.data_start = AosTransferFrame::HEADER_SIZE;
421        self.data_end = self.data_start + data.len();
422        Ok(())
423    }
424
425    fn data_field(&self) -> &[u8] {
426        &self.buf[self.data_start..self.data_end]
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use crate::ids::{Scid, Vcid};
434
435    #[test]
436    fn test_aos_builder_and_parser() {
437        let mut buf = [0u8; 1024];
438        let payload = [0xAA, 0xBB, 0xCC];
439
440        let frame = AosTransferFrame::builder()
441            .buffer(&mut buf)
442            .scid(Scid::new(0x12))
443            .vcid(Vcid::new(0x3F)) // Max VCID
444            .vc_frame_count(0x123456)
445            .replay_flag(true)
446            .usage_flag(false)
447            .payload(&payload)
448            .build()
449            .unwrap();
450
451        assert_eq!(frame.header.scid(), Scid::new(0x12));
452        assert_eq!(frame.header.vcid(), Vcid::new(0x3F));
453        assert_eq!(frame.header.vc_frame_count(), 0x123456);
454        assert!(frame.header.is_replay());
455        assert_eq!(frame.data(), &payload);
456    }
457}