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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0-1 | U16 | channel_1 | Roll (1000-2000) |
| 2-3 | U16 | channel_2 | Pitch |
| 4-5 | U16 | channel_3 | Throttle |
| 6-7 | U16 | channel_4 | Yaw |
| 8-9 | U16 | channel_5 | AUX1 |
| ... | ... | ... | Up to 18 channels |
Response
Empty (ACK only).
Important Notes
- Requires
RX_MSPfeature 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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0-1 | U16 | motor_1 | Motor 1 PWM (1000-2000) |
| 2-3 | U16 | motor_2 | Motor 2 PWM |
| ... | ... | ... | ... |
| 14-15 | U16 | motor_8 | Motor 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):
| Offset | Type | Field |
|---|---|---|
| 0 | U8 | Roll P |
| 1 | U8 | Roll I |
| 2 | U8 | Roll D |
| 3 | U8 | Pitch P |
| 4 | U8 | Pitch I |
| 5 | U8 | Pitch 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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | reboot_type | Optional: 0=normal, 1=bootloader, 2=MSC |
Response
Empty (may not receive if FC reboots immediately).
Reboot Types
| Value | Type | Description |
|---|---|---|
| 0 | Normal | Standard reboot |
| 1 | Bootloader | Enter DFU/bootloader mode |
| 2 | MSC | Mass 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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | profile_index | Profile 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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | command | 0=runaway takeoff, 1=crash detected |
| 1 | U8 | disable_running | Disable 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
-
Never arm via MSP without proper safety checks
- Always verify sensor health
- Implement failsafe for lost connection
- Use hardware kill switch as backup
-
Rate limit RC override commands
- Send at 50Hz minimum for smooth control
- Implement timeout detection
-
Test motor control carefully
- Remove propellers during development
- Start with low throttle values
- Implement emergency stop
-
Backup before reset
- Use configurator CLI dump before factory reset
- Configuration cannot be recovered after reset