MSPv2 Betaflight Extensions
MSPv2 introduces 16-bit command codes, enabling firmware-specific extensions. Betaflight extensions use the 0x3000 range.
Command Code Ranges
| Range | Allocation |
|---|---|
| 0x0000-0x00FF | MSPv1 compatible |
| 0x1000-0x1FFF | MSPv2 Common (shared across firmware) |
| 0x3000-0x3FFF | Betaflight-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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0-7 | U8[8] | motor_map | Output 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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0-7 | U8[8] | motor_map | New 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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | motor_index | Motor index (255 = all) |
| 1 | U8 | command_count | Number of times to send |
| 2 | U8 | command | DShot command code |
DShot Commands
| Code | Command | Description |
|---|---|---|
| 0 | DSHOT_CMD_MOTOR_STOP | Stop motor |
| 1-5 | DSHOT_CMD_BEEP_x | Beep patterns |
| 7 | DSHOT_CMD_SPIN_DIRECTION_1 | Normal direction |
| 8 | DSHOT_CMD_SPIN_DIRECTION_2 | Reverse direction |
| 9 | DSHOT_CMD_3D_MODE_OFF | Disable 3D mode |
| 10 | DSHOT_CMD_3D_MODE_ON | Enable 3D mode |
| 12 | DSHOT_CMD_SAVE_SETTINGS | Save ESC settings |
| 13 | DSHOT_CMD_SPIN_DIRECTION_NORMAL | Normal rotation |
| 14 | DSHOT_CMD_SPIN_DIRECTION_REVERSED | Reversed rotation |
| 20 | DSHOT_CMD_LED0_ON | Turn on LED 0 |
| 21 | DSHOT_CMD_LED1_ON | Turn on LED 1 |
| 22 | DSHOT_CMD_LED2_ON | Turn on LED 2 |
| 23 | DSHOT_CMD_LED3_ON | Turn on LED 3 |
| 24 | DSHOT_CMD_LED0_OFF | Turn off LED 0 |
| 25 | DSHOT_CMD_LED1_OFF | Turn off LED 1 |
| 26 | DSHOT_CMD_LED2_OFF | Turn off LED 2 |
| 27 | DSHOT_CMD_LED3_OFF | Turn 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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | device_type | VTX device type |
| 1 | U8 | band | Current band |
| 2 | U8 | channel | Current channel |
| 3 | U8 | power_index | Power level index |
| 4 | U8 | pit_mode | Pit mode active |
| 5-6 | U16 | frequency | Current frequency MHz |
| 7 | U8 | device_ready | Device ready flag |
| 8-9 | U16 | low_power_disarm | Low power disarm setting |
| 10-11 | U16 | pit_mode_freq | Pit 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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | warning_count | Number of warnings |
| 1+ | char[] | warning_text | Warning 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
| Value | Type |
|---|---|
| 1 | Pilot name |
| 2 | Craft name |
| 3 | PID profile name |
| 4 | Rate profile name |
| 5 | Build key |
| 6 | Release name |
| 7-10 | Custom messages 0-3 |
MSP2_GET_TEXT Request
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | text_type | Text type to retrieve |
MSP2_GET_TEXT Response
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | text_type | Text type |
| 1+ | char[] | text | Text content |
MSP2_SET_TEXT Request
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | text_type | Text type |
| 1+ | char[] | text | New 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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0-3 | U32 | clock_speed | CPU clock in Hz |
| 4-11 | U8[8] | reserved | Reserved |
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
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | U8 | instance | Sensor instance |
| 1-2 | U16 | gps_week | GPS week number |
| 3-6 | U32 | ms_tow | Milliseconds of week |
| 7 | U8 | fix_type | Fix type |
| 8 | U8 | satellites | Satellite count |
| 9-12 | I32 | lat | Latitude × 10^7 |
| 13-16 | I32 | lon | Longitude × 10^7 |
| 17-20 | I32 | alt | Altitude in mm |
| 21-22 | U16 | ground_speed | Speed in cm/s |
| 23-24 | U16 | ground_course | Course × 100 (degrees) |
| 25-26 | U16 | hdop | HDOP × 100 |
| 27-28 | U16 | vdop | VDOP × 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")
}