uf_crsf/packets/
remote.rs

1use crate::packets::{CrsfPacket, PacketType};
2use crate::CrsfParsingError;
3use core::mem::size_of;
4
5// Sub-type for the Timing Correction packet
6const TIMING_CORRECTION_SUB_TYPE: u8 = 0x10;
7const TIMING_CORRECTION_PAYLOAD_SIZE: usize = size_of::<u32>() + size_of::<i32>();
8
9/// Represents a Remote-related packet (frame type 0x3A).
10///
11/// This is a container for various sub-packets related to remote functionality,
12/// identified by a sub-type.
13#[derive(Clone, Debug, PartialEq)]
14#[cfg_attr(feature = "defmt", derive(defmt::Format))]
15pub struct Remote {
16    pub dst_addr: u8,
17    pub src_addr: u8,
18    pub payload: RemotePayload,
19}
20
21impl Remote {
22    pub fn new(
23        dst_addr: u8,
24        src_addr: u8,
25        payload: RemotePayload,
26    ) -> Result<Self, CrsfParsingError> {
27        Ok(Self {
28            dst_addr,
29            src_addr,
30            payload,
31        })
32    }
33}
34
35/// Enum for the different payloads of a Remote packet.
36#[derive(Clone, Debug, PartialEq)]
37#[cfg_attr(feature = "defmt", derive(defmt::Format))]
38pub enum RemotePayload {
39    TimingCorrection(TimingCorrection),
40    // Future subtypes can be added here.
41}
42
43/// Represents a Timing Correction (CRSF Shot) sub-packet (sub-type 0x10).
44///
45/// This packet is used for timing synchronization between the transmitter and receiver.
46#[derive(Clone, Debug, PartialEq)]
47#[cfg_attr(feature = "defmt", derive(defmt::Format))]
48pub struct TimingCorrection {
49    /// Update interval in 100ns units.
50    pub update_interval: u32,
51    /// Timing offset in 100ns units.
52    /// Positive values mean the data came too early, negative means late.
53    pub offset: i32,
54}
55
56impl CrsfPacket for Remote {
57    const PACKET_TYPE: PacketType = PacketType::RadioId;
58    // Minimum payload for an extended header with a sub-type and its data.
59    // For TimingCorrection: 1 (dst) + 1 (src) + 1 (sub-type) + 8 (data) = 11 bytes
60    const MIN_PAYLOAD_SIZE: usize = 2 + 1 + TIMING_CORRECTION_PAYLOAD_SIZE;
61
62    fn from_bytes(data: &[u8]) -> Result<Self, CrsfParsingError> {
63        if data.len() < 3 {
64            return Err(CrsfParsingError::InvalidPayloadLength);
65        }
66
67        let dst_addr = data[0];
68        let src_addr = data[1];
69        let sub_type = data[2];
70        let sub_payload = &data[3..];
71
72        let payload = match sub_type {
73            TIMING_CORRECTION_SUB_TYPE => {
74                if sub_payload.len() < TIMING_CORRECTION_PAYLOAD_SIZE {
75                    return Err(CrsfParsingError::InvalidPayloadLength);
76                }
77                let timing_correction = TimingCorrection {
78                    update_interval: u32::from_be_bytes(
79                        sub_payload[0..size_of::<u32>()]
80                            .try_into()
81                            .expect("infallible due to length check"),
82                    ),
83                    offset: i32::from_be_bytes(
84                        sub_payload[size_of::<u32>()..TIMING_CORRECTION_PAYLOAD_SIZE]
85                            .try_into()
86                            .expect("infallible due to length check"),
87                    ),
88                };
89                RemotePayload::TimingCorrection(timing_correction)
90            }
91            _ => return Err(CrsfParsingError::InvalidPayload), // Unknown sub-type
92        };
93
94        Ok(Self {
95            dst_addr,
96            src_addr,
97            payload,
98        })
99    }
100
101    fn to_bytes(&self, buffer: &mut [u8]) -> Result<usize, CrsfParsingError> {
102        match &self.payload {
103            RemotePayload::TimingCorrection(p) => {
104                const LEN: usize = 2 + 1 + TIMING_CORRECTION_PAYLOAD_SIZE;
105                if buffer.len() < LEN {
106                    return Err(CrsfParsingError::BufferOverflow);
107                }
108                buffer[0] = self.dst_addr;
109                buffer[1] = self.src_addr;
110                buffer[2] = TIMING_CORRECTION_SUB_TYPE;
111                buffer[3..7].copy_from_slice(&p.update_interval.to_be_bytes());
112                buffer[7..11].copy_from_slice(&p.offset.to_be_bytes());
113                Ok(LEN)
114            }
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_timing_correction_from_bytes() {
125        // Full payload for a 0x3A packet
126        let data: [u8; 11] = [
127            0xEA, // dst_addr
128            0xEE, // src_addr
129            TIMING_CORRECTION_SUB_TYPE,
130            0x00,
131            0x00,
132            0xC3,
133            0x50, // update_interval = 50000
134            0xFF,
135            0xFF,
136            0xFF,
137            0xF9, // offset = -7
138        ];
139        let packet = Remote::from_bytes(&data).unwrap();
140        assert_eq!(packet.dst_addr, 0xEA);
141        assert_eq!(packet.src_addr, 0xEE);
142        match packet.payload {
143            RemotePayload::TimingCorrection(tc) => {
144                assert_eq!(tc.update_interval, 50000);
145                assert_eq!(tc.offset, -7);
146            }
147        }
148    }
149
150    #[test]
151    fn test_timing_correction_to_bytes() {
152        let packet = Remote {
153            dst_addr: 0xEA,
154            src_addr: 0xEE,
155            payload: RemotePayload::TimingCorrection(TimingCorrection {
156                update_interval: 50000,
157                offset: -7,
158            }),
159        };
160        let mut buffer = [0u8; 11];
161        let len = packet.to_bytes(&mut buffer).unwrap();
162        assert_eq!(len, 11);
163        let expected: [u8; 11] = [
164            0xEA,
165            0xEE,
166            TIMING_CORRECTION_SUB_TYPE,
167            0x00,
168            0x00,
169            0xC3,
170            0x50,
171            0xFF,
172            0xFF,
173            0xFF,
174            0xF9,
175        ];
176        assert_eq!(buffer, expected);
177    }
178
179    #[test]
180    fn test_remote_round_trip() {
181        let packet = Remote {
182            dst_addr: 0xC8,
183            src_addr: 0xEC,
184            payload: RemotePayload::TimingCorrection(TimingCorrection {
185                update_interval: 12345,
186                offset: -6789,
187            }),
188        };
189        let mut buffer = [0u8; 11];
190        packet.to_bytes(&mut buffer).unwrap();
191        let round_trip = Remote::from_bytes(&buffer).unwrap();
192        assert_eq!(packet, round_trip);
193    }
194
195    #[test]
196    fn test_from_bytes_invalid_len() {
197        let data: [u8; 2] = [0; 2];
198        let result = Remote::from_bytes(&data);
199        assert!(matches!(
200            result,
201            Err(CrsfParsingError::InvalidPayloadLength)
202        ));
203    }
204
205    #[test]
206    fn test_from_bytes_unknown_subtype() {
207        let data: [u8; 11] = [0xEA, 0xEE, 0x11, 0, 0, 0, 0, 0, 0, 0, 0];
208        let result = Remote::from_bytes(&data);
209        assert!(matches!(result, Err(CrsfParsingError::InvalidPayload)));
210    }
211}