Skip to main content

Connection Sequence Example

This example demonstrates the recommended sequence for establishing communication with a Betaflight flight controller.

Overview​

When connecting to a flight controller, you should:

  1. Open the serial port
  2. Query API version (protocol detection)
  3. Query firmware variant and version
  4. Query board information
  5. Optionally query additional status

Complete Example​

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

// MSP Command Codes
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;
const MSP_NAME: u8 = 10;
const MSP_STATUS: u8 = 101;

/// Flight controller connection information
#[derive(Debug)]
pub struct FlightControllerInfo {
pub protocol_version: u8,
pub api_version: (u8, u8),
pub fc_variant: String,
pub fc_version: (u8, u8, u8),
pub board_id: String,
pub target_name: String,
pub craft_name: String,
}

/// MSP communication client
pub struct MspClient<T> {
port: T,
buffer: [u8; 256],
}

impl<T: Read + Write> MspClient<T> {
/// Create a new MSP client
pub fn new(port: T) -> Self {
MspClient {
port,
buffer: [0u8; 256],
}
}

/// Build MSPv1 request frame
fn build_request(&self, command: u8, payload: &[u8]) -> Vec<u8> {
let size = payload.len() as u8;
let mut frame = vec![b'$', b'M', b'<', size, command];
frame.extend_from_slice(payload);

// Calculate checksum
let mut checksum = size ^ command;
for byte in payload {
checksum ^= byte;
}
frame.push(checksum);

frame
}

/// Send request and receive response
fn query(&mut self, command: u8) -> Result<Vec<u8>, String> {
self.query_with_payload(command, &[])
}

/// Send request with payload and receive response
fn query_with_payload(&mut self, command: u8, payload: &[u8]) -> Result<Vec<u8>, String> {
let request = self.build_request(command, payload);

// Send request
self.port.write_all(&request)
.map_err(|e| format!("Write failed: {}", e))?;

// Read response with timeout handling
let mut total_read = 0;
let mut attempts = 0;
const MAX_ATTEMPTS: usize = 10;

while total_read < 6 && attempts < MAX_ATTEMPTS {
match self.port.read(&mut self.buffer[total_read..]) {
Ok(0) => {
attempts += 1;
std::thread::sleep(Duration::from_millis(10));
}
Ok(n) => {
total_read += n;
attempts = 0;
}
Err(e) if e.kind() == ErrorKind::WouldBlock => {
attempts += 1;
std::thread::sleep(Duration::from_millis(10));
}
Err(e) => return Err(format!("Read failed: {}", e)),
}
}

if total_read < 6 {
return Err("Response timeout".to_string());
}

// Validate header
if &self.buffer[0..2] != b"$M" {
return Err(format!(
"Invalid header: {:02X} {:02X}",
self.buffer[0], self.buffer[1]
));
}

let direction = self.buffer[2];
if direction == b'!' {
return Err("Error response from FC".to_string());
}
if direction != b'>' {
return Err(format!("Unexpected direction: {:02X}", direction));
}

let size = self.buffer[3] as usize;
let resp_cmd = self.buffer[4];

// Read remaining payload if needed
let required = 6 + size;
while total_read < required && attempts < MAX_ATTEMPTS {
match self.port.read(&mut self.buffer[total_read..]) {
Ok(0) => {
attempts += 1;
std::thread::sleep(Duration::from_millis(10));
}
Ok(n) => {
total_read += n;
attempts = 0;
}
Err(e) if e.kind() == ErrorKind::WouldBlock => {
attempts += 1;
std::thread::sleep(Duration::from_millis(10));
}
Err(e) => return Err(format!("Read failed: {}", e)),
}
}

if total_read < required {
return Err("Incomplete response".to_string());
}

// Verify checksum
let payload = &self.buffer[5..5 + size];
let received_checksum = self.buffer[5 + size];
let mut expected_checksum = size as u8 ^ resp_cmd;
for byte in payload {
expected_checksum ^= byte;
}

if received_checksum != expected_checksum {
return Err("Checksum mismatch".to_string());
}

Ok(payload.to_vec())
}

/// Connect and identify the flight controller
pub fn connect(&mut self) -> Result<FlightControllerInfo, String> {
println!("Connecting to flight controller...");

// Step 1: Query API version
println!(" Querying API version...");
let api_data = self.query(MSP_API_VERSION)?;
if api_data.len() < 3 {
return Err("Invalid API version response".to_string());
}
let protocol_version = api_data[0];
let api_major = api_data[1];
let api_minor = api_data[2];
println!(" Protocol: {}, API: {}.{}", protocol_version, api_major, api_minor);

// Step 2: Query FC variant
println!(" Querying FC variant...");
let variant_data = self.query(MSP_FC_VARIANT)?;
let fc_variant = String::from_utf8_lossy(&variant_data).to_string();
println!(" Variant: {}", fc_variant);

// Step 3: Query FC version
println!(" Querying FC version...");
let version_data = self.query(MSP_FC_VERSION)?;
if version_data.len() < 3 {
return Err("Invalid version response".to_string());
}
let fc_version = (version_data[0], version_data[1], version_data[2]);
println!(" Version: {}.{}.{}", fc_version.0, fc_version.1, fc_version.2);

// Step 4: Query board info
println!(" Querying board info...");
let board_data = self.query(MSP_BOARD_INFO)?;
let board_id = if board_data.len() >= 4 {
String::from_utf8_lossy(&board_data[0..4]).to_string()
} else {
String::new()
};
let target_name = if board_data.len() > 8 {
let name_len = board_data[8] as usize;
if board_data.len() >= 9 + name_len {
String::from_utf8_lossy(&board_data[9..9 + name_len]).to_string()
} else {
String::new()
}
} else {
String::new()
};
println!(" Board: {} ({})", board_id, target_name);

// Step 5: Query craft name
println!(" Querying craft name...");
let name_data = self.query(MSP_NAME)?;
let craft_name = String::from_utf8_lossy(&name_data)
.trim_end_matches('\0')
.to_string();
println!(" Name: {}", if craft_name.is_empty() { "(not set)" } else { &craft_name });

println!("Connection established!");

Ok(FlightControllerInfo {
protocol_version,
api_version: (api_major, api_minor),
fc_variant,
fc_version,
board_id,
target_name,
craft_name,
})
}

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure serial port
// Note: In production, use a proper serial library like `serialport`

// Example with std::fs (Linux/macOS)
let port = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/ttyUSB0")?;

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

match client.connect() {
Ok(info) => {
println!("\nFlight Controller Information:");
println!(" Firmware: {} {}.{}.{}",
info.fc_variant,
info.fc_version.0,
info.fc_version.1,
info.fc_version.2);
println!(" API Version: {}.{}",
info.api_version.0,
info.api_version.1);
println!(" Board: {} ({})",
info.board_id,
info.target_name);
println!(" Craft Name: {}",
if info.craft_name.is_empty() { "(none)" } else { &info.craft_name });
println!(" MSPv2 Support: {}",
if client.supports_v2(&info) { "Yes" } else { "No" });
}
Err(e) => {
eprintln!("Connection failed: {}", e);
}
}

Ok(())
}

Expected Output​

Connecting to flight controller...
Querying API version...
Protocol: 2, API: 1.48
Querying FC variant...
Variant: BTFL
Querying FC version...
Version: 4.4.2
Querying board info...
Board: SPR3 (SPRACINGF3EVO)
Querying craft name...
Name: RACER01
Connection established!

Flight Controller Information:
Firmware: BTFL 4.4.2
API Version: 1.48
Board: SPR3 (SPRACINGF3EVO)
Craft Name: RACER01
MSPv2 Support: Yes

Error Handling​

Common connection errors and solutions:

ErrorPossible CauseSolution
"Write failed"Port not openCheck port path and permissions
"Response timeout"FC not respondingVerify baud rate, check FC power
"Invalid header"Noise on lineCheck wiring, reduce baud rate
"Error response"Command not supportedCheck FC firmware version
"Checksum mismatch"Data corruptionCheck wiring, reduce baud rate

Serial Port Configuration​

Typical settings for Betaflight:

// Using the `serialport` crate (recommended)
use serialport::{self, SerialPort};

fn open_port(path: &str) -> Result<Box<dyn SerialPort>, serialport::Error> {
serialport::new(path, 115200)
.timeout(Duration::from_millis(500))
.data_bits(serialport::DataBits::Eight)
.parity(serialport::Parity::None)
.stop_bits(serialport::StopBits::One)
.flow_control(serialport::FlowControl::None)
.open()
}

Next Steps​

After successful connection:

  1. Telemetry Loop - Continuous data polling
  2. Configuration Write - Modifying settings