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

Step 2: Configure Flight Controller Firmware

For Betaflight (MSP)

  1. Open Betaflight Configurator and open the Ports tab.
  2. Find the UART connected to your companion board (e.g., UART2 or UART6).
  3. Toggle the MSP switch to ON for that UART.
  4. Set the Baud Rate to 115200 (or 57600).
  5. Click Save and Reboot.

For INAV (MSP or MAVLink)

  1. Open INAV Configurator and open the Ports tab.
  2. Under the designated UART, set the telemetry or data dropdown to:
    • MSP (Baud rate 115200): Recommended for custom Python scripts, sensor queries, or light command telemetry.
    • MAVLink (Baud rate 115200 or 57600): Recommended if integrating with ROS 2, MAVROS, or offboard mission planners.
  3. Click Save and Reboot.

Step 3: Configure Companion Computer Serial Port

On Raspberry Pi (Raspberry Pi OS / Ubuntu)

  1. Open terminal and run sudo raspi-config $\rightarrow$ Interface Options $\rightarrow$ Serial Port.
  2. Select NO to “Would you like a login shell to be accessible over serial?”.
  3. Select YES to “Would you like the serial port hardware to be enabled?”.
  4. The physical GPIO serial port will map to /dev/ttyAMA0 or /dev/ttyS0.
  5. Grant serial access permissions: sudo usermod -aG dialout $USER.

On NVIDIA Jetson (Jetpack)

  1. Disable the default Linux kernel OS serial console running on /dev/ttyTHS1 (or /dev/ttyTHS0 on Orin Nano series):Bashsudo systemctl stop nvgetty sudo systemctl disable nvgetty sudo usermod -aG dialout $USER
  2. 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

ProtocolCompatible FirmwareBandwidth OverheadPrimary Use Case
MSPBetaflight, INAV, CleanflightVery Low (Compact binary)Querying sensor streams, modifying OSD text, sending raw RC channel inputs (MSP_SET_RAW_RC).
MAVLinkINAV, ArduPilot, PX4Moderate (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

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

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

  1. Connect your flight controller to INAV Configurator.
  2. 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 (or 57600).
  3. 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 TopicMessage TypeDescription
/mavros/imu/datasensor_msgs/msg/ImuFiltered IMU readings (Orientation, Gyro, Accel).
/mavros/global_position/globalsensor_msgs/msg/NavSatFixGPS Latitude, Longitude, and Altitude.
/mavros/statemavros_msgs/msg/StateArming status, flight mode, and heartbeat connection.
/mavros/batterysensor_msgs/msg/BatteryStateMain LiPo voltage and current drain.