MSP_ATTITUDE (108)
Returns the aircraft's current orientation angles (roll, pitch, yaw).
Request
Empty payload (0 bytes).
Response Structure
| Offset | Type | Field | Range | Description |
|---|---|---|---|---|
| 0-1 | I16 | roll | -1800 to +1800 | Roll angle × 10 (degrees) |
| 2-3 | I16 | pitch | -900 to +900 | Pitch angle × 10 (degrees) |
| 4-5 | I16 | yaw | 0 to 3600 | Heading × 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
| Application | Rate | Interval |
|---|---|---|
| Real-time display | 20-50 Hz | 20-50 ms |
| Data logging | 10-20 Hz | 50-100 ms |
| Stabilization feedback | 50-100 Hz | 10-20 ms |
| Waypoint navigation | 5-10 Hz | 100-200 ms |
Notes
-
Yaw vs Heading: The yaw value represents magnetic heading if a magnetometer is present, otherwise it's relative to the initial orientation.
-
Angle Limits: Roll and pitch are typically limited to ±180° and ±90° respectively, but exact limits depend on firmware configuration.
-
Update Rate: Attitude is computed at the gyro sample rate but MSP responses are limited by serial port bandwidth.
-
Calibration: Accurate attitude requires calibrated accelerometer and gyroscope. Use
MSP_ACC_CALIBRATION(205) for level calibration.