Payload Structures
This section provides detailed byte-by-byte documentation for the most commonly used MSP command payloads.
Data Type Conventions
| Type | Rust Type | Size | Description |
|---|---|---|---|
| U8 | u8 | 1 byte | Unsigned 8-bit integer |
| U16 | u16 | 2 bytes | Unsigned 16-bit, little-endian |
| U32 | u32 | 4 bytes | Unsigned 32-bit, little-endian |
| I8 | i8 | 1 byte | Signed 8-bit integer |
| I16 | i16 | 2 bytes | Signed 16-bit, little-endian |
| I32 | i32 | 4 bytes | Signed 32-bit, little-endian |
Byte Order
All multi-byte values in MSP are transmitted in little-endian format:
// Encoding a u16 value
let value: u16 = 0x1234;
let bytes = value.to_le_bytes(); // [0x34, 0x12]
// Decoding a u16 from bytes
let decoded = u16::from_le_bytes([bytes[0], bytes[1]]); // 0x1234
Common Patterns
Reading Fixed-Size Payloads
/// Generic payload reader trait
pub trait FromMspPayload: Sized {
fn from_payload(data: &[u8]) -> Option<Self>;
}
/// Example: Reading a 6-byte attitude payload
#[derive(Debug, Clone, Copy)]
pub struct Attitude {
pub roll: i16,
pub pitch: i16,
pub yaw: i16,
}
impl FromMspPayload for Attitude {
fn from_payload(data: &[u8]) -> Option<Self> {
if data.len() < 6 {
return None;
}
Some(Attitude {
roll: i16::from_le_bytes([data[0], data[1]]),
pitch: i16::from_le_bytes([data[2], data[3]]),
yaw: i16::from_le_bytes([data[4], data[5]]),
})
}
}
Writing Payloads
/// Generic payload writer trait
pub trait ToMspPayload {
fn to_payload(&self) -> Vec<u8>;
}
/// Example: Writing motor values
#[derive(Debug)]
pub struct MotorOutput {
pub motors: [u16; 8],
}
impl ToMspPayload for MotorOutput {
fn to_payload(&self) -> Vec<u8> {
let mut payload = Vec::with_capacity(16);
for motor in &self.motors {
payload.extend_from_slice(&motor.to_le_bytes());
}
payload
}
}
Variable-Length Payloads
Some payloads have variable length (e.g., RC channels, mode ranges):
/// Parse variable-length array of u16 values
pub fn parse_u16_array(data: &[u8]) -> Vec<u16> {
data.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect()
}
/// Parse array of fixed-size structs
pub fn parse_struct_array<T, F>(data: &[u8], struct_size: usize, parser: F) -> Vec<T>
where
F: Fn(&[u8]) -> Option<T>,
{
data.chunks(struct_size)
.filter_map(|chunk| parser(chunk))
.collect()
}
Scaling and Units
Many values use scaled integers instead of floating-point:
| Measurement | Raw Type | Scale | Unit |
|---|---|---|---|
| Attitude angles | I16 | ÷10 | degrees |
| Voltage | U16 | ÷100 | volts |
| Current | I16 | ÷100 | amps |
| Altitude | I32 | ÷100 | meters |
| GPS coordinates | I32 | ÷10^7 | degrees |
| Gyroscope | I16 | ÷4.096 | °/s |
| RSSI | U16 | ÷1023×100 | percent |
/// Convert raw attitude to degrees
pub fn raw_to_degrees(raw: i16) -> f32 {
raw as f32 / 10.0
}
/// Convert raw voltage to volts
pub fn raw_to_volts(raw: u16) -> f32 {
raw as f32 / 100.0
}
/// Convert raw GPS coordinate to degrees
pub fn raw_to_gps_degrees(raw: i32) -> f64 {
raw as f64 / 10_000_000.0
}
Payload Index
| Command | Name | Size | Description |
|---|---|---|---|
| 101 | MSP_STATUS | 13+ bytes | FC status and sensor flags |
| 108 | MSP_ATTITUDE | 6 bytes | Roll, pitch, yaw angles |
| 102 | MSP_RAW_IMU | 18 bytes | Accelerometer, gyro, magnetometer |
| 105 | MSP_RC | Variable | RC channel values |
| 104 | MSP_MOTOR | 16 bytes | Motor outputs |
| 112 | MSP_PID | Variable | PID coefficients |
| 110 | MSP_ANALOG | 9 bytes | Battery and RSSI |
Common Payload Structure Module
//! MSP payload structures and parsing utilities
mod status;
mod attitude;
mod raw_imu;
mod rc_channels;
mod motors;
mod pid;
mod analog;
pub use status::*;
pub use attitude::*;
pub use raw_imu::*;
pub use rc_channels::*;
pub use motors::*;
pub use pid::*;
pub use analog::*;
/// Trait for types that can be parsed from MSP payloads
pub trait MspPayload: Sized {
/// MSP command code for this payload type
const COMMAND: u8;
/// Minimum required payload size
const MIN_SIZE: usize;
/// Parse from raw bytes
fn parse(data: &[u8]) -> Option<Self>;
}
/// Generic MSP response wrapper
#[derive(Debug)]
pub struct MspResponse<T: MspPayload> {
pub command: u8,
pub payload: T,
}
impl<T: MspPayload> MspResponse<T> {
pub fn parse(command: u8, data: &[u8]) -> Option<Self> {
if command != T::COMMAND {
return None;
}
if data.len() < T::MIN_SIZE {
return None;
}
let payload = T::parse(data)?;
Some(MspResponse { command, payload })
}
}
Error Handling
/// MSP payload parsing errors
#[derive(Debug, Clone, Copy)]
pub enum PayloadError {
/// Payload too short
TooShort { expected: usize, got: usize },
/// Invalid field value
InvalidValue { field: &'static str },
/// Checksum mismatch
ChecksumError,
}
impl std::fmt::Display for PayloadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PayloadError::TooShort { expected, got } => {
write!(f, "Payload too short: expected {} bytes, got {}", expected, got)
}
PayloadError::InvalidValue { field } => {
write!(f, "Invalid value in field: {}", field)
}
PayloadError::ChecksumError => {
write!(f, "Checksum verification failed")
}
}
}
}
impl std::error::Error for PayloadError {}