Skip to main content

MSP_ATTITUDE (108)

Returns the aircraft's current orientation angles (roll, pitch, yaw).

Request​

Empty payload (0 bytes).

Response Structure​

OffsetTypeFieldRangeDescription
0-1I16roll-1800 to +1800Roll angle × 10 (degrees)
2-3I16pitch-900 to +900Pitch angle × 10 (degrees)
4-5I16yaw0 to 3600Heading × 1 (degrees, 0-360)

Total size: 6 bytes

Angle Conventions​

  • Roll: Positive = right wing down, Negative = left wing down
  • Pitch: Positive = nose up, Negative = nose down
  • Yaw: Heading in degrees (0 = North, 90 = East, etc.)
         Nose Up (+pitch)
↑
|
Left ←────┼────→ Right (+roll)
|
↓
Nose Down (-pitch)

Rust Implementation​

/// MSP_ATTITUDE response payload
#[derive(Debug, Clone, Copy, Default)]
pub struct MspAttitude {
/// Roll angle in tenths of degrees (-1800 to +1800)
pub roll_raw: i16,
/// Pitch angle in tenths of degrees (-900 to +900)
pub pitch_raw: i16,
/// Heading in degrees (0 to 360)
pub yaw_raw: i16,
}

impl MspAttitude {
pub const COMMAND: u8 = 108;
pub const SIZE: usize = 6;

/// Parse from MSP response payload
pub fn from_payload(data: &[u8]) -> Option<Self> {
if data.len() < Self::SIZE {
return None;
}

Some(MspAttitude {
roll_raw: i16::from_le_bytes([data[0], data[1]]),
pitch_raw: i16::from_le_bytes([data[2], data[3]]),
yaw_raw: i16::from_le_bytes([data[4], data[5]]),
})
}

/// Roll angle in degrees (-180.0 to +180.0)
pub fn roll(&self) -> f32 {
self.roll_raw as f32 / 10.0
}

/// Pitch angle in degrees (-90.0 to +90.0)
pub fn pitch(&self) -> f32 {
self.pitch_raw as f32 / 10.0
}

/// Heading in degrees (0.0 to 360.0)
pub fn yaw(&self) -> f32 {
self.yaw_raw as f32
}

/// Roll angle in radians
pub fn roll_rad(&self) -> f32 {
self.roll() * core::f32::consts::PI / 180.0
}

/// Pitch angle in radians
pub fn pitch_rad(&self) -> f32 {
self.pitch() * core::f32::consts::PI / 180.0
}

/// Heading in radians (0 to 2π)
pub fn yaw_rad(&self) -> f32 {
self.yaw() * core::f32::consts::PI / 180.0
}

/// Check if aircraft is approximately level
pub fn is_level(&self, tolerance_deg: f32) -> bool {
self.roll().abs() < tolerance_deg && self.pitch().abs() < tolerance_deg
}

/// Check if aircraft is inverted (upside down)
pub fn is_inverted(&self) -> bool {
self.roll().abs() > 90.0
}
}

impl core::fmt::Display for MspAttitude {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"Roll: {:+.1}° Pitch: {:+.1}° Yaw: {:.0}°",
self.roll(),
self.pitch(),
self.yaw()
)
}
}

Raw Hex Example​

Request frame:

24 4D 3C 00 6C 6C
│ │ │ │ │ └─ Checksum (0 ^ 108 = 108 = 0x6C)
│ │ │ │ └──── Command (108 = 0x6C)
│ │ │ └─────── Size (0)
│ │ └────────── Direction (<)
│ └───────────── Header M
└──────────────── Header $

Response frame (aircraft level, heading north):

24 4D 3E 06 6C 00 00 00 00 00 00 6A
│ │ │ │ │ │
│ │ │ │ └──┴─ yaw = 0x0000 = 0° (North)
│ │ └──┴─────── pitch = 0x0000 = 0°
└──┴───────────── roll = 0x0000 = 0°

Response frame (banked right 15°, pitched up 5°, heading east):

24 4D 3E 06 6C 96 00 32 00 5A 00 XX
│ │ │ │ │ │
│ │ │ │ └──┴─ yaw = 0x005A = 90° (East)
│ │ └──┴─────── pitch = 0x0032 = 50 → 5.0°
└──┴───────────── roll = 0x0096 = 150 → 15.0°

Usage Example​

fn main() {
// Example: Aircraft banked right 15°, pitched up 5°, heading east
let payload = [
0x96, 0x00, // roll = 150 (15.0°)
0x32, 0x00, // pitch = 50 (5.0°)
0x5A, 0x00, // yaw = 90 (90°)
];

if let Some(attitude) = MspAttitude::from_payload(&payload) {
println!("{}", attitude);
println!("Level: {}", attitude.is_level(5.0));
println!("Inverted: {}", attitude.is_inverted());

// Convert to radians for navigation calculations
println!(
"Radians: roll={:.3} pitch={:.3} yaw={:.3}",
attitude.roll_rad(),
attitude.pitch_rad(),
attitude.yaw_rad()
);
}
}

Output:

Roll: +15.0° Pitch: +5.0° Yaw: 90°
Level: false
Inverted: false
Radians: roll=0.262 pitch=0.087 yaw=1.571

Quaternion Conversion​

For applications requiring quaternion representation:

/// Convert Euler angles to quaternion (ZYX rotation order)
pub fn euler_to_quaternion(roll: f32, pitch: f32, yaw: f32) -> [f32; 4] {
let cr = (roll / 2.0).cos();
let sr = (roll / 2.0).sin();
let cp = (pitch / 2.0).cos();
let sp = (pitch / 2.0).sin();
let cy = (yaw / 2.0).cos();
let sy = (yaw / 2.0).sin();

[
cr * cp * cy + sr * sp * sy, // w
sr * cp * cy - cr * sp * sy, // x
cr * sp * cy + sr * cp * sy, // y
cr * cp * sy - sr * sp * cy, // z
]
}

impl MspAttitude {
/// Convert to quaternion [w, x, y, z]
pub fn to_quaternion(&self) -> [f32; 4] {
euler_to_quaternion(self.roll_rad(), self.pitch_rad(), self.yaw_rad())
}
}

Polling Rate Recommendations​

ApplicationRateInterval
Real-time display20-50 Hz20-50 ms
Data logging10-20 Hz50-100 ms
Stabilization feedback50-100 Hz10-20 ms
Waypoint navigation5-10 Hz100-200 ms

Notes​

  1. Yaw vs Heading: The yaw value represents magnetic heading if a magnetometer is present, otherwise it's relative to the initial orientation.

  2. Angle Limits: Roll and pitch are typically limited to ±180° and ±90° respectively, but exact limits depend on firmware configuration.

  3. Update Rate: Attitude is computed at the gyro sample rate but MSP responses are limited by serial port bandwidth.

  4. Calibration: Accurate attitude requires calibrated accelerometer and gyroscope. Use MSP_ACC_CALIBRATION (205) for level calibration.