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
| Byte | Field | Description |
|---|---|---|
| 0 | protocol_version | 0 = MSPv1, 2 = MSPv2 capable |
| 1 | api_major | API major version |
| 2 | api_minor | API 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:
| Code | Firmware | Notes |
|---|---|---|
BTFL | Betaflight | Most common for racing/freestyle |
INAV | INAV | Navigation-focused |
CLFL | Cleanflight | Legacy |
BAFL | Baseflight | Legacy |
MWII | MultiWii | Original AVR implementation |
RCFL | Raceflight | Performance-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:
| Byte | Field |
|---|---|
| 0 | Major version |
| 1 | Minor version |
| 2 | Patch 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:
| Offset | Size | Field |
|---|---|---|
| 0-3 | 4 | Board identifier (e.g., "SPR3") |
| 4-5 | 2 | Hardware revision |
| 6 | 1 | Board type (0=FC, 1=VTX, etc.) |
| 7+ | var | Additional 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:
| Feature | Min API Version | Notes |
|---|---|---|
| Basic MSPv1 | 1.0 | All versions |
| MSPv2 support | 1.31 | Protocol version 2 |
| OSD configuration | 1.21 | MSP_OSD_CONFIG |
| LED strip config | 1.22 | Enhanced LED commands |
| VTX control | 1.31 | MSP_VTX_CONFIG |
| GPS rescue | 1.42 | MSP_GPS_RESCUE |
| Simplified tuning | 1.44 | Betaflight 4.3+ |
| Motor output reordering | 1.44 | MSP2 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")
}
}