VTOL Offboard Transition

I am trying to offboard transtion the gazebo standard VTOL in SITL, and am running into lots of problems. I am using PX4 v1.14.0, ROS2 humble and uDDS, all on ubuntu 22.04.

No matter what configuration I try, the VTOL does not seem to transition. It will go at my specified velocity and then slowly lose altitude and speed before the quad-chute deploys. In gazebo it looks like the pusher prop never turns on, which is very confusing. I can transition the VTOL in other modes through the QGroundControl GUI, using hold and stabalized mode with the on screen buttons to manually transition, but I really want to accomplish this in offboard mode.

I have tried many different configurations of the code below, all to no sucess. I thought the VTOL transition command should be a binary switch type of thing, so I have tried just sending the command once then continuing to send attitude setpoints, just trying to transition and then nothing, transitioning right after takeoff, etc. I have included my log file of my most recent trial, as well as my full code.

#!/usr/bin/env python3
import rclpy 
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy, DurabilityPolicy
from px4_msgs.msg import OffboardControlMode, TrajectorySetpoint, VehicleCommand, VehicleLocalPosition, VehicleStatus, VehicleOdometry, VehicleCommandAck, VehicleAttitudeSetpoint
import numpy as np
class OffboardControl(Node):
        """ Node for controlling a vehicle in offboard mode. """

        def __init__(self) -> None:
                super().__init__('offboard_control_takeoff_and_land')

                # Configure QoS profile for publishing and subscribing
                qos_profile = QoSProfile(reliability = ReliabilityPolicy.BEST_EFFORT, 
                                         durability = DurabilityPolicy.TRANSIENT_LOCAL,
                                         history = HistoryPolicy.KEEP_LAST, 
                                         depth = 1)
                
                # Create publishers
                self.offboard_control_mode_publisher = self.create_publisher(
                        OffboardControlMode, '/fmu/in/offboard_control_mode', qos_profile)
                self.trajectory_setpoint_publisher = self.create_publisher(
                        TrajectorySetpoint, '/fmu/in/trajectory_setpoint', qos_profile)
                self.vehicle_command_publisher = self.create_publisher(
                        VehicleCommand, '/fmu/in/vehicle_command', qos_profile)
                self.vehicle_attitude_publisher = self.create_publisher(
                        VehicleAttitudeSetpoint, '/fmu/in/vehicle_attitude_setpoint', qos_profile) 

                # Create subscribers
                self.vehicle_local_position_subscriber = self.create_subscription(
                        VehicleLocalPosition, '/fmu/out/vehicle_local_position', self.vehicle_local_position_callback, qos_profile)
                self.vehicle_status_subscriber = self.create_subscription(
                        VehicleStatus, '/fmu/out/vehicle_status', self.vehicle_status_callback, qos_profile)
                self.vehicle_odometry_subscriber = self.create_subscription(
                        VehicleOdometry, '/fmu/out/vehicle_odometry', self.vehicle_odometry_callback, qos_profile)
                self.vehicle_command_ack_subscriber = self.create_subscription(
                        VehicleCommandAck, '/fmu/out/vehicle_command_ack', self.vehicle_command_ack_callback, qos_profile)

                # Initialize variables
                self.offboard_setpoint_counter = 0
                self.vehicle_local_position = VehicleLocalPosition()
                self.vehicle_status = VehicleStatus()
                self.vehicle_odometry = VehicleOdometry()
                self.vehicle_command_ack = VehicleCommandAck()

                self.vehicle_vel_x = self.vehicle_odometry.velocity[0]

                self.takeoff_height = -30.0

                #Create a timer to publish control commands
                self.timer = self.create_timer(0.1, self.timer_callback)

                self.taken_off = None
                self.first_setpoint = None
                self.second_setpoint = None
                self.landed = None
                self.transitioned = None

                self.vehicle_state = self.vehicle_status.vehicle_type

                self.x_vel = self.vehicle_odometry.velocity[0]
                
        def vehicle_local_position_callback(self, vehicle_local_position):
                """Callback function for vehicle_local_position topic subscriber"""
                self.vehicle_local_position = vehicle_local_position
        
        def vehicle_status_callback(self, vehicle_status):
                """Callback function for vehicle_status topic subscriber."""
                self.vehicle_status = vehicle_status

        def vehicle_odometry_callback(self, vehicle_odometry):
                """Callback function for vehicle_status topic subscriber."""
                self.vehicle_odometry = vehicle_odometry

        def vehicle_command_ack_callback(self, vehicle_command_ack):
                """Callback function for vehicle_status topic subscriber."""
                self.vehicle_command_ack= vehicle_command_ack

        def extra_callback(self): 
                self.x_vel = self.vehicle_odometry.velocity[0]
                self.vehicle_state = self.vehicle_status.vehicle_type

        def arm(self):
                """Send an arm command to the vehicle."""
                self.publish_vehicle_command(
                        VehicleCommand.VEHICLE_CMD_COMPONENT_ARM_DISARM, param1=1.0)
                self.get_logger().info('Arm command sent')
        
        def disarm(self):
                """Send a disarm command to the vehicle."""
                self.publish_vehicle_command(
                        VehicleCommand.VEHICLE_CMD_COMPONENT_ARM_DISARM, param1=0.0)
                self.get_logger().info('Disarm command sent')

        def engage_offboard_mode(self):
                """Switch to offboard mode."""
                self.publish_vehicle_command(
                        VehicleCommand.VEHICLE_CMD_DO_SET_MODE, param1=1.0, param2=6.0)
                self.get_logger().info("Switching to offboard mode")

        def land(self):
                """Switch to land mode."""
                self.publish_vehicle_command(VehicleCommand.VEHICLE_CMD_NAV_LAND)
                self.get_logger().info("Switching to land mode")

        def publish_offboard_control_heartbeat_signal(self):
                """Publish the offboard control mode."""
                msg = OffboardControlMode()
                msg.position = True 
                msg.velocity = True
                msg.acceleration = False
                msg.attitude = True
                msg.body_rate = False
                msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
                self.offboard_control_mode_publisher.publish(msg)

        def publish_position_setpoint(self, x: float, y: float, z: float):
                """Publish the trajectory setpoint."""
                msg = TrajectorySetpoint()
                msg.position = [x, y, z]
                msg.yaw = 0.0 # (0 degree)
                msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
                self.trajectory_setpoint_publisher.publish(msg)
                self.get_logger().info(f"Publishing position setpoints {[x, y, z]}")

        def publish_vehicle_command(self, command, **params) -> None:
                """Publish a vehicle command."""
                msg = VehicleCommand()
                msg.command = command
                msg.param1 = params.get("param1", 0.0)
                msg.param2 = params.get("param2", 0.0)
                msg.param3 = params.get("param3", 0.0)
                msg.param4 = params.get("param4", 0.0)
                msg.param5 = params.get("param5", 0.0)
                msg.param6 = params.get("param6", 0.0)
                msg.param7 = params.get("param7", 0.0)
                msg.target_system = 1
                msg.target_component = 1
                msg.source_system = 1
                msg.source_component = 1
                msg.from_external = True
                msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
                self.vehicle_command_publisher.publish(msg)

        def publish_velocity_setpoint(self, vx, vy, vz):
                """Publish a velocity setpoint."""
                msg = TrajectorySetpoint()
                msg.position = [np.nan, np.nan, np.nan]
                msg.velocity = [vx, vy, vz]
                msg.acceleration = [np.nan, np.nan, np.nan]
                msg.jerk = [np.nan, np.nan, np.nan]
                msg.yaw = 0.0
                msg.yawspeed = np.nan
                msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
                self.trajectory_setpoint_publisher.publish(msg)
                self.get_logger().info(f"Publishing velocity setpoint: [{vx}, {vy}, {vz}]")

        def publish_vehicle_command_vtol_transition(self, transition_type) -> None:

                self.publish_vehicle_command(command = VehicleCommand.VEHICLE_CMD_DO_VTOL_TRANSITION,
                                             param1 = transition_type)
                
                self.get_logger().info(f"VTOL transition command sent. Transition Type: {transition_type}")

        def publish_attitude_setpoint(self):
               """Publish an attidtude setpoint"""
               msg = VehicleAttitudeSetpoint()
               msg.roll_body = 0.0 
               msg.pitch_body = 0.0
               msg.yaw_body = 0.0

               msg.yaw_sp_move_rate = 0.0 

               msg.q_d = [0.0, 0.0, 0.0, 1.0]

               msg.thrust_body = [1.0, 0.0, 0.0]

               msg.reset_integral = False 
               msg.fw_control_yaw_wheel = False
               
               msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
               self.vehicle_attitude_publisher.publish(msg)
               self.get_logger().info(f"Publishing attitude setpoint")


        def timer_callback(self) -> None:
                """Callback function for the timer."""

        def timer_callback(self) -> None:
                """Callback function for the timer."""

                self.publish_offboard_control_heartbeat_signal()
                self.extra_callback()
                self.get_logger().info(f"{self.vehicle_state}, {self.x_vel}")

                if self.offboard_setpoint_counter == 10:
                    self.engage_offboard_mode()
                    self.arm()

                if self.vehicle_local_position.z > self.takeoff_height and self.vehicle_status.nav_state == VehicleStatus.NAVIGATION_STATE_OFFBOARD and self.taken_off is None:
                    self.publish_position_setpoint(0.0, 0.0, self.takeoff_height)
                    self.publish_velocity_setpoint(0.0, 0.0, -3.0)
                    if self.vehicle_local_position.z < self.takeoff_height + 0.5:
                        self.taken_off = True
                        self.publish_velocity_setpoint(0.0, 0.0, 0.0)
                        print("taken off")
                        self.taken_off = True
              
                if self.vehicle_local_position.z < self.takeoff_height + 0.2 and self.taken_off and self.first_setpoint is None:
                    self.publish_position_setpoint(400.0, 0.0, self.takeoff_height)
                    self.publish_velocity_setpoint(10.0, 0.0, 0.0)
                    if self.vehicle_local_position.x >= 39.5: 
                           self.first_setpoint = True 
                           self.publish_velocity_setpoint(0.0, 0.0, 0.0)
                           print("first setpoint")

                if self.first_setpoint and self.transitioned is None:
                    self.publish_attitude_setpoint()
                    self.publish_vehicle_command_vtol_transition(4.0)
                    if self.vehicle_state == 2: 
                        self.transitioned = True 
                
                if self.transitioned: 
                    self.publish_attitude_setpoint()
                       
                if self.offboard_setpoint_counter < 11:
                       self.offboard_setpoint_counter += 1
        
def main(args=None) -> None:
        print('Starting offboard control node...')
        rclpy.init(args=args)
        offboard_control = OffboardControl()
        rclpy.spin(offboard_control)
        offboard_control.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
        try:
            main()
        except Exception as e:
            print(e)

This code took inspiration from the offboard example code on the PX4 github.
Log File: https://review.px4.io/plot_app?log=cc442dc8-a1d4-4848-acbc-670e101c49b6

Any suggestions are appreciated! This is my first time using ROS2 and PX4 so I appreciate all feedback I can get. Thank you for your time in advance.

@sfuhrer Is this something you can provide debugging tips for?

I’m not familiar at all with offboard I’m afraid. Maybe @Jaeyoung-Lim has an idea what’s going on, and what you can even do on VTOLs in offboard.

Your transition command is irrelevant for offboard mode.

You need to debug why this is not happening, without offboard mode being involved and then you can start looking into offboard mode.

@Jaeyoung-Lim, sorry for the confusion. This is a purley sofware based project right now and I have no practical model to test in so I am using SITL. I can command a transition from the QGroundControl GUI to the VTOL in other modes (hold, position, etc) and it does transition. Offboard control however, does not work as described above. Some tips on offboard VTOL transitions would be very helpful!

@gabedavid In that case, it looks like it is not possible to transition during offboard mode

So offboard mode does not support VTOL transitions?

I was thinking / hoping that it was most likely a software issue on my end. It seems as though PX4 is recognizing the transition and trying to transition, but something is stopping it. I am thinking I am sending either too many / not enough commands and over / under saturating the flight controller and then the VTOL cannot transtion. PX4 eventually deploys the quadchute, so it must be recognizing the transition command, no?