ci(mavros): merge mission+offboard into one workflow, migrate to noetic and Python 3
Build all targets / Scan for Board Targets (push) Has been cancelled
Build all targets / Build [${{ matrix.runner }}][${{ matrix.group }}] (push) Has been cancelled
Build all targets / Upload Artifacts (push) Has been cancelled
Checks / Gate Checks [check_format] (push) Has been cancelled
Checks / Gate Checks [check_newlines] (push) Has been cancelled
Checks / Gate Checks [module_documentation] (push) Has been cancelled
Checks / Gate Checks [shellcheck_all] (push) Has been cancelled
Checks / Gate Checks [validate_module_configs] (push) Has been cancelled
Checks / Unit Tests (push) Has been cancelled
Static Analysis / Clang-Tidy (push) Has been cancelled
MacOS build / build (push) Has been cancelled
Ubuntu environment build / Build and Test (ubuntu:22.04) (push) Has been cancelled
Ubuntu environment build / Build and Test (ubuntu:24.04) (push) Has been cancelled
Container build / Set Tags and Variables (push) Has been cancelled
Container build / Build Container (amd64) (push) Has been cancelled
Container build / Build Container (arm64) (push) Has been cancelled
Container build / Deploy To Registry (push) Has been cancelled
Docs - Orchestrator / T1: Detect Changes (push) Has been cancelled
Docs - Orchestrator / T2: PR Metadata (push) Has been cancelled
Docs - Orchestrator / T2: Metadata Sync (push) Has been cancelled
Docs - Orchestrator / T2: Link Check (push) Has been cancelled
Docs - Orchestrator / T3: Build Site (push) Has been cancelled
Docs - Orchestrator / T4: Deploy (push) Has been cancelled
Docs - Crowdin - Upload Guide sources (en) / upload-to-crowdin (push) Has been cancelled
Failsafe Simulator Build / build (failsafe_web) (push) Has been cancelled
FLASH usage analysis / Analyzing px4_fmu-v5x (push) Has been cancelled
FLASH usage analysis / Analyzing px4_fmu-v6x (push) Has been cancelled
FLASH usage analysis / Publish Results (push) Has been cancelled
ITCM check / Checking nxp_mr-tropic (push) Has been cancelled
ITCM check / Checking nxp_tropic-community (push) Has been cancelled
ITCM check / Checking px4_fmu-v5x (push) Has been cancelled
ITCM check / Checking px4_fmu-v6xrt (push) Has been cancelled
MAVROS Tests / MAVROS Mission (push) Has been cancelled
MAVROS Tests / MAVROS Offboard (push) Has been cancelled
Python CI Checks / build (push) Has been cancelled
ROS Integration Tests / build (push) Has been cancelled
ROS Translation Node Tests / Build and test [humble] (push) Has been cancelled
ROS Translation Node Tests / Build and test [jazzy] (push) Has been cancelled
SITL Tests / Testing PX4 iris (push) Has been cancelled
EKF Update Change Indicator / unit_tests (push) Has been cancelled

Consolidate mavros_mission_tests.yml and mavros_offboard_tests.yml into a
single mavros_tests.yml with a matrix strategy. Switch from docker-in-docker
with px4-dev-ros-melodic to a native container using px4-dev-ros-noetic,
enabling ccache and composite actions (setup-ccache, build-gazebo-sitl,
save-ccache). Migrate all five MAVROS Python test files from Python 2 to
Python 3 (remove six/xrange, from __future__ imports, replace px4tools
with pyulog for estimator analysis). Bump git-auto-commit-action from v4
to v7 in ekf_update_change_indicator.yml.

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
This commit is contained in:
Ramon Roche
2026-04-09 21:55:41 -07:00
parent 9eddd0cdbc
commit 0f15eea283
9 changed files with 142 additions and 136 deletions
@@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
#***************************************************************************
#
# Copyright (c) 2015 PX4 Development Team. All rights reserved.
@@ -35,9 +35,6 @@
#
# @author Andreas Antener <andreas@uaventure.com>
#
# The shebang of this file is currently Python2 because some
# dependencies such as pymavlink don't play well with Python3 yet.
from __future__ import division
PKG = 'px4'
@@ -46,7 +43,6 @@ from geometry_msgs.msg import Quaternion, Vector3
from mavros_msgs.msg import AttitudeTarget
from mavros_test_common import MavrosTestCommon
from pymavlink import mavutil
from six.moves import xrange
from std_msgs.msg import Header
from threading import Thread
from tf.transformations import quaternion_from_euler
@@ -124,7 +120,7 @@ class MavrosOffboardAttctlTest(MavrosTestCommon):
loop_freq = 2 # Hz
rate = rospy.Rate(loop_freq)
crossed = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
if (self.local_position.pose.position.x > boundary_x and
self.local_position.pose.position.y > boundary_y and
self.local_position.pose.position.z > boundary_z):
@@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
#***************************************************************************
#
# Copyright (c) 2015 PX4 Development Team. All rights reserved.
@@ -35,9 +35,6 @@
#
# @author Andreas Antener <andreas@uaventure.com>
#
# The shebang of this file is currently Python2 because some
# dependencies such as pymavlink don't play well with Python3 yet.
from __future__ import division
PKG = 'px4'
@@ -48,7 +45,6 @@ from geometry_msgs.msg import PoseStamped, Quaternion
from mavros_msgs.msg import ParamValue
from mavros_test_common import MavrosTestCommon
from pymavlink import mavutil
from six.moves import xrange
from std_msgs.msg import Header
from threading import Thread
from tf.transformations import quaternion_from_euler
@@ -132,7 +128,7 @@ class MavrosOffboardPosctlTest(MavrosTestCommon):
loop_freq = 2 # Hz
rate = rospy.Rate(loop_freq)
reached = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
if self.is_at_position(self.pos.pose.position.x,
self.pos.pose.position.y,
self.pos.pose.position.z, self.radius):
@@ -174,7 +170,7 @@ class MavrosOffboardPosctlTest(MavrosTestCommon):
positions = ((0, 0, 0), (50, 50, 20), (50, -50, 20), (-50, -50, 20),
(0, 0, 20))
for i in xrange(len(positions)):
for i in range(len(positions)):
self.reach_position(positions[i][0], positions[i][1],
positions[i][2], 30)
@@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
#***************************************************************************
#
# Copyright (c) 2020 PX4 Development Team. All rights reserved.
@@ -35,8 +35,6 @@
# @author Pedro Roque <padr@kth.se>
#
from __future__ import division
PKG = 'px4'
import rospy
@@ -44,7 +42,6 @@ from geometry_msgs.msg import Quaternion, Vector3
from mavros_msgs.msg import AttitudeTarget
from mavros_test_common import MavrosTestCommon
from pymavlink import mavutil
from six.moves import xrange
from std_msgs.msg import Header
from threading import Thread
from tf.transformations import quaternion_from_euler
@@ -134,7 +131,7 @@ class MavrosOffboardYawrateTest(MavrosTestCommon):
loop_freq = 2 # Hz
rate = rospy.Rate(loop_freq)
crossed = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
if (self.local_position.pose.position.x < boundary_x and
self.local_position.pose.position.x > -boundary_x and
self.local_position.pose.position.y < boundary_y and
@@ -1,5 +1,4 @@
#!/usr/bin/env python2
from __future__ import division
#!/usr/bin/env python3
import unittest
import rospy
@@ -11,7 +10,6 @@ from mavros_msgs.srv import CommandBool, ParamGet, ParamSet, SetMode, SetModeReq
WaypointPush
from pymavlink import mavutil
from sensor_msgs.msg import NavSatFix, Imu
from six.moves import xrange
class MavrosTestCommon(unittest.TestCase):
@@ -183,7 +181,7 @@ class MavrosTestCommon(unittest.TestCase):
loop_freq = 1 # Hz
rate = rospy.Rate(loop_freq)
arm_set = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
if self.state.armed == arm:
arm_set = True
rospy.loginfo("set arm success | seconds: {0} of {1}".format(
@@ -213,7 +211,7 @@ class MavrosTestCommon(unittest.TestCase):
loop_freq = 1 # Hz
rate = rospy.Rate(loop_freq)
mode_set = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
if self.state.mode == mode:
mode_set = True
rospy.loginfo("set mode success | seconds: {0} of {1}".format(
@@ -247,7 +245,7 @@ class MavrosTestCommon(unittest.TestCase):
loop_freq = 1 # Hz
rate = rospy.Rate(loop_freq)
param_set = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
try:
res = self.set_param_srv(param_id, param_value)
if res.success:
@@ -274,7 +272,7 @@ class MavrosTestCommon(unittest.TestCase):
loop_freq = 1 # Hz
rate = rospy.Rate(loop_freq)
simulation_ready = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
if all(value for value in self.sub_topics_ready.values()):
simulation_ready = True
rospy.loginfo("simulation topics ready | seconds: {0} of {1}".
@@ -297,7 +295,7 @@ class MavrosTestCommon(unittest.TestCase):
loop_freq = 10 # Hz
rate = rospy.Rate(loop_freq)
landed_state_confirmed = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
if self.extended_state.landed_state == desired_landed_state:
landed_state_confirmed = True
rospy.loginfo("landed state confirmed | seconds: {0} of {1}".
@@ -325,7 +323,7 @@ class MavrosTestCommon(unittest.TestCase):
loop_freq = 10 # Hz
rate = rospy.Rate(loop_freq)
transitioned = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
if transition == self.extended_state.vtol_state:
rospy.loginfo("transitioned | seconds: {0} of {1}".format(
i / loop_freq, timeout))
@@ -348,7 +346,7 @@ class MavrosTestCommon(unittest.TestCase):
loop_freq = 1 # Hz
rate = rospy.Rate(loop_freq)
wps_cleared = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
if not self.mission_wp.waypoints:
wps_cleared = True
rospy.loginfo("clear waypoints success | seconds: {0} of {1}".
@@ -381,7 +379,7 @@ class MavrosTestCommon(unittest.TestCase):
rate = rospy.Rate(loop_freq)
wps_sent = False
wps_verified = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
if not wps_sent:
try:
res = self.wp_push_srv(start_index=0, waypoints=waypoints)
@@ -417,7 +415,7 @@ class MavrosTestCommon(unittest.TestCase):
loop_freq = 1 # Hz
rate = rospy.Rate(loop_freq)
res = False
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
try:
res = self.get_param_srv('MAV_TYPE')
if res.success:
@@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
#***************************************************************************
#
# Copyright (c) 2015-2016 PX4 Development Team. All rights reserved.
@@ -36,10 +36,6 @@
# @author Andreas Antener <andreas@uaventure.com>
#
# The shebang of this file is currently Python2 because some
# dependencies such as pymavlink don't play well with Python3 yet.
from __future__ import division
PKG = 'px4'
import rospy
@@ -47,13 +43,13 @@ import glob
import json
import math
import os
from px4tools import ulog
import numpy as np
from pyulog import ULog
import sys
from mavros import mavlink
from mavros_msgs.msg import Mavlink, Waypoint, WaypointReached
from mavros_test_common import MavrosTestCommon
from pymavlink import mavutil
from six.moves import xrange
from threading import Thread
@@ -70,6 +66,49 @@ def get_last_log():
return last_log
def analyze_estimator_attitude(log_file):
"""Compute attitude estimator error metrics from a ULog file."""
ulog = ULog(log_file)
att = ulog.get_dataset('vehicle_attitude').data
att_gt = ulog.get_dataset('vehicle_attitude_groundtruth').data
def quat_to_euler(q0, q1, q2, q3):
"""Quaternion (w,x,y,z) to (roll, pitch, yaw) via ZYX Tait-Bryan."""
roll = np.arctan2(2 * (q0 * q1 + q2 * q3), 1 - 2 * (q1**2 + q2**2))
sinp = 2 * (q0 * q2 - q3 * q1)
pitch = np.where(np.abs(sinp) >= 1,
np.copysign(np.pi / 2, sinp), np.arcsin(sinp))
yaw = np.arctan2(2 * (q0 * q3 + q1 * q2), 1 - 2 * (q2**2 + q3**2))
return roll, pitch, yaw
roll, pitch, yaw = quat_to_euler(
att['q[0]'], att['q[1]'], att['q[2]'], att['q[3]'])
roll_gt, pitch_gt, yaw_gt = quat_to_euler(
att_gt['q[0]'], att_gt['q[1]'], att_gt['q[2]'], att_gt['q[3]'])
# interpolate groundtruth onto attitude timestamps
ts = att['timestamp']
ts_gt = att_gt['timestamp']
roll_gt = np.interp(ts, ts_gt, roll_gt)
pitch_gt = np.interp(ts, ts_gt, pitch_gt)
yaw_gt = np.interp(ts, ts_gt, yaw_gt)
wrap = lambda x: np.arcsin(np.sin(x))
e_roll = wrap(roll - roll_gt)
e_pitch = wrap(pitch - pitch_gt)
e_yaw = wrap(yaw - yaw_gt)
return {
'roll_error_mean': np.rad2deg(np.mean(e_roll)),
'pitch_error_mean': np.rad2deg(np.mean(e_pitch)),
'yaw_error_mean': np.rad2deg(np.mean(e_yaw)),
'roll_error_std': np.rad2deg(np.std(e_roll)),
'pitch_error_std': np.rad2deg(np.std(e_pitch)),
'yaw_error_std': np.rad2deg(np.std(e_yaw)),
}
def read_mission(mission_filename):
wps = []
with open(mission_filename, 'r') as f:
@@ -188,7 +227,7 @@ class MavrosMissionTest(MavrosTestCommon):
# does it reach the position in 'timeout' seconds?
loop_freq = 2 # Hz
rate = rospy.Rate(loop_freq)
for i in xrange(timeout * loop_freq):
for i in range(timeout * loop_freq):
pos_xy_d, pos_z_d = self.distance_to_wp(lat, lon, alt)
# remember best distances
@@ -295,9 +334,7 @@ class MavrosMissionTest(MavrosTestCommon):
rospy.loginfo("mission done, calculating performance metrics")
last_log = get_last_log()
rospy.loginfo("log file {0}".format(last_log))
data = ulog.read_ulog(last_log).concat(dt=0.1)
data = ulog.compute_data(data)
res = ulog.estimator_analysis(data, False)
res = analyze_estimator_attitude(last_log)
# enforce performance
self.assertTrue(abs(res['roll_error_mean']) < 5.0, str(res))