Skip to main content

leodos_protocols/network/cfe/
tc.rs

1//! CFE-specific packet definitions, views, and builders.
2//!
3//! This module builds upon the generic CCSDS `SpacePacket` to provide
4//! types that match the exact memory layout of cFE Command and Telemetry messages.
5
6use crate::network::spp;
7use crate::network::spp::Apid;
8use crate::network::spp::PacketType;
9use crate::network::spp::PrimaryHeader;
10use crate::network::spp::SecondaryHeaderFlag;
11use crate::network::spp::SequenceCount;
12use crate::network::spp::SequenceFlag;
13use crate::network::spp::SpacePacket;
14use crate::utils::checksum_u8;
15use crate::utils::validate_checksum_u8;
16use bon::bon;
17use core::mem::size_of;
18use core::ops::Deref;
19use core::ops::DerefMut;
20use zerocopy::FromBytes;
21use zerocopy::Immutable;
22use zerocopy::IntoBytes;
23use zerocopy::KnownLayout;
24use zerocopy::Unaligned;
25
26/// A zero-copy view over a complete CFE command packet (headers + payload).
27/// This is the primary struct you will use to represent a command.
28/// ```text
29/// +------------------------------------+---------+
30/// | Field Name                         | Size    |
31/// +------------------------------------+---------+
32/// + -- Primary Header (6 bytes) ------ | ------- |
33/// |                                    |         |
34/// | - Packet Type is always Telecommand|         |
35/// | - Sec. Hdr. Flag is always Present |         |
36/// |                                    |         |
37/// + -- cFE Secondary Header (2 bytes)  | ------- |
38/// |                                    |         |
39/// | Function Code                      | 1 byte  |
40/// | Checksum                           | 1 byte  |
41/// |                                    |         |
42/// + -- User Data Field (Variable) ---- | ------- |
43/// |                                    |         |
44/// | Payload                            | 1-65534 |
45/// |                                    | bytes   |
46/// +------------------------------------+---------+
47/// ```
48#[repr(C)]
49#[derive(FromBytes, IntoBytes, Unaligned, Immutable, KnownLayout)]
50pub struct Telecommand {
51    /// CCSDS SPP primary header.
52    pub primary: PrimaryHeader,
53    /// CFE command secondary header containing function code and checksum.
54    pub secondary: TelecommandSecondaryHeader,
55    /// Variable-length command payload.
56    pub payload: [u8],
57}
58/// The CFE command secondary header (2 bytes).
59#[repr(C)]
60#[derive(IntoBytes, FromBytes, Unaligned, Immutable, KnownLayout, Default, Copy, Clone, Debug)]
61pub struct TelecommandSecondaryHeader {
62    function_code: u8,
63    checksum: u8,
64}
65
66impl TelecommandSecondaryHeader {
67    /// Returns the 8-bit function code.
68    pub fn function_code(&self) -> u8 {
69        self.function_code
70    }
71
72    /// Sets the 8-bit function code.
73    pub fn set_function_code(&mut self, function_code: u8) {
74        self.function_code = function_code;
75    }
76
77    /// Returns the 8-bit checksum value.
78    pub fn checksum(&self) -> u8 {
79        self.checksum
80    }
81
82    /// Sets the 8-bit checksum value.
83    pub fn set_checksum(&mut self, checksum: u8) {
84        self.checksum = checksum;
85    }
86}
87
88/// An error that can occur when building a CFE packet.
89#[derive(Debug, Copy, Clone, Eq, PartialEq, thiserror::Error)]
90pub enum TelecommandError {
91    /// The provided buffer is too small to hold the packet.
92    #[error("Buffer too small: required {required_len} bytes, but provided {provided_len} bytes")]
93    BufferTooSmall {
94        /// Minimum number of bytes needed.
95        required_len: usize,
96        /// Actual buffer size provided.
97        provided_len: usize,
98    },
99    /// The underlying SPP builder returned an error.
100    #[error("SpacePacket build error: {0}")]
101    SpacePacketBuildError(#[from] spp::BuildError),
102    /// The underlying SPP parser returned an error.
103    #[error("SpacePacket parse error: {0}")]
104    SpacePacketParseError(#[from] spp::ParseError),
105    /// The secondary header flag is not set to Present.
106    #[error("Secondary header flag is not set to Present")]
107    MissingSecondaryHeader,
108    /// The packet data field does not match the expected layout.
109    #[error("Packet data field does not match expected layout")]
110    PayloadMismatch,
111    /// The packet type does not match (e.g. telemetry instead of telecommand).
112    #[error("Packet type is not Telecommand")]
113    TypeMismatch,
114}
115
116impl Deref for Telecommand {
117    type Target = SpacePacket;
118
119    fn deref(&self) -> &Self::Target {
120        SpacePacket::ref_from_bytes(self.as_bytes())
121            .expect("Telecommand should always be a valid SpacePacket")
122    }
123}
124
125impl DerefMut for Telecommand {
126    fn deref_mut(&mut self) -> &mut Self::Target {
127        SpacePacket::mut_from_bytes(self.as_mut_bytes())
128            .expect("Telecommand should always be a valid SpacePacket")
129    }
130}
131
132impl<'a> TryFrom<&'a SpacePacket> for &'a Telecommand {
133    type Error = TelecommandError;
134
135    fn try_from(sp: &'a SpacePacket) -> Result<Self, Self::Error> {
136        if sp.secondary_header_flag() != SecondaryHeaderFlag::Present {
137            return Err(TelecommandError::MissingSecondaryHeader);
138        }
139
140        let bytes = sp.as_bytes();
141
142        match sp.packet_type() {
143            PacketType::Telecommand => {
144                Telecommand::ref_from_bytes(bytes).map_err(|_| TelecommandError::PayloadMismatch)
145            }
146            PacketType::Telemetry => Err(TelecommandError::TypeMismatch),
147        }
148    }
149}
150
151#[bon]
152impl Telecommand {
153    #[builder]
154    /// Creates a new telecommand packet in the provided buffer.
155    pub fn new<'a>(
156        buffer: &'a mut [u8],
157        apid: Apid,
158        sequence_count: SequenceCount,
159        function_code: u8,
160        payload_len: usize,
161    ) -> Result<&'a mut Telecommand, TelecommandError> {
162        let sp = SpacePacket::builder()
163            .buffer(buffer)
164            .apid(apid)
165            .packet_type(PacketType::Telecommand)
166            .sequence_count(sequence_count)
167            .secondary_header(SecondaryHeaderFlag::Present)
168            .sequence_flag(SequenceFlag::Unsegmented)
169            .data_len(size_of::<TelecommandSecondaryHeader>() + payload_len)
170            .build()
171            .map_err(TelecommandError::SpacePacketBuildError)?;
172
173        let buffer = sp.as_mut_bytes();
174        let provided_len = buffer.len();
175        let required_len = payload_len;
176        let tc = Telecommand::mut_from_bytes_with_elems(buffer, required_len).map_err(|_| {
177            TelecommandError::BufferTooSmall {
178                required_len,
179                provided_len,
180            }
181        })?;
182
183        tc.set_function_code(function_code);
184        tc.set_cfe_checksum();
185
186        Ok(tc)
187    }
188
189    /// Minimum size of a telecommand packet (primary + secondary headers).
190    pub const fn size_minimum() -> usize {
191        size_of::<PrimaryHeader>() + size_of::<TelecommandSecondaryHeader>()
192    }
193
194    /// Returns the function code from the secondary header.
195    pub fn function_code(&self) -> u8 {
196        self.secondary.function_code()
197    }
198    /// Sets the function code in the secondary header.
199    pub fn set_function_code(&mut self, function_code: u8) {
200        self.secondary.set_function_code(function_code);
201    }
202
203    /// Calculates and sets the 8-bit cFE checksum for this command packet.
204    ///
205    /// The algorithm is a byte-wise XOR sum of the entire packet,
206    /// with the checksum field itself treated as zero during calculation.
207    pub fn set_cfe_checksum(&mut self) {
208        // Temporarily set the checksum byte to 0 for calculation.
209        self.secondary.set_checksum(0);
210        self.secondary.set_checksum(checksum_u8(self.as_bytes()));
211    }
212
213    /// Validates the 8-bit cFE checksum.
214    ///
215    /// Returns `true` if the checksum is valid, `false` otherwise.
216    pub fn validate_cfe_checksum(&self) -> bool {
217        validate_checksum_u8(self.as_bytes())
218    }
219
220    /// Returns the command payload bytes.
221    pub fn payload(&self) -> &[u8] {
222        &self.payload
223    }
224
225    /// Returns a mutable reference to the command payload bytes.
226    pub fn payload_mut(&mut self) -> &mut [u8] {
227        &mut self.payload
228    }
229
230    /// Parses a byte slice as a CFE telecommand packet.
231    pub fn parse<'a>(bytes: &'a [u8]) -> Result<&'a Telecommand, TelecommandError> {
232        let sp = SpacePacket::parse(bytes).map_err(TelecommandError::SpacePacketParseError)?;
233        <&'a Telecommand>::try_from(sp)
234    }
235
236    /// Returns a reference to the underlying `SpacePacket`.
237    pub fn as_spacepacket(&self) -> &SpacePacket {
238        &**self
239    }
240}