Configuration Commands (32-62)
These commands manage flight controller settings including battery, modes, features, and hardware configuration.
MSP_BATTERY_CONFIG (32)
Query battery monitoring configuration.
Response
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | vbat_min_cell | Minimum cell voltage × 10 |
| 1 | U8 | vbat_max_cell | Maximum cell voltage × 10 |
| 2 | U8 | vbat_warning_cell | Warning voltage × 10 |
| 3-4 | U16 | capacity | Battery capacity in mAh |
| 5 | U8 | voltage_source | 0=none, 1=ADC, 2=ESC |
| 6 | U8 | current_source | 0=none, 1=ADC, 2=ESC, 3=MSP |
#[derive(Debug, Clone, Copy)]
#[repr(C, packed)]
pub struct MspBatteryConfig {
pub vbat_min_cell: u8, // Minimum cell voltage × 10 (e.g., 33 = 3.3V)
pub vbat_max_cell: u8, // Maximum cell voltage × 10 (e.g., 42 = 4.2V)
pub vbat_warning_cell: u8, // Warning voltage × 10 (e.g., 35 = 3.5V)
pub capacity: u16, // mAh
pub voltage_source: u8,
pub current_source: u8,
}
impl MspBatteryConfig {
pub const COMMAND: u8 = 32;
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 7 {
return None;
}
Some(MspBatteryConfig {
vbat_min_cell: data[0],
vbat_max_cell: data[1],
vbat_warning_cell: data[2],
capacity: u16::from_le_bytes([data[3], data[4]]),
voltage_source: data[5],
current_source: data[6],
})
}
pub fn min_cell_voltage(&self) -> f32 {
self.vbat_min_cell as f32 / 10.0
}
pub fn max_cell_voltage(&self) -> f32 {
self.vbat_max_cell as f32 / 10.0
}
}
MSP_MODE_RANGES (34)
Query flight mode activation ranges. Each mode can be activated based on an auxiliary channel position.
Response
Array of mode range entries (40 entries typical):
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | box_id | Flight mode box ID |
| 1 | U8 | aux_channel | Aux channel index (0 = AUX1) |
| 2 | U8 | range_start | Start position (0-47, step=25, offset=900) |
| 3 | U8 | range_end | End position (0-47) |
Range Calculation
- PWM range: 900-2100 µs
- Step size: 25 µs
- Value 0 = 900 µs, Value 48 = 2100 µs
PWM = 900 + (value × 25)
#[derive(Debug, Clone, Copy)]
pub struct MspModeRange {
pub box_id: u8,
pub aux_channel: u8,
pub range_start: u8,
pub range_end: u8,
}
impl MspModeRange {
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 4 {
return None;
}
Some(MspModeRange {
box_id: data[0],
aux_channel: data[1],
range_start: data[2],
range_end: data[3],
})
}
/// Convert range value to PWM microseconds
pub fn range_to_pwm(value: u8) -> u16 {
900 + (value as u16 * 25)
}
pub fn start_pwm(&self) -> u16 {
Self::range_to_pwm(self.range_start)
}
pub fn end_pwm(&self) -> u16 {
Self::range_to_pwm(self.range_end)
}
/// Check if a PWM value activates this mode
pub fn is_active(&self, pwm: u16) -> bool {
pwm >= self.start_pwm() && pwm <= self.end_pwm()
}
}
/// Parse all mode ranges from response
pub fn parse_mode_ranges(data: &[u8]) -> Vec<MspModeRange> {
data.chunks(4)
.filter_map(MspModeRange::from_bytes)
.filter(|r| r.box_id != 0 || r.aux_channel != 0) // Skip empty slots
.collect()
}
MSP_FEATURE_CONFIG (36)
Query enabled features as a bitmask.
Response
| Offset | Type | Field | Description |
|---|---|---|---|
| 0-3 | U32 | features | Feature flags bitmask |
Feature Flags
bitflags::bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct Features: u32 {
const RX_PPM = 1 << 0;
const INFLIGHT_ACC_CAL = 1 << 2;
const RX_SERIAL = 1 << 3;
const MOTOR_STOP = 1 << 4;
const SERVO_TILT = 1 << 5;
const SOFTSERIAL = 1 << 6;
const GPS = 1 << 7;
const RANGEFINDER = 1 << 9;
const TELEMETRY = 1 << 10;
const THREE_D = 1 << 12;
const RX_PARALLEL_PWM = 1 << 13;
const RX_MSP = 1 << 14;
const RSSI_ADC = 1 << 15;
const LED_STRIP = 1 << 16;
const DISPLAY = 1 << 17;
const OSD = 1 << 18;
const CHANNEL_FORWARDING = 1 << 20;
const TRANSPONDER = 1 << 21;
const AIRMODE = 1 << 22;
const RX_SPI = 1 << 25;
const ESC_SENSOR = 1 << 27;
const ANTI_GRAVITY = 1 << 28;
}
}
impl Features {
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 4 {
return None;
}
let bits = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
Some(Features::from_bits_truncate(bits))
}
}
MSP_RX_CONFIG (44)
Query receiver configuration.
Response
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | serialrx_provider | Serial RX protocol |
| 1-2 | U16 | max_check | Max valid channel value |
| 3-4 | U16 | mid_rc | Center stick value |
| 5-6 | U16 | min_check | Min valid channel value |
| 7 | U8 | spektrum_sat_bind | Spektrum satellite bind |
| 8-9 | U16 | rx_min_usec | Minimum valid PWM |
| 10-11 | U16 | rx_max_usec | Maximum valid PWM |
| 12 | U8 | rc_interpolation | Interpolation type |
| 13 | U8 | rc_interpolation_interval | Interval in ms |
| 14-15 | U16 | airmode_start_throttle | Airmode activation throttle |
| 16 | U8 | rx_spi_protocol | SPI RX protocol |
| 17-20 | U32 | rx_spi_id | SPI RX ID |
| 21 | U8 | rx_spi_rf_channel_count | SPI RF channels |
| 22 | U8 | fpv_cam_angle_degrees | FPV camera angle |
| 23 | U8 | rc_smoothing | Smoothing config |
Serial RX Protocols
| Value | Protocol |
|---|---|
| 0 | SPEK1024 |
| 1 | SPEK2048 |
| 2 | SBUS |
| 3 | SUMD |
| 4 | SUMH |
| 5 | XB-B |
| 6 | XB-B-RJ01 |
| 7 | IBUS |
| 8 | JETIEXBUS |
| 9 | CRSF |
| 10 | SRXL |
| 11 | CUSTOM |
| 12 | FPORT |
| 13 | SRXL2 |
| 14 | GHST |
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum SerialRxProtocol {
Spektrum1024 = 0,
Spektrum2048 = 1,
Sbus = 2,
Sumd = 3,
Sumh = 4,
XBusB = 5,
XBusBRJ01 = 6,
Ibus = 7,
JetiExBus = 8,
Crsf = 9,
Srxl = 10,
Custom = 11,
Fport = 12,
Srxl2 = 13,
Ghst = 14,
}
#[derive(Debug, Clone)]
pub struct MspRxConfig {
pub serialrx_provider: u8,
pub max_check: u16,
pub mid_rc: u16,
pub min_check: u16,
pub rx_min_usec: u16,
pub rx_max_usec: u16,
pub rc_interpolation: u8,
pub rc_interpolation_interval: u8,
}
impl MspRxConfig {
pub const COMMAND: u8 = 44;
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 14 {
return None;
}
Some(MspRxConfig {
serialrx_provider: data[0],
max_check: u16::from_le_bytes([data[1], data[2]]),
mid_rc: u16::from_le_bytes([data[3], data[4]]),
min_check: u16::from_le_bytes([data[5], data[6]]),
rx_min_usec: u16::from_le_bytes([data[8], data[9]]),
rx_max_usec: u16::from_le_bytes([data[10], data[11]]),
rc_interpolation: data[12],
rc_interpolation_interval: data[13],
})
}
}
MSP_CF_SERIAL_CONFIG (54)
Query serial port configuration.
Response
Array of port configurations:
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | identifier | Port identifier |
| 1-2 | U16 | function_mask | Function flags |
| 3 | U8 | msp_baud | MSP baud rate index |
| 4 | U8 | gps_baud | GPS baud rate index |
| 5 | U8 | telemetry_baud | Telemetry baud rate |
| 6 | U8 | blackbox_baud | Blackbox baud rate |
Port Identifiers
| Value | Port |
|---|---|
| 0 | USB VCP |
| 1 | UART1 |
| 2 | UART2 |
| 3 | UART3 |
| 4 | UART4 |
| 5 | UART5 |
| 6 | UART6 |
| 20 | SOFTSERIAL1 |
| 21 | SOFTSERIAL2 |
Function Flags
bitflags::bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct SerialPortFunction: u16 {
const NONE = 0;
const MSP = 1 << 0;
const GPS = 1 << 1;
const TELEMETRY_FRSKY = 1 << 2;
const TELEMETRY_HOTT = 1 << 3;
const TELEMETRY_LTM = 1 << 4;
const TELEMETRY_SMARTPORT = 1 << 5;
const RX_SERIAL = 1 << 6;
const BLACKBOX = 1 << 7;
const TELEMETRY_MAVLINK = 1 << 9;
const ESC_SENSOR = 1 << 10;
const VTX_SMARTAUDIO = 1 << 11;
const TELEMETRY_IBUS = 1 << 12;
const VTX_TRAMP = 1 << 13;
const RCDEVICE = 1 << 14;
const LIDAR_TF = 1 << 15;
}
}
#[derive(Debug, Clone)]
pub struct SerialPortConfig {
pub identifier: u8,
pub function_mask: SerialPortFunction,
pub msp_baud_index: u8,
pub gps_baud_index: u8,
pub telemetry_baud_index: u8,
pub blackbox_baud_index: u8,
}
impl SerialPortConfig {
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 7 {
return None;
}
Some(SerialPortConfig {
identifier: data[0],
function_mask: SerialPortFunction::from_bits_truncate(
u16::from_le_bytes([data[1], data[2]])
),
msp_baud_index: data[3],
gps_baud_index: data[4],
telemetry_baud_index: data[5],
blackbox_baud_index: data[6],
})
}
}
MSP_ARMING_CONFIG (61)
Query arming configuration.
Response
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | auto_disarm_delay | Auto-disarm timeout (seconds) |
| 1 | U8 | disarm_kill_switch | Disarm switch behavior |
| 2 | U8 | small_angle | Max arm angle (degrees) |
#[derive(Debug, Clone, Copy)]
pub struct MspArmingConfig {
pub auto_disarm_delay: u8, // Seconds, 0 = disabled
pub disarm_kill_switch: u8, // 0 = normal, 1 = kill switch
pub small_angle: u8, // Max arming angle in degrees
}
impl MspArmingConfig {
pub const COMMAND: u8 = 61;
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 3 {
return None;
}
Some(MspArmingConfig {
auto_disarm_delay: data[0],
disarm_kill_switch: data[1],
small_angle: data[2],
})
}
}
MSP_SET Commands
MSP_SET_FEATURE_CONFIG (37)
Set feature flags.
pub fn build_set_feature_request(features: Features) -> Vec<u8> {
const MSP_SET_FEATURE_CONFIG: u8 = 37;
let bits = features.bits();
let payload = bits.to_le_bytes();
let size = 4u8;
let mut checksum = size ^ MSP_SET_FEATURE_CONFIG;
for b in &payload {
checksum ^= b;
}
vec![
b'$', b'M', b'<',
size,
MSP_SET_FEATURE_CONFIG,
payload[0], payload[1], payload[2], payload[3],
checksum,
]
}
MSP_SET_ARMING_CONFIG (62)
Set arming configuration.
pub fn build_set_arming_config(
auto_disarm_delay: u8,
disarm_kill_switch: u8,
small_angle: u8,
) -> Vec<u8> {
const MSP_SET_ARMING_CONFIG: u8 = 62;
let payload = [auto_disarm_delay, disarm_kill_switch, small_angle];
let size = 3u8;
let mut checksum = size ^ MSP_SET_ARMING_CONFIG;
for b in &payload {
checksum ^= b;
}
vec![
b'$', b'M', b'<',
size,
MSP_SET_ARMING_CONFIG,
payload[0], payload[1], payload[2],
checksum,
]
}
Important Notes
-
Configuration changes require EEPROM write: After modifying settings, send
MSP_EEPROM_WRITE(250) to persist changes. -
Some changes require reboot: Feature flags and serial port changes typically require a reboot (
MSP_REBOOTcommand 68). -
Verify before write: Always read the current configuration, modify only needed fields, and write back the complete structure.