Skip to main content

leodos_protocols/network/isl/gossip/
packet.rs

1use bon::bon;
2use zerocopy::FromBytes;
3use zerocopy::Immutable;
4use zerocopy::IntoBytes;
5use zerocopy::KnownLayout;
6use zerocopy::Unaligned;
7use zerocopy::network_endian::U16;
8
9use crate::network::cfe::tc::Telecommand;
10use crate::network::cfe::tc::TelecommandError;
11use crate::network::cfe::tc::TelecommandSecondaryHeader;
12use crate::network::isl::address::Address;
13use crate::network::isl::address::RawAddress;
14use crate::network::spp::Apid;
15use crate::network::spp::PrimaryHeader;
16use crate::network::spp::SequenceCount;
17use crate::utils::checksum_u8;
18use crate::utils::validate_checksum_u8;
19
20/// A zero-copy view over a complete `GossipTelecommand` in a raw byte buffer.
21///
22/// This is a specialized `Telecommand` where the payload contains a `GossipHeader`
23/// that provides information for duplicate detection (`epoch`) and sender
24/// identification, followed by the actual data being gossiped.
25///
26/// ```text
27/// +---------------------------------+-----------+
28/// | Field Name                      | Size      |
29/// +---------------------------------+-----------+
30/// + -- cFE Telecommand Hdrs ------- | --------- |
31/// |                                 |           |
32/// | Primary Header                  | 6 bytes   |
33/// | Secondary Header                | 2 bytes   |
34/// |                                 |           |
35/// + -- Gossip Header -------------- | --------- |
36/// |                                 |           |
37/// | Origin Address                  | 2 bytes   |
38/// | From Address                    | 2 bytes   |
39/// | Service Area Min                | 1 byte    |
40/// | Service Area Max                | 1 byte    |
41/// | Epoch                           | 2 bytes   |
42/// | Action Code                     | 1 byte    |
43/// |                                 |           |
44/// | -- Gossip Payload (Variable) -- | --------- |
45/// |                                 |           |
46/// | Data being Gossiped             | 0-65524 B |
47/// +---------------------------------+-----------+
48/// ```
49#[repr(C)]
50#[derive(FromBytes, IntoBytes, KnownLayout, Unaligned, Immutable)]
51pub struct IslGossipTelecommand {
52    /// CCSDS SPP primary header.
53    pub primary: PrimaryHeader,
54    /// CFE command secondary header.
55    pub secondary: TelecommandSecondaryHeader,
56    /// ISL-specific gossip header.
57    pub gossip_header: IslGossipHeader,
58    /// Variable-length gossip data payload.
59    pub payload: [u8],
60}
61
62impl core::fmt::Debug for IslGossipTelecommand {
63    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64        f.debug_struct("GossipTelecommand")
65            .field("primary", &self.primary)
66            .field("secondary", &self.secondary)
67            .field("gossip_header", &self.gossip_header)
68            .field("payload_len", &self.payload.len())
69            .finish()
70    }
71}
72
73/// A gossip epoch number used for duplicate detection.
74#[derive(
75    Clone, Copy, Debug, PartialEq, Eq, IntoBytes, FromBytes, Immutable, KnownLayout, Unaligned,
76)]
77#[repr(transparent)]
78pub struct Epoch(pub U16);
79
80/// The ISL-specific header for a gossip message.
81#[repr(C, packed)]
82#[derive(FromBytes, IntoBytes, KnownLayout, Unaligned, Immutable, Copy, Clone, Debug)]
83pub struct IslGossipHeader {
84    origin: RawAddress,
85    predecessor: RawAddress,
86    pub(crate) service_area_min: u8,
87    pub(crate) service_area_max: u8,
88    epoch: Epoch,
89}
90
91/// An error that can occur when building or parsing a Gossip message.
92#[derive(Debug, Copy, Clone, Eq, PartialEq, thiserror::Error)]
93pub enum GossipMessageError {
94    /// An error from the underlying CFE telecommand layer.
95    #[error("CFE Telecommand error: {0}")]
96    Cfe(#[from] TelecommandError),
97    /// The payload is too small to contain a gossip header.
98    #[error("Payload too small to contain a valid gossip header")]
99    PayloadTooSmall,
100    /// The payload exceeds the maximum allowed size.
101    #[error(
102        "Payload too large: maximum allowed is {max} bytes, but {provided} bytes were provided"
103    )]
104    PayloadTooLarge {
105        /// Maximum allowed payload size.
106        max: usize,
107        /// Actual payload size provided.
108        provided: usize,
109    },
110}
111
112impl IslGossipHeader {
113    /// Address of the node that originated this gossip.
114    pub fn origin(&self) -> Address {
115        self.origin.parse()
116    }
117
118    /// Sets the origin address in the gossip header.
119    pub fn set_origin(&mut self, addr: Address) {
120        self.origin = RawAddress::from(addr);
121    }
122
123    /// Address of the immediate sender (for routing — don't echo back).
124    pub fn predecessor(&self) -> Address {
125        self.predecessor.parse()
126    }
127
128    /// Sets the from address in the gossip header.
129    pub fn set_predecessor(&mut self, addr: Address) {
130        self.predecessor = RawAddress::from(addr);
131    }
132
133    /// The unique sequence number for this piece of gossip, used for duplicate detection.
134    pub fn epoch(&self) -> Epoch {
135        self.epoch
136    }
137
138    /// Sets the epoch number in the gossip header.
139    pub fn set_epoch(&mut self, epoch: Epoch) {
140        self.epoch = epoch;
141    }
142
143    /// The maximum service area (in hops) for this gossip. Nodes with a service area
144    /// greater than this value should not forward the gossip further.
145    pub fn service_area_min(&self) -> u8 {
146        self.service_area_min
147    }
148
149    /// Sets the minimum service area in the gossip header.
150    pub fn set_service_area_min(&mut self, area: u8) {
151        self.service_area_min = area;
152    }
153
154    /// The maximum service area (in hops) for this gossip. Nodes with a service area
155    /// greater than this value should not forward the gossip further.
156    pub fn service_area_max(&self) -> u8 {
157        self.service_area_max
158    }
159
160    /// Sets the maximum service area in the gossip header.
161    pub fn set_service_area_max(&mut self, area: u8) {
162        self.service_area_max = area;
163    }
164}
165
166#[bon]
167impl IslGossipTelecommand {
168    /// Builder for creating a complete ISL Gossip message.
169    #[builder]
170    pub fn new<'a>(
171        buffer: &'a mut [u8],
172        apid: Apid,
173        function_code: u8,
174        origin: Address,
175        predecessor: Address,
176        service_area_min: u8,
177        service_area_max: u8,
178        epoch: Epoch,
179        payload_len: usize,
180    ) -> Result<&'a mut Self, GossipMessageError> {
181        if payload_len > u16::MAX as usize {
182            return Err(GossipMessageError::PayloadTooLarge {
183                max: u16::MAX as usize,
184                provided: payload_len,
185            });
186        }
187
188        let tc_payload_len = size_of::<IslGossipHeader>() + payload_len;
189        let tc = Telecommand::builder()
190            .buffer(buffer)
191            .apid(apid)
192            .sequence_count(SequenceCount::new())
193            .function_code(function_code)
194            .payload_len(tc_payload_len)
195            .build()
196            .map_err(GossipMessageError::Cfe)?;
197
198        let buffer = tc.as_mut_bytes();
199        let provided_len = buffer.len();
200        let gossip_tc = Self::mut_from_bytes_with_elems(buffer, payload_len).map_err(|_| {
201            GossipMessageError::Cfe(TelecommandError::BufferTooSmall {
202                required_len: size_of::<PrimaryHeader>()
203                    + size_of::<TelecommandSecondaryHeader>()
204                    + tc_payload_len,
205                provided_len,
206            })
207        })?;
208
209        gossip_tc.gossip_header.set_origin(origin);
210        gossip_tc.gossip_header.set_predecessor(predecessor);
211        gossip_tc.gossip_header.service_area_min = service_area_min;
212        gossip_tc.gossip_header.service_area_max = service_area_max;
213        gossip_tc.gossip_header.set_epoch(epoch);
214
215        gossip_tc.set_cfe_checksum();
216
217        Ok(gossip_tc)
218    }
219
220    /// Safely parses a generic `Telecommand` as a `GossipTelecommand`.
221    pub fn from_telecommand(tc: &Telecommand) -> Result<&Self, GossipMessageError> {
222        if tc.payload().len() < size_of::<IslGossipHeader>() {
223            return Err(GossipMessageError::PayloadTooSmall);
224        }
225        Ok(Self::ref_from_bytes(tc.as_bytes()).unwrap())
226    }
227
228    /// Calculates and sets the 8-bit cFE checksum for this gossip packet.
229    pub fn set_cfe_checksum(&mut self) {
230        self.secondary.set_checksum(0);
231        self.secondary.set_checksum(checksum_u8(self.as_bytes()));
232    }
233
234    /// Returns `true` if the cFE checksum is valid.
235    pub fn validate_cfe_checksum(&self) -> bool {
236        validate_checksum_u8(self.as_bytes())
237    }
238
239    /// Returns the length of the gossip data payload in bytes.
240    pub fn data_len(&self) -> usize {
241        self.payload.len()
242    }
243}