Interfacing an edge companion computer (Raspberry Pi or NVIDIA Jetson) with a Flight Controller (FC) running Betaflight or INAV relies on establishing a hardware UART serial connection to pass structured messaging protocols: MSP (MultiWii Serial Protocol) for Betaflight/INAV or MAVLink for INAV/ArduPilot.
Step 1: Hardware Wiring (UART)
Connect a hardware UART port on your companion computer directly to an unused UART port on your Flight Controller.
[ Companion Computer ] [ Flight Controller (FC) ]
(Raspberry Pi / Jetson) (Betaflight / INAV)
──────────────────────── ─────────────────────────
TXD (GPIO 14 / UART1_TX) ─────────► RX (e.g., RX2 / RX6)
RXD (GPIO 15 / UART1_RX) ◄───────── TX (e.g., TX2 / TX6)
GND ─────────► GND
- Logic Voltage: Raspberry Pi and Jetson GPIO ports run on 3.3V logic. Ensure your FC UART pins are 3.3V compliant (almost all modern F4, F7, and H7 FCs are 3.3V signal level).
- Power Isolation: Do not power a Jetson board or Raspberry Pi directly off the Flight Controller’s internal 5V 1A/2A BEC. Use a dedicated high-amperage 5V step-down BEC wired directly to the main LiPo power leads.
Step 2: Configure Flight Controller Firmware
For Betaflight (MSP)
- Open Betaflight Configurator and open the Ports tab.
- Find the UART connected to your companion board (e.g., UART2 or UART6).
- Toggle the MSP switch to ON for that UART.
- Set the Baud Rate to
115200(or57600). - Click Save and Reboot.
For INAV (MSP or MAVLink)
- Open INAV Configurator and open the Ports tab.
- Under the designated UART, set the telemetry or data dropdown to:
MSP(Baud rate115200): Recommended for custom Python scripts, sensor queries, or light command telemetry.MAVLink(Baud rate115200or57600): Recommended if integrating with ROS 2, MAVROS, or offboard mission planners.
- Click Save and Reboot.
Step 3: Configure Companion Computer Serial Port
On Raspberry Pi (Raspberry Pi OS / Ubuntu)
- Open terminal and run
sudo raspi-config$\rightarrow$ Interface Options $\rightarrow$ Serial Port. - Select NO to “Would you like a login shell to be accessible over serial?”.
- Select YES to “Would you like the serial port hardware to be enabled?”.
- The physical GPIO serial port will map to
/dev/ttyAMA0or/dev/ttyS0. - Grant serial access permissions:
sudo usermod -aG dialout $USER.
On NVIDIA Jetson (Jetpack)
- Disable the default Linux kernel OS serial console running on
/dev/ttyTHS1(or/dev/ttyTHS0on Orin Nano series):Bashsudo systemctl stop nvgetty sudo systemctl disable nvgetty sudo usermod -aG dialout $USER - Reboot the Jetson.
Step 4: Python Communication Code
Option A: Interfacing via MSP (pymultiwii / Python)
Install the required serial packages:
Bash
pip install pymultiwii pyserial
Python Script (read_msp.py):
Python
import time
from pymultiwii import MultiWii
# Port mapping: '/dev/ttyAMA0' on Pi, '/dev/ttyTHS1' on Jetson
SERIAL_PORT = "/dev/ttyAMA0"
try:
board = MultiWii(SERIAL_PORT)
print("Connected to Flight Controller via MSP!")
while True:
# Request attitude vector (Roll, Pitch, Yaw)
board.getData(MultiWii.ATTITUDE)
attitude = board.attitudeData
print(f"Roll: {attitude['angx']}° | Pitch: {attitude['angy']}° | Heading: {attitude['heading']}°")
time.sleep(0.1)
except Exception as e:
print(f"MSP Communication Error: {e}")
Option B: Interfacing via MAVLink (pymavlink / INAV)
Install PyMAVLink:
Bash
pip install pymavlink
Python Script (read_mavlink.py):
Python
import time
from pymavlink import mavutil
# Open connection to INAV MAVLink UART stream
master = mavutil.mavlink_connection('/dev/ttyAMA0', baud=115200)
print("Waiting for MAVLink Heartbeat from INAV...")
master.wait_heartbeat()
print(f"Connected! System ID: {master.target_system}, Component ID: {master.target_component}")
# Request continuous stream update
master.mav.request_data_stream_send(
master.target_system,
master.target_component,
mavutil.mavlink.MAV_DATA_STREAM_ALL,
10, # 10 Hz rate
1 # Enable stream
)
while True:
msg = master.recv_match(type='ATTITUDE', blocking=True)
if msg:
print(f"Roll: {msg.roll:.2f} rad | Pitch: {msg.pitch:.2f} rad | Yaw: {msg.yaw:.2f} rad")
time.sleep(0.05)
Protocol Comparison Matrix
| Protocol | Compatible Firmware | Bandwidth Overhead | Primary Use Case |
| MSP | Betaflight, INAV, Cleanflight | Very Low (Compact binary) | Querying sensor streams, modifying OSD text, sending raw RC channel inputs (MSP_SET_RAW_RC). |
| MAVLink | INAV, ArduPilot, PX4 | Moderate (Standard header frames) | Offboard waypoint control, ROS 2 / MAVROS node integration, automated position hold systems. |
To control Betaflight motor speeds via a companion computer like a Raspberry Pi using MSP_SET_RAW_RC (Command ID 200), you must override the RC receiver channels directly over serial.
Prerequisites & Safety
- REMOVE ALL PROPELLERS before executing any code.
- Betaflight Configurator Setup:
- Go to the Receiver tab.
- Set Receiver Mode to
RX_MSP(This tells Betaflight to accept stick commands from MSP instead of an ELRS/CRSF hardware radio). - Ensure your UART port has MSP enabled under the Ports tab.
Python Script (msp_rc_control.py)
This script constructs raw MSP frames containing 8 channels encoded as 16-bit unsigned integers ([Roll, Pitch, Throttle, Yaw, AUX1, AUX2, AUX3, AUX4]) and streams them continuously to avoid Betaflight’s fail-safe timeout.
Python
import time
import struct
import serial
# Define MSP Command Code for MSP_SET_RAW_RC
MSP_SET_RAW_RC = 200
def create_msp_frame(cmd, data_payload):
"""
Constructs a standard MSP v1 Message Frame:
$M< + Size + Command ID + Data + Checksum
"""
size = len(data_payload)
header = b'$M<'
# Calculate XOR checksum over Size, Command, and Data Payload
checksum = size ^ cmd
for byte in data_payload:
checksum ^= byte
frame = header + bytes([size, cmd]) + data_payload + bytes([checksum])
return frame
def pack_rc_channels(channels):
"""
Packs channel array (1000-2000 us values) into 16-bit Little Endian bytes
"""
# Requires 8 channels: Roll, Pitch, Throttle, Yaw, AUX1, AUX2, AUX3, AUX4
return struct.pack('<8H', *channels)
def send_rc_command(ser, roll=1500, pitch=1500, throttle=1000, yaw=1500, aux1=1000, aux2=1000, aux3=1000, aux4=1000):
"""
Packs RC channel values and writes frame to serial interface
"""
rc_data = [roll, pitch, throttle, yaw, aux1, aux2, aux3, aux4]
payload = pack_rc_channels(rc_data)
frame = create_msp_frame(MSP_SET_RAW_RC, payload)
ser.write(frame)
def main():
# Serial Port Setup: '/dev/ttyAMA0' (Raspberry Pi GPIO) or '/dev/ttyUSB0' (USB Cable)
SERIAL_PORT = '/dev/ttyAMA0'
BAUD_RATE = 115200
try:
ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=0.1)
print(f"Serial connection opened on {SERIAL_PORT}")
# Give serial interface time to initialize
time.sleep(1.0)
print("\n--- STAGE 1: Disarmed Baseline Stream ---")
# Must send continuous streams at 20Hz+ to prevent MSP Receiver Failsafe
for _ in range(50):
# DISARMED: Throttle=1000, AUX1=1000 (Disarmed state)
send_rc_command(ser, roll=1500, pitch=1500, throttle=1000, yaw=1500, aux1=1000)
time.sleep(0.05)
print("\n--- STAGE 2: Arming Quadcopter ---")
# Set AUX1=1800 to trigger Betaflight ARM (Ensure AUX1 is set to ARM in Betaflight Modes tab)
for _ in range(40):
send_rc_command(ser, roll=1500, pitch=1500, throttle=1000, yaw=1500, aux1=1800)
time.sleep(0.05)
print("\n--- STAGE 3: Spin Motors (Low Throttle Test) ---")
# Throttle raised to 1200 (~20% speed spin test)
for _ in range(100):
send_rc_command(ser, roll=1500, pitch=1500, throttle=1200, yaw=1500, aux1=1800)
time.sleep(0.05)
print("\n--- STAGE 4: Disarming & Stopping Motors ---")
# Throttle zeroed and AUX1 dropped to DISARM
for _ in range(30):
send_rc_command(ser, roll=1500, pitch=1500, throttle=1000, yaw=1500, aux1=1000)
time.sleep(0.05)
print("Test Complete.")
except serial.SerialException as e:
print(f"Serial Error: {e}")
finally:
if 'ser' in locals() and ser.is_open:
ser.close()
if __name__ == '__main__':
main()
Key Frame Specifications
- Command Code:
MSP_SET_RAW_RC=200 - Structure:
$M<+[Payload Size = 16 bytes]+[Command ID = 200]+[Payload Data]+[Checksum] - Pulse Width Values: Channels range strictly from
1000to2000microseconds (1500= Center). - Channel Map Order: Standard MultiWii mapping expects:
RollPitchThrottleYawAUX1(Arming switch)AUX2AUX3AUX4
- Stream Rate: You must loop and transmit
MSP_SET_RAW_RCframes at 20 Hz – 50 Hz. If Betaflight drops data for longer than the receiver failsafe delay (default: 1.0s), it will automatically disarm the drone.
To bridge INAV flight data into ROS 2 Humble using MAVROS (MAVROS2), you need to set up the MAVLink telemetry stream on INAV, install MAVROS and its geographic datasets, and launch the ROS 2 node targeted at your connection URL.
Step 1: Configure MAVLink on INAV
- Connect your flight controller to INAV Configurator.
- Go to the Ports tab:
- Select the UART connected to your companion computer (e.g., UART2 or UART6).
- Under Telemetry Output, choose MAVLink.
- Set the baud rate to
115200(or57600).
- Click Save and Reboot.
Step 2: Install MAVROS on ROS 2 Humble
Run the following commands on your companion computer (Ubuntu 22.04 / ROS 2 Humble):
Bash
# Update package list and install MAVROS2
sudo apt update
sudo apt install -y ros-humble-mavros ros-humble-mavros-extras
# Install mandatory GeographicLib datasets (required for GPS / NavSatFix conversions)
sudo ros2 run mavros install_geographiclib_datasets.sh
Step 3: Launch the MAVROS Node
Launch MAVROS using the node.launch or apm.launch launch file, substituting your FC connection URL.
Option A: Direct Hardware Serial (GPIO UART / USB)
If connected via GPIO serial (/dev/ttyAMA0 or /dev/ttyTHS1) or USB Serial (/dev/ttyUSB0):
Bash
ros2 launch mavros node.launch \
fcu_url:="serial:///dev/ttyAMA0:115200"
Option B: Network / UDP Proxy
If streaming MAVLink over a local network or via a proxy (e.g., MAVProxy / UDP):
Bash
ros2 launch mavros node.launch \
fcu_url:="udp://:14550@"
Step 4: Verify Data Bridging
Open a new terminal, source ROS 2, and inspect the incoming topics published by MAVROS:
Bash
# List all active MAVROS topics
ros2 topic list | grep mavros
# Echo IMU sensor data (Orientation, Angular Velocity, Acceleration)
ros2 topic echo /mavros/imu/data
# Echo GPS Position Fix
ros2 topic echo /mavros/global_position/global
# Echo Flight Controller Connection State
ros2 topic echo /mavros/state
Common ROS 2 Topics Streamed
| ROS Topic | Message Type | Description |
|---|---|---|
/mavros/imu/data | sensor_msgs/msg/Imu | Filtered IMU readings (Orientation, Gyro, Accel). |
/mavros/global_position/global | sensor_msgs/msg/NavSatFix | GPS Latitude, Longitude, and Altitude. |
/mavros/state | mavros_msgs/msg/State | Arming status, flight mode, and heartbeat connection. |
/mavros/battery | sensor_msgs/msg/BatteryState | Main LiPo voltage and current drain. |