1pub const MSP_SET_RAW_RC: u16 = 200;
32
33pub const DIR_TO_FC: u8 = b'<';
35pub const DIR_FROM_FC: u8 = b'>';
37pub const DIR_ERROR: u8 = b'!';
39
40pub fn crc8_dvb_s2(mut crc: u8, byte: u8) -> u8 {
43 crc ^= byte;
44 for _ in 0..8 {
45 if crc & 0x80 != 0 {
46 crc = (crc << 1) ^ 0xD5;
47 } else {
48 crc <<= 1;
49 }
50 }
51 crc
52}
53
54pub fn crc8_dvb_s2_buf(data: &[u8]) -> u8 {
56 data.iter().fold(0u8, |c, &b| crc8_dvb_s2(c, b))
57}
58
59pub fn encode_v1(cmd: u16, payload: &[u8]) -> Option<Vec<u8>> {
64 if cmd > u8::MAX as u16 || payload.len() > u8::MAX as usize {
65 return None;
66 }
67 let size = payload.len() as u8;
68 let cmd = cmd as u8;
69 let mut out = Vec::with_capacity(6 + payload.len());
70 out.extend_from_slice(&[b'$', b'M', DIR_TO_FC, size, cmd]);
71 out.extend_from_slice(payload);
72 let mut crc = size ^ cmd;
73 for &b in payload {
74 crc ^= b;
75 }
76 out.push(crc);
77 Some(out)
78}
79
80pub fn encode_v2(cmd: u16, payload: &[u8]) -> Vec<u8> {
82 encode_v2_flagged(0, cmd, payload)
83}
84
85pub fn encode_v2_flagged(flag: u8, cmd: u16, payload: &[u8]) -> Vec<u8> {
87 let size = payload.len() as u16;
88 let mut out = Vec::with_capacity(9 + payload.len());
89 out.extend_from_slice(&[b'$', b'X', DIR_TO_FC]);
90 let mut header = Vec::with_capacity(5);
93 header.push(flag);
94 header.extend_from_slice(&cmd.to_le_bytes());
95 header.extend_from_slice(&size.to_le_bytes());
96 out.extend_from_slice(&header);
97 out.extend_from_slice(payload);
98 let mut crc = crc8_dvb_s2_buf(&header);
99 for &b in payload {
100 crc = crc8_dvb_s2(crc, b);
101 }
102 out.push(crc);
103 out
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct MspFrame {
109 pub cmd: u16,
110 pub dir: u8,
112 pub payload: Vec<u8>,
113 pub version: u8,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum MspDecodeError {
120 TooShort,
122 BadPreamble,
124 BadCrc { expected: u8, found: u8 },
126}
127
128pub fn decode_frame(buf: &[u8]) -> Result<(MspFrame, usize), MspDecodeError> {
135 if buf.len() < 3 {
136 return Err(MspDecodeError::TooShort);
137 }
138 if buf[0] != b'$' {
139 return Err(MspDecodeError::BadPreamble);
140 }
141 match buf[1] {
142 b'M' => decode_v1(buf),
143 b'X' => decode_v2(buf),
144 _ => Err(MspDecodeError::BadPreamble),
145 }
146}
147
148fn decode_v1(buf: &[u8]) -> Result<(MspFrame, usize), MspDecodeError> {
149 if buf.len() < 6 {
151 return Err(MspDecodeError::TooShort);
152 }
153 let dir = buf[2];
154 let size = buf[3] as usize;
155 let cmd = buf[4] as u16;
156 let total = 6 + size;
157 if buf.len() < total {
158 return Err(MspDecodeError::TooShort);
159 }
160 let payload = &buf[5..5 + size];
161 let mut crc = buf[3] ^ buf[4];
162 for &b in payload {
163 crc ^= b;
164 }
165 let found = buf[total - 1];
166 if crc != found {
167 return Err(MspDecodeError::BadCrc {
168 expected: crc,
169 found,
170 });
171 }
172 Ok((
173 MspFrame {
174 cmd,
175 dir,
176 payload: payload.to_vec(),
177 version: 1,
178 },
179 total,
180 ))
181}
182
183fn decode_v2(buf: &[u8]) -> Result<(MspFrame, usize), MspDecodeError> {
184 if buf.len() < 9 {
186 return Err(MspDecodeError::TooShort);
187 }
188 let dir = buf[2];
189 let flag = buf[3];
190 let cmd = u16::from_le_bytes([buf[4], buf[5]]);
191 let size = u16::from_le_bytes([buf[6], buf[7]]) as usize;
192 let total = 9 + size;
193 if buf.len() < total {
194 return Err(MspDecodeError::TooShort);
195 }
196 let payload = &buf[8..8 + size];
197 let mut crc = crc8_dvb_s2_buf(&buf[3..8]); let _ = flag; for &b in payload {
200 crc = crc8_dvb_s2(crc, b);
201 }
202 let found = buf[total - 1];
203 if crc != found {
204 return Err(MspDecodeError::BadCrc {
205 expected: crc,
206 found,
207 });
208 }
209 Ok((
210 MspFrame {
211 cmd,
212 dir,
213 payload: payload.to_vec(),
214 version: 2,
215 },
216 total,
217 ))
218}
219
220pub fn bipolar_to_pwm(v: f32) -> u16 {
224 let clamped = v.clamp(-1.0, 1.0);
225 (1500.0 + clamped * 500.0).round() as u16
226}
227
228pub fn throttle_to_pwm(t: f32) -> u16 {
232 let clamped = t.clamp(0.0, 1.0);
233 (1000.0 + clamped * 1000.0).round() as u16
234}
235
236pub fn set_raw_rc(channels: &[u16], v2: bool) -> Option<Vec<u8>> {
241 let mut payload = Vec::with_capacity(channels.len() * 2);
242 for &ch in channels {
243 payload.extend_from_slice(&ch.to_le_bytes());
244 }
245 if v2 {
246 Some(encode_v2(MSP_SET_RAW_RC, &payload))
247 } else {
248 encode_v1(MSP_SET_RAW_RC, &payload)
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
261 fn crc8_dvb_s2_matches_known_values() {
262 assert_eq!(crc8_dvb_s2_buf(&[0x00]), 0x00);
264 assert_eq!(crc8_dvb_s2_buf(&[0x01]), 0xD5);
266 }
267
268 #[test]
269 fn v1_msp_status_request_golden() {
270 let frame = encode_v1(101, &[]).unwrap();
272 assert_eq!(frame, vec![b'$', b'M', b'<', 0x00, 0x65, 0x65]);
273 }
274
275 #[test]
276 fn v1_with_payload_golden() {
277 let frame = encode_v1(200, &[0xE8, 0x03]).unwrap();
280 let crc = 2u8 ^ 200 ^ 0xE8 ^ 0x03;
281 assert_eq!(frame, vec![b'$', b'M', b'<', 0x02, 0xC8, 0xE8, 0x03, crc]);
282 }
283
284 #[test]
285 fn v1_rejects_oversize() {
286 assert!(encode_v1(256, &[]).is_none()); assert!(encode_v1(1, &vec![0u8; 256]).is_none()); }
289
290 #[test]
291 fn v2_msp_status_request_golden() {
292 let frame = encode_v2(101, &[]);
297 assert_eq!(
298 frame,
299 vec![b'$', b'X', b'<', 0x00, 0x65, 0x00, 0x00, 0x00, 0xCA]
300 );
301 }
302
303 #[test]
304 fn set_raw_rc_v2_golden() {
305 let bytes = set_raw_rc(&[1500, 1500], true).unwrap();
308 assert_eq!(
309 bytes,
310 vec![b'$', b'X', b'<', 0x00, 0xC8, 0x00, 0x04, 0x00, 0xDC, 0x05, 0xDC, 0x05, 0xB4]
311 );
312 }
313
314 #[test]
315 fn decode_roundtrips_v1_and_v2() {
316 for &v2 in &[false, true] {
317 let bytes = set_raw_rc(&[1000, 1500, 2000, 1500], v2).unwrap();
318 let (frame, consumed) = decode_frame(&bytes).unwrap();
319 assert_eq!(consumed, bytes.len());
320 assert_eq!(frame.cmd, MSP_SET_RAW_RC);
321 assert_eq!(frame.dir, DIR_TO_FC);
322 assert_eq!(frame.version, if v2 { 2 } else { 1 });
323 let (pairs, rest) = frame.payload.as_chunks::<2>();
325 assert!(rest.is_empty(), "an RC payload is whole u16 channels");
326 let chans: Vec<u16> = pairs.iter().copied().map(u16::from_le_bytes).collect();
327 assert_eq!(chans, vec![1000, 1500, 2000, 1500]);
328 }
329 }
330
331 #[test]
332 fn decode_rejects_a_corrupt_crc() {
333 let mut bytes = set_raw_rc(&[1500], true).unwrap();
334 let last = bytes.len() - 1;
335 bytes[last] ^= 0xFF; assert!(matches!(
337 decode_frame(&bytes),
338 Err(MspDecodeError::BadCrc { .. })
339 ));
340 let mut v1 = set_raw_rc(&[1500], false).unwrap();
342 let l = v1.len() - 1;
343 v1[l] ^= 0xFF;
344 assert!(matches!(
345 decode_frame(&v1),
346 Err(MspDecodeError::BadCrc { .. })
347 ));
348 }
349
350 #[test]
351 fn decode_reports_short_and_bad_preamble() {
352 assert_eq!(decode_frame(b"$"), Err(MspDecodeError::TooShort));
353 assert_eq!(
354 decode_frame(&[b'$', b'Z', b'<', 0, 0, 0]),
355 Err(MspDecodeError::BadPreamble)
356 );
357 assert_eq!(
359 decode_frame(&[b'$', b'X', b'<', 0, 0xC8, 0, 4, 0]),
360 Err(MspDecodeError::TooShort)
361 );
362 }
363
364 #[test]
365 fn stick_scaling_matches_the_gcs() {
366 assert_eq!(bipolar_to_pwm(0.0), 1500);
368 assert_eq!(bipolar_to_pwm(1.0), 2000);
369 assert_eq!(bipolar_to_pwm(-1.0), 1000);
370 assert_eq!(bipolar_to_pwm(2.0), 2000); assert_eq!(bipolar_to_pwm(-2.0), 1000); assert_eq!(bipolar_to_pwm(0.5), 1750);
373 assert_eq!(throttle_to_pwm(0.0), 1000);
376 assert_eq!(throttle_to_pwm(1.0), 2000);
377 assert_eq!(throttle_to_pwm(0.5), 1500);
378 assert_eq!(throttle_to_pwm(-0.1), 1000); assert_eq!(throttle_to_pwm(1.1), 2000); }
381}