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:
- Open the serial port
- Query API version (protocol detection)
- Query firmware variant and version
- Query board information
- 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:
| Error | Possible Cause | Solution |
|---|---|---|
| "Write failed" | Port not open | Check port path and permissions |
| "Response timeout" | FC not responding | Verify baud rate, check FC power |
| "Invalid header" | Noise on line | Check wiring, reduce baud rate |
| "Error response" | Command not supported | Check FC firmware version |
| "Checksum mismatch" | Data corruption | Check 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:
- Telemetry Loop - Continuous data polling
- Configuration Write - Modifying settings