pub const MSP_SET_RAW_RC: u16 = 200;
pub const DIR_TO_FC: u8 = b'<';
pub const DIR_FROM_FC: u8 = b'>';
pub const DIR_ERROR: u8 = b'!';
pub fn crc8_dvb_s2(mut crc: u8, byte: u8) -> u8 {
crc ^= byte;
for _ in 0..8 {
if crc & 0x80 != 0 {
crc = (crc << 1) ^ 0xD5;
} else {
crc <<= 1;
}
}
crc
}
pub fn crc8_dvb_s2_buf(data: &[u8]) -> u8 {
data.iter().fold(0u8, |c, &b| crc8_dvb_s2(c, b))
}
pub fn encode_v1(cmd: u16, payload: &[u8]) -> Option<Vec<u8>> {
if cmd > u8::MAX as u16 || payload.len() > u8::MAX as usize {
return None;
}
let size = payload.len() as u8;
let cmd = cmd as u8;
let mut out = Vec::with_capacity(6 + payload.len());
out.extend_from_slice(&[b'$', b'M', DIR_TO_FC, size, cmd]);
out.extend_from_slice(payload);
let mut crc = size ^ cmd;
for &b in payload {
crc ^= b;
}
out.push(crc);
Some(out)
}
pub fn encode_v2(cmd: u16, payload: &[u8]) -> Vec<u8> {
encode_v2_flagged(0, cmd, payload)
}
pub fn encode_v2_flagged(flag: u8, cmd: u16, payload: &[u8]) -> Vec<u8> {
let size = payload.len() as u16;
let mut out = Vec::with_capacity(9 + payload.len());
out.extend_from_slice(&[b'$', b'X', DIR_TO_FC]);
let mut header = Vec::with_capacity(5);
header.push(flag);
header.extend_from_slice(&cmd.to_le_bytes());
header.extend_from_slice(&size.to_le_bytes());
out.extend_from_slice(&header);
out.extend_from_slice(payload);
let mut crc = crc8_dvb_s2_buf(&header);
for &b in payload {
crc = crc8_dvb_s2(crc, b);
}
out.push(crc);
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MspFrame {
pub cmd: u16,
pub dir: u8,
pub payload: Vec<u8>,
pub version: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MspDecodeError {
TooShort,
BadPreamble,
BadCrc { expected: u8, found: u8 },
}
pub fn decode_frame(buf: &[u8]) -> Result<(MspFrame, usize), MspDecodeError> {
if buf.len() < 3 {
return Err(MspDecodeError::TooShort);
}
if buf[0] != b'$' {
return Err(MspDecodeError::BadPreamble);
}
match buf[1] {
b'M' => decode_v1(buf),
b'X' => decode_v2(buf),
_ => Err(MspDecodeError::BadPreamble),
}
}
fn decode_v1(buf: &[u8]) -> Result<(MspFrame, usize), MspDecodeError> {
if buf.len() < 6 {
return Err(MspDecodeError::TooShort);
}
let dir = buf[2];
let size = buf[3] as usize;
let cmd = buf[4] as u16;
let total = 6 + size;
if buf.len() < total {
return Err(MspDecodeError::TooShort);
}
let payload = &buf[5..5 + size];
let mut crc = buf[3] ^ buf[4];
for &b in payload {
crc ^= b;
}
let found = buf[total - 1];
if crc != found {
return Err(MspDecodeError::BadCrc {
expected: crc,
found,
});
}
Ok((
MspFrame {
cmd,
dir,
payload: payload.to_vec(),
version: 1,
},
total,
))
}
fn decode_v2(buf: &[u8]) -> Result<(MspFrame, usize), MspDecodeError> {
if buf.len() < 9 {
return Err(MspDecodeError::TooShort);
}
let dir = buf[2];
let flag = buf[3];
let cmd = u16::from_le_bytes([buf[4], buf[5]]);
let size = u16::from_le_bytes([buf[6], buf[7]]) as usize;
let total = 9 + size;
if buf.len() < total {
return Err(MspDecodeError::TooShort);
}
let payload = &buf[8..8 + size];
let mut crc = crc8_dvb_s2_buf(&buf[3..8]); let _ = flag; for &b in payload {
crc = crc8_dvb_s2(crc, b);
}
let found = buf[total - 1];
if crc != found {
return Err(MspDecodeError::BadCrc {
expected: crc,
found,
});
}
Ok((
MspFrame {
cmd,
dir,
payload: payload.to_vec(),
version: 2,
},
total,
))
}
pub fn bipolar_to_pwm(v: f32) -> u16 {
let clamped = v.clamp(-1.0, 1.0);
(1500.0 + clamped * 500.0).round() as u16
}
pub fn throttle_to_pwm(t: f32) -> u16 {
let clamped = t.clamp(0.0, 1.0);
(1000.0 + clamped * 1000.0).round() as u16
}
pub fn set_raw_rc(channels: &[u16], v2: bool) -> Option<Vec<u8>> {
let mut payload = Vec::with_capacity(channels.len() * 2);
for &ch in channels {
payload.extend_from_slice(&ch.to_le_bytes());
}
if v2 {
Some(encode_v2(MSP_SET_RAW_RC, &payload))
} else {
encode_v1(MSP_SET_RAW_RC, &payload)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crc8_dvb_s2_matches_known_values() {
assert_eq!(crc8_dvb_s2_buf(&[0x00]), 0x00);
assert_eq!(crc8_dvb_s2_buf(&[0x01]), 0xD5);
}
#[test]
fn v1_msp_status_request_golden() {
let frame = encode_v1(101, &[]).unwrap();
assert_eq!(frame, vec![b'$', b'M', b'<', 0x00, 0x65, 0x65]);
}
#[test]
fn v1_with_payload_golden() {
let frame = encode_v1(200, &[0xE8, 0x03]).unwrap();
let crc = 2u8 ^ 200 ^ 0xE8 ^ 0x03;
assert_eq!(frame, vec![b'$', b'M', b'<', 0x02, 0xC8, 0xE8, 0x03, crc]);
}
#[test]
fn v1_rejects_oversize() {
assert!(encode_v1(256, &[]).is_none()); assert!(encode_v1(1, &vec![0u8; 256]).is_none()); }
#[test]
fn v2_msp_status_request_golden() {
let frame = encode_v2(101, &[]);
assert_eq!(
frame,
vec![b'$', b'X', b'<', 0x00, 0x65, 0x00, 0x00, 0x00, 0xCA]
);
}
#[test]
fn set_raw_rc_v2_golden() {
let bytes = set_raw_rc(&[1500, 1500], true).unwrap();
assert_eq!(
bytes,
vec![b'$', b'X', b'<', 0x00, 0xC8, 0x00, 0x04, 0x00, 0xDC, 0x05, 0xDC, 0x05, 0xB4]
);
}
#[test]
fn decode_roundtrips_v1_and_v2() {
for &v2 in &[false, true] {
let bytes = set_raw_rc(&[1000, 1500, 2000, 1500], v2).unwrap();
let (frame, consumed) = decode_frame(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(frame.cmd, MSP_SET_RAW_RC);
assert_eq!(frame.dir, DIR_TO_FC);
assert_eq!(frame.version, if v2 { 2 } else { 1 });
let (pairs, rest) = frame.payload.as_chunks::<2>();
assert!(rest.is_empty(), "an RC payload is whole u16 channels");
let chans: Vec<u16> = pairs.iter().copied().map(u16::from_le_bytes).collect();
assert_eq!(chans, vec![1000, 1500, 2000, 1500]);
}
}
#[test]
fn decode_rejects_a_corrupt_crc() {
let mut bytes = set_raw_rc(&[1500], true).unwrap();
let last = bytes.len() - 1;
bytes[last] ^= 0xFF; assert!(matches!(
decode_frame(&bytes),
Err(MspDecodeError::BadCrc { .. })
));
let mut v1 = set_raw_rc(&[1500], false).unwrap();
let l = v1.len() - 1;
v1[l] ^= 0xFF;
assert!(matches!(
decode_frame(&v1),
Err(MspDecodeError::BadCrc { .. })
));
}
#[test]
fn decode_reports_short_and_bad_preamble() {
assert_eq!(decode_frame(b"$"), Err(MspDecodeError::TooShort));
assert_eq!(
decode_frame(&[b'$', b'Z', b'<', 0, 0, 0]),
Err(MspDecodeError::BadPreamble)
);
assert_eq!(
decode_frame(&[b'$', b'X', b'<', 0, 0xC8, 0, 4, 0]),
Err(MspDecodeError::TooShort)
);
}
#[test]
fn stick_scaling_matches_the_gcs() {
assert_eq!(bipolar_to_pwm(0.0), 1500);
assert_eq!(bipolar_to_pwm(1.0), 2000);
assert_eq!(bipolar_to_pwm(-1.0), 1000);
assert_eq!(bipolar_to_pwm(2.0), 2000); assert_eq!(bipolar_to_pwm(-2.0), 1000); assert_eq!(bipolar_to_pwm(0.5), 1750);
assert_eq!(throttle_to_pwm(0.0), 1000);
assert_eq!(throttle_to_pwm(1.0), 2000);
assert_eq!(throttle_to_pwm(0.5), 1500);
assert_eq!(throttle_to_pwm(-0.1), 1000); assert_eq!(throttle_to_pwm(1.1), 2000); }
}