Skip to main content

Version Detection

This page covers how to detect and negotiate MSP protocol versions when connecting to a flight controller.

Protocol Detection Flow​

┌─────────────────────────────────────────────────────────────────┐
│ Connection Sequence │
├─────────────────────────────────────────────────────────────────┤
│ 1. Send MSP_API_VERSION (v1) │
│ 2. Parse response → get protocol_version field │
│ 3. If protocol_version >= 2, use MSPv2 │
│ 4. Query MSP_FC_VARIANT for firmware identification │
│ 5. Query MSP_FC_VERSION for firmware version │
│ 6. Proceed with appropriate protocol version │
└─────────────────────────────────────────────────────────────────┘

MSP_API_VERSION Response​

Command code: 1

ByteFieldDescription
0protocol_version0 = MSPv1, 2 = MSPv2 capable
1api_majorAPI major version
2api_minorAPI minor version
/// API version information
#[derive(Debug, Clone, Copy)]
pub struct ApiVersion {
/// Protocol version (0 = v1 only, 2 = v2 capable)
pub protocol_version: u8,
/// API major version number
pub api_major: u8,
/// API minor version number
pub api_minor: u8,
}

impl ApiVersion {
/// Parse from MSP_API_VERSION response payload
pub fn from_payload(data: &[u8]) -> Option<Self> {
if data.len() < 3 {
return None;
}
Some(ApiVersion {
protocol_version: data[0],
api_major: data[1],
api_minor: data[2],
})
}

/// Check if MSPv2 is supported
pub fn supports_v2(&self) -> bool {
self.protocol_version >= 2
}

/// Get API version as comparable tuple
pub fn version(&self) -> (u8, u8) {
(self.api_major, self.api_minor)
}
}

Complete Version Detection​

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

const MSP_API_VERSION: u8 = 1;
const MSP_FC_VARIANT: u8 = 2;
const MSP_FC_VERSION: u8 = 3;
const MSP_BOARD_INFO: u8 = 4;
const MSP_BUILD_INFO: u8 = 5;

/// Flight controller information
#[derive(Debug)]
pub struct FcInfo {
pub api_version: ApiVersion,
pub variant: String, // e.g., "BTFL"
pub version: (u8, u8, u8), // major.minor.patch
pub board_id: String, // e.g., "SPRACINGF3"
}

/// MSP communication handler
pub struct MspClient<T> {
port: T,
use_v2: bool,
}

impl<T: Read + Write> MspClient<T> {
pub fn new(port: T) -> Self {
MspClient {
port,
use_v2: false,
}
}

/// Build MSPv1 request frame
fn build_v1_request(&self, command: u8) -> Vec<u8> {
vec![
b'$', b'M', b'<',
0, // size
command,
command, // checksum = 0 ^ command
]
}

/// Send request and read response
fn query(&mut self, command: u8) -> Result<Vec<u8>, &'static str> {
let request = self.build_v1_request(command);
self.port.write_all(&request).map_err(|_| "Write failed")?;

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

// Parse response
if n < 6 {
return Err("Response too short");
}

if &buffer[0..2] != b"$M" {
return Err("Invalid header");
}

let direction = buffer[2];
if direction == b'!' {
return Err("Error response");
}
if direction != b'>' {
return Err("Invalid direction");
}

let size = buffer[3] as usize;
let _cmd = buffer[4];

if n < 6 + size {
return Err("Incomplete response");
}

Ok(buffer[5..5 + size].to_vec())
}

/// Detect flight controller and negotiate protocol version
pub fn detect(&mut self) -> Result<FcInfo, &'static str> {
// Step 1: Query API version
let api_payload = self.query(MSP_API_VERSION)?;
let api_version = ApiVersion::from_payload(&api_payload)
.ok_or("Invalid API version response")?;

// Enable v2 if supported
self.use_v2 = api_version.supports_v2();

// Step 2: Query FC variant
let variant_payload = self.query(MSP_FC_VARIANT)?;
let variant = String::from_utf8_lossy(&variant_payload).to_string();

// Step 3: Query FC version
let version_payload = self.query(MSP_FC_VERSION)?;
let version = if version_payload.len() >= 3 {
(version_payload[0], version_payload[1], version_payload[2])
} else {
(0, 0, 0)
};

// Step 4: Query board info
let board_payload = self.query(MSP_BOARD_INFO)?;
let board_id = if board_payload.len() >= 4 {
String::from_utf8_lossy(&board_payload[0..4]).to_string()
} else {
String::new()
};

Ok(FcInfo {
api_version,
variant,
version,
board_id,
})
}
}

Firmware Identification​

MSP_FC_VARIANT (2)​

Returns a 4-character firmware identifier:

CodeFirmwareNotes
BTFLBetaflightMost common for racing/freestyle
INAVINAVNavigation-focused
CLFLCleanflightLegacy
BAFLBaseflightLegacy
MWIIMultiWiiOriginal AVR implementation
RCFLRaceflightPerformance-focused (discontinued)
/// Known flight controller variants
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FcVariant {
Betaflight,
Inav,
Cleanflight,
Baseflight,
MultiWii,
Raceflight,
Unknown,
}

impl FcVariant {
pub fn from_code(code: &str) -> Self {
match code.to_uppercase().as_str() {
"BTFL" => FcVariant::Betaflight,
"INAV" => FcVariant::Inav,
"CLFL" => FcVariant::Cleanflight,
"BAFL" => FcVariant::Baseflight,
"MWII" => FcVariant::MultiWii,
"RCFL" => FcVariant::Raceflight,
_ => FcVariant::Unknown,
}
}

pub fn code(&self) -> &'static str {
match self {
FcVariant::Betaflight => "BTFL",
FcVariant::Inav => "INAV",
FcVariant::Cleanflight => "CLFL",
FcVariant::Baseflight => "BAFL",
FcVariant::MultiWii => "MWII",
FcVariant::Raceflight => "RCFL",
FcVariant::Unknown => "????",
}
}
}

MSP_FC_VERSION (3)​

Returns firmware version as three bytes:

ByteField
0Major version
1Minor version
2Patch version
/// Firmware version
#[derive(Debug, Clone, Copy)]
pub struct FcVersion {
pub major: u8,
pub minor: u8,
pub patch: u8,
}

impl FcVersion {
pub fn from_payload(data: &[u8]) -> Option<Self> {
if data.len() < 3 {
return None;
}
Some(FcVersion {
major: data[0],
minor: data[1],
patch: data[2],
})
}

/// Compare versions (returns true if self >= other)
pub fn at_least(&self, major: u8, minor: u8, patch: u8) -> bool {
match self.major.cmp(&major) {
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Less => false,
std::cmp::Ordering::Equal => match self.minor.cmp(&minor) {
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Less => false,
std::cmp::Ordering::Equal => self.patch >= patch,
},
}
}
}

impl std::fmt::Display for FcVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}

MSP_BOARD_INFO (4)​

Returns board identification:

OffsetSizeField
0-34Board identifier (e.g., "SPR3")
4-52Hardware revision
61Board type (0=FC, 1=VTX, etc.)
7+varAdditional info (version dependent)
/// Board information
#[derive(Debug)]
pub struct BoardInfo {
pub identifier: String,
pub hardware_revision: u16,
pub board_type: u8,
}

impl BoardInfo {
pub fn from_payload(data: &[u8]) -> Option<Self> {
if data.len() < 6 {
return None;
}

let identifier = String::from_utf8_lossy(&data[0..4]).to_string();
let hardware_revision = u16::from_le_bytes([data[4], data[5]]);
let board_type = if data.len() > 6 { data[6] } else { 0 };

Some(BoardInfo {
identifier,
hardware_revision,
board_type,
})
}
}

Feature Detection​

Different firmware versions support different features. Use version checks:

/// Feature availability based on version
pub struct FeatureSupport {
pub fc_version: FcVersion,
pub api_version: ApiVersion,
}

impl FeatureSupport {
/// Check if DShot commands are supported
pub fn has_dshot_commands(&self) -> bool {
// Available in Betaflight 4.0+
self.fc_version.at_least(4, 0, 0)
}

/// Check if GPS rescue is available
pub fn has_gps_rescue(&self) -> bool {
// Available in Betaflight 4.1+
self.fc_version.at_least(4, 1, 0)
}

/// Check if MSPv2 extensions are available
pub fn has_msp_v2_extensions(&self) -> bool {
self.api_version.supports_v2()
}

/// Check if OSD configuration is available
pub fn has_osd_config(&self) -> bool {
// OSD support varies by board
self.api_version.version() >= (1, 21)
}
}

Minimum Version Requirements​

Common API version requirements:

FeatureMin API VersionNotes
Basic MSPv11.0All versions
MSPv2 support1.31Protocol version 2
OSD configuration1.21MSP_OSD_CONFIG
LED strip config1.22Enhanced LED commands
VTX control1.31MSP_VTX_CONFIG
GPS rescue1.42MSP_GPS_RESCUE
Simplified tuning1.44Betaflight 4.3+
Motor output reordering1.44MSP2 command

Version Negotiation Example​

/// Complete version negotiation
fn connect_and_detect(port_path: &str) -> Result<(), Box<dyn std::error::Error>> {
// Open serial port
let port = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(port_path)?;

let mut client = MspClient::new(port);

// Detect FC
let info = client.detect()?;

println!("Flight Controller Detected:");
println!(" Firmware: {} v{}.{}.{}",
info.variant,
info.version.0,
info.version.1,
info.version.2
);
println!(" Board: {}", info.board_id);
println!(" API: {}.{}",
info.api_version.api_major,
info.api_version.api_minor
);
println!(" MSPv2 Support: {}",
if info.api_version.supports_v2() { "Yes" } else { "No" }
);

// Check feature support
let features = FeatureSupport {
fc_version: FcVersion {
major: info.version.0,
minor: info.version.1,
patch: info.version.2,
},
api_version: info.api_version,
};

if features.has_dshot_commands() {
println!(" DShot commands: Available");
}

if features.has_gps_rescue() {
println!(" GPS Rescue: Available");
}

Ok(())
}

Protocol Auto-Selection​

/// Protocol handler with automatic version selection
pub struct MspConnection<T> {
transport: T,
protocol: Protocol,
}

#[derive(Debug, Clone, Copy)]
pub enum Protocol {
V1Only,
V2Preferred,
V2OverV1, // For systems requiring V1 framing
}

impl<T: Read + Write> MspConnection<T> {
/// Create connection and auto-detect protocol
pub fn connect(mut transport: T) -> Result<Self, &'static str> {
// Try V1 first (universal support)
let api_version = Self::query_api_version(&mut transport)?;

let protocol = if api_version.supports_v2() {
Protocol::V2Preferred
} else {
Protocol::V1Only
};

Ok(MspConnection {
transport,
protocol,
})
}

fn query_api_version(transport: &mut T) -> Result<ApiVersion, &'static str> {
// Send V1 query
let request = [b'$', b'M', b'<', 0, 1, 1]; // MSP_API_VERSION
transport.write_all(&request).map_err(|_| "Write failed")?;

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

if n < 9 || &buf[0..3] != b"$M>" {
return Err("Invalid response");
}

ApiVersion::from_payload(&buf[5..8]).ok_or("Parse failed")
}
}