Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@ name: Coverage
on:
push:
branches: [master, ros2]
paths-ignore:
- 'docs/**'
- '**/*.md'
- 'LICENSE*'
- '.github/dependabot.yml'
pull_request:
branches: [master, ros2]
paths-ignore:
- 'docs/**'
- '**/*.md'
- 'LICENSE*'
- '.github/dependabot.yml'

permissions:
contents: read
Expand Down Expand Up @@ -40,22 +50,30 @@ jobs:
COMPONENT_NAME: ${{ matrix.component.name }}
COMPONENT_PACKAGES: ${{ matrix.component.packages }}
COMPONENT_FILTER: ${{ matrix.component.source_filter }}
CCACHE_DIR: ${{ github.workspace }}/.ccache
# Enable the libmavconn e2e test dep (python3-pymavlink-pip) and allow
# rosdep to pip-install it on PEP 668 distros (jazzy+)
TEST_ENABLE_E2E: "1"
PIP_BREAK_SYSTEM_PACKAGES: "1"

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7 # nosemgrep

- uses: ros-tooling/setup-ros@v0.7
- uses: ros-tooling/setup-ros@649ef6bcd696da05bc27ceb3fab69d810c0daeab
with:
required-ros-distributions: ${{ env.ROS_DISTRO }}

- name: Install Coverage Tools
run: |
sudo apt-get update
sudo apt-get install -y gcovr
sudo apt-get install -y gcovr ccache

- uses: actions/cache@v6 # nosemgrep
with:
path: ${{ env.CCACHE_DIR }}
key: "ccache-cov-${{ runner.os }}-${{ matrix.ros_distro }}-${{ matrix.component.name }}-${{ hashFiles('**/package.xml', '**/CMakeLists.txt') }}"
restore-keys: |
ccache-cov-${{ runner.os }}-${{ matrix.ros_distro }}-${{ matrix.component.name }}-

- name: Install Dependencies
shell: bash
Expand All @@ -76,7 +94,9 @@ jobs:
colcon build --packages-up-to ${COMPONENT_PACKAGES} --cmake-args \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS=--coverage \
-DCMAKE_CXX_FLAGS=--coverage
-DCMAKE_CXX_FLAGS=--coverage \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache

- name: Run Tests
shell: bash
Expand All @@ -100,7 +120,7 @@ jobs:
"${GITHUB_WORKSPACE}/build"

- name: Upload Coverage Artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v7 # nosemgrep
with:
name: coverage-${{ matrix.component.name }}-${{ matrix.ros_distro }}
path: coverage/${{ matrix.component.name }}
Expand Down
14 changes: 12 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@ name: CI
on:
push:
branches: [master, ros2]
paths-ignore:
- 'docs/**'
- '**/*.md'
- 'LICENSE*'
- '.github/dependabot.yml'
pull_request:
branches: [master, ros2]
paths-ignore:
- 'docs/**'
- '**/*.md'
- 'LICENSE*'
- '.github/dependabot.yml'

permissions:
contents: read
Expand Down Expand Up @@ -44,8 +54,8 @@ jobs:
PIP_BREAK_SYSTEM_PACKAGES: "1"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/cache@v6
- uses: actions/checkout@v7 # nosemgrep
- uses: actions/cache@v6 # nosemgrep
with:
path: "${{ env.CCACHE_DIR }}"
key: "ccache-${{ runner.os }}-${{ matrix.env.ROS_DISTRO }}-${{ matrix.env.ROS_REPO }}-${{ hashFiles('**/package.xml', '**/CMakeLists.txt') }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/semgrep-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
if: (github.actor != 'dependabot[bot]')

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7 # nosemgrep
- run: semgrep ci
env:
SEMGREP_RULES: p/default
7 changes: 7 additions & 0 deletions mavros/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ if(message_filters_VERSION VERSION_LESS 5.0.0)
)
endif()

if(rclcpp_VERSION VERSION_GREATER_EQUAL 30.0.0)
# Lyrical and newer: Callback Group Events executor (EventsCBGExecutor)
add_definitions(
-DMAVROS_HAVE_EVENTS_CBG_EXECUTOR
)
endif()

# [[[cog:
# import mavros_cog
# ]]]
Expand Down
48 changes: 48 additions & 0 deletions mavros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,31 @@ Main node. Allow disable GCS proxy by setting empty URL.
ros2 run mavros mavros_node --ros-args --params-file params.yaml


Executors
---------

MAVROS builds its executors via a factory that honors two environment variables:

- `MAVROS_EXECUTOR_TYPE` - executor used by both `mavros_node` container and
the UAS plugin executor:
- `mt` (default) - `rclcpp::executors::MultiThreadedExecutor`
- `events` (alias `cbg`) - the Callback Group Events executor
(`rclcpp::executors::EventsCBGExecutor`), available only on Lyrical+
(rclcpp >= 30.0.0). On older distros the request is ignored with a
warning and `MultiThreadedExecutor` is used.
- `MAVROS_UAS_EXECUTOR_THREADS` - number of threads for the UAS plugin
executor (default: clamped hardware concurrency, min 4 max 16; must be
>= 2 if set).

Example, running `mavros_node` with the events executor:

MAVROS_EXECUTOR_TYPE=events ros2 run mavros mavros_node

When MAVROS is used as composable nodes inside a component container, the
container's executor is chosen with the container's own `--executor-type`
argument instead; see the composable launch below.


Launch Files
------------

Expand All @@ -119,6 +144,29 @@ Launch files are provided for use with common FCUs, in particular [Pixhawk](pixh

* [px4.launch](launch/px4.launch) -- for use with the PX4 Autopilot (for VTOL, multicopters and planes)
* [apm.launch](launch/apm.launch) -- for use with APM flight stacks (e.g., all versions of ArduPlane, ArduCopter, etc)
* [test_compose.launch.py](launch/test_compose.launch.py) -- loads `mavros::router::Router` and one or two
`mavros::uas::UAS` nodes as composable nodes into a component container.

`test_compose.launch.py` accepts:

- `fcu_url` (default `udp://0.0.0.0:14540@`) - FCU connection URL
- `gcs_url` (default `udp://127.0.0.1:14555@`) - GCS connection URL
- `executor` (default `mt`) - container executor: `mt`, `events`, or `auto`
(`events` on Lyrical+, `mt` otherwise). The events (Callback Group
Events) executor support inside a component container is still immature
upstream (ros2/rclcpp#3186), so the reliable `component_container_mt` is
the default.

Example:

ros2 launch mavros test_compose.launch.py fcu_url:=udp://@192.168.60.192:15000 executor:=events

Components can also be loaded/unloaded at runtime against a running
container:

ros2 component load /mavros_container mavros mavros::router::Router
ros2 component load /mavros_container mavros mavros::uas::UAS
ros2 component unload /mavros_container <component_uid>

Examples:

Expand Down
7 changes: 7 additions & 0 deletions mavros/include/mavros/mavros_uas.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,13 @@ class UAS : public rclcpp::Node

~UAS() override;

// UAS is not copyable/movable: it owns an executor thread, TF listeners,
// subscriptions and plugin nodes.
UAS(const UAS &) = delete;
UAS & operator=(const UAS &) = delete;
UAS(UAS &&) = delete;
UAS & operator=(UAS &&) = delete;

/**
* @brief Mavros diagnostic updater
*/
Expand Down
17 changes: 7 additions & 10 deletions mavros/include/mavros/plugin.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ namespace plugin
{

using mavros::uas::UAS;
using UASPtr = std::shared_ptr<UAS>;
// NOTE(vooon): non-owning pointer to avoid a reference cycle: the UAS node
// owns the plugins (loaded_plugins), so the plugins must not own the UAS.
using UASPtr = UAS *;
using r_unique_lock = std::unique_lock<std::recursive_mutex>;
using s_unique_lock = std::unique_lock<std::shared_timed_mutex>;
using s_shared_lock = std::shared_lock<std::shared_timed_mutex>;
Expand Down Expand Up @@ -74,18 +76,13 @@ class Plugin : public std::enable_shared_from_this<Plugin>
//! Subscriptions vector
using Subscriptions = std::vector<HandlerInfo>;

explicit Plugin(UASPtr uas_)
: uas(uas_), node(std::dynamic_pointer_cast<rclcpp::Node>(uas_))
{}
// NOTE(vooon): ctors are implemented in plugin.cpp because UAS is an
// incomplete type at the point plugin.hpp is included (from mavros_uas.hpp).
explicit Plugin(UASPtr uas_);

explicit Plugin(
UASPtr uas_, const std::string & subnode,
const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
: uas(uas_),
// node(std::dynamic_pointer_cast<rclcpp::Node>(uas_)->create_sub_node(subnode)) // https://github.com/ros2/rclcpp/issues/731
node(rclcpp::Node::make_shared(subnode,
std::dynamic_pointer_cast<rclcpp::Node>(uas_)->get_fully_qualified_name(), options))
{}
const rclcpp::NodeOptions & options = rclcpp::NodeOptions());

virtual ~Plugin() = default;

Expand Down
2 changes: 1 addition & 1 deletion mavros/include/mavros/plugin_filter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ namespace filter
{
using mavros::plugin::Filter;
using mavros::uas::UAS;
using UASPtr = UAS::SharedPtr;
using UASPtr = UAS *;
using mavconn::Framing;


Expand Down
37 changes: 28 additions & 9 deletions mavros/include/mavros/uas_executor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,41 +19,60 @@
#ifndef MAVROS__UAS_EXECUTOR_HPP_
#define MAVROS__UAS_EXECUTOR_HPP_

#include <atomic>
#include <chrono>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"

#include "mavros/utils.hpp"

namespace mavros
{
namespace uas
{

/**
* Executor for UAS Plugin nodes
*
* Thin wrapper around rclcpp::Executor that lets the user pick the
* underlying executor type at runtime (MAVROS_EXECUTOR_TYPE env var).
*/
class UASExecutor : public rclcpp::executors::MultiThreadedExecutor
class UASExecutor
{
public:
explicit UASExecutor(const rclcpp::ExecutorOptions & options = rclcpp::ExecutorOptions());
~UASExecutor() = default;

void set_ids(uint8_t sysid, uint8_t compid);

protected:
void run(size_t thread_id);
void spin();
void cancel();
void add_node(const std::shared_ptr<rclcpp::Node> & node);
size_t get_number_of_threads() const;

private:
RCLCPP_DISABLE_COPY(UASExecutor)

static size_t select_number_of_threads();

uint8_t source_system, source_component;
std::unique_ptr<rclcpp::Executor> impl_;
size_t number_of_threads_;
};

/**
* Factory for executors honoring the MAVROS_EXECUTOR_TYPE env var:
* - "events" (or "cbg"): rclcpp::executors::EventsCBGExecutor (Lyrical+ only)
* - otherwise (default): rclcpp::executors::MultiThreadedExecutor
*
* \param options common options for all executors
* \param number_of_threads number of threads to have in the thread pool
* \param yield_before_execute MultiThreadedExecutor option, ignored for
* the events executor
* \param timeout maximum time to wait
*/
std::unique_ptr<rclcpp::Executor> make_executor(
const rclcpp::ExecutorOptions & options,
size_t number_of_threads,
bool yield_before_execute = false,
std::chrono::nanoseconds timeout = std::chrono::nanoseconds(-1));

} // namespace uas
} // namespace mavros

Expand Down
60 changes: 51 additions & 9 deletions mavros/launch/test_compose.launch.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
from launch import LaunchDescription
import os

from launch import LaunchContext, LaunchDescription
from launch.actions import DeclareLaunchArgument, OpaqueFunction
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode


def generate_launch_description():
"""Generate launch description for MAVROS composable node."""
def _build_container(context: LaunchContext, fcu_url, gcs_url, executor):
"""Build the composable container once launch arguments are resolved."""
fcu = context.perform_substitution(fcu_url)
gcs = context.perform_substitution(gcs_url)
exec_type = context.launch_configurations["executor"]

# The Callback Group Events executor is only available on Lyrical+
# (rclcpp >= 30.0.0); its component-container support is still
# upstream-immature (see ros2/rclcpp#3186), so default to MT.
distro = os.environ.get("ROS_DISTRO", "")
if exec_type == "events" or (
exec_type == "auto" and distro in ("lyrical", "rolling")):
container_executable = "component_container"
container_arguments = ["--executor-type", "events-cbg"]
else:
container_executable = "component_container_mt"
container_arguments = []

container = ComposableNodeContainer(
name="mavros_container",
namespace="",
package="rclcpp_components",
executable="component_container_mt",
executable=container_executable,
composable_node_descriptions=[
ComposableNode(
package="mavros",
plugin="mavros::router::Router",
name="mavros_router",
parameters=[
# {"fcu_urls": ["tcp://127.0.0.1:5760"]},
{"fcu_urls": ["udp://0.0.0.0:14540@"]},
{"gcs_urls": ["udp://127.0.0.1:14555@"]},
{"fcu_urls": [fcu]},
{"gcs_urls": [gcs]},
{"uas_urls": ["/uas1", "/uas2"]},
{"fcu_protocol": "v2.0"},
],
Expand Down Expand Up @@ -55,8 +74,31 @@ def generate_launch_description():
extra_arguments=[{"use_intra_process_comms": True}],
),
],
arguments=container_arguments + ["--ros-args", "--log-level", "INFO"],
output="screen",
arguments=["--ros-args", "--log-level", "DEBUG"],
)

return LaunchDescription([container])
return [container]


def generate_launch_description():
"""Generate launch description for MAVROS composable node."""
fcu_url = LaunchConfiguration("fcu_url")
gcs_url = LaunchConfiguration("gcs_url")
executor = LaunchConfiguration("executor")

return LaunchDescription([
DeclareLaunchArgument(
"fcu_url", default_value="udp://0.0.0.0:14540@",
description="FCU connection URL"
),
DeclareLaunchArgument(
"gcs_url", default_value="udp://127.0.0.1:14555@",
description="GCS connection URL"
),
DeclareLaunchArgument(
"executor", default_value="mt",
description="Container executor: mt, events, or auto"
),
OpaqueFunction(function=_build_container, args=[fcu_url, gcs_url, executor]),
])
Loading
Loading