Skip to main content

MSPv2 Betaflight Extensions

MSPv2 introduces 16-bit command codes, enabling firmware-specific extensions. Betaflight extensions use the 0x3000 range.

Command Code Ranges​

RangeAllocation
0x0000-0x00FFMSPv1 compatible
0x1000-0x1FFFMSPv2 Common (shared across firmware)
0x3000-0x3FFFBetaflight-specific extensions

MSPv2 Frame Format Reminder​

┌──────┬─────┬───────┬─────────┬────────┬─────────┬───────┐
│ "$X" │ dir │ flags │ cmd(16) │ sz(16) │ payload │ crc8 │
└──────┴─────┴───────┴─────────┴────────┴─────────┴───────┘

Betaflight Extensions (0x3000+)​

MSP2_BETAFLIGHT_BIND (0x3000)​

Trigger multi-protocol receiver binding.

pub const MSP2_BETAFLIGHT_BIND: u16 = 0x3000;

pub fn build_bind_request() -> Vec<u8> {
build_mspv2_request(MSP2_BETAFLIGHT_BIND, &[])
}

MSP2_MOTOR_OUTPUT_REORDERING (0x3001)​

Query motor output mapping.

Response​

OffsetTypeFieldDescription
0-7U8[8]motor_mapOutput index for each motor
pub const MSP2_MOTOR_OUTPUT_REORDERING: u16 = 0x3001;

#[derive(Debug, Clone, Copy)]
pub struct MotorOutputReordering {
pub motor_map: [u8; 8],
}

impl MotorOutputReordering {
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 8 {
return None;
}
let mut motor_map = [0u8; 8];
motor_map.copy_from_slice(&data[0..8]);
Some(MotorOutputReordering { motor_map })
}
}

MSP2_SET_MOTOR_OUTPUT_REORDERING (0x3002)​

Configure motor output mapping.

Request​

OffsetTypeFieldDescription
0-7U8[8]motor_mapNew output mapping
pub const MSP2_SET_MOTOR_OUTPUT_REORDERING: u16 = 0x3002;

pub fn build_set_motor_reordering(motor_map: [u8; 8]) -> Vec<u8> {
build_mspv2_request(MSP2_SET_MOTOR_OUTPUT_REORDERING, &motor_map)
}

MSP2_SEND_DSHOT_COMMAND (0x3003)​

Send DShot protocol command to ESCs.

Request​

OffsetTypeFieldDescription
0U8motor_indexMotor index (255 = all)
1U8command_countNumber of times to send
2U8commandDShot command code

DShot Commands​

CodeCommandDescription
0DSHOT_CMD_MOTOR_STOPStop motor
1-5DSHOT_CMD_BEEP_xBeep patterns
7DSHOT_CMD_SPIN_DIRECTION_1Normal direction
8DSHOT_CMD_SPIN_DIRECTION_2Reverse direction
9DSHOT_CMD_3D_MODE_OFFDisable 3D mode
10DSHOT_CMD_3D_MODE_ONEnable 3D mode
12DSHOT_CMD_SAVE_SETTINGSSave ESC settings
13DSHOT_CMD_SPIN_DIRECTION_NORMALNormal rotation
14DSHOT_CMD_SPIN_DIRECTION_REVERSEDReversed rotation
20DSHOT_CMD_LED0_ONTurn on LED 0
21DSHOT_CMD_LED1_ONTurn on LED 1
22DSHOT_CMD_LED2_ONTurn on LED 2
23DSHOT_CMD_LED3_ONTurn on LED 3
24DSHOT_CMD_LED0_OFFTurn off LED 0
25DSHOT_CMD_LED1_OFFTurn off LED 1
26DSHOT_CMD_LED2_OFFTurn off LED 2
27DSHOT_CMD_LED3_OFFTurn off LED 3
pub const MSP2_SEND_DSHOT_COMMAND: u16 = 0x3003;

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum DshotCommand {
MotorStop = 0,
Beep1 = 1,
Beep2 = 2,
Beep3 = 3,
Beep4 = 4,
Beep5 = 5,
SpinDirection1 = 7,
SpinDirection2 = 8,
Mode3dOff = 9,
Mode3dOn = 10,
SaveSettings = 12,
SpinDirectionNormal = 13,
SpinDirectionReversed = 14,
Led0On = 20,
Led1On = 21,
Led2On = 22,
Led3On = 23,
Led0Off = 24,
Led1Off = 25,
Led2Off = 26,
Led3Off = 27,
}

pub fn build_dshot_command(motor_index: u8, command: DshotCommand, count: u8) -> Vec<u8> {
let payload = [motor_index, count, command as u8];
build_mspv2_request(MSP2_SEND_DSHOT_COMMAND, &payload)
}

/// Send beep to all motors
pub fn build_dshot_beep_all(beep_number: u8) -> Vec<u8> {
let command = match beep_number {
1 => DshotCommand::Beep1,
2 => DshotCommand::Beep2,
3 => DshotCommand::Beep3,
4 => DshotCommand::Beep4,
_ => DshotCommand::Beep5,
};
build_dshot_command(255, command, 1)
}

MSP2_GET_VTX_DEVICE_STATUS (0x3004)​

Query video transmitter status.

Response​

OffsetTypeFieldDescription
0U8device_typeVTX device type
1U8bandCurrent band
2U8channelCurrent channel
3U8power_indexPower level index
4U8pit_modePit mode active
5-6U16frequencyCurrent frequency MHz
7U8device_readyDevice ready flag
8-9U16low_power_disarmLow power disarm setting
10-11U16pit_mode_freqPit mode frequency
pub const MSP2_GET_VTX_DEVICE_STATUS: u16 = 0x3004;

#[derive(Debug, Clone, Copy)]
pub struct VtxDeviceStatus {
pub device_type: u8,
pub band: u8,
pub channel: u8,
pub power_index: u8,
pub pit_mode: bool,
pub frequency_mhz: u16,
pub device_ready: bool,
}

impl VtxDeviceStatus {
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 8 {
return None;
}
Some(VtxDeviceStatus {
device_type: data[0],
band: data[1],
channel: data[2],
power_index: data[3],
pit_mode: data[4] != 0,
frequency_mhz: u16::from_le_bytes([data[5], data[6]]),
device_ready: data[7] != 0,
})
}
}

MSP2_GET_OSD_WARNINGS (0x3005)​

Query active OSD warning messages.

Response​

OffsetTypeFieldDescription
0U8warning_countNumber of warnings
1+char[]warning_textWarning text (null-terminated)
pub const MSP2_GET_OSD_WARNINGS: u16 = 0x3005;

pub fn parse_osd_warnings(data: &[u8]) -> Vec<String> {
if data.is_empty() {
return Vec::new();
}

let warning_text = String::from_utf8_lossy(&data[1..]);
warning_text
.split('\0')
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
}

MSP2_GET_TEXT / MSP2_SET_TEXT (0x3006 / 0x3007)​

Get or set text strings (pilot name, craft name, profile names).

Text Types​

ValueType
1Pilot name
2Craft name
3PID profile name
4Rate profile name
5Build key
6Release name
7-10Custom messages 0-3

MSP2_GET_TEXT Request​

OffsetTypeFieldDescription
0U8text_typeText type to retrieve

MSP2_GET_TEXT Response​

OffsetTypeFieldDescription
0U8text_typeText type
1+char[]textText content

MSP2_SET_TEXT Request​

OffsetTypeFieldDescription
0U8text_typeText type
1+char[]textNew text content
pub const MSP2_GET_TEXT: u16 = 0x3006;
pub const MSP2_SET_TEXT: u16 = 0x3007;

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum TextType {
PilotName = 1,
CraftName = 2,
PidProfileName = 3,
RateProfileName = 4,
BuildKey = 5,
ReleaseName = 6,
CustomMsg0 = 7,
CustomMsg1 = 8,
CustomMsg2 = 9,
CustomMsg3 = 10,
}

pub fn build_get_text_request(text_type: TextType) -> Vec<u8> {
build_mspv2_request(MSP2_GET_TEXT, &[text_type as u8])
}

pub fn build_set_text_request(text_type: TextType, text: &str) -> Vec<u8> {
let mut payload = vec![text_type as u8];
payload.extend_from_slice(text.as_bytes());
build_mspv2_request(MSP2_SET_TEXT, &payload)
}

MSP2_MCU_INFO (0x300C)​

Query microcontroller information.

Response​

OffsetTypeFieldDescription
0-3U32clock_speedCPU clock in Hz
4-11U8[8]reservedReserved
pub const MSP2_MCU_INFO: u16 = 0x300C;

#[derive(Debug, Clone, Copy)]
pub struct McuInfo {
pub clock_speed_hz: u32,
}

impl McuInfo {
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 4 {
return None;
}
Some(McuInfo {
clock_speed_hz: u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
})
}

pub fn clock_speed_mhz(&self) -> f32 {
self.clock_speed_hz as f32 / 1_000_000.0
}
}

MSPv2 Common Commands (0x1000+)​

MSP2_COMMON_SERIAL_CONFIG (0x1009)​

Query serial port configuration (MSPv2 version).

pub const MSP2_COMMON_SERIAL_CONFIG: u16 = 0x1009;

MSP2_SENSOR_GPS (0x1F03)​

Inject GPS data from external sensor.

Request​

OffsetTypeFieldDescription
0U8instanceSensor instance
1-2U16gps_weekGPS week number
3-6U32ms_towMilliseconds of week
7U8fix_typeFix type
8U8satellitesSatellite count
9-12I32latLatitude × 10^7
13-16I32lonLongitude × 10^7
17-20I32altAltitude in mm
21-22U16ground_speedSpeed in cm/s
23-24U16ground_courseCourse × 100 (degrees)
25-26U16hdopHDOP × 100
27-28U16vdopVDOP × 100
pub const MSP2_SENSOR_GPS: u16 = 0x1F03;

#[derive(Debug, Clone)]
pub struct GpsSensorData {
pub instance: u8,
pub gps_week: u16,
pub ms_tow: u32,
pub fix_type: u8,
pub satellites: u8,
pub lat: i32,
pub lon: i32,
pub alt_mm: i32,
pub ground_speed_cms: u16,
pub ground_course: u16,
pub hdop: u16,
pub vdop: u16,
}

impl GpsSensorData {
pub fn to_bytes(&self) -> Vec<u8> {
let mut data = Vec::with_capacity(29);
data.push(self.instance);
data.extend_from_slice(&self.gps_week.to_le_bytes());
data.extend_from_slice(&self.ms_tow.to_le_bytes());
data.push(self.fix_type);
data.push(self.satellites);
data.extend_from_slice(&self.lat.to_le_bytes());
data.extend_from_slice(&self.lon.to_le_bytes());
data.extend_from_slice(&self.alt_mm.to_le_bytes());
data.extend_from_slice(&self.ground_speed_cms.to_le_bytes());
data.extend_from_slice(&self.ground_course.to_le_bytes());
data.extend_from_slice(&self.hdop.to_le_bytes());
data.extend_from_slice(&self.vdop.to_le_bytes());
data
}
}

pub fn build_gps_sensor_injection(gps: &GpsSensorData) -> Vec<u8> {
build_mspv2_request(MSP2_SENSOR_GPS, &gps.to_bytes())
}

Helper Function​

/// Build MSPv2 native request frame
pub fn build_mspv2_request(command: u16, payload: &[u8]) -> Vec<u8> {
let size = payload.len() as u16;
let mut frame = Vec::with_capacity(9 + payload.len());

// Header
frame.push(b'$');
frame.push(b'X');
frame.push(b'<');

// Flags
frame.push(0);

// Command (little-endian)
frame.push((command & 0xFF) as u8);
frame.push((command >> 8) as u8);

// Size (little-endian)
frame.push((size & 0xFF) as u8);
frame.push((size >> 8) as u8);

// Payload
frame.extend_from_slice(payload);

// CRC8-DVB-S2 (covers flags through payload)
let crc = crc8_dvb_s2(&frame[3..]);
frame.push(crc);

frame
}

fn crc8_dvb_s2(data: &[u8]) -> u8 {
let mut crc: u8 = 0;
for byte in data {
crc ^= byte;
for _ in 0..8 {
if crc & 0x80 != 0 {
crc = (crc << 1) ^ 0xD5;
} else {
crc <<= 1;
}
}
}
crc
}

Complete MSPv2 Example​

/// Query VTX status using MSPv2
fn query_vtx_status<T: std::io::Read + std::io::Write>(
port: &mut T
) -> Result<VtxDeviceStatus, &'static str> {
// Build and send request
let request = build_mspv2_request(MSP2_GET_VTX_DEVICE_STATUS, &[]);
port.write_all(&request).map_err(|_| "Write failed")?;

// Read response
let mut buf = [0u8; 64];
let n = port.read(&mut buf).map_err(|_| "Read failed")?;

// Parse MSPv2 response
if n < 9 || &buf[0..2] != b"$X" {
return Err("Invalid response header");
}

if buf[2] == b'!' {
return Err("Error response");
}

let _flags = buf[3];
let cmd = u16::from_le_bytes([buf[4], buf[5]]);
let size = u16::from_le_bytes([buf[6], buf[7]]) as usize;

if cmd != MSP2_GET_VTX_DEVICE_STATUS {
return Err("Unexpected command");
}

let payload = &buf[8..8 + size];

VtxDeviceStatus::from_bytes(payload).ok_or("Failed to parse VTX status")
}