Skip to content

Betaflight SITL with gazebo

SITL network flow

flowchart LR
    configurator["Betaflight Configurator<br/>Browser app"]
    websockify["websockify<br/>WS :6761 -> TCP :5761"]
    sitl["Betaflight SITL"]
    gazebo["Gazebo<br/>Betaflight bridge plugin"]
    rc["External RC script"]
    realflight["RealFlight bridge"]

    configurator -- "WebSocket<br/>ws://127.0.0.1:6761" --> websockify
    websockify -- "TCP MSP/UART<br/>127.0.0.1:5761" --> sitl
    sitl -- "UDP motor output<br/>:9002" --> gazebo
    gazebo -- "UDP FDM / sensors<br/>:9003" --> sitl
    rc -- "UDP RC channels<br/>:9004" --> sitl
    sitl -- "UDP raw PWM<br/>:9001" --> realflight

Betaflight SITL port usage

Source: Betaflight SITL Autopilot Testing with Gazebo

Port Protocol Direction Usage
9001 UDP Betaflight SITL -> RealFlight Raw PWM output for the RealFlight bridge.
9002 UDP Betaflight SITL -> Gazebo Motor speed commands consumed by the Gazebo Betaflight bridge plugin.
9003 UDP Gazebo -> Betaflight SITL Flight dynamics model data, including IMU, position, velocity, GPS, and barometer state.
9004 UDP External RC script -> Betaflight SITL RC channel input, typically 16 channels in the 1000-2000 range.
5761 TCP Betaflight App/configurator -> Betaflight SITL SITL UART/MSP connection exposed by Betaflight.
6761 WebSocket Betaflight App -> websockify -> TCP 5761 Browser-friendly proxy endpoint, for example ws://127.0.0.1:6761.

When started manually, Betaflight SITL opens UDP servers on 9003 and 9004, sends motor output to Gazebo on 9002, and can also send raw PWM output to RealFlight on 9001.

RC Port 9004

Send RC command
#!/usr/bin/env python3
import socket
import struct
import time


PORT = 9004
RATE_HZ = 50
DT = 1.0 / RATE_HZ

ROLL = 0
PITCH = 1
THROTTLE = 2
YAW = 3
ARM = 4
AUTOPILOT = 5

RC_MIN = 1000
RC_MID = 1500
RC_MAX = 2000

THROTTLE_TAKEOFF = 1650
THROTTLE_LAND = 1050
PITCH_FORWARD = 1600


sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)


def send_rc(channels):
    # little-endian: double + 16 uint16
    pkt = struct.pack("<d16H", time.time(), *channels)
    sock.sendto(pkt, ("127.0.0.1", PORT))


def channels(throttle=RC_MIN, pitch=RC_MID, armed=False):
    ch = [RC_MID] * 16
    ch[ROLL] = RC_MID
    ch[PITCH] = pitch
    ch[THROTTLE] = throttle
    ch[YAW] = RC_MID
    ch[ARM] = RC_MAX if armed else RC_MIN
    ch[AUTOPILOT] = RC_MAX
    return ch


def hold(duration, throttle=RC_MIN, pitch=RC_MID, armed=False):
    end_time = time.monotonic() + duration
    ch = channels(throttle=throttle, pitch=pitch, armed=armed)

    while time.monotonic() < end_time:
        send_rc(ch)
        time.sleep(DT)


def step(message, duration, throttle=RC_MIN, pitch=RC_MID, armed=False):
    print(f"{time.strftime('%H:%M:%S')} - {message}", flush=True)
    hold(duration, throttle=throttle, pitch=pitch, armed=armed)


def main():
    try:
        step("Send neutral RC with throttle low", 1.0, throttle=RC_MIN, armed=False)

        step("Arm with throttle low", 1.0, throttle=RC_MIN, armed=True)

        step("Take off", 4.0, throttle=THROTTLE_TAKEOFF, armed=True)

        step("Hold hover throttle", 3.0, throttle=RC_MID, armed=True)

        step("Land by lowering throttle", 3.0, throttle=THROTTLE_LAND, armed=True)
        step("Disarm", 1.0, throttle=RC_MIN, armed=False)
    except KeyboardInterrupt:
        print(f"{time.strftime('%H:%M:%S')} - Interrupted", flush=True)
    finally:
        step("Send final disarmed low-throttle command", 0.5, throttle=RC_MIN, armed=False)


if __name__ == "__main__":
    main()

files

├── bin
│      ├── libBetaflightPlugin.so
│      └── betaflight_2025.12.2_SITL    
├── models
│   └── betaloop_iris_with_standoffs
│      └── meshes
├── plugins (source)
├── worlds
│   └── betaloop_iris_with_standoffs.sdf
└── scripts
    └── start_gz.sh