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):
| Offset | Type | Field | Range | Description |
|---|---|---|---|---|
| 0-1 | U16 | channel_1 | 1000-2000 | Roll |
| 2-3 | U16 | channel_2 | 1000-2000 | Pitch |
| 4-5 | U16 | channel_3 | 1000-2000 | Throttle |
| 6-7 | U16 | channel_4 | 1000-2000 | Yaw |
| 8-9 | U16 | channel_5 | 1000-2000 | AUX1 |
| ... | ... | ... | ... | Additional AUX channels |
Total size: 2 × channel_count bytes
Channel Conventions
| Channel | Default Function | Center | Notes |
|---|---|---|---|
| 1 | Roll (Aileron) | 1500 | Left/Right |
| 2 | Pitch (Elevator) | 1500 | Forward/Back |
| 3 | Throttle | 1000 | Low = 1000 |
| 4 | Yaw (Rudder) | 1500 | Left/Right rotation |
| 5+ | AUX1-AUXn | Varies | Mode 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_MSPfeature must be enabled- Or flight controller configured for MSP receiver
Request Structure
Array of U16 values (typically 8-16 channels):
| 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+ | U16[] | aux_channels | AUX1-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
-
Failsafe: Always implement connection loss detection. Stop sending RC overrides → FC enters failsafe.
-
Arming: Never arm without proper safety checks:
- Verify throttle is low
- Confirm intended arm state
- Have hardware kill switch available
-
Update Rate: Send at least 50Hz (20ms intervals). Lower rates may trigger failsafe.
-
Channel Limits: Always clamp values to 1000-2000 range. Out-of-range values may cause undefined behavior.
-
Testing: Remove propellers during MSP RC development and testing.