Skip to main content

RC Channel Commands

MSP_RC (105)​

Query current RC channel input values.

Request​

Empty payload (0 bytes).

Response Structure​

Variable length array of U16 values (typically 8-18 channels):

OffsetTypeFieldRangeDescription
0-1U16channel_11000-2000Roll
2-3U16channel_21000-2000Pitch
4-5U16channel_31000-2000Throttle
6-7U16channel_41000-2000Yaw
8-9U16channel_51000-2000AUX1
............Additional AUX channels

Total size: 2 × channel_count bytes

Channel Conventions​

ChannelDefault FunctionCenterNotes
1Roll (Aileron)1500Left/Right
2Pitch (Elevator)1500Forward/Back
3Throttle1000Low = 1000
4Yaw (Rudder)1500Left/Right rotation
5+AUX1-AUXnVariesMode switches, etc.

Rust Implementation​

/// RC channel values from receiver
#[derive(Debug, Clone)]
pub struct MspRc {
/// Channel values in microseconds (1000-2000 typical)
pub channels: Vec<u16>,
}

impl MspRc {
pub const COMMAND: u8 = 105;

/// Parse from MSP response payload
pub fn from_payload(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 }
}

/// Get channel value by index (1-based for user convenience)
pub fn channel(&self, index: usize) -> Option<u16> {
if index == 0 { return None; }
self.channels.get(index - 1).copied()
}

/// Roll input (channel 1)
pub fn roll(&self) -> u16 {
self.channels.get(0).copied().unwrap_or(1500)
}

/// Pitch input (channel 2)
pub fn pitch(&self) -> u16 {
self.channels.get(1).copied().unwrap_or(1500)
}

/// Throttle input (channel 3)
pub fn throttle(&self) -> u16 {
self.channels.get(2).copied().unwrap_or(1000)
}

/// Yaw input (channel 4)
pub fn yaw(&self) -> u16 {
self.channels.get(3).copied().unwrap_or(1500)
}

/// Auxiliary channel (0 = AUX1, 1 = AUX2, etc.)
pub fn aux(&self, aux_index: usize) -> u16 {
self.channels.get(4 + aux_index).copied().unwrap_or(1000)
}

/// Convert channel value to normalized range (-1.0 to +1.0)
pub fn normalize(value: u16) -> f32 {
(value as f32 - 1500.0) / 500.0
}

/// Convert channel value to percentage (0-100%)
pub fn to_percent(value: u16) -> f32 {
((value.saturating_sub(1000)) as f32 / 10.0).clamp(0.0, 100.0)
}

/// Get throttle as percentage (0-100%)
pub fn throttle_percent(&self) -> f32 {
Self::to_percent(self.throttle())
}

/// Check if throttle is at minimum (disarm safe)
pub fn throttle_low(&self) -> bool {
self.throttle() < 1050
}

/// Number of channels available
pub fn channel_count(&self) -> usize {
self.channels.len()
}
}

MSP_SET_RAW_RC (200)​

Override RC channel values via MSP.

Prerequisites​

  • RX_MSP feature must be enabled
  • Or flight controller configured for MSP receiver

Request Structure​

Array of U16 values (typically 8-16 channels):

OffsetTypeFieldDescription
0-1U16channel_1Roll (1000-2000)
2-3U16channel_2Pitch
4-5U16channel_3Throttle
6-7U16channel_4Yaw
8+U16[]aux_channelsAUX1-AUXn

Response​

Empty (ACK only).

Rust Implementation​

/// MSP RC override command
#[derive(Debug, Clone)]
pub struct MspSetRawRc {
pub channels: Vec<u16>,
}

impl MspSetRawRc {
pub const COMMAND: u8 = 200;
pub const MAX_CHANNELS: usize = 18;

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

/// Create with all channels at specified value
pub fn uniform(value: u16, channel_count: usize) -> Self {
let count = channel_count.min(Self::MAX_CHANNELS);
MspSetRawRc {
channels: vec![value.clamp(1000, 2000); count],
}
}

/// Set roll input
pub fn with_roll(mut self, value: u16) -> Self {
if !self.channels.is_empty() {
self.channels[0] = value.clamp(1000, 2000);
}
self
}

/// Set pitch input
pub fn with_pitch(mut self, value: u16) -> Self {
if self.channels.len() > 1 {
self.channels[1] = value.clamp(1000, 2000);
}
self
}

/// Set throttle input
pub fn with_throttle(mut self, value: u16) -> Self {
if self.channels.len() > 2 {
self.channels[2] = value.clamp(1000, 2000);
}
self
}

/// Set yaw input
pub fn with_yaw(mut self, value: u16) -> Self {
if self.channels.len() > 3 {
self.channels[3] = value.clamp(1000, 2000);
}
self
}

/// Set auxiliary channel
pub fn with_aux(mut self, aux_index: usize, value: u16) -> Self {
let channel_index = 4 + aux_index;
if channel_index < self.channels.len() {
self.channels[channel_index] = value.clamp(1000, 2000);
}
self
}

/// Convert to MSP request payload
pub fn to_payload(&self) -> Vec<u8> {
let mut payload = Vec::with_capacity(self.channels.len() * 2);
for ch in &self.channels {
payload.extend_from_slice(&ch.to_le_bytes());
}
payload
}

/// Build complete MSP request frame
pub fn to_frame(&self) -> Vec<u8> {
let payload = self.to_payload();
let size = payload.len() as u8;

let mut frame = vec![b'$', b'M', b'<', size, Self::COMMAND];
frame.extend_from_slice(&payload);

// Calculate checksum
let mut checksum = size ^ Self::COMMAND;
for byte in &payload {
checksum ^= byte;
}
frame.push(checksum);

frame
}
}

Raw Hex Examples​

MSP_RC Request​

24 4D 3C 00 69 69
│ └─ Checksum
└──── Command (105 = 0x69)

MSP_RC Response (8 channels, centered)​

24 4D 3E 10 69
DC 05 ← CH1 (Roll) = 1500
DC 05 ← CH2 (Pitch) = 1500
E8 03 ← CH3 (Throttle) = 1000
DC 05 ← CH4 (Yaw) = 1500
E8 03 ← CH5 (AUX1) = 1000
E8 03 ← CH6 (AUX2) = 1000
E8 03 ← CH7 (AUX3) = 1000
E8 03 ← CH8 (AUX4) = 1000
XX ← Checksum

MSP_SET_RAW_RC Request​

24 4D 3C 10 C8
DC 05 ← CH1 = 1500 (centered roll)
DC 05 ← CH2 = 1500 (centered pitch)
A4 06 ← CH3 = 1700 (70% throttle)
DC 05 ← CH4 = 1500 (centered yaw)
E8 03 ← CH5 = 1000 (AUX1 low)
E8 03 ← CH6 = 1000
E8 03 ← CH7 = 1000
E8 03 ← CH8 = 1000
XX ← Checksum

Usage Example​

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

/// RC control loop example
fn rc_control_loop<T: Read + Write>(
port: &mut T,
target_throttle: f32,
) -> Result<(), &'static str> {
// Create RC command with target throttle
let throttle_pwm = 1000 + (target_throttle * 10.0) as u16;

let rc = MspSetRawRc::centered(8)
.with_throttle(throttle_pwm)
.with_aux(0, 1800); // AUX1 high (arm)

// Send RC override at 50Hz
let interval = Duration::from_millis(20);
let frame = rc.to_frame();

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

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

// Optional: read ACK
let mut buf = [0u8; 16];
let _ = port.read(&mut buf);

// Maintain timing
let elapsed = start.elapsed();
if elapsed < interval {
std::thread::sleep(interval - elapsed);
}
}
}

fn main() {
// Parse example response
let payload = [
0xDC, 0x05, // 1500
0xDC, 0x05, // 1500
0xA4, 0x06, // 1700
0xDC, 0x05, // 1500
0xD0, 0x07, // 2000
0xE8, 0x03, // 1000
0xE8, 0x03, // 1000
0xE8, 0x03, // 1000
];

let rc = MspRc::from_payload(&payload);

println!("Channel count: {}", rc.channel_count());
println!("Roll: {} ({})", rc.roll(), MspRc::normalize(rc.roll()));
println!("Pitch: {} ({})", rc.pitch(), MspRc::normalize(rc.pitch()));
println!("Throttle: {} ({}%)", rc.throttle(), rc.throttle_percent());
println!("Yaw: {}", rc.yaw());
println!("AUX1: {} (arm switch)", rc.aux(0));
println!("Throttle low: {}", rc.throttle_low());
}

Output:

Channel count: 8
Roll: 1500 (0)
Pitch: 1500 (0)
Throttle: 1700 (70%)
Yaw: 1500
AUX1: 2000 (arm switch)
Throttle low: false

Safety Considerations​

  1. Failsafe: Always implement connection loss detection. Stop sending RC overrides → FC enters failsafe.

  2. Arming: Never arm without proper safety checks:

    • Verify throttle is low
    • Confirm intended arm state
    • Have hardware kill switch available
  3. Update Rate: Send at least 50Hz (20ms intervals). Lower rates may trigger failsafe.

  4. Channel Limits: Always clamp values to 1000-2000 range. Out-of-range values may cause undefined behavior.

  5. Testing: Remove propellers during MSP RC development and testing.