PX4 ROS2 Offboard Attitude – Motors spin but no takeoff (Gazebo X500)

Hi everyone,

I’m trying to control a drone in offboard attitude mode using ROS2 + PX4 (SITL Gazebo, X500 model).

What works

  • Offboard mode is correctly engaged

  • Vehicle arms successfully

  • Motors spin

  • Attitude setpoints are published at 50 Hz

Problem

The drone never takes off, no matter the thrust value (tested from 0.5 up to 1.0).


Code

import rclpy
from rclpy.node import Node
import numpy as np

from px4_msgs.msg import OffboardControlMode
from px4_msgs.msg import VehicleAttitudeSetpoint
from px4_msgs.msg import VehicleCommand


class AttitudeTakeoff(Node):

    def __init__(self):
        super().__init__('attitude_takeoff_node')

        self.offboard_pub = self.create_publisher(
            OffboardControlMode,
            '/fmu/in/offboard_control_mode',
            10)

        self.attitude_pub = self.create_publisher(
            VehicleAttitudeSetpoint,
            '/fmu/in/vehicle_attitude_setpoint',
            10)

        self.cmd_pub = self.create_publisher(
            VehicleCommand,
            '/fmu/in/vehicle_command',
            10)

        self.timer = self.create_timer(0.02, self.timer_callback)
        self.counter = 0

    def arm(self):
        msg = VehicleCommand()
        msg.command = VehicleCommand.VEHICLE_CMD_COMPONENT_ARM_DISARM
        msg.param1 = 1.0
        msg.target_system = 1
        msg.target_component = 1
        msg.from_external = True
        self.cmd_pub.publish(msg)

    def set_offboard_mode(self):
        msg = VehicleCommand()
        msg.command = VehicleCommand.VEHICLE_CMD_DO_SET_MODE
        msg.param1 = 1.0
        msg.param2 = 6.0  # OFFBOARD
        msg.target_system = 1
        msg.target_component = 1
        msg.from_external = True
        self.cmd_pub.publish(msg)

    def level_quaternion(self):
        return [1.0, 0.0, 0.0, 0.0]

    def timer_callback(self):

        offboard_msg = OffboardControlMode()
        offboard_msg.attitude = True
        offboard_msg.body_rate = False
        offboard_msg.position = False
        offboard_msg.velocity = False

        offboard_msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
        self.offboard_pub.publish(offboard_msg)

        attitude_msg = VehicleAttitudeSetpoint()
        attitude_msg.q_d = self.level_quaternion()

        thrust = -1.0
        attitude_msg.thrust_body = [0.0, 0.0, thrust]

        attitude_msg.timestamp = offboard_msg.timestamp
        self.attitude_pub.publish(attitude_msg)

        if self.counter == 50:
            self.set_offboard_mode()
            self.arm()

        self.counter += 1


def main(args=None):
    rclpy.init(args=args)
    node = AttitudeTakeoff()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()


Questions

  1. Is it actually possible to take off using only VehicleAttitudeSetpoint in PX4 offboard mode?

  2. Could internal PX4 controllers still override or limit thrust in this mode?

  3. Is there any takeoff protection preventing lift-off in attitude mode?

  4. Does the X500 model require specific parameters or thrust scaling?

Thanks a lot for your help

I tested your script and the drone is taking off. Thrust is normalized between 0 and 1 using hover thrust estimator. x500 hover thrust is around 0.72 but you can get by using HoverThrustEstimate (UORB message) | PX4 Guide (main) message.