Skip to main content

Control Commands (200-229)

These commands allow modifying flight controller settings, overriding RC channels, and triggering calibration routines.

MSP_SET_RAW_RC (200)​

Override RC channel values. Used for ground station control or MSP-based receivers.

Request​

OffsetTypeFieldDescription
0-1U16channel_1Roll (1000-2000)
2-3U16channel_2Pitch
4-5U16channel_3Throttle
6-7U16channel_4Yaw
8-9U16channel_5AUX1
.........Up to 18 channels

Response​

Empty (ACK only).

Important Notes​

  • Requires RX_MSP feature enabled
  • Values must be sent continuously (typically 50Hz minimum)
  • Channels revert to failsafe if no updates received
pub struct MspSetRawRc {
pub channels: Vec<u16>,
}

impl MspSetRawRc {
pub const COMMAND: u8 = 200;

/// Build MSP_SET_RAW_RC request frame
pub fn to_frame(&self) -> Vec<u8> {
let size = (self.channels.len() * 2) as u8;
let mut frame = vec![b'$', b'M', b'<', size, Self::COMMAND];

for ch in &self.channels {
frame.extend_from_slice(&ch.to_le_bytes());
}

// Calculate checksum
let mut checksum = size ^ Self::COMMAND;
for i in 5..frame.len() {
checksum ^= frame[i];
}
frame.push(checksum);

frame
}

/// Create with default center values
pub fn centered(channel_count: usize) -> Self {
let mut channels = vec![1500u16; channel_count.min(18)];
channels[2] = 1000; // Throttle at minimum
MspSetRawRc { channels }
}
}

Example​

// Set RC channels: Roll=1500, Pitch=1500, Throttle=1000, Yaw=1500, AUX1=1000
let rc = MspSetRawRc {
channels: vec![1500, 1500, 1000, 1500, 1000, 1000, 1000, 1000],
};

let frame = rc.to_frame();
port.write_all(&frame)?;

MSP_SET_MOTOR (214)​

Set motor output values directly. Only works when disarmed or in motor test mode.

Request​

OffsetTypeFieldDescription
0-1U16motor_1Motor 1 PWM (1000-2000)
2-3U16motor_2Motor 2 PWM
............
14-15U16motor_8Motor 8 PWM

Response​

Empty (ACK only).

Safety Notes​

  • Motors will only spin if armed OR if motor protocol supports motor test mode
  • Set all motors to 1000 (min throttle) to stop
  • Always implement failsafe in your application
pub struct MspSetMotor {
pub motors: [u16; 8],
}

impl MspSetMotor {
pub const COMMAND: u8 = 214;

pub fn to_frame(&self) -> Vec<u8> {
let size = 16u8;
let mut frame = vec![b'$', b'M', b'<', size, Self::COMMAND];

for motor in &self.motors {
frame.extend_from_slice(&motor.to_le_bytes());
}

let mut checksum = size ^ Self::COMMAND;
for i in 5..frame.len() {
checksum ^= frame[i];
}
frame.push(checksum);

frame
}

/// Stop all motors
pub fn stop() -> Self {
MspSetMotor {
motors: [1000; 8],
}
}

/// Set single motor (others at min throttle)
pub fn single(motor_index: usize, value: u16) -> Self {
let mut motors = [1000u16; 8];
if motor_index < 8 {
motors[motor_index] = value.clamp(1000, 2000);
}
MspSetMotor { motors }
}
}

MSP_SET_PID (202)​

Write PID coefficients.

Request​

Array of PID tuples (3 bytes each):

OffsetTypeField
0U8Roll P
1U8Roll I
2U8Roll D
3U8Pitch P
4U8Pitch I
5U8Pitch D
.........

Response​

Empty (ACK only).

pub struct MspSetPid {
pub items: Vec<PidCoefficients>,
}

impl MspSetPid {
pub const COMMAND: u8 = 202;

pub fn to_frame(&self) -> Vec<u8> {
let size = (self.items.len() * 3) as u8;
let mut frame = vec![b'$', b'M', b'<', size, Self::COMMAND];

for pid in &self.items {
frame.push(pid.p);
frame.push(pid.i);
frame.push(pid.d);
}

let mut checksum = size ^ Self::COMMAND;
for i in 5..frame.len() {
checksum ^= frame[i];
}
frame.push(checksum);

frame
}
}

#[derive(Debug, Clone, Copy)]
pub struct PidCoefficients {
pub p: u8,
pub i: u8,
pub d: u8,
}

MSP_ACC_CALIBRATION (205)​

Trigger accelerometer calibration. The aircraft must be level and stationary.

Request​

Empty payload.

Response​

Empty (ACK only).

Notes​

  • Calibration takes several seconds
  • FC should be motionless on a level surface
  • Calibration results are saved automatically
pub fn build_acc_calibration_request() -> [u8; 6] {
const MSP_ACC_CALIBRATION: u8 = 205;
let checksum = 0 ^ MSP_ACC_CALIBRATION;
[b'$', b'M', b'<', 0, MSP_ACC_CALIBRATION, checksum]
}

MSP_MAG_CALIBRATION (206)​

Trigger magnetometer calibration. Rotate the aircraft during calibration.

Request​

Empty payload.

Response​

Empty (ACK only).

Notes​

  • Rotate aircraft through all axes during calibration
  • Takes approximately 30 seconds
  • Keep away from metal objects and electronics
pub fn build_mag_calibration_request() -> [u8; 6] {
const MSP_MAG_CALIBRATION: u8 = 206;
let checksum = 0 ^ MSP_MAG_CALIBRATION;
[b'$', b'M', b'<', 0, MSP_MAG_CALIBRATION, checksum]
}

MSP_RESET_CONF (208)​

Reset all settings to factory defaults.

Request​

Empty payload.

Response​

Empty (ACK only).

Warning​

This erases all configuration including:

  • PID tuning
  • Rates and expo
  • Mode assignments
  • Serial port configuration
  • OSD layout
pub fn build_reset_conf_request() -> [u8; 6] {
const MSP_RESET_CONF: u8 = 208;
let checksum = 0 ^ MSP_RESET_CONF;
[b'$', b'M', b'<', 0, MSP_RESET_CONF, checksum]
}

MSP_REBOOT (68)​

Reboot the flight controller.

Request​

OffsetTypeFieldDescription
0U8reboot_typeOptional: 0=normal, 1=bootloader, 2=MSC

Response​

Empty (may not receive if FC reboots immediately).

Reboot Types​

ValueTypeDescription
0NormalStandard reboot
1BootloaderEnter DFU/bootloader mode
2MSCMass Storage Class mode
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum RebootType {
Normal = 0,
Bootloader = 1,
MassStorage = 2,
}

pub fn build_reboot_request(reboot_type: RebootType) -> Vec<u8> {
const MSP_REBOOT: u8 = 68;
let payload = [reboot_type as u8];
let size = 1u8;
let mut checksum = size ^ MSP_REBOOT ^ payload[0];

vec![b'$', b'M', b'<', size, MSP_REBOOT, payload[0], checksum]
}

// Simple reboot (no payload)
pub fn build_simple_reboot_request() -> [u8; 6] {
const MSP_REBOOT: u8 = 68;
let checksum = 0 ^ MSP_REBOOT;
[b'$', b'M', b'<', 0, MSP_REBOOT, checksum]
}

MSP_EEPROM_WRITE (250)​

Save current settings to EEPROM/flash.

Request​

Empty payload.

Response​

Empty (ACK only).

Important​

Always call after making configuration changes to persist them across reboots.

pub fn build_eeprom_write_request() -> [u8; 6] {
const MSP_EEPROM_WRITE: u8 = 250;
let checksum = 0 ^ MSP_EEPROM_WRITE;
[b'$', b'M', b'<', 0, MSP_EEPROM_WRITE, checksum]
}

MSP_SELECT_SETTING (210)​

Switch to a different settings profile.

Request​

OffsetTypeFieldDescription
0U8profile_indexProfile index (0-based)

Response​

Empty (ACK only).

pub fn build_select_setting_request(profile: u8) -> Vec<u8> {
const MSP_SELECT_SETTING: u8 = 210;
let size = 1u8;
let checksum = size ^ MSP_SELECT_SETTING ^ profile;

vec![b'$', b'M', b'<', size, MSP_SELECT_SETTING, profile, checksum]
}

MSP_SET_ARMING_DISABLED (99)​

Enable or disable arming with a reason code.

Request​

OffsetTypeFieldDescription
0U8command0=runaway takeoff, 1=crash detected
1U8disable_runningDisable flag

Response​

Empty (ACK only).

Use Cases​

  • Ground station preventing arming during configuration
  • Companion computer enforcing safety conditions
pub fn build_arming_disabled_request(command: u8, disabled: bool) -> Vec<u8> {
const MSP_SET_ARMING_DISABLED: u8 = 99;
let size = 2u8;
let disable_byte = if disabled { 1u8 } else { 0u8 };
let checksum = size ^ MSP_SET_ARMING_DISABLED ^ command ^ disable_byte;

vec![b'$', b'M', b'<', size, MSP_SET_ARMING_DISABLED, command, disable_byte, checksum]
}

Complete Configuration Example​

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

/// Configuration write sequence
fn configure_pid<T: Read + Write>(
port: &mut T,
roll: PidCoefficients,
pitch: PidCoefficients,
yaw: PidCoefficients,
) -> Result<(), &'static str> {
// Step 1: Read current PID values
let checksum = 0 ^ 112;
let request = [b'$', b'M', b'<', 0, 112, 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>" {
return Err("Invalid response");
}

let size = buf[3] as usize;
let mut current_pids = buf[5..5 + size].to_vec();

// Step 2: Modify Roll, Pitch, Yaw (first 9 bytes)
if current_pids.len() >= 9 {
current_pids[0] = roll.p;
current_pids[1] = roll.i;
current_pids[2] = roll.d;
current_pids[3] = pitch.p;
current_pids[4] = pitch.i;
current_pids[5] = pitch.d;
current_pids[6] = yaw.p;
current_pids[7] = yaw.i;
current_pids[8] = yaw.d;
}

// Step 3: Write modified PIDs
let set_pid = MspSetPid {
items: current_pids.chunks(3)
.map(|c| PidCoefficients { p: c[0], i: c[1], d: c[2] })
.collect(),
};
port.write_all(&set_pid.to_frame()).map_err(|_| "Write failed")?;

// Read ACK
let n = port.read(&mut buf).map_err(|_| "Read failed")?;
if n < 6 || buf[2] == b'!' {
return Err("PID write failed");
}

// Step 4: Save to EEPROM
let eeprom_write = build_eeprom_write_request();
port.write_all(&eeprom_write).map_err(|_| "Write failed")?;

// Read ACK
let n = port.read(&mut buf).map_err(|_| "Read failed")?;
if n < 6 || buf[2] == b'!' {
return Err("EEPROM write failed");
}

println!("PID values saved successfully");
Ok(())
}

Safety Considerations​

  1. Never arm via MSP without proper safety checks

    • Always verify sensor health
    • Implement failsafe for lost connection
    • Use hardware kill switch as backup
  2. Rate limit RC override commands

    • Send at 50Hz minimum for smooth control
    • Implement timeout detection
  3. Test motor control carefully

    • Remove propellers during development
    • Start with low throttle values
    • Implement emergency stop
  4. Backup before reset

    • Use configurator CLI dump before factory reset
    • Configuration cannot be recovered after reset