Skip to main content

Telemetry Commands (101-166)

These commands provide real-time flight data including status, sensors, RC inputs, and motor outputs.

MSP_STATUS (101)​

Query flight controller status including sensor availability, flight modes, and system load.

Response​

OffsetTypeFieldDescription
0-1U16cycle_timePID loop time in µs
2-3U16i2c_error_countI2C bus errors
4-5U16sensor_flagsActive sensor bitmask
6-9U32flight_mode_flagsActive flight mode flags
10U8current_pid_profileActive PID profile index
11-12U16system_loadCPU load percentage × 10

Sensor Flags​

BitSensor
0Accelerometer
1Barometer
2Magnetometer
3GPS
4Rangefinder
5Gyroscope
6Optical Flow
bitflags::bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct SensorFlags: u16 {
const ACC = 1 << 0;
const BARO = 1 << 1;
const MAG = 1 << 2;
const GPS = 1 << 3;
const RANGEFINDER = 1 << 4;
const GYRO = 1 << 5;
const OPTICAL_FLOW = 1 << 6;
}
}

#[derive(Debug, Clone, Copy)]
pub struct MspStatus {
pub cycle_time: u16,
pub i2c_error_count: u16,
pub sensors: SensorFlags,
pub flight_mode_flags: u32,
pub current_pid_profile: u8,
pub system_load: u16,
}

impl MspStatus {
pub const COMMAND: u8 = 101;

pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 13 {
return None;
}
Some(MspStatus {
cycle_time: u16::from_le_bytes([data[0], data[1]]),
i2c_error_count: u16::from_le_bytes([data[2], data[3]]),
sensors: SensorFlags::from_bits_truncate(
u16::from_le_bytes([data[4], data[5]])
),
flight_mode_flags: u32::from_le_bytes([data[6], data[7], data[8], data[9]]),
current_pid_profile: data[10],
system_load: u16::from_le_bytes([data[11], data[12]]),
})
}

pub fn system_load_percent(&self) -> f32 {
self.system_load as f32 / 10.0
}

pub fn pid_loop_frequency(&self) -> u32 {
if self.cycle_time > 0 {
1_000_000 / self.cycle_time as u32
} else {
0
}
}
}

MSP_RAW_IMU (102)​

Query raw sensor data from accelerometer, gyroscope, and magnetometer.

Response​

OffsetTypeFieldDescription
0-1I16acc_xAccelerometer X (raw)
2-3I16acc_yAccelerometer Y (raw)
4-5I16acc_zAccelerometer Z (raw)
6-7I16gyro_xGyroscope X (°/s × 4.096)
8-9I16gyro_yGyroscope Y
10-11I16gyro_zGyroscope Z
12-13I16mag_xMagnetometer X
14-15I16mag_yMagnetometer Y
16-17I16mag_zMagnetometer Z

Units​

  • Accelerometer: Raw ADC units, scale depends on configuration (typically ±8g)
  • Gyroscope: Degrees/second × 4.096 (divide by 4.096 to get °/s)
  • Magnetometer: Raw units, scale depends on sensor
#[derive(Debug, Clone, Copy)]
pub struct MspRawImu {
pub acc: [i16; 3],
pub gyro: [i16; 3],
pub mag: [i16; 3],
}

impl MspRawImu {
pub const COMMAND: u8 = 102;

pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 18 {
return None;
}

let read_i16 = |offset: usize| -> i16 {
i16::from_le_bytes([data[offset], data[offset + 1]])
};

Some(MspRawImu {
acc: [read_i16(0), read_i16(2), read_i16(4)],
gyro: [read_i16(6), read_i16(8), read_i16(10)],
mag: [read_i16(12), read_i16(14), read_i16(16)],
})
}

/// Get gyroscope values in degrees per second
pub fn gyro_dps(&self) -> [f32; 3] {
[
self.gyro[0] as f32 / 4.096,
self.gyro[1] as f32 / 4.096,
self.gyro[2] as f32 / 4.096,
]
}
}

MSP_MOTOR (104)​

Query current motor output values.

Response​

8 motor values, each as U16 (little-endian):

OffsetTypeFieldDescription
0-1U16motor_1Motor 1 PWM (1000-2000)
2-3U16motor_2Motor 2 PWM
............
14-15U16motor_8Motor 8 PWM
#[derive(Debug, Clone, Copy)]
pub struct MspMotor {
pub motors: [u16; 8],
}

impl MspMotor {
pub const COMMAND: u8 = 104;

pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 16 {
return None;
}

let mut motors = [0u16; 8];
for i in 0..8 {
motors[i] = u16::from_le_bytes([data[i * 2], data[i * 2 + 1]]);
}

Some(MspMotor { motors })
}

/// Get motor throttle percentage (0-100%)
pub fn throttle_percent(&self, motor_index: usize) -> f32 {
let value = self.motors.get(motor_index).copied().unwrap_or(1000);
((value.saturating_sub(1000)) as f32 / 10.0).min(100.0)
}
}

MSP_RC (105)​

Query RC channel input values.

Response​

Variable length, typically 8-18 channels × 2 bytes each:

OffsetTypeFieldDescription
0-1U16channel_1Roll (1000-2000 µs)
2-3U16channel_2Pitch
4-5U16channel_3Throttle
6-7U16channel_4Yaw
8-9U16channel_5AUX1
.........Additional AUX channels
#[derive(Debug, Clone)]
pub struct MspRc {
pub channels: Vec<u16>,
}

impl MspRc {
pub const COMMAND: u8 = 105;

pub fn from_bytes(data: &[u8]) -> Self {
let channel_count = data.len() / 2;
let mut channels = Vec::with_capacity(channel_count);

for i in 0..channel_count {
let value = u16::from_le_bytes([data[i * 2], data[i * 2 + 1]]);
channels.push(value);
}

MspRc { channels }
}

pub fn roll(&self) -> u16 {
self.channels.get(0).copied().unwrap_or(1500)
}

pub fn pitch(&self) -> u16 {
self.channels.get(1).copied().unwrap_or(1500)
}

pub fn throttle(&self) -> u16 {
self.channels.get(2).copied().unwrap_or(1000)
}

pub fn yaw(&self) -> u16 {
self.channels.get(3).copied().unwrap_or(1500)
}

pub fn aux(&self, index: usize) -> u16 {
self.channels.get(4 + index).copied().unwrap_or(1000)
}
}

MSP_ATTITUDE (108)​

Query aircraft orientation angles.

Response​

OffsetTypeFieldDescription
0-1I16rollRoll angle × 10 (degrees)
2-3I16pitchPitch angle × 10 (degrees)
4-5I16yawHeading × 1 (degrees, 0-360)
#[derive(Debug, Clone, Copy)]
pub struct MspAttitude {
pub roll: i16, // Degrees × 10
pub pitch: i16, // Degrees × 10
pub yaw: i16, // Degrees (0-360)
}

impl MspAttitude {
pub const COMMAND: u8 = 108;

pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 6 {
return None;
}
Some(MspAttitude {
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]]),
})
}

pub fn roll_degrees(&self) -> f32 {
self.roll as f32 / 10.0
}

pub fn pitch_degrees(&self) -> f32 {
self.pitch as f32 / 10.0
}

pub fn yaw_degrees(&self) -> f32 {
self.yaw as f32
}
}

MSP_ALTITUDE (109)​

Query altitude and vertical speed.

Response​

OffsetTypeFieldDescription
0-3I32altitudeAltitude in cm
4-5I16varioVertical speed in cm/s
#[derive(Debug, Clone, Copy)]
pub struct MspAltitude {
pub altitude_cm: i32,
pub vario_cms: i16,
}

impl MspAltitude {
pub const COMMAND: u8 = 109;

pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 6 {
return None;
}
Some(MspAltitude {
altitude_cm: i32::from_le_bytes([data[0], data[1], data[2], data[3]]),
vario_cms: i16::from_le_bytes([data[4], data[5]]),
})
}

pub fn altitude_meters(&self) -> f32 {
self.altitude_cm as f32 / 100.0
}

pub fn vario_ms(&self) -> f32 {
self.vario_cms as f32 / 100.0
}
}

MSP_ANALOG (110)​

Query battery voltage, current draw, and RSSI.

Response​

OffsetTypeFieldDescription
0U8vbat_legacyBattery voltage (legacy, deprecated)
1-2U16mah_drawnConsumed capacity in mAh
3-4U16rssiRSSI value (0-1023)
5-6I16amperageCurrent in 0.01A steps
7-8U16vbatBattery voltage in 0.01V steps
#[derive(Debug, Clone, Copy)]
pub struct MspAnalog {
pub mah_drawn: u16,
pub rssi: u16,
pub amperage: i16, // In 0.01A units
pub vbat: u16, // In 0.01V units
}

impl MspAnalog {
pub const COMMAND: u8 = 110;

pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 9 {
return None;
}
Some(MspAnalog {
mah_drawn: u16::from_le_bytes([data[1], data[2]]),
rssi: u16::from_le_bytes([data[3], data[4]]),
amperage: i16::from_le_bytes([data[5], data[6]]),
vbat: u16::from_le_bytes([data[7], data[8]]),
})
}

pub fn voltage(&self) -> f32 {
self.vbat as f32 / 100.0
}

pub fn current_amps(&self) -> f32 {
self.amperage as f32 / 100.0
}

pub fn rssi_percent(&self) -> f32 {
(self.rssi as f32 / 1023.0) * 100.0
}
}

MSP_PID (112)​

Query PID coefficients for all controller items.

Response​

Array of PID tuples (typically 8-10 items):

OffsetTypeFieldDescription
0U8PProportional gain
1U8IIntegral gain
2U8DDerivative gain

PID Items​

IndexItem
0Roll
1Pitch
2Yaw
3Alt (if applicable)
4Pos (if applicable)
5PosR (if applicable)
6NavR (if applicable)
7Level
8Mag (if applicable)
9Vel (if applicable)
#[derive(Debug, Clone, Copy)]
pub struct PidCoefficients {
pub p: u8,
pub i: u8,
pub d: u8,
}

#[derive(Debug, Clone)]
pub struct MspPid {
pub items: Vec<PidCoefficients>,
}

impl MspPid {
pub const COMMAND: u8 = 112;

pub fn from_bytes(data: &[u8]) -> Self {
let item_count = data.len() / 3;
let mut items = Vec::with_capacity(item_count);

for i in 0..item_count {
items.push(PidCoefficients {
p: data[i * 3],
i: data[i * 3 + 1],
d: data[i * 3 + 2],
});
}

MspPid { items }
}

pub fn roll(&self) -> Option<&PidCoefficients> {
self.items.get(0)
}

pub fn pitch(&self) -> Option<&PidCoefficients> {
self.items.get(1)
}

pub fn yaw(&self) -> Option<&PidCoefficients> {
self.items.get(2)
}
}

MSP_RAW_GPS (106)​

Query raw GPS data.

Response​

OffsetTypeFieldDescription
0U8fix_type0=none, 1=fix, 2=DGPS
1U8num_satSatellite count
2-5I32latLatitude × 10^7
6-9I32lonLongitude × 10^7
10-11I16altAltitude in meters
12-13U16speedSpeed in cm/s
14-15U16ground_courseCourse × 10 (degrees)
#[derive(Debug, Clone, Copy)]
pub struct MspRawGps {
pub fix_type: u8,
pub num_sat: u8,
pub lat: i32, // Degrees × 10^7
pub lon: i32, // Degrees × 10^7
pub alt_meters: i16,
pub speed_cms: u16,
pub ground_course: u16, // Degrees × 10
}

impl MspRawGps {
pub const COMMAND: u8 = 106;

pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 16 {
return None;
}
Some(MspRawGps {
fix_type: data[0],
num_sat: data[1],
lat: i32::from_le_bytes([data[2], data[3], data[4], data[5]]),
lon: i32::from_le_bytes([data[6], data[7], data[8], data[9]]),
alt_meters: i16::from_le_bytes([data[10], data[11]]),
speed_cms: u16::from_le_bytes([data[12], data[13]]),
ground_course: u16::from_le_bytes([data[14], data[15]]),
})
}

pub fn latitude(&self) -> f64 {
self.lat as f64 / 10_000_000.0
}

pub fn longitude(&self) -> f64 {
self.lon as f64 / 10_000_000.0
}

pub fn speed_ms(&self) -> f32 {
self.speed_cms as f32 / 100.0
}

pub fn has_fix(&self) -> bool {
self.fix_type >= 1
}
}

MSP_BATTERY_STATE (130)​

Query detailed battery state.

Response​

OffsetTypeFieldDescription
0U8cell_countDetected cell count
1-2U16capacityBattery capacity mAh
3U8voltage_legacyLegacy voltage
4-5U16mah_drawnConsumed mAh
6-7I16amperageCurrent in 0.01A
8U8stateBattery state
9-10U16voltageVoltage in 0.01V

Battery States​

ValueState
0OK
1WARNING
2CRITICAL
3NOT_PRESENT
4INIT
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum BatteryState {
Ok = 0,
Warning = 1,
Critical = 2,
NotPresent = 3,
Init = 4,
}

#[derive(Debug, Clone, Copy)]
pub struct MspBatteryState {
pub cell_count: u8,
pub capacity_mah: u16,
pub mah_drawn: u16,
pub amperage: i16,
pub state: u8,
pub voltage: u16,
}

impl MspBatteryState {
pub const COMMAND: u8 = 130;

pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 11 {
return None;
}
Some(MspBatteryState {
cell_count: data[0],
capacity_mah: u16::from_le_bytes([data[1], data[2]]),
mah_drawn: u16::from_le_bytes([data[4], data[5]]),
amperage: i16::from_le_bytes([data[6], data[7]]),
state: data[8],
voltage: u16::from_le_bytes([data[9], data[10]]),
})
}

pub fn voltage_volts(&self) -> f32 {
self.voltage as f32 / 100.0
}

pub fn cell_voltage(&self) -> f32 {
if self.cell_count > 0 {
self.voltage_volts() / self.cell_count as f32
} else {
0.0
}
}

pub fn remaining_percent(&self) -> f32 {
if self.capacity_mah > 0 {
((self.capacity_mah - self.mah_drawn) as f32 / self.capacity_mah as f32) * 100.0
} else {
0.0
}
}
}

Telemetry Polling Example​

use std::io::{Read, Write};
use std::time::{Duration, Instant};

/// Continuous telemetry polling loop
fn telemetry_loop<T: Read + Write>(port: &mut T) -> Result<(), &'static str> {
let commands = [
(101, "STATUS"),
(108, "ATTITUDE"),
(110, "ANALOG"),
(105, "RC"),
];

let poll_interval = Duration::from_millis(20); // 50 Hz

loop {
let start = Instant::now();

for (cmd, name) in &commands {
let checksum = 0 ^ cmd;
let request = [b'$', b'M', b'<', 0, *cmd, checksum];

port.write_all(&request).map_err(|_| "Write failed")?;

let mut buf = [0u8; 64];
let n = port.read(&mut buf).map_err(|_| "Read failed")?;

if n >= 6 && &buf[0..3] == b"$M>" {
let size = buf[3] as usize;
let payload = &buf[5..5 + size];

match *cmd {
108 => {
if let Some(att) = MspAttitude::from_bytes(payload) {
println!("Roll: {:.1}° Pitch: {:.1}° Yaw: {:.0}°",
att.roll_degrees(), att.pitch_degrees(), att.yaw_degrees());
}
}
110 => {
if let Some(analog) = MspAnalog::from_bytes(payload) {
println!("Battery: {:.2}V {:.1}A",
analog.voltage(), analog.current_amps());
}
}
_ => {}
}
}
}

let elapsed = start.elapsed();
if elapsed < poll_interval {
std::thread::sleep(poll_interval - elapsed);
}
}
}