diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index ed64bcd94c..62007ffc2d 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -2,5 +2,6 @@
ROS2 Distro | Branch | Build status | Documentation | Released packages
:---------: | :----: | :----------: | :-----------: | :---------------:
**Rolling** | [`master`](https://github.com/ros-controls/ros2_control/tree/master) | [![Rolling Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-binary-build.yml?branch=master) [![Rolling Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-semi-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-semi-binary-build.yml?branch=master) [![Rolling Source Build](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-source-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-source-build.yml?branch=master) [![Debian Rolling Source Build](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-debian-build.yml/badge.svg)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-debian-build.yml) [![RHEL Rolling Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-rhel-binary-build.yml/badge.svg)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-rhel-binary-build.yml) | [Documentation](https://control.ros.org/master/index.html) [API Reference](https://control.ros.org/master/doc/api/index.html) | [ros2_control](https://index.ros.org/p/ros2_control/#rolling)
+**Jazzy** | [`master`](https://github.com/ros-controls/ros2_control/tree/master) | [![Jazzy Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-binary-build.yml?branch=master) [![Jazzy Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-semi-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-semi-binary-build.yml?branch=master) [![Jazzy Source Build](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-source-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-source-build.yml?branch=master) [![Debian Jazzy Source Build](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-debian-build.yml/badge.svg)](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-debian-build.yml) [![RHEL Jazzy Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-rhel-binary-build.yml/badge.svg)](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-rhel-binary-build.yml) | [Documentation](https://control.ros.org/jazzy/index.html) [API Reference](https://control.ros.org/jazzy/doc/api/index.html) | [ros2_control](https://index.ros.org/p/ros2_control/#jazzy)
**Iron** | [`iron`](https://github.com/ros-controls/ros2_control/tree/master) | [![Iron Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-binary-build.yml?branch=master) [![Iron Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-semi-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-semi-binary-build.yml?branch=master) [![Iron Source Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-source-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-source-build.yml?branch=master) [![Debian Iron Source Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-debian-build.yml/badge.svg)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-debian-build.yml) [![RHEL Iron Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-rhel-binary-build.yml/badge.svg)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-rhel-binary-build.yml) | [Documentation](https://control.ros.org/iron/index.html) [API Reference](https://control.ros.org/iron/doc/api/index.html) | [ros2_control](https://index.ros.org/p/ros2_control/#iron)
**Humble** | [`humble`](https://github.com/ros-controls/ros2_control/tree/humble) | [![Humble Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/humble-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-binary-build.yml?branch=master) [![Humble Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/humble-semi-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-semi-binary-build.yml?branch=master) [![Humble Source Build](https://github.com/ros-controls/ros2_control/actions/workflows/humble-source-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-source-build.yml?branch=master) [![Debian Humble Source Build](https://github.com/ros-controls/ros2_control/actions/workflows/humble-debian-build.yml/badge.svg)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-debian-build.yml) [![RHEL Humble Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/humble-rhel-binary-build.yml/badge.svg)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-rhel-binary-build.yml) | [Documentation](https://control.ros.org/humble/index.html) [API Reference](https://control.ros.org/humble/doc/api/index.html) | [ros2_control](https://index.ros.org/p/ros2_control/#humble)
diff --git a/.github/workflows/humble-abi-compatibility.yml b/.github/workflows/humble-abi-compatibility.yml
index 5c288fabfb..0e2488d31e 100644
--- a/.github/workflows/humble-abi-compatibility.yml
+++ b/.github/workflows/humble-abi-compatibility.yml
@@ -4,6 +4,15 @@ on:
pull_request:
branches:
- humble
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/humble-abi-compatibility.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.humble.repos'
jobs:
abi_check:
diff --git a/.github/workflows/humble-binary-build.yml b/.github/workflows/humble-binary-build.yml
index 2cf14105f5..6392ba8e68 100644
--- a/.github/workflows/humble-binary-build.yml
+++ b/.github/workflows/humble-binary-build.yml
@@ -7,9 +7,27 @@ on:
pull_request:
branches:
- humble
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/humble-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.humble.repos'
push:
branches:
- humble
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/humble-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.humble.repos'
schedule:
# Run every morning to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/humble-check-docs.yml b/.github/workflows/humble-check-docs.yml
new file mode 100644
index 0000000000..f3c31703cd
--- /dev/null
+++ b/.github/workflows/humble-check-docs.yml
@@ -0,0 +1,18 @@
+name: Humble Check Docs
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - humble
+ paths:
+ - '**.rst'
+ - '**.md'
+ - '**.yaml'
+
+jobs:
+ check-docs:
+ name: Check Docs
+ uses: ros-controls/control.ros.org/.github/workflows/reusable-sphinx-check-single-version.yml@humble
+ with:
+ ROS2_CONTROL_PR: ${{ github.ref }}
diff --git a/.github/workflows/humble-coverage-build.yml b/.github/workflows/humble-coverage-build.yml
index 0910572227..209c931d4e 100644
--- a/.github/workflows/humble-coverage-build.yml
+++ b/.github/workflows/humble-coverage-build.yml
@@ -4,9 +4,29 @@ on:
push:
branches:
- humble
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/humble-coverage-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.humble.repos'
+ - 'codecov.yml'
pull_request:
branches:
- humble
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/humble-coverage-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.humble.repos'
+ - 'codecov.yml'
jobs:
coverage_humble:
@@ -14,4 +34,3 @@ jobs:
secrets: inherit
with:
ros_distro: humble
- os_name: ubuntu-22.04
diff --git a/.github/workflows/humble-debian-build.yml b/.github/workflows/humble-debian-build.yml
index 3b8a1c6287..85a1b38241 100644
--- a/.github/workflows/humble-debian-build.yml
+++ b/.github/workflows/humble-debian-build.yml
@@ -4,6 +4,15 @@ on:
pull_request:
branches:
- humble
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/humble-debian-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.humble.repos'
schedule:
# Run every day to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/humble-pre-commit.yml b/.github/workflows/humble-pre-commit.yml
index be8c84b05b..5bb2408578 100644
--- a/.github/workflows/humble-pre-commit.yml
+++ b/.github/workflows/humble-pre-commit.yml
@@ -11,4 +11,3 @@ jobs:
uses: ros-controls/ros2_control_ci/.github/workflows/reusable-pre-commit.yml@master
with:
ros_distro: humble
- os_name: ubuntu-22.04
diff --git a/.github/workflows/humble-rhel-binary-build.yml b/.github/workflows/humble-rhel-binary-build.yml
index 4c00d2f2ad..62c5cd62ba 100644
--- a/.github/workflows/humble-rhel-binary-build.yml
+++ b/.github/workflows/humble-rhel-binary-build.yml
@@ -4,6 +4,15 @@ on:
pull_request:
branches:
- humble
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/humble-rhel-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.humble.repos'
schedule:
# Run every day to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/humble-semi-binary-build.yml b/.github/workflows/humble-semi-binary-build.yml
index 19637c4897..560ac37cff 100644
--- a/.github/workflows/humble-semi-binary-build.yml
+++ b/.github/workflows/humble-semi-binary-build.yml
@@ -7,9 +7,27 @@ on:
pull_request:
branches:
- humble
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/humble-semi-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.humble.repos'
push:
branches:
- humble
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/humble-semi-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.humble.repos'
schedule:
# Run every morning to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/humble-source-build.yml b/.github/workflows/humble-source-build.yml
index 7b4427d6d6..878ad92677 100644
--- a/.github/workflows/humble-source-build.yml
+++ b/.github/workflows/humble-source-build.yml
@@ -4,6 +4,15 @@ on:
push:
branches:
- humble
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/humble-source-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.humble.repos'
schedule:
# Run every day to detect flakiness and broken dependencies
- cron: '03 3 * * *'
@@ -15,3 +24,4 @@ jobs:
ros_distro: humble
ref: humble
ros2_repo_branch: humble
+ os_name: ubuntu-22.04
diff --git a/.github/workflows/iron-abi-compatibility.yml b/.github/workflows/iron-abi-compatibility.yml
index ab6642625f..c2d9c19110 100644
--- a/.github/workflows/iron-abi-compatibility.yml
+++ b/.github/workflows/iron-abi-compatibility.yml
@@ -4,6 +4,15 @@ on:
pull_request:
branches:
- iron
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/iron-abi-compatibility.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.iron.repos'
jobs:
abi_check:
diff --git a/.github/workflows/iron-binary-build.yml b/.github/workflows/iron-binary-build.yml
index 911ccafae5..ef90e256a0 100644
--- a/.github/workflows/iron-binary-build.yml
+++ b/.github/workflows/iron-binary-build.yml
@@ -7,9 +7,27 @@ on:
pull_request:
branches:
- iron
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/iron-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.iron.repos'
push:
branches:
- iron
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/iron-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.iron.repos'
schedule:
# Run every morning to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/ci-check-docs.yml b/.github/workflows/iron-check-docs.yml
similarity index 57%
rename from .github/workflows/ci-check-docs.yml
rename to .github/workflows/iron-check-docs.yml
index 90a822aa72..e9295dad44 100644
--- a/.github/workflows/ci-check-docs.yml
+++ b/.github/workflows/iron-check-docs.yml
@@ -1,12 +1,18 @@
-name: Check Docs
+name: Iron Check Docs
on:
workflow_dispatch:
pull_request:
+ branches:
+ - iron
+ paths:
+ - '**.rst'
+ - '**.md'
+ - '**.yaml'
jobs:
check-docs:
name: Check Docs
- uses: ros-controls/control.ros.org/.github/workflows/reusable-sphinx-check-single-version.yml@master
+ uses: ros-controls/control.ros.org/.github/workflows/reusable-sphinx-check-single-version.yml@iron
with:
ROS2_CONTROL_PR: ${{ github.ref }}
diff --git a/.github/workflows/iron-coverage-build.yml b/.github/workflows/iron-coverage-build.yml
index d82c52bf51..ff5be81d7d 100644
--- a/.github/workflows/iron-coverage-build.yml
+++ b/.github/workflows/iron-coverage-build.yml
@@ -4,9 +4,29 @@ on:
push:
branches:
- iron
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/iron-coverage-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.iron.repos'
+ - 'codecov.yml'
pull_request:
branches:
- iron
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/iron-coverage-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.iron.repos'
+ - 'codecov.yml'
jobs:
coverage_iron:
@@ -14,4 +34,3 @@ jobs:
secrets: inherit
with:
ros_distro: iron
- os_name: ubuntu-22.04
diff --git a/.github/workflows/iron-debian-build.yml b/.github/workflows/iron-debian-build.yml
index ff503d64a9..3cbe0c5127 100644
--- a/.github/workflows/iron-debian-build.yml
+++ b/.github/workflows/iron-debian-build.yml
@@ -4,6 +4,15 @@ on:
pull_request:
branches:
- iron
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/iron-debian-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.iron.repos'
schedule:
# Run every day to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/iron-pre-commit.yml b/.github/workflows/iron-pre-commit.yml
index 60ad26d073..a128958031 100644
--- a/.github/workflows/iron-pre-commit.yml
+++ b/.github/workflows/iron-pre-commit.yml
@@ -11,4 +11,3 @@ jobs:
uses: ros-controls/ros2_control_ci/.github/workflows/reusable-pre-commit.yml@master
with:
ros_distro: iron
- os_name: ubuntu-22.04
diff --git a/.github/workflows/iron-rhel-binary-build.yml b/.github/workflows/iron-rhel-binary-build.yml
index 981893524d..f308c495f3 100644
--- a/.github/workflows/iron-rhel-binary-build.yml
+++ b/.github/workflows/iron-rhel-binary-build.yml
@@ -4,6 +4,15 @@ on:
pull_request:
branches:
- iron
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/iron-rhel-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.iron.repos'
schedule:
# Run every day to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/iron-semi-binary-build.yml b/.github/workflows/iron-semi-binary-build.yml
index 59f8f347dd..30a83e5367 100644
--- a/.github/workflows/iron-semi-binary-build.yml
+++ b/.github/workflows/iron-semi-binary-build.yml
@@ -7,9 +7,27 @@ on:
pull_request:
branches:
- iron
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/iron-semi-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.iron.repos'
push:
branches:
- iron
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/iron-semi-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.iron.repos'
schedule:
# Run every morning to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/iron-source-build.yml b/.github/workflows/iron-source-build.yml
index 3609dcfc41..3b7c53f6ff 100644
--- a/.github/workflows/iron-source-build.yml
+++ b/.github/workflows/iron-source-build.yml
@@ -4,6 +4,15 @@ on:
push:
branches:
- iron
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/iron-source-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.iron.repos'
schedule:
# Run every day to detect flakiness and broken dependencies
- cron: '03 3 * * *'
@@ -15,3 +24,4 @@ jobs:
ros_distro: iron
ref: iron
ros2_repo_branch: iron
+ os_name: ubuntu-22.04
diff --git a/.github/workflows/jazzy-abi-compatibility.yml b/.github/workflows/jazzy-abi-compatibility.yml
new file mode 100644
index 0000000000..367b3736fb
--- /dev/null
+++ b/.github/workflows/jazzy-abi-compatibility.yml
@@ -0,0 +1,27 @@
+name: Jazzy - ABI Compatibility Check
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/jazzy-abi-compatibility.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.jazzy.repos'
+
+jobs:
+ abi_check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ros-industrial/industrial_ci@master
+ env:
+ ROS_DISTRO: jazzy
+ ROS_REPO: testing
+ ABICHECK_URL: github:${{ github.repository }}#${{ github.base_ref }}
+ NOT_TEST_BUILD: true
diff --git a/.github/workflows/jazzy-binary-build.yml b/.github/workflows/jazzy-binary-build.yml
new file mode 100644
index 0000000000..5be853ebfc
--- /dev/null
+++ b/.github/workflows/jazzy-binary-build.yml
@@ -0,0 +1,47 @@
+name: Jazzy Binary Build
+# author: Denis Štogl
+# description: 'Build & test all dependencies from released (binary) packages.'
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/jazzy-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.jazzy.repos'
+ push:
+ branches:
+ - master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/jazzy-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.jazzy.repos'
+ schedule:
+ # Run every morning to detect flakiness and broken dependencies
+ - cron: '03 1 * * *'
+
+jobs:
+ binary:
+ uses: ros-controls/ros2_control_ci/.github/workflows/reusable-industrial-ci-with-cache.yml@master
+ strategy:
+ fail-fast: false
+ matrix:
+ ROS_DISTRO: [jazzy]
+ ROS_REPO: [main, testing]
+ with:
+ ros_distro: ${{ matrix.ROS_DISTRO }}
+ ros_repo: ${{ matrix.ROS_REPO }}
+ upstream_workspace: ros2_control-not-released.${{ matrix.ROS_DISTRO }}.repos
+ ref_for_scheduled_build: master
diff --git a/.github/workflows/jazzy-check-docs.yml b/.github/workflows/jazzy-check-docs.yml
new file mode 100644
index 0000000000..cbdf6c30bd
--- /dev/null
+++ b/.github/workflows/jazzy-check-docs.yml
@@ -0,0 +1,18 @@
+name: Jazzy Check Docs
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - master
+ paths:
+ - '**.rst'
+ - '**.md'
+ - '**.yaml'
+
+jobs:
+ check-docs:
+ name: Check Docs
+ uses: ros-controls/control.ros.org/.github/workflows/reusable-sphinx-check-single-version.yml@jazzy
+ with:
+ ROS2_CONTROL_PR: ${{ github.ref }}
diff --git a/.github/workflows/jazzy-coverage-build.yml b/.github/workflows/jazzy-coverage-build.yml
new file mode 100644
index 0000000000..aa345d1e80
--- /dev/null
+++ b/.github/workflows/jazzy-coverage-build.yml
@@ -0,0 +1,35 @@
+name: Coverage Build - Jazzy
+on:
+ workflow_dispatch:
+ # TODO(anyone) activate when branched for Jazzy
+ # push:
+ # branches:
+ # - master
+ # paths:
+ # - '**.hpp'
+ # - '**.h'
+ # - '**.cpp'
+ # - '.github/workflows/jazzy-coverage-build.yml'
+ # - '**/package.xml'
+ # - '**/CMakeLists.txt'
+ # - 'ros2_control.jazzy.repos'
+ # - 'codecov.yml'
+ # pull_request:
+ # branches:
+ # - master
+ # paths:
+ # - '**.hpp'
+ # - '**.h'
+ # - '**.cpp'
+ # - '.github/workflows/jazzy-coverage-build.yml'
+ # - '**/package.xml'
+ # - '**/CMakeLists.txt'
+ # - 'ros2_control.jazzy.repos'
+ # - 'codecov.yml'
+
+jobs:
+ coverage_jazzy:
+ uses: ros-controls/ros2_control_ci/.github/workflows/reusable-build-coverage.yml@master
+ secrets: inherit
+ with:
+ ros_distro: jazzy
diff --git a/.github/workflows/jazzy-debian-build.yml b/.github/workflows/jazzy-debian-build.yml
new file mode 100644
index 0000000000..4ec6a29fff
--- /dev/null
+++ b/.github/workflows/jazzy-debian-build.yml
@@ -0,0 +1,32 @@
+name: Debian Jazzy Source Build
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/jazzy-debian-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.jazzy.repos'
+ schedule:
+ # Run every day to detect flakiness and broken dependencies
+ - cron: '03 1 * * *'
+
+
+jobs:
+ debian_source_build:
+ uses: ros-controls/ros2_control_ci/.github/workflows/reusable-debian-build.yml@master
+ strategy:
+ fail-fast: false
+ matrix:
+ ROS_DISTRO: [jazzy]
+ with:
+ ros_distro: ${{ matrix.ROS_DISTRO }}
+ upstream_workspace: ros2_control.${{ matrix.ROS_DISTRO }}.repos
+ ref_for_scheduled_build: master
+ skip_packages: rqt_controller_manager
diff --git a/.github/workflows/jazzy-pre-commit.yml b/.github/workflows/jazzy-pre-commit.yml
new file mode 100644
index 0000000000..d9ec610bbc
--- /dev/null
+++ b/.github/workflows/jazzy-pre-commit.yml
@@ -0,0 +1,13 @@
+name: Pre-Commit - Jazzy
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ pre-commit:
+ uses: ros-controls/ros2_control_ci/.github/workflows/reusable-pre-commit.yml@master
+ with:
+ ros_distro: jazzy
diff --git a/.github/workflows/jazzy-rhel-binary-build.yml b/.github/workflows/jazzy-rhel-binary-build.yml
new file mode 100644
index 0000000000..0dcc912dab
--- /dev/null
+++ b/.github/workflows/jazzy-rhel-binary-build.yml
@@ -0,0 +1,31 @@
+name: RHEL Jazzy Semi-Binary Build
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/jazzy-rhel-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.jazzy.repos'
+ schedule:
+ # Run every day to detect flakiness and broken dependencies
+ - cron: '03 1 * * *'
+
+jobs:
+ rhel_semi_binary_build:
+ uses: ros-controls/ros2_control_ci/.github/workflows/reusable-rhel-binary-build.yml@master
+ strategy:
+ fail-fast: false
+ matrix:
+ ROS_DISTRO: [jazzy]
+ with:
+ ros_distro: ${{ matrix.ROS_DISTRO }}
+ upstream_workspace: ros2_control.${{ matrix.ROS_DISTRO }}.repos
+ ref_for_scheduled_build: master
+ skip_packages: rqt_controller_manager
diff --git a/.github/workflows/jazzy-semi-binary-build.yml b/.github/workflows/jazzy-semi-binary-build.yml
new file mode 100644
index 0000000000..9634732cf9
--- /dev/null
+++ b/.github/workflows/jazzy-semi-binary-build.yml
@@ -0,0 +1,47 @@
+name: Jazzy Semi-Binary Build
+# author: Denis Štogl
+# description: 'Build & test all dependencies from released (binary) packages.'
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/jazzy-semi-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.jazzy.repos'
+ push:
+ branches:
+ - master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/jazzy-semi-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.jazzy.repos'
+ schedule:
+ # Run every morning to detect flakiness and broken dependencies
+ - cron: '03 1 * * *'
+
+jobs:
+ binary:
+ uses: ros-controls/ros2_control_ci/.github/workflows/reusable-industrial-ci-with-cache.yml@master
+ strategy:
+ fail-fast: false
+ matrix:
+ ROS_DISTRO: [jazzy]
+ ROS_REPO: [main, testing]
+ with:
+ ros_distro: ${{ matrix.ROS_DISTRO }}
+ ros_repo: ${{ matrix.ROS_REPO }}
+ upstream_workspace: ros2_control.${{ matrix.ROS_DISTRO }}.repos
+ ref_for_scheduled_build: master
diff --git a/.github/workflows/jazzy-source-build.yml b/.github/workflows/jazzy-source-build.yml
new file mode 100644
index 0000000000..65066a4bf2
--- /dev/null
+++ b/.github/workflows/jazzy-source-build.yml
@@ -0,0 +1,27 @@
+name: Jazzy Source Build
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/jazzy-source-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.jazzy.repos'
+ schedule:
+ # Run every day to detect flakiness and broken dependencies
+ - cron: '03 3 * * *'
+
+jobs:
+ source:
+ uses: ros-controls/ros2_control_ci/.github/workflows/reusable-ros-tooling-source-build.yml@master
+ with:
+ ros_distro: jazzy
+ ref: master
+ ros2_repo_branch: master
+ container: ubuntu:24.04
diff --git a/.github/workflows/rolling-abi-compatibility.yml b/.github/workflows/rolling-abi-compatibility.yml
index 73055ef3e5..b7828390fb 100644
--- a/.github/workflows/rolling-abi-compatibility.yml
+++ b/.github/workflows/rolling-abi-compatibility.yml
@@ -4,6 +4,15 @@ on:
pull_request:
branches:
- master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/rolling-abi-compatibility.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.rolling.repos'
jobs:
abi_check:
diff --git a/.github/workflows/rolling-binary-build.yml b/.github/workflows/rolling-binary-build.yml
index b083cc46fc..24a28f16ae 100644
--- a/.github/workflows/rolling-binary-build.yml
+++ b/.github/workflows/rolling-binary-build.yml
@@ -7,9 +7,27 @@ on:
pull_request:
branches:
- master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/rolling-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.rolling.repos'
push:
branches:
- master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/rolling-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control-not-released.rolling.repos'
schedule:
# Run every morning to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/rolling-check-docs.yml b/.github/workflows/rolling-check-docs.yml
new file mode 100644
index 0000000000..bd83c0caca
--- /dev/null
+++ b/.github/workflows/rolling-check-docs.yml
@@ -0,0 +1,19 @@
+name: Rolling Check Docs
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - master
+ paths:
+ - '**.rst'
+ - '**.md'
+ - '**.yaml'
+ - '.github/workflows/rolling-check-docs.yml'
+
+jobs:
+ check-docs:
+ name: Check Docs
+ uses: ros-controls/control.ros.org/.github/workflows/reusable-sphinx-check-single-version.yml@rolling
+ with:
+ ROS2_CONTROL_PR: ${{ github.ref }}
diff --git a/.github/workflows/rolling-coverage-build.yml b/.github/workflows/rolling-coverage-build.yml
index 4d4750c54c..45b10876e7 100644
--- a/.github/workflows/rolling-coverage-build.yml
+++ b/.github/workflows/rolling-coverage-build.yml
@@ -4,9 +4,29 @@ on:
push:
branches:
- master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/rolling-coverage-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.rolling.repos'
+ - 'codecov.yml'
pull_request:
branches:
- master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/rolling-coverage-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.rolling.repos'
+ - 'codecov.yml'
jobs:
coverage_rolling:
@@ -14,4 +34,3 @@ jobs:
secrets: inherit
with:
ros_distro: rolling
- os_name: ubuntu-22.04
diff --git a/.github/workflows/rolling-debian-build.yml b/.github/workflows/rolling-debian-build.yml
index e792963cc6..00d4ad844b 100644
--- a/.github/workflows/rolling-debian-build.yml
+++ b/.github/workflows/rolling-debian-build.yml
@@ -4,6 +4,15 @@ on:
pull_request:
branches:
- master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/rolling-debian-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.rolling.repos'
schedule:
# Run every day to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/rolling-pre-commit.yml b/.github/workflows/rolling-pre-commit.yml
index 9c87311bd7..7bc07ba802 100644
--- a/.github/workflows/rolling-pre-commit.yml
+++ b/.github/workflows/rolling-pre-commit.yml
@@ -11,4 +11,3 @@ jobs:
uses: ros-controls/ros2_control_ci/.github/workflows/reusable-pre-commit.yml@master
with:
ros_distro: rolling
- os_name: ubuntu-22.04
diff --git a/.github/workflows/rolling-rhel-binary-build.yml b/.github/workflows/rolling-rhel-binary-build.yml
index 38f375d6b6..c8939d6015 100644
--- a/.github/workflows/rolling-rhel-binary-build.yml
+++ b/.github/workflows/rolling-rhel-binary-build.yml
@@ -4,6 +4,15 @@ on:
pull_request:
branches:
- master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/rolling-rhel-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.jazzy.repos'
schedule:
# Run every day to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/rolling-semi-binary-build.yml b/.github/workflows/rolling-semi-binary-build.yml
index f5c7885139..4cdb7ab585 100644
--- a/.github/workflows/rolling-semi-binary-build.yml
+++ b/.github/workflows/rolling-semi-binary-build.yml
@@ -7,9 +7,27 @@ on:
pull_request:
branches:
- master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/rolling-semi-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.rolling.repos'
push:
branches:
- master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/rolling-semi-binary-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.rolling.repos'
schedule:
# Run every morning to detect flakiness and broken dependencies
- cron: '03 1 * * *'
diff --git a/.github/workflows/rolling-source-build.yml b/.github/workflows/rolling-source-build.yml
index f34a8e6bb5..9bbf09cda4 100644
--- a/.github/workflows/rolling-source-build.yml
+++ b/.github/workflows/rolling-source-build.yml
@@ -4,6 +4,15 @@ on:
push:
branches:
- master
+ paths:
+ - '**.hpp'
+ - '**.h'
+ - '**.cpp'
+ - '**.py'
+ - '.github/workflows/rolling-source-build.yml'
+ - '**/package.xml'
+ - '**/CMakeLists.txt'
+ - 'ros2_control.rolling.repos'
schedule:
# Run every day to detect flakiness and broken dependencies
- cron: '03 3 * * *'
@@ -15,3 +24,4 @@ jobs:
ros_distro: rolling
ref: master
ros2_repo_branch: master
+ container: ubuntu:24.04
diff --git a/.github/workflows/update-pre-commit.yml b/.github/workflows/update-pre-commit.yml
index 8b9545dff1..6bedaa0c97 100644
--- a/.github/workflows/update-pre-commit.yml
+++ b/.github/workflows/update-pre-commit.yml
@@ -5,7 +5,7 @@ name: Auto Update pre-commit
on:
workflow_dispatch:
schedule:
- - cron: '0 0 * * 0' # Run every Sunday at midnight
+ - cron: '0 0 1 * *' # Runs at 00:00, on day 1 of the month
jobs:
auto_update_and_create_pr:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 63dc2afe69..8e604b2f7f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -49,7 +49,7 @@ repos:
args: ["--ignore=D100,D101,D102,D103,D104,D105,D106,D107,D203,D212,D404"]
- repo: https://github.com/psf/black
- rev: 24.3.0
+ rev: 24.4.2
hooks:
- id: black
args: ["--line-length=99"]
@@ -62,7 +62,7 @@ repos:
# CPP hooks
- repo: https://github.com/pre-commit/mirrors-clang-format
- rev: v18.1.3
+ rev: v18.1.5
hooks:
- id: clang-format
args: ['-fallback-style=none', '-i']
@@ -104,6 +104,7 @@ repos:
description: Check if copyright notice is available in all files.
entry: ament_copyright
language: system
+ exclude: .*/conf\.py$
# Docs - RestructuredText hooks
- repo: https://github.com/PyCQA/doc8
@@ -124,14 +125,14 @@ repos:
# Spellcheck in comments and docs
# skipping of *.svg files is not working...
- repo: https://github.com/codespell-project/codespell
- rev: v2.2.6
+ rev: v2.3.0
hooks:
- id: codespell
args: ['--write-changes', '--uri-ignore-words-list=ist', '-L manuel']
exclude: CHANGELOG\.rst|\.(svg|pyc|drawio)$
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.28.1
+ rev: 0.28.4
hooks:
- id: check-github-workflows
args: ["--verbose"]
diff --git a/README.md b/README.md
index e88ca5b7bd..40d2a3c189 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ For more, please check the [documentation](https://control.ros.org/).
ROS2 Distro | Branch | Build status | Documentation | Released packages
:---------: | :----: | :----------: | :-----------: | :---------------:
**Rolling** | [`master`](https://github.com/ros-controls/ros2_control/tree/master) | [![Rolling Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-binary-build.yml?branch=master) [![Rolling Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-semi-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-semi-binary-build.yml?branch=master) | [Documentation](https://control.ros.org/master/index.html) [API Reference](https://control.ros.org/master/doc/api/index.html) | [ros2_control](https://index.ros.org/p/ros2_control/#rolling)
+**Jazzy** | [`master`](https://github.com/ros-controls/ros2_control/tree/master) | [![Rolling Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-binary-build.yml?branch=master) [![Rolling Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-semi-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/jazzy-semi-binary-build.yml?branch=master) | [Documentation](https://control.ros.org/jazzy/index.html) [API Reference](https://control.ros.org/jazzy/doc/api/index.html) | [ros2_control](https://index.ros.org/p/ros2_control/#jazzy)
**Iron** | [`iron`](https://github.com/ros-controls/ros2_control/tree/iron) | [![Iron Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-binary-build.yml/badge.svg?branch=iron)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-binary-build.yml?branch=iron) [![Iron Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-semi-binary-build.yml/badge.svg?branch=iron)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-semi-binary-build.yml?branch=iron) | [Documentation](https://control.ros.org/iron/index.html) [API Reference](https://control.ros.org/iron/doc/api/index.html) | [ros2_control](https://index.ros.org/p/ros2_control/#iron)
**Humble** | [`humble`](https://github.com/ros-controls/ros2_control/tree/humble) | [![Humble Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/humble-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-binary-build.yml?branch=master) [![Humble Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/humble-semi-binary-build.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-semi-binary-build.yml?branch=master) | [Documentation](https://control.ros.org/humble/index.html) [API Reference](https://control.ros.org/humble/doc/api/index.html) | [ros2_control](https://index.ros.org/p/ros2_control/#humble)
diff --git a/codecov.yml b/codecov.yml
index d8a5fde3e0..97106f32ff 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -22,4 +22,5 @@ flags:
- controller_interface
- controller_manager
- hardware_interface
+ - joint_limits
- transmission_interface
diff --git a/controller_interface/CHANGELOG.rst b/controller_interface/CHANGELOG.rst
index 3ed820dfe5..8d77f4203f 100644
--- a/controller_interface/CHANGELOG.rst
+++ b/controller_interface/CHANGELOG.rst
@@ -2,6 +2,22 @@
Changelog for package controller_interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+4.11.0 (2024-05-14)
+-------------------
+* Fix dependencies for source build (`#1533 `_)
+* Add find_package for ament_cmake_gen_version_h (`#1534 `_)
+* Contributors: Christoph Fröhlich
+
+4.10.0 (2024-05-08)
+-------------------
+* Working async controllers and components [not synchronized] (`#1041 `_)
+* Contributors: Márk Szitanics
+
+4.9.0 (2024-04-30)
+------------------
+* return the proper const object of the pointer in the const method (`#1494 `_)
+* Contributors: Sai Kishor Kothakota
+
4.8.0 (2024-03-27)
------------------
* generate version.h file per package using the ament_generate_version_header (`#1449 `_)
diff --git a/controller_interface/CMakeLists.txt b/controller_interface/CMakeLists.txt
index 2b7ccb9203..3dc3bc4d0a 100644
--- a/controller_interface/CMakeLists.txt
+++ b/controller_interface/CMakeLists.txt
@@ -12,6 +12,7 @@ set(THIS_PACKAGE_INCLUDE_DEPENDS
)
find_package(ament_cmake REQUIRED)
+find_package(ament_cmake_gen_version_h REQUIRED)
foreach(Dependency IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS})
find_package(${Dependency} REQUIRED)
endforeach()
diff --git a/controller_interface/include/controller_interface/async_controller.hpp b/controller_interface/include/controller_interface/async_controller.hpp
new file mode 100644
index 0000000000..5601ff4703
--- /dev/null
+++ b/controller_interface/include/controller_interface/async_controller.hpp
@@ -0,0 +1,111 @@
+// Copyright 2024 ros2_control development team
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef CONTROLLER_INTERFACE__ASYNC_CONTROLLER_HPP_
+#define CONTROLLER_INTERFACE__ASYNC_CONTROLLER_HPP_
+
+#include
+#include
+#include
+
+#include "controller_interface_base.hpp"
+#include "lifecycle_msgs/msg/state.hpp"
+
+namespace controller_interface
+{
+
+class AsyncControllerThread
+{
+public:
+ /// Constructor for the AsyncControllerThread object.
+ /**
+ *
+ * \param[in] controller shared pointer to a controller.
+ * \param[in] cm_update_rate the controller manager's update rate.
+ */
+ AsyncControllerThread(
+ std::shared_ptr & controller, int cm_update_rate)
+ : terminated_(false), controller_(controller), thread_{}, cm_update_rate_(cm_update_rate)
+ {
+ }
+
+ AsyncControllerThread(const AsyncControllerThread & t) = delete;
+ AsyncControllerThread(AsyncControllerThread && t) = delete;
+
+ // Destructor, called when the component is erased from its map.
+ ~AsyncControllerThread()
+ {
+ terminated_.store(true, std::memory_order_seq_cst);
+ if (thread_.joinable())
+ {
+ thread_.join();
+ }
+ }
+
+ /// Creates the controller's thread.
+ /**
+ * Called when the controller is activated.
+ *
+ */
+ void activate()
+ {
+ thread_ = std::thread(&AsyncControllerThread::controller_update_callback, this);
+ }
+
+ /// Periodically execute the controller's update method.
+ /**
+ * Callback of the async controller's thread.
+ * **Not synchronized with the controller manager's write and read currently**
+ *
+ */
+ void controller_update_callback()
+ {
+ using TimePoint = std::chrono::system_clock::time_point;
+ unsigned int used_update_rate =
+ controller_->get_update_rate() == 0 ? cm_update_rate_ : controller_->get_update_rate();
+
+ auto previous_time = controller_->get_node()->now();
+ while (!terminated_.load(std::memory_order_relaxed))
+ {
+ auto const period = std::chrono::nanoseconds(1'000'000'000 / used_update_rate);
+ TimePoint next_iteration_time =
+ TimePoint(std::chrono::nanoseconds(controller_->get_node()->now().nanoseconds()));
+
+ if (controller_->get_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE)
+ {
+ auto const current_time = controller_->get_node()->now();
+ auto const measured_period = current_time - previous_time;
+ previous_time = current_time;
+ controller_->update(
+ controller_->get_node()->now(),
+ (controller_->get_update_rate() != cm_update_rate_ && controller_->get_update_rate() != 0)
+ ? rclcpp::Duration::from_seconds(1.0 / controller_->get_update_rate())
+ : measured_period);
+ }
+
+ next_iteration_time += period;
+ std::this_thread::sleep_until(next_iteration_time);
+ }
+ }
+
+private:
+ std::atomic terminated_;
+ std::shared_ptr controller_;
+ std::thread thread_;
+ unsigned int cm_update_rate_;
+};
+
+} // namespace controller_interface
+
+#endif // CONTROLLER_INTERFACE__ASYNC_CONTROLLER_HPP_
diff --git a/controller_interface/package.xml b/controller_interface/package.xml
index 8143d8f9c5..9a99449043 100644
--- a/controller_interface/package.xml
+++ b/controller_interface/package.xml
@@ -2,7 +2,7 @@
controller_interface
- 4.8.0
+ 4.11.0Description of controller_interfaceBence MagyarDenis Štogl
diff --git a/controller_interface/test/test_controller_interface.cpp b/controller_interface/test/test_controller_interface.cpp
index 3976b2a81e..03838b1a2e 100644
--- a/controller_interface/test/test_controller_interface.cpp
+++ b/controller_interface/test/test_controller_interface.cpp
@@ -88,7 +88,8 @@ TEST(TestableControllerInterface, setting_update_rate_in_configure)
ASSERT_EQ(controller.get_update_rate(), 2812u);
// Test updating of update_rate parameter
- EXPECT_EQ(std::system("ros2 param set /testable_controller_interface update_rate 623"), 0);
+ auto res = controller.get_node()->set_parameter(rclcpp::Parameter("update_rate", 623));
+ EXPECT_EQ(res.successful, true);
// Keep the same update rate until transition from 'UNCONFIGURED' TO 'INACTIVE' does not happen
controller.configure(); // No transition so the update rate should stay intact
ASSERT_NE(controller.get_update_rate(), 623u);
diff --git a/controller_manager/CHANGELOG.rst b/controller_manager/CHANGELOG.rst
index 53005b3800..2248f5f668 100644
--- a/controller_manager/CHANGELOG.rst
+++ b/controller_manager/CHANGELOG.rst
@@ -2,6 +2,26 @@
Changelog for package controller_manager
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+4.11.0 (2024-05-14)
+-------------------
+* Add find_package for ament_cmake_gen_version_h (`#1534 `_)
+* Contributors: Christoph Fröhlich
+
+4.10.0 (2024-05-08)
+-------------------
+* allow extra spawner arguments to not declare every argument in launch utils (`#1505 `_)
+* Working async controllers and components [not synchronized] (`#1041 `_)
+* Add fallback controllers list to the ControllerInfo (`#1503 `_)
+* Add a functionality to look for the controller type in the params file when not parsed (`#1502 `_)
+* Add controller exception handling in controller manager (`#1507 `_)
+* Contributors: Márk Szitanics, Sai Kishor Kothakota
+
+4.9.0 (2024-04-30)
+------------------
+* Deactivate the controllers when they return error similar to hardware (`#1499 `_)
+* Component parser: Get mimic information from URDF (`#1256 `_)
+* Contributors: Christoph Fröhlich, Sai Kishor Kothakota
+
4.8.0 (2024-03-27)
------------------
* generate version.h file per package using the ament_generate_version_header (`#1449 `_)
diff --git a/controller_manager/CMakeLists.txt b/controller_manager/CMakeLists.txt
index e267856eb1..c3c0de7739 100644
--- a/controller_manager/CMakeLists.txt
+++ b/controller_manager/CMakeLists.txt
@@ -21,6 +21,7 @@ set(THIS_PACKAGE_INCLUDE_DEPENDS
find_package(ament_cmake REQUIRED)
find_package(ament_cmake_python REQUIRED)
find_package(ament_cmake_core REQUIRED)
+find_package(ament_cmake_gen_version_h REQUIRED)
find_package(backward_ros REQUIRED)
foreach(Dependency IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS})
find_package(${Dependency} REQUIRED)
@@ -190,6 +191,12 @@ if(BUILD_TESTING)
ros2_control_test_assets::ros2_control_test_assets
)
+ install(FILES test/test_controller_spawner_with_fallback_controllers.yaml
+ DESTINATION test)
+
+ install(FILES test/test_controller_spawner_with_type.yaml
+ DESTINATION test)
+
ament_add_gmock(test_hardware_management_srvs
test/test_hardware_management_srvs.cpp
)
diff --git a/controller_manager/controller_manager/launch_utils.py b/controller_manager/controller_manager/launch_utils.py
index 5a23c02cec..01528552d0 100644
--- a/controller_manager/controller_manager/launch_utils.py
+++ b/controller_manager/controller_manager/launch_utils.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import warnings
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, PythonExpression
@@ -20,7 +21,7 @@
def generate_load_controller_launch_description(
- controller_name, controller_type=None, controller_params_file=None
+ controller_name, controller_type=None, controller_params_file=None, extra_spawner_args=[]
):
"""
Generate launch description for loading a controller using spawner.
@@ -39,7 +40,8 @@ def generate_load_controller_launch_description(
'joint_state_broadcaster',
controller_type='joint_state_broadcaster/JointStateBroadcaster',
controller_params_file=os.path.join(get_package_share_directory('my_pkg'),
- 'config', 'controller_params.yaml')
+ 'config', 'controller_params.yaml'),
+ extra_spawner_args=[--load-only]
)
"""
@@ -61,6 +63,12 @@ def generate_load_controller_launch_description(
]
if controller_type:
+ warnings.warn(
+ "The 'controller_type' argument is deprecated and will be removed in future releases."
+ " Declare the controller type parameter in the param file instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
spawner_arguments += ["--controller-type", controller_type]
if controller_params_file:
@@ -79,6 +87,9 @@ def generate_load_controller_launch_description(
)
]
+ if extra_spawner_args:
+ spawner_arguments += extra_spawner_args
+
spawner = Node(
package="controller_manager",
executable="spawner",
diff --git a/controller_manager/controller_manager/spawner.py b/controller_manager/controller_manager/spawner.py
index 1d11d80fe1..dbefd360b9 100644
--- a/controller_manager/controller_manager/spawner.py
+++ b/controller_manager/controller_manager/spawner.py
@@ -19,6 +19,7 @@
import sys
import time
import warnings
+import yaml
from controller_manager import (
configure_controller,
@@ -135,6 +136,21 @@ def is_controller_loaded(node, controller_manager, controller_name):
return any(c.name == controller_name for c in controllers)
+def get_parameter_from_param_file(controller_name, parameter_file, parameter_name):
+ with open(parameter_file) as f:
+ parameters = yaml.safe_load(f)
+ if controller_name in parameters:
+ value = parameters[controller_name]
+ if not isinstance(value, dict) or "ros__parameters" not in value:
+ raise RuntimeError(
+ f"YAML file : {parameter_file} is not a valid ROS parameter file for controller : {controller_name}"
+ )
+ if parameter_name in parameters[controller_name]["ros__parameters"]:
+ return parameters[controller_name]["ros__parameters"][parameter_name]
+ else:
+ return None
+
+
def main(args=None):
rclpy.init(args=args, signal_handler_options=SignalHandlerOptions.NO)
parser = argparse.ArgumentParser()
@@ -170,7 +186,7 @@ def main(args=None):
parser.add_argument(
"-t",
"--controller-type",
- help="If not provided it should exist in the controller manager namespace",
+ help="If not provided it should exist in the controller manager namespace (deprecated)",
default=None,
required=False,
)
@@ -194,6 +210,14 @@ def main(args=None):
action="store_true",
required=False,
)
+ parser.add_argument(
+ "--fallback_controllers",
+ help="Fallback controllers list are activated as a fallback strategy when the"
+ " spawned controllers fail. When the argument is provided, it takes precedence over"
+ " the fallback_controllers list in the param file",
+ default=None,
+ nargs="+",
+ )
command_line_args = rclpy.utilities.remove_ros_args(args=sys.argv)[1:]
args = parser.parse_args(command_line_args)
@@ -201,9 +225,16 @@ def main(args=None):
controller_manager_name = args.controller_manager
controller_namespace = args.namespace
param_file = args.param_file
- controller_type = args.controller_type
controller_manager_timeout = args.controller_manager_timeout
+ if args.controller_type:
+ warnings.filterwarnings("always")
+ warnings.warn(
+ "The '--controller-type' argument is deprecated and will be removed in future releases."
+ " Declare the controller type parameter in the param file instead.",
+ DeprecationWarning,
+ )
+
if param_file and not os.path.isfile(param_file):
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), param_file)
@@ -226,6 +257,8 @@ def main(args=None):
return 1
for controller_name in controller_names:
+ fallback_controllers = args.fallback_controllers
+ controller_type = args.controller_type
prefixed_controller_name = controller_name
if controller_namespace:
prefixed_controller_name = controller_namespace + "/" + controller_name
@@ -237,6 +270,10 @@ def main(args=None):
+ bcolors.ENDC
)
else:
+ if not controller_type and param_file:
+ controller_type = get_parameter_from_param_file(
+ controller_name, param_file, "type"
+ )
if controller_type:
parameter = Parameter()
parameter.name = prefixed_controller_name + ".type"
@@ -301,6 +338,43 @@ def main(args=None):
)
return 1
+ if not fallback_controllers and param_file:
+ fallback_controllers = get_parameter_from_param_file(
+ controller_name, param_file, "fallback_controllers"
+ )
+
+ if fallback_controllers:
+ parameter = Parameter()
+ parameter.name = prefixed_controller_name + ".fallback_controllers"
+ parameter.value = get_parameter_value(string_value=str(fallback_controllers))
+
+ response = call_set_parameters(
+ node=node, node_name=controller_manager_name, parameters=[parameter]
+ )
+ assert len(response.results) == 1
+ result = response.results[0]
+ if result.successful:
+ node.get_logger().info(
+ bcolors.OKCYAN
+ + 'Setting fallback_controllers to ["'
+ + ",".join(fallback_controllers)
+ + '"] for '
+ + bcolors.BOLD
+ + prefixed_controller_name
+ + bcolors.ENDC
+ )
+ else:
+ node.get_logger().fatal(
+ bcolors.FAIL
+ + 'Could not set fallback_controllers to ["'
+ + ",".join(fallback_controllers)
+ + '"] for '
+ + bcolors.BOLD
+ + prefixed_controller_name
+ + bcolors.ENDC
+ )
+ return 1
+
ret = load_controller(node, controller_manager_name, controller_name)
if not ret.ok:
node.get_logger().fatal(
diff --git a/controller_manager/doc/userdoc.rst b/controller_manager/doc/userdoc.rst
index c8aa68d774..4deb85291b 100644
--- a/controller_manager/doc/userdoc.rst
+++ b/controller_manager/doc/userdoc.rst
@@ -225,6 +225,10 @@ Note that not all controllers have to be restarted, e.g., broadcasters.
Restarting hardware
^^^^^^^^^^^^^^^^^^^^^
-If hardware gets restarted then you should go through its lifecycle again.
-This can be simply achieved by returning ``ERROR`` from ``write`` and ``read`` methods of interface implementation.
-**NOT IMPLEMENTED YET - PLEASE STOP/RESTART ALL CONTROLLERS MANUALLY FOR NOW** The controller manager detects that and stops all the controllers that are commanding that hardware and restarts broadcasters that are listening to its states.
+If hardware gets restarted then you should go through its lifecycle again in order to reconfigure and export the interfaces
+
+Hardware and Controller Errors
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If the hardware during it's ``read`` or ``write`` method returns ``return_type::ERROR``, the controller manager will stop all controllers that are using the hardware's command and state interfaces.
+Likewise, if a controller returns ``return_type::ERROR`` from its ``update`` method, the controller manager will deactivate the respective controller. In future, the controller manager will try to start any fallback controllers if available.
diff --git a/controller_manager/include/controller_manager/controller_manager.hpp b/controller_manager/include/controller_manager/controller_manager.hpp
index 9a709c5f8f..845fa2dee0 100644
--- a/controller_manager/include/controller_manager/controller_manager.hpp
+++ b/controller_manager/include/controller_manager/controller_manager.hpp
@@ -23,6 +23,7 @@
#include
#include
+#include "controller_interface/async_controller.hpp"
#include "controller_interface/chainable_controller_interface.hpp"
#include "controller_interface/controller_interface.hpp"
#include "controller_interface/controller_interface_base.hpp"
@@ -196,6 +197,15 @@ class ControllerManager : public rclcpp::Node
CONTROLLER_MANAGER_PUBLIC
unsigned int get_update_rate() const;
+ /// Deletes all async controllers and components.
+ /**
+ * Needed to join the threads immediately after the control loop is ended
+ * to avoid unnecessary iterations. Otherwise
+ * the threads will be joined only when the controller manager gets destroyed.
+ */
+ CONTROLLER_MANAGER_PUBLIC
+ void shutdown_async_controllers_and_components();
+
protected:
CONTROLLER_MANAGER_PUBLIC
void init_services();
@@ -563,65 +573,7 @@ class ControllerManager : public rclcpp::Node
SwitchParams switch_params_;
- class ControllerThreadWrapper
- {
- public:
- ControllerThreadWrapper(
- std::shared_ptr & controller,
- int cm_update_rate)
- : terminated_(false), controller_(controller), thread_{}, cm_update_rate_(cm_update_rate)
- {
- }
-
- ControllerThreadWrapper(const ControllerThreadWrapper & t) = delete;
- ControllerThreadWrapper(ControllerThreadWrapper && t) = default;
- ~ControllerThreadWrapper()
- {
- terminated_.store(true, std::memory_order_seq_cst);
- if (thread_.joinable())
- {
- thread_.join();
- }
- }
-
- void activate()
- {
- thread_ = std::thread(&ControllerThreadWrapper::call_controller_update, this);
- }
-
- void call_controller_update()
- {
- using TimePoint = std::chrono::system_clock::time_point;
- unsigned int used_update_rate =
- controller_->get_update_rate() == 0
- ? cm_update_rate_
- : controller_
- ->get_update_rate(); // determines if the controller's or CM's update rate is used
-
- while (!terminated_.load(std::memory_order_relaxed))
- {
- auto const period = std::chrono::nanoseconds(1'000'000'000 / used_update_rate);
- TimePoint next_iteration_time =
- TimePoint(std::chrono::nanoseconds(controller_->get_node()->now().nanoseconds()));
-
- if (controller_->get_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE)
- {
- // critical section, not implemented yet
- }
-
- next_iteration_time += period;
- std::this_thread::sleep_until(next_iteration_time);
- }
- }
-
- private:
- std::atomic terminated_;
- std::shared_ptr controller_;
- std::thread thread_;
- unsigned int cm_update_rate_;
- };
-
- std::unordered_map>
+ std::unordered_map>
async_controller_threads_;
};
diff --git a/controller_manager/package.xml b/controller_manager/package.xml
index 7de94fc053..e2aa998332 100644
--- a/controller_manager/package.xml
+++ b/controller_manager/package.xml
@@ -2,7 +2,7 @@
controller_manager
- 4.8.0
+ 4.11.0Description of controller_managerBence MagyarDenis Štogl
diff --git a/controller_manager/src/controller_manager.cpp b/controller_manager/src/controller_manager.cpp
index eadd1b0d78..41fe1ad95d 100644
--- a/controller_manager/src/controller_manager.cpp
+++ b/controller_manager/src/controller_manager.cpp
@@ -482,6 +482,17 @@ controller_interface::ControllerInterfaceBaseSharedPtr ControllerManager::load_c
controller_spec.info.parameters_file = parameters_file;
}
+ const std::string fallback_ctrl_param = controller_name + ".fallback_controllers";
+ std::vector fallback_controllers;
+ if (!has_parameter(fallback_ctrl_param))
+ {
+ declare_parameter(fallback_ctrl_param, rclcpp::ParameterType::PARAMETER_STRING_ARRAY);
+ }
+ if (get_parameter(fallback_ctrl_param, fallback_controllers) && !fallback_controllers.empty())
+ {
+ controller_spec.info.fallback_controllers_names = fallback_controllers;
+ }
+
return add_controller_impl(controller_spec);
}
@@ -562,11 +573,26 @@ controller_interface::return_type ControllerManager::unload_controller(
get_logger(), "Controller '%s' is cleaned-up before unloading!", controller_name.c_str());
// TODO(destogl): remove reference interface if chainable; i.e., add a separate method for
// cleaning-up controllers?
- const auto new_state = controller.c->get_node()->cleanup();
- if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED)
+ try
{
- RCLCPP_WARN(
- get_logger(), "Failed to clean-up the controller '%s' before unloading!",
+ const auto new_state = controller.c->get_node()->cleanup();
+ if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED)
+ {
+ RCLCPP_WARN(
+ get_logger(), "Failed to clean-up the controller '%s' before unloading!",
+ controller_name.c_str());
+ }
+ }
+ catch (const std::exception & e)
+ {
+ RCLCPP_ERROR(
+ get_logger(), "Failed to clean-up the controller '%s' before unloading: %s",
+ controller_name.c_str(), e.what());
+ }
+ catch (...)
+ {
+ RCLCPP_ERROR(
+ get_logger(), "Failed to clean-up the controller '%s' before unloading",
controller_name.c_str());
}
}
@@ -631,22 +657,49 @@ controller_interface::return_type ControllerManager::configure_controller(
get_logger(), "Controller '%s' is cleaned-up before configuring", controller_name.c_str());
// TODO(destogl): remove reference interface if chainable; i.e., add a separate method for
// cleaning-up controllers?
- new_state = controller->get_node()->cleanup();
- if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED)
+ try
+ {
+ new_state = controller->get_node()->cleanup();
+ if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED)
+ {
+ RCLCPP_ERROR(
+ get_logger(), "Controller '%s' can not be cleaned-up before configuring",
+ controller_name.c_str());
+ return controller_interface::return_type::ERROR;
+ }
+ }
+ catch (...)
{
RCLCPP_ERROR(
- get_logger(), "Controller '%s' can not be cleaned-up before configuring",
+ get_logger(), "Caught exception while cleaning-up controller '%s' before configuring",
controller_name.c_str());
return controller_interface::return_type::ERROR;
}
}
- new_state = controller->configure();
- if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE)
+ try
+ {
+ new_state = controller->configure();
+ if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE)
+ {
+ RCLCPP_ERROR(
+ get_logger(), "After configuring, controller '%s' is in state '%s' , expected inactive.",
+ controller_name.c_str(), new_state.label().c_str());
+ return controller_interface::return_type::ERROR;
+ }
+ }
+ catch (const std::exception & e)
{
RCLCPP_ERROR(
- get_logger(), "After configuring, controller '%s' is in state '%s' , expected inactive.",
- controller_name.c_str(), new_state.label().c_str());
+ get_logger(), "Caught exception while configuring controller '%s': %s",
+ controller_name.c_str(), e.what());
+ return controller_interface::return_type::ERROR;
+ }
+ catch (...)
+ {
+ RCLCPP_ERROR(
+ get_logger(), "Caught unknown exception while configuring controller '%s'",
+ controller_name.c_str());
return controller_interface::return_type::ERROR;
}
@@ -654,7 +707,8 @@ controller_interface::return_type ControllerManager::configure_controller(
if (controller->is_async())
{
async_controller_threads_.emplace(
- controller_name, std::make_unique(controller, update_rate_));
+ controller_name,
+ std::make_unique(controller, update_rate_));
}
const auto controller_update_rate = controller->get_update_rate();
@@ -1286,14 +1340,35 @@ controller_interface::ControllerInterfaceBaseSharedPtr ControllerManager::add_co
}
const rclcpp::NodeOptions controller_node_options = determine_controller_node_options(controller);
- if (
- controller.c->init(
- controller.info.name, robot_description_, get_update_rate(), get_namespace(),
- controller_node_options) == controller_interface::return_type::ERROR)
+ // Catch whatever exception the controller might throw
+ try
+ {
+ if (
+ controller.c->init(
+ controller.info.name, robot_description_, get_update_rate(), get_namespace(),
+ controller_node_options) == controller_interface::return_type::ERROR)
+ {
+ to.clear();
+ RCLCPP_ERROR(
+ get_logger(), "Could not initialize the controller named '%s'",
+ controller.info.name.c_str());
+ return nullptr;
+ }
+ }
+ catch (const std::exception & e)
{
to.clear();
RCLCPP_ERROR(
- get_logger(), "Could not initialize the controller named '%s'", controller.info.name.c_str());
+ get_logger(), "Caught exception while initializing controller '%s': %s",
+ controller.info.name.c_str(), e.what());
+ return nullptr;
+ }
+ catch (...)
+ {
+ to.clear();
+ RCLCPP_ERROR(
+ get_logger(), "Caught unknown exception while initializing controller '%s'",
+ controller.info.name.c_str());
return nullptr;
}
@@ -1336,18 +1411,37 @@ void ControllerManager::deactivate_controllers(
auto controller = found_it->c;
if (is_controller_active(*controller))
{
- const auto new_state = controller->get_node()->deactivate();
- controller->release_interfaces();
- // if it is a chainable controller, make the reference interfaces unavailable on deactivation
- if (controller->is_chainable())
+ try
+ {
+ const auto new_state = controller->get_node()->deactivate();
+ controller->release_interfaces();
+
+ // if it is a chainable controller, make the reference interfaces unavailable on
+ // deactivation
+ if (controller->is_chainable())
+ {
+ resource_manager_->make_controller_reference_interfaces_unavailable(controller_name);
+ }
+ if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE)
+ {
+ RCLCPP_ERROR(
+ get_logger(), "After deactivating, controller '%s' is in state '%s', expected Inactive",
+ controller_name.c_str(), new_state.label().c_str());
+ }
+ }
+ catch (const std::exception & e)
{
- resource_manager_->make_controller_reference_interfaces_unavailable(controller_name);
+ RCLCPP_ERROR(
+ get_logger(), "Caught exception while deactivating the controller '%s': %s",
+ controller_name.c_str(), e.what());
+ continue;
}
- if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE)
+ catch (...)
{
RCLCPP_ERROR(
- get_logger(), "After deactivating, controller '%s' is in state '%s', expected Inactive",
- controller_name.c_str(), new_state.label().c_str());
+ get_logger(), "Caught unknown exception while deactivating the controller '%s'",
+ controller_name.c_str());
+ continue;
}
}
}
@@ -1503,15 +1597,32 @@ void ControllerManager::activate_controllers(
}
controller->assign_interfaces(std::move(command_loans), std::move(state_loans));
- const auto new_state = controller->get_node()->activate();
- if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE)
+ try
+ {
+ const auto new_state = controller->get_node()->activate();
+ if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE)
+ {
+ RCLCPP_ERROR(
+ get_logger(),
+ "After activation, controller '%s' is in state '%s' (%d), expected '%s' (%d).",
+ controller->get_node()->get_name(), new_state.label().c_str(), new_state.id(),
+ hardware_interface::lifecycle_state_names::ACTIVE,
+ lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE);
+ }
+ }
+ catch (const std::exception & e)
{
RCLCPP_ERROR(
- get_logger(),
- "After activation, controller '%s' is in state '%s' (%d), expected '%s' (%d).",
- controller->get_node()->get_name(), new_state.label().c_str(), new_state.id(),
- hardware_interface::lifecycle_state_names::ACTIVE,
- lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE);
+ get_logger(), "Caught exception while activating the controller '%s': %s",
+ controller_name.c_str(), e.what());
+ continue;
+ }
+ catch (...)
+ {
+ RCLCPP_ERROR(
+ get_logger(), "Caught unknown exception while activating the controller '%s'",
+ controller_name.c_str());
+ continue;
}
// if it is a chainable controller, make the reference interfaces available on activation
@@ -2023,6 +2134,7 @@ controller_interface::return_type ControllerManager::update(
++update_loop_counter_;
update_loop_counter_ %= update_rate_;
+ std::vector failed_controllers_list;
for (const auto & loaded_controller : rt_controller_list)
{
// TODO(v-lopez) we could cache this information
@@ -2049,7 +2161,26 @@ controller_interface::return_type ControllerManager::update(
{
const auto controller_actual_period =
(time - *loaded_controller.next_update_cycle_time) + controller_period;
- auto controller_ret = loaded_controller.c->update(time, controller_actual_period);
+ auto controller_ret = controller_interface::return_type::OK;
+ // Catch exceptions thrown by the controller update function
+ try
+ {
+ controller_ret = loaded_controller.c->update(time, controller_actual_period);
+ }
+ catch (const std::exception & e)
+ {
+ RCLCPP_ERROR(
+ get_logger(), "Caught exception while updating controller '%s': %s",
+ loaded_controller.info.name.c_str(), e.what());
+ controller_ret = controller_interface::return_type::ERROR;
+ }
+ catch (...)
+ {
+ RCLCPP_ERROR(
+ get_logger(), "Caught unknown exception while updating controller '%s'",
+ loaded_controller.info.name.c_str());
+ controller_ret = controller_interface::return_type::ERROR;
+ }
if (
*loaded_controller.next_update_cycle_time ==
@@ -2061,11 +2192,25 @@ controller_interface::return_type ControllerManager::update(
if (controller_ret != controller_interface::return_type::OK)
{
+ failed_controllers_list.push_back(loaded_controller.info.name);
ret = controller_ret;
}
}
}
}
+ if (!failed_controllers_list.empty())
+ {
+ std::string failed_controllers;
+ for (const auto & controller : failed_controllers_list)
+ {
+ failed_controllers += "\n\t- " + controller;
+ }
+ RCLCPP_ERROR(
+ get_logger(), "Deactivating following controllers as their update resulted in an error :%s",
+ failed_controllers.c_str());
+
+ deactivate_controllers(rt_controller_list, failed_controllers_list);
+ }
// there are controllers to (de)activate
if (switch_params_.do_switch)
@@ -2173,6 +2318,13 @@ std::pair ControllerManager::split_command_interface(
unsigned int ControllerManager::get_update_rate() const { return update_rate_; }
+void ControllerManager::shutdown_async_controllers_and_components()
+{
+ async_controller_threads_.erase(
+ async_controller_threads_.begin(), async_controller_threads_.end());
+ resource_manager_->shutdown_async_components();
+}
+
void ControllerManager::propagate_deactivation_of_chained_mode(
const std::vector & controllers)
{
diff --git a/controller_manager/src/ros2_control_node.cpp b/controller_manager/src/ros2_control_node.cpp
index 2747e79a1b..6dd7d72fb2 100644
--- a/controller_manager/src/ros2_control_node.cpp
+++ b/controller_manager/src/ros2_control_node.cpp
@@ -83,6 +83,8 @@ int main(int argc, char ** argv)
next_iteration_time += period;
std::this_thread::sleep_until(next_iteration_time);
}
+
+ cm->shutdown_async_controllers_and_components();
});
executor->add_node(cm);
diff --git a/controller_manager/test/test_controller/test_controller.cpp b/controller_manager/test/test_controller/test_controller.cpp
index 7585ae36e5..df41ebf34e 100644
--- a/controller_manager/test/test_controller/test_controller.cpp
+++ b/controller_manager/test/test_controller/test_controller.cpp
@@ -76,6 +76,14 @@ controller_interface::return_type TestController::update(
{
for (size_t i = 0; i < command_interfaces_.size(); ++i)
{
+ if (!std::isfinite(external_commands_for_testing_[i]))
+ {
+ RCLCPP_ERROR(
+ get_node()->get_logger(),
+ "External command value for command interface '%s' is not finite",
+ command_interfaces_[i].get_name().c_str());
+ return controller_interface::return_type::ERROR;
+ }
RCLCPP_INFO(
get_node()->get_logger(), "Setting value of command interface '%s' to %f",
command_interfaces_[i].get_name().c_str(), external_commands_for_testing_[i]);
diff --git a/controller_manager/test/test_controller_manager_hardware_error_handling.cpp b/controller_manager/test/test_controller_manager_hardware_error_handling.cpp
index 4c800d41c2..6e2fba23db 100644
--- a/controller_manager/test/test_controller_manager_hardware_error_handling.cpp
+++ b/controller_manager/test/test_controller_manager_hardware_error_handling.cpp
@@ -405,6 +405,106 @@ TEST_P(TestControllerManagerWithTestableCM, stop_controllers_on_hardware_read_er
}
}
+TEST_P(TestControllerManagerWithTestableCM, stop_controllers_on_controller_error)
+{
+ auto strictness = GetParam().strictness;
+ SetupAndConfigureControllers(strictness);
+
+ rclcpp_lifecycle::State state_active(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE,
+ hardware_interface::lifecycle_state_names::ACTIVE);
+
+ {
+ EXPECT_EQ(controller_interface::return_type::OK, cm_->update(time_, PERIOD));
+ EXPECT_GE(test_controller_actuator->internal_counter, 1u)
+ << "Controller is started at the end of update";
+ EXPECT_GE(test_controller_system->internal_counter, 1u)
+ << "Controller is started at the end of update";
+ EXPECT_GE(test_broadcaster_all->internal_counter, 1u)
+ << "Controller is started at the end of update";
+ EXPECT_GE(test_broadcaster_sensor->internal_counter, 1u)
+ << "Controller is started at the end of update";
+ }
+
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, test_controller_actuator->get_state().id());
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, test_controller_system->get_state().id());
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, test_broadcaster_all->get_state().id());
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, test_broadcaster_sensor->get_state().id());
+
+ // Execute first time without any errors
+ {
+ auto new_counter = test_controller_actuator->internal_counter + 1;
+ EXPECT_EQ(controller_interface::return_type::OK, cm_->update(time_, PERIOD));
+ EXPECT_EQ(test_controller_actuator->internal_counter, new_counter) << "Execute without errors";
+ EXPECT_EQ(test_controller_system->internal_counter, new_counter) << "Execute without errors";
+ EXPECT_EQ(test_broadcaster_all->internal_counter, new_counter) << "Execute without errors";
+ EXPECT_EQ(test_broadcaster_sensor->internal_counter, new_counter) << "Execute without errors";
+ }
+
+ // Simulate error in update method of the controllers but not in hardware
+ test_controller_actuator->external_commands_for_testing_[0] =
+ std::numeric_limits::quiet_NaN();
+ test_controller_system->external_commands_for_testing_[0] =
+ std::numeric_limits::quiet_NaN();
+ {
+ auto new_counter = test_controller_actuator->internal_counter + 1;
+ EXPECT_EQ(controller_interface::return_type::ERROR, cm_->update(time_, PERIOD));
+ EXPECT_EQ(test_controller_actuator->internal_counter, new_counter)
+ << "Executes the current cycle and returns ERROR";
+ EXPECT_EQ(
+ test_controller_actuator->get_state().id(),
+ lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE);
+ EXPECT_EQ(test_controller_system->internal_counter, new_counter)
+ << "Executes the current cycle and returns ERROR";
+ EXPECT_EQ(
+ test_controller_system->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE);
+ EXPECT_EQ(test_broadcaster_all->internal_counter, new_counter)
+ << "Execute without errors to write value";
+ EXPECT_EQ(test_broadcaster_sensor->internal_counter, new_counter)
+ << "Execute without errors to write value";
+ }
+
+ {
+ auto previous_counter = test_controller_actuator->internal_counter;
+ auto new_counter = test_controller_system->internal_counter + 1;
+
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE,
+ test_controller_actuator->get_state().id());
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, test_controller_system->get_state().id());
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, test_broadcaster_all->get_state().id());
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, test_broadcaster_sensor->get_state().id());
+
+ EXPECT_EQ(controller_interface::return_type::OK, cm_->update(time_, PERIOD));
+ EXPECT_EQ(test_controller_actuator->internal_counter, previous_counter)
+ << "Cannot execute as it should be currently deactivated";
+ EXPECT_EQ(test_controller_system->internal_counter, previous_counter)
+ << "Cannot execute as it should be currently deactivated";
+ EXPECT_EQ(test_broadcaster_all->internal_counter, new_counter)
+ << "Broadcaster all interfaces without errors";
+ EXPECT_EQ(test_broadcaster_sensor->internal_counter, new_counter)
+ << "Execute without errors to write value";
+
+ // The states shouldn't change as there are no more controller errors
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE,
+ test_controller_actuator->get_state().id());
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, test_controller_system->get_state().id());
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, test_broadcaster_all->get_state().id());
+ EXPECT_EQ(
+ lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, test_broadcaster_sensor->get_state().id());
+ }
+}
+
TEST_P(TestControllerManagerWithTestableCM, stop_controllers_on_hardware_write_error)
{
auto strictness = GetParam().strictness;
diff --git a/controller_manager/test/test_controller_spawner_with_fallback_controllers.yaml b/controller_manager/test/test_controller_spawner_with_fallback_controllers.yaml
new file mode 100644
index 0000000000..bc93f162c1
--- /dev/null
+++ b/controller_manager/test/test_controller_spawner_with_fallback_controllers.yaml
@@ -0,0 +1,13 @@
+ctrl_1:
+ ros__parameters:
+ joint_names: ["joint1"]
+
+ctrl_2:
+ ros__parameters:
+ joint_names: ["joint2"]
+ fallback_controllers: ["ctrl_6", "ctrl_7", "ctrl_8"]
+
+ctrl_3:
+ ros__parameters:
+ joint_names: ["joint3"]
+ fallback_controllers: ["ctrl_9"]
diff --git a/controller_manager/test/test_controller_spawner_with_type.yaml b/controller_manager/test/test_controller_spawner_with_type.yaml
new file mode 100644
index 0000000000..95a6efba30
--- /dev/null
+++ b/controller_manager/test/test_controller_spawner_with_type.yaml
@@ -0,0 +1,13 @@
+ctrl_with_parameters_and_type:
+ ros__parameters:
+ type: "controller_manager/test_controller"
+ joint_names: ["joint0"]
+
+chainable_ctrl_with_parameters_and_type:
+ ros__parameters:
+ type: "controller_manager/test_chainable_controller"
+ joint_names: ["joint1"]
+
+ctrl_with_parameters_and_no_type:
+ ros__parameters:
+ joint_names: ["joint2"]
diff --git a/controller_manager/test/test_spawner_unspawner.cpp b/controller_manager/test/test_spawner_unspawner.cpp
index 3b774035aa..a5dd8fcda3 100644
--- a/controller_manager/test/test_spawner_unspawner.cpp
+++ b/controller_manager/test/test_spawner_unspawner.cpp
@@ -23,6 +23,7 @@
#include "controller_manager/controller_manager.hpp"
#include "controller_manager_test_common.hpp"
#include "lifecycle_msgs/msg/state.hpp"
+#include "test_chainable_controller/test_chainable_controller.hpp"
#include "test_controller/test_controller.hpp"
using ::testing::_;
@@ -259,12 +260,63 @@ TEST_F(TestLoadController, spawner_test_type_in_arg)
ASSERT_EQ(ctrl_2.c->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE);
}
+TEST_F(TestLoadController, spawner_test_type_in_params_file)
+{
+ const std::string test_file_path = ament_index_cpp::get_package_prefix("controller_manager") +
+ "/test/test_controller_spawner_with_type.yaml";
+
+ // Provide controller type via the parsed file
+ EXPECT_EQ(
+ call_spawner(
+ "ctrl_with_parameters_and_type chainable_ctrl_with_parameters_and_type --load-only -c "
+ "test_controller_manager -p " +
+ test_file_path),
+ 0);
+
+ ASSERT_EQ(cm_->get_loaded_controllers().size(), 2ul);
+
+ auto ctrl_with_parameters_and_type = cm_->get_loaded_controllers()[0];
+ ASSERT_EQ(ctrl_with_parameters_and_type.info.name, "ctrl_with_parameters_and_type");
+ ASSERT_EQ(ctrl_with_parameters_and_type.info.type, test_controller::TEST_CONTROLLER_CLASS_NAME);
+ ASSERT_EQ(
+ ctrl_with_parameters_and_type.c->get_state().id(),
+ lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED);
+
+ auto chain_ctrl_with_parameters_and_type = cm_->get_loaded_controllers()[1];
+ ASSERT_EQ(
+ chain_ctrl_with_parameters_and_type.info.name, "chainable_ctrl_with_parameters_and_type");
+ ASSERT_EQ(
+ chain_ctrl_with_parameters_and_type.info.type,
+ test_chainable_controller::TEST_CONTROLLER_CLASS_NAME);
+ ASSERT_EQ(
+ chain_ctrl_with_parameters_and_type.c->get_state().id(),
+ lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED);
+
+ EXPECT_EQ(
+ call_spawner(
+ "ctrl_with_parameters_and_no_type -c test_controller_manager -p " + test_file_path),
+ 256);
+ // Will still be same as the current call will fail
+ ASSERT_EQ(cm_->get_loaded_controllers().size(), 2ul);
+
+ auto ctrl_1 = cm_->get_loaded_controllers()[0];
+ ASSERT_EQ(ctrl_1.info.name, "ctrl_with_parameters_and_type");
+ ASSERT_EQ(ctrl_1.info.type, test_controller::TEST_CONTROLLER_CLASS_NAME);
+ ASSERT_EQ(ctrl_1.c->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED);
+
+ auto ctrl_2 = cm_->get_loaded_controllers()[1];
+ ASSERT_EQ(ctrl_2.info.name, "chainable_ctrl_with_parameters_and_type");
+ ASSERT_EQ(ctrl_2.info.type, test_chainable_controller::TEST_CONTROLLER_CLASS_NAME);
+ ASSERT_EQ(ctrl_2.c->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED);
+}
+
TEST_F(TestLoadController, unload_on_kill)
{
// Launch spawner with unload on kill
// timeout command will kill it after the specified time with signal SIGINT
std::stringstream ss;
- ss << "timeout --signal=INT 5 " << "ros2 run controller_manager spawner "
+ ss << "timeout --signal=INT 5 "
+ << "ros2 run controller_manager spawner "
<< "ctrl_3 -c test_controller_manager -t "
<< std::string(test_controller::TEST_CONTROLLER_CLASS_NAME) << " --unload-on-kill";
@@ -273,3 +325,58 @@ TEST_F(TestLoadController, unload_on_kill)
ASSERT_EQ(cm_->get_loaded_controllers().size(), 0ul);
}
+
+TEST_F(TestLoadController, spawner_test_fallback_controllers)
+{
+ const std::string test_file_path = ament_index_cpp::get_package_prefix("controller_manager") +
+ "/test/test_controller_spawner_with_fallback_controllers.yaml";
+
+ cm_->set_parameter(rclcpp::Parameter("ctrl_1.type", test_controller::TEST_CONTROLLER_CLASS_NAME));
+ cm_->set_parameter(rclcpp::Parameter("ctrl_2.type", test_controller::TEST_CONTROLLER_CLASS_NAME));
+ cm_->set_parameter(rclcpp::Parameter("ctrl_3.type", test_controller::TEST_CONTROLLER_CLASS_NAME));
+
+ ControllerManagerRunner cm_runner(this);
+ EXPECT_EQ(
+ call_spawner(
+ "ctrl_1 -c test_controller_manager --load-only --fallback_controllers ctrl_3 ctrl_4 ctrl_5 "
+ "-p " +
+ test_file_path),
+ 0);
+
+ ASSERT_EQ(cm_->get_loaded_controllers().size(), 1ul);
+ {
+ auto ctrl_1 = cm_->get_loaded_controllers()[0];
+ ASSERT_EQ(ctrl_1.info.name, "ctrl_1");
+ ASSERT_EQ(ctrl_1.info.type, test_controller::TEST_CONTROLLER_CLASS_NAME);
+ ASSERT_THAT(
+ ctrl_1.info.fallback_controllers_names, testing::ElementsAre("ctrl_3", "ctrl_4", "ctrl_5"));
+ ASSERT_EQ(ctrl_1.c->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED);
+ }
+
+ // Try to spawn now the controller with fallback controllers inside the yaml
+ EXPECT_EQ(
+ call_spawner("ctrl_2 ctrl_3 -c test_controller_manager --load-only -p " + test_file_path), 0);
+
+ ASSERT_EQ(cm_->get_loaded_controllers().size(), 3ul);
+ {
+ auto ctrl_1 = cm_->get_loaded_controllers()[0];
+ ASSERT_EQ(ctrl_1.info.name, "ctrl_1");
+ ASSERT_EQ(ctrl_1.info.type, test_controller::TEST_CONTROLLER_CLASS_NAME);
+ ASSERT_THAT(
+ ctrl_1.info.fallback_controllers_names, testing::ElementsAre("ctrl_3", "ctrl_4", "ctrl_5"));
+ ASSERT_EQ(ctrl_1.c->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED);
+
+ auto ctrl_2 = cm_->get_loaded_controllers()[1];
+ ASSERT_EQ(ctrl_2.info.name, "ctrl_2");
+ ASSERT_EQ(ctrl_2.info.type, test_controller::TEST_CONTROLLER_CLASS_NAME);
+ ASSERT_THAT(
+ ctrl_2.info.fallback_controllers_names, testing::ElementsAre("ctrl_6", "ctrl_7", "ctrl_8"));
+ ASSERT_EQ(ctrl_2.c->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED);
+
+ auto ctrl_3 = cm_->get_loaded_controllers()[2];
+ ASSERT_EQ(ctrl_3.info.name, "ctrl_3");
+ ASSERT_EQ(ctrl_3.info.type, test_controller::TEST_CONTROLLER_CLASS_NAME);
+ ASSERT_THAT(ctrl_3.info.fallback_controllers_names, testing::ElementsAre("ctrl_9"));
+ ASSERT_EQ(ctrl_3.c->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED);
+ }
+}
diff --git a/controller_manager_msgs/CHANGELOG.rst b/controller_manager_msgs/CHANGELOG.rst
index 11d3ba2989..6cc2a09a43 100644
--- a/controller_manager_msgs/CHANGELOG.rst
+++ b/controller_manager_msgs/CHANGELOG.rst
@@ -2,6 +2,15 @@
Changelog for package controller_manager_msgs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+4.11.0 (2024-05-14)
+-------------------
+
+4.10.0 (2024-05-08)
+-------------------
+
+4.9.0 (2024-04-30)
+------------------
+
4.8.0 (2024-03-27)
------------------
diff --git a/controller_manager_msgs/package.xml b/controller_manager_msgs/package.xml
index db64f0c28d..ddd840154e 100644
--- a/controller_manager_msgs/package.xml
+++ b/controller_manager_msgs/package.xml
@@ -2,7 +2,7 @@
controller_manager_msgs
- 4.8.0
+ 4.11.0Messages and services for the controller manager.Bence MagyarDenis Štogl
diff --git a/doc/migration/Foxy.rst b/doc/migration/Galactic.rst
similarity index 94%
rename from doc/migration/Foxy.rst
rename to doc/migration/Galactic.rst
index a1afeddc70..4c6e958951 100644
--- a/doc/migration/Foxy.rst
+++ b/doc/migration/Galactic.rst
@@ -1,3 +1,5 @@
+:github_url: https://github.com/ros-controls/ros2_control/blob/{REPOS_FILE_BRANCH}/doc/release_notes/Galactic.rst
+
Foxy to Galactic
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/doc/migration/Iron.rst b/doc/migration/Iron.rst
deleted file mode 100644
index 21e28fc43f..0000000000
--- a/doc/migration/Iron.rst
+++ /dev/null
@@ -1,50 +0,0 @@
-Iron to Jazzy
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-component parser
-*****************
-Changes from `(PR #1256) `__
-
-* All ``joints`` defined in the ````-tag have to be present in the URDF received :ref:`by the controller manager `, otherwise a ``std::runtime_error`` is thrown. This is to ensure that the URDF and the ````-tag are consistent. E.g., for configuration ports use ``gpio`` tags instead.
-* The syntax for mimic joints is changed to the `official URDF specification `__. The parameters within the ``ros2_control`` tag are not supported any more. Instead of
-
- .. code-block:: xml
-
-
-
-
-
- 0.15
-
-
-
-
-
- right_finger_joint
- 1
-
-
-
-
-
-
-
- define your mimic joints directly in the joint definitions:
-
- .. code-block:: xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/migration/Jazzy.rst b/doc/migration/Jazzy.rst
new file mode 100644
index 0000000000..1880f5d380
--- /dev/null
+++ b/doc/migration/Jazzy.rst
@@ -0,0 +1,70 @@
+:github_url: https://github.com/ros-controls/ros2_control/blob/{REPOS_FILE_BRANCH}/doc/migration/Jazzy.rst
+
+Iron to Jazzy
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+controller_manager
+******************
+* Rename ``class_type`` to ``plugin_name`` (`#780 `_)
+* CM now subscribes to ``robot_description`` topic instead of ``~/robot_description`` (`#1410 `_). As a consequence, when using multiple controller managers, you have to remap the topic within the launch file, an example for a python launch file:
+
+ .. code-block:: python
+
+ remappings=[
+ ('/robot_description', '/custom_1/robot_description'),
+ ]
+
+* Changes from `(PR #1256) `__
+
+ * All ``joints`` defined in the ````-tag have to be present in the URDF received :ref:`by the controller manager `, otherwise the following error is shown:
+
+ The published robot description file (URDF) seems not to be genuine. The following error was caught: not found in URDF.
+
+ This is to ensure that the URDF and the ````-tag are consistent. E.g., for configuration ports use ``gpio`` interface types instead.
+
+ * The syntax for mimic joints is changed to the `official URDF specification `__. The parameters within the ``ros2_control`` tag are not supported any more. Instead of
+
+ .. code-block:: xml
+
+
+
+
+
+ 0.15
+
+
+
+
+
+ right_finger_joint
+ 1
+
+
+
+
+
+
+
+ define your mimic joints directly in the joint definitions:
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+hardware_interface
+******************
+* ``test_components`` was moved to its own package. Update the dependencies if you are using them. (`#1325 `_)
diff --git a/doc/release_notes/Jazzy.rst b/doc/release_notes/Jazzy.rst
new file mode 100644
index 0000000000..4074cbca63
--- /dev/null
+++ b/doc/release_notes/Jazzy.rst
@@ -0,0 +1,109 @@
+:github_url: https://github.com/ros-controls/ros2_control/blob/{REPOS_FILE_BRANCH}/doc/release_notes/Jazzy.rst
+
+Iron to Jazzy
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+General
+*******
+* A ``version.h`` file will be generated per package using the ament_generate_version_header (`#1449 `_). For example, you can include the version of the package in the logs.
+
+ .. code-block:: cpp
+
+ #include
+ ...
+ RCLCPP_INFO(get_logger(), "controller_manager version: %s", CONTROLLER_MANAGER_VERSION_STR);
+
+controller_interface
+********************
+For details see the controller_manager section.
+
+* Pass URDF to controllers on init (`#1088 `_).
+* Pass controller manager update rate on the init of the controller interface (`#1141 `_)
+* A method to get node options to setup the controller node #api-breaking (`#1169 `_)
+
+controller_manager
+******************
+* URDF is now passed to controllers on init (`#1088 `_)
+ This should help avoiding extra legwork in controllers to get access to the ``/robot_description``.
+* Pass controller manager update rate on the init of the controller interface (`#1141 `_)
+* Report inactive controllers as a diagnostics ok instead of an error (`#1184 `_)
+* Set chained controller interfaces 'available' for activated controllers (`#1098 `_)
+
+ * Configured chainable controller: Listed exported interfaces are unavailable and unclaimed
+ * Active chainable controller (not in chained mode): Listed exported interfaces are available but unclaimed
+ * Active chainable controller (in chained mode): Listed exported interfaces are available and claimed
+* Try using SCHED_FIFO on any kernel (`#1142 `_)
+* A method to get node options to setup the controller node was added (`#1169 `_): ``get_node_options`` can be overridden by controllers, this would make it easy for other controllers to be able to setup their own custom node options
+* CM now subscribes to ``robot_description`` topic instead of ``~/robot_description`` (`#1410 `_).
+* Change the controller sorting with an approach similar to directed acyclic graphs (`#1384 `_)
+* Changes from `(PR #1256) `__
+
+ * All ``joints`` defined in the ````-tag have to be present in the URDF received :ref:`by the controller manager `, otherwise the following error is shown:
+
+ The published robot description file (URDF) seems not to be genuine. The following error was caught: not found in URDF.
+
+ This is to ensure that the URDF and the ````-tag are consistent. E.g., for configuration ports use ``gpio`` interface types instead.
+
+ * The syntax for mimic joints is changed to the `official URDF specification `__.
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The parameters within the ``ros2_control`` tag are not supported any more.
+
+hardware_interface
+******************
+* A portable version for string-to-double conversion was added: ``hardware_interface::stod`` (`#1257 `_)
+* ``test_components`` was moved to its own package (`#1325 `_)
+* The ``ros2_control`` tag now supports parsing of the limits from the URDF into the ``HardwareInfo`` structure. More conservative limits can be defined using the ``min`` and ``max`` attributes per interface (`#1472 `_)
+
+ .. code:: xml
+
+
+
+ ros2_control_demo_hardware/RRBotSystemPositionOnlyHardware
+ 2.0
+ 3.0
+ 2.0
+
+
+
+ -1
+ 1
+
+
+
+
+
+
+
+
+* Soft limits are also parsed from the URDF into the ``HardwareInfo`` structure for the defined joints (`#1488 `_)
+
+joint_limits
+************
+* Add header to import limits from standard URDF definition (`#1298 `_)
+
+ros2controlcli
+**************
+* Spawner colours were added to ``list_controllers`` depending upon active or inactive (`#1409 `_)
+* The ``set_hardware_component_state`` verb was added (`#1248 `_). Use the following command to set the state of a hardware component
+
+ .. code-block:: bash
+
+ ros2 control set_hardware_component_state
diff --git a/hardware_interface/CHANGELOG.rst b/hardware_interface/CHANGELOG.rst
index 1cb22a501a..77185ecf28 100644
--- a/hardware_interface/CHANGELOG.rst
+++ b/hardware_interface/CHANGELOG.rst
@@ -2,6 +2,27 @@
Changelog for package hardware_interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+4.11.0 (2024-05-14)
+-------------------
+* Add find_package for ament_cmake_gen_version_h (`#1534 `_)
+* Parse URDF soft_limits into the HardwareInfo structure (`#1488 `_)
+* Contributors: Christoph Fröhlich, adriaroig
+
+4.10.0 (2024-05-08)
+-------------------
+* Add hardware components exception handling in resource manager (`#1508 `_)
+* Working async controllers and components [not synchronized] (`#1041 `_)
+* Parse URDF joint hard limits into the HardwareInfo structure (`#1472 `_)
+* Add fallback controllers list to the ControllerInfo (`#1503 `_)
+* Add more common hardware interface type constants (`#1500 `_)
+* Contributors: Márk Szitanics, Sai Kishor Kothakota
+
+4.9.0 (2024-04-30)
+------------------
+* Add missing calculate_dynamics (`#1498 `_)
+* Component parser: Get mimic information from URDF (`#1256 `_)
+* Contributors: Christoph Fröhlich
+
4.8.0 (2024-03-27)
------------------
* generate version.h file per package using the ament_generate_version_header (`#1449 `_)
diff --git a/hardware_interface/CMakeLists.txt b/hardware_interface/CMakeLists.txt
index 4ea7fdc2f7..c557a4970f 100644
--- a/hardware_interface/CMakeLists.txt
+++ b/hardware_interface/CMakeLists.txt
@@ -15,10 +15,12 @@ set(THIS_PACKAGE_INCLUDE_DEPENDS
rcutils
TinyXML2
tinyxml2_vendor
+ joint_limits
urdf
)
find_package(ament_cmake REQUIRED)
+find_package(ament_cmake_gen_version_h REQUIRED)
foreach(Dependency IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS})
find_package(${Dependency} REQUIRED)
endforeach()
diff --git a/hardware_interface/doc/hardware_interface_types_userdoc.rst b/hardware_interface/doc/hardware_interface_types_userdoc.rst
index ecf852cb94..d8338bf7a6 100644
--- a/hardware_interface/doc/hardware_interface_types_userdoc.rst
+++ b/hardware_interface/doc/hardware_interface_types_userdoc.rst
@@ -41,6 +41,12 @@ The ```` tag can be used as a child of all three types of hardware compone
Because ports implemented as ````-tag are typically very application-specific, there exists no generic publisher
within the ros2_control framework. A custom gpio-controller has to be implemented for each application. As an example, see :ref:`the GPIO controller example ` as part of the demo repository.
+Hardware Groups
+*****************************
+Hardware Component Groups serve as a critical organizational mechanism within complex systems, facilitating error handling and fault tolerance. By grouping related hardware components together, such as actuators within a manipulator, users can establish a unified framework for error detection and response.
+
+Hardware Component Groups play a vital role in propagating errors across interconnected hardware components. For instance, in a manipulator system, grouping actuators together allows for error propagation. If one actuator fails within the group, the error can propagate to the other actuators, signaling a potential issue across the system. By default, the actuator errors are isolated to their own hardware component, allowing the rest to continue operation unaffected. In the provided ros2_control configuration, the ```` tag within each ```` block signifies the grouping of hardware components, enabling error propagation mechanisms within the system.
+
Examples
*****************************
The following examples show how to use the different hardware interface types in a ``ros2_control`` URDF.
@@ -152,3 +158,66 @@ They can be combined together within the different hardware component types (sys
+
+4. Robot with multiple hardware components belonging to same group : ``Group1``
+
+ - RRBot System 1 and 2
+ - Digital: Total 4 inputs and 2 outputs
+ - Analog: Total 2 inputs and 1 output
+ - Vacuum valve at the flange (on/off)
+ - Group: Group1
+
+ .. code:: xml
+
+
+
+ ros2_control_demo_hardware/RRBotSystemPositionOnlyHardware
+ Group1
+ 2.0
+ 3.0
+ 2.0
+
+
+
+ -1
+ 1
+
+
+
+
+
+
+ 3.1
+
+
+
+
+
+
+
+
+
+
+
+ ros2_control_demo_hardware/RRBotSystemPositionOnlyHardware
+ Group1
+ 2.0
+ 3.0
+ 2.0
+
+
+
+ -1
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hardware_interface/doc/mock_components_userdoc.rst b/hardware_interface/doc/mock_components_userdoc.rst
index 782d3e01ea..8eca65c52b 100644
--- a/hardware_interface/doc/mock_components_userdoc.rst
+++ b/hardware_interface/doc/mock_components_userdoc.rst
@@ -28,27 +28,74 @@ Features:
Parameters
,,,,,,,,,,
+A full example including all optional parameters (with default values):
+
+.. code-block:: xml
+
+
+
+ mock_components/GenericSystem
+ false
+
+ false
+ false
+ false
+ 0.0
+
+
+
+
+
+ 3.45
+
+
+
+
+
+
+
+
+ 2.78
+
+
+
+
+
+
+
+
+
+
+
+See :ref:`example_2 ` for an example using ``calculate_dynamics`` or :ref:`example_10 ` for using in combination with GPIO interfaces.
+
+Component Parameters
+####################
+
+calculate_dynamics (optional; boolean; default: false)
+ Calculation of states from commands by using Euler-forward integration or finite differences.
+
+custom_interface_with_following_offset (optional; string; default: "")
+ Mapping of offsetted commands to a custom interface.
+
disable_commands (optional; boolean; default: false)
Disables mirroring commands to states.
This option is helpful to simulate an erroneous connection to the hardware when nothing breaks, but suddenly there is no feedback from a hardware interface.
Or it can help you to test your setup when the hardware is running without feedback, i.e., in open loop configuration.
-mock_sensor_commands (optional; boolean; default: false)
- Creates fake command interfaces for faking sensor measurements with an external command.
- Those interfaces are usually used by a :ref:`forward controller ` to provide access from ROS-world.
-
mock_gpio_commands (optional; boolean; default: false)
Creates fake command interfaces for faking GPIO states with an external command.
Those interfaces are usually used by a :ref:`forward controller ` to provide access from ROS-world.
-position_state_following_offset (optional; double; default: 0.0)
- Following offset added to the commanded values when mirrored to states.
+mock_sensor_commands (optional; boolean; default: false)
+ Creates fake command interfaces for faking sensor measurements with an external command.
+ Those interfaces are usually used by a :ref:`forward controller ` to provide access from ROS-world.
-custom_interface_with_following_offset (optional; string; default: "")
- Mapping of offsetted commands to a custom interface.
+position_state_following_offset (optional; double; default: 0.0)
+ Following offset added to the commanded values when mirrored to states. Only applied, if ``custom_interface_with_following_offset`` is false.
-Per-interface Parameters
-,,,,,,,,,,,,,,,,,,,,,,,,
+Per-Interface Parameters
+########################
initial_value (optional; double)
Initial value of certain state interface directly after startup. Example:
diff --git a/hardware_interface/include/hardware_interface/actuator.hpp b/hardware_interface/include/hardware_interface/actuator.hpp
index 4082863370..b23b913d75 100644
--- a/hardware_interface/include/hardware_interface/actuator.hpp
+++ b/hardware_interface/include/hardware_interface/actuator.hpp
@@ -83,6 +83,9 @@ class Actuator final
HARDWARE_INTERFACE_PUBLIC
std::string get_name() const;
+ HARDWARE_INTERFACE_PUBLIC
+ std::string get_group_name() const;
+
HARDWARE_INTERFACE_PUBLIC
const rclcpp_lifecycle::State & get_state() const;
diff --git a/hardware_interface/include/hardware_interface/actuator_interface.hpp b/hardware_interface/include/hardware_interface/actuator_interface.hpp
index abfd8eb45a..1ae05aa5f7 100644
--- a/hardware_interface/include/hardware_interface/actuator_interface.hpp
+++ b/hardware_interface/include/hardware_interface/actuator_interface.hpp
@@ -131,7 +131,7 @@ class ActuatorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNod
* \note All starting and stopping interface keys are passed to all components, so the function
* should return return_type::OK by default when given interface keys not relevant for this
* component. \param[in] start_interfaces vector of string identifiers for the command interfaces
- * starting. \param[in] stop_interfaces vector of string identifiers for the command interfacs
+ * starting. \param[in] stop_interfaces vector of string identifiers for the command interfaces
* stopping. \return return_type::OK if the new command interface combination can be prepared, or
* if the interface key is not relevant to this system. Returns return_type::ERROR otherwise.
*/
@@ -150,7 +150,7 @@ class ActuatorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNod
* \note All starting and stopping interface keys are passed to all components, so the function
* should return return_type::OK by default when given interface keys not relevant for this
* component. \param[in] start_interfaces vector of string identifiers for the command interfaces
- * starting. \param[in] stop_interfaces vector of string identifiers for the command interfacs
+ * starting. \param[in] stop_interfaces vector of string identifiers for the command interfaces
* stopping. \return return_type::OK if the new command interface combination can be switched to,
* or if the interface key is not relevant to this system. Returns return_type::ERROR otherwise.
*/
@@ -190,6 +190,12 @@ class ActuatorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNod
*/
virtual std::string get_name() const { return info_.name; }
+ /// Get name of the actuator hardware group to which it belongs to.
+ /**
+ * \return group name.
+ */
+ virtual std::string get_group_name() const { return info_.group; }
+
/// Get life-cycle state of the actuator hardware.
/**
* \return state.
diff --git a/hardware_interface/include/hardware_interface/async_components.hpp b/hardware_interface/include/hardware_interface/async_components.hpp
index 2fa6fd7f85..73495c92f8 100644
--- a/hardware_interface/include/hardware_interface/async_components.hpp
+++ b/hardware_interface/include/hardware_interface/async_components.hpp
@@ -17,6 +17,7 @@
#include
#include
+#include
#include
#include "hardware_interface/actuator.hpp"
@@ -34,29 +35,23 @@ class AsyncComponentThread
{
public:
explicit AsyncComponentThread(
- Actuator * component, unsigned int update_rate,
+ unsigned int update_rate,
rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface)
- : hardware_component_(component), cm_update_rate_(update_rate), clock_interface_(clock_interface)
+ : cm_update_rate_(update_rate), clock_interface_(clock_interface)
{
}
- explicit AsyncComponentThread(
- System * component, unsigned int update_rate,
- rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface)
- : hardware_component_(component), cm_update_rate_(update_rate), clock_interface_(clock_interface)
- {
- }
-
- explicit AsyncComponentThread(
- Sensor * component, unsigned int update_rate,
- rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface)
- : hardware_component_(component), cm_update_rate_(update_rate), clock_interface_(clock_interface)
+ // Fills the internal variant with the desired component.
+ template
+ void register_component(T * component)
{
+ hardware_component_ = component;
}
AsyncComponentThread(const AsyncComponentThread & t) = delete;
- AsyncComponentThread(AsyncComponentThread && t) = default;
+ AsyncComponentThread(AsyncComponentThread && t) = delete;
+ // Destructor, called when the component is erased from its map.
~AsyncComponentThread()
{
terminated_.store(true, std::memory_order_seq_cst);
@@ -65,9 +60,19 @@ class AsyncComponentThread
write_and_read_.join();
}
}
-
+ /// Creates the component's thread.
+ /**
+ * Called when the component is activated.
+ *
+ */
void activate() { write_and_read_ = std::thread(&AsyncComponentThread::write_and_read, this); }
+ /// Periodically execute the component's write and read methods.
+ /**
+ * Callback of the async component's thread.
+ * **Not synchronized with the controller manager's update currently**
+ *
+ */
void write_and_read()
{
using TimePoint = std::chrono::system_clock::time_point;
@@ -88,8 +93,12 @@ class AsyncComponentThread
auto measured_period = current_time - previous_time;
previous_time = current_time;
- // write
- // read
+ if (!first_iteration)
+ {
+ component->write(clock_interface_->get_clock()->now(), measured_period);
+ }
+ component->read(clock_interface_->get_clock()->now(), measured_period);
+ first_iteration = false;
}
next_iteration_time += period;
std::this_thread::sleep_until(next_iteration_time);
@@ -104,6 +113,7 @@ class AsyncComponentThread
std::thread write_and_read_{};
unsigned int cm_update_rate_;
+ bool first_iteration = true;
rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface_;
};
diff --git a/hardware_interface/include/hardware_interface/controller_info.hpp b/hardware_interface/include/hardware_interface/controller_info.hpp
index 61a51a134a..a38fb99cb3 100644
--- a/hardware_interface/include/hardware_interface/controller_info.hpp
+++ b/hardware_interface/include/hardware_interface/controller_info.hpp
@@ -38,6 +38,9 @@ struct ControllerInfo
/// List of claimed interfaces by the controller.
std::vector claimed_interfaces;
+
+ /// List of fallback controller names to be activated if this controller fails.
+ std::vector fallback_controllers_names;
};
} // namespace hardware_interface
diff --git a/hardware_interface/include/hardware_interface/hardware_component_info.hpp b/hardware_interface/include/hardware_interface/hardware_component_info.hpp
index 45afebdb34..e7d47bcaa4 100644
--- a/hardware_interface/include/hardware_interface/hardware_component_info.hpp
+++ b/hardware_interface/include/hardware_interface/hardware_component_info.hpp
@@ -39,6 +39,9 @@ struct HardwareComponentInfo
/// Component "classification": "actuator", "sensor" or "system"
std::string type;
+ /// Component group
+ std::string group;
+
/// Component pluginlib plugin name.
std::string plugin_name;
diff --git a/hardware_interface/include/hardware_interface/hardware_info.hpp b/hardware_interface/include/hardware_interface/hardware_info.hpp
index 97abc00438..2bd2099e69 100644
--- a/hardware_interface/include/hardware_interface/hardware_info.hpp
+++ b/hardware_interface/include/hardware_interface/hardware_info.hpp
@@ -19,6 +19,8 @@
#include
#include
+#include "joint_limits/joint_limits.hpp"
+
namespace hardware_interface
{
/**
@@ -42,6 +44,8 @@ struct InterfaceInfo
std::string data_type;
/// (Optional) If the handle is an array, the size of the array. Used by GPIOs.
int size;
+ /// (Optional) enable or disable the limits for the command interfaces
+ bool enable_limits;
};
/// @brief This structure stores information about a joint that is mimicking another joint
@@ -129,6 +133,8 @@ struct HardwareInfo
std::string name;
/// Type of the hardware: actuator, sensor or system.
std::string type;
+ /// Hardware group to which the hardware belongs.
+ std::string group;
/// Component is async
bool is_async;
/// Name of the pluginlib plugin of the hardware that will be loaded.
@@ -163,6 +169,17 @@ struct HardwareInfo
* The XML contents prior to parsing
*/
std::string original_xml;
+ /**
+ * The URDF parsed limits of the hardware components joint command interfaces
+ */
+ std::unordered_map limits;
+
+ /**
+ * Map of software joint limits used for clamping the command where the key is the joint name.
+ * Optional If not specified or less restrictive than the JointLimits uses the previous
+ * JointLimits.
+ */
+ std::unordered_map soft_limits;
};
} // namespace hardware_interface
diff --git a/hardware_interface/include/hardware_interface/resource_manager.hpp b/hardware_interface/include/hardware_interface/resource_manager.hpp
index 743e548a4c..31def4598b 100644
--- a/hardware_interface/include/hardware_interface/resource_manager.hpp
+++ b/hardware_interface/include/hardware_interface/resource_manager.hpp
@@ -353,7 +353,7 @@ class HARDWARE_INTERFACE_PUBLIC ResourceManager
* \note it is assumed that `prepare_command_mode_switch` is called just before this method
* with the same input arguments.
* \param[in] start_interfaces vector of string identifiers for the command interfaces starting.
- * \param[in] stop_interfaces vector of string identifiers for the command interfacs stopping.
+ * \param[in] stop_interfaces vector of string identifiers for the command interfaces stopping.
* \return true if switch is performed, false if a component rejects switching.
*/
bool perform_command_mode_switch(
@@ -376,6 +376,12 @@ class HARDWARE_INTERFACE_PUBLIC ResourceManager
return_type set_component_state(
const std::string & component_name, rclcpp_lifecycle::State & target_state);
+ /// Deletes all async components from the resource storage
+ /**
+ * Needed to join the threads immediately after the control loop is ended.
+ */
+ void shutdown_async_components();
+
/// Reads all loaded hardware components.
/**
* Reads from all active hardware components.
diff --git a/hardware_interface/include/hardware_interface/sensor.hpp b/hardware_interface/include/hardware_interface/sensor.hpp
index 5d0677c587..4c267bef77 100644
--- a/hardware_interface/include/hardware_interface/sensor.hpp
+++ b/hardware_interface/include/hardware_interface/sensor.hpp
@@ -71,6 +71,9 @@ class Sensor final
HARDWARE_INTERFACE_PUBLIC
std::string get_name() const;
+ HARDWARE_INTERFACE_PUBLIC
+ std::string get_group_name() const;
+
HARDWARE_INTERFACE_PUBLIC
const rclcpp_lifecycle::State & get_state() const;
diff --git a/hardware_interface/include/hardware_interface/sensor_interface.hpp b/hardware_interface/include/hardware_interface/sensor_interface.hpp
index 14a59e4588..5a3601afa8 100644
--- a/hardware_interface/include/hardware_interface/sensor_interface.hpp
+++ b/hardware_interface/include/hardware_interface/sensor_interface.hpp
@@ -129,6 +129,12 @@ class SensorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI
*/
virtual std::string get_name() const { return info_.name; }
+ /// Get name of the actuator hardware group to which it belongs to.
+ /**
+ * \return group name.
+ */
+ virtual std::string get_group_name() const { return info_.group; }
+
/// Get life-cycle state of the actuator hardware.
/**
* \return state.
diff --git a/hardware_interface/include/hardware_interface/system.hpp b/hardware_interface/include/hardware_interface/system.hpp
index ece14f814d..fb28929948 100644
--- a/hardware_interface/include/hardware_interface/system.hpp
+++ b/hardware_interface/include/hardware_interface/system.hpp
@@ -84,6 +84,9 @@ class System final
HARDWARE_INTERFACE_PUBLIC
std::string get_name() const;
+ HARDWARE_INTERFACE_PUBLIC
+ std::string get_group_name() const;
+
HARDWARE_INTERFACE_PUBLIC
const rclcpp_lifecycle::State & get_state() const;
diff --git a/hardware_interface/include/hardware_interface/system_interface.hpp b/hardware_interface/include/hardware_interface/system_interface.hpp
index e5c6f2f542..6b2c38b915 100644
--- a/hardware_interface/include/hardware_interface/system_interface.hpp
+++ b/hardware_interface/include/hardware_interface/system_interface.hpp
@@ -132,7 +132,7 @@ class SystemInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI
* \note All starting and stopping interface keys are passed to all components, so the function
* should return return_type::OK by default when given interface keys not relevant for this
* component. \param[in] start_interfaces vector of string identifiers for the command interfaces
- * starting. \param[in] stop_interfaces vector of string identifiers for the command interfacs
+ * starting. \param[in] stop_interfaces vector of string identifiers for the command interfaces
* stopping. \return return_type::OK if the new command interface combination can be prepared, or
* if the interface key is not relevant to this system. Returns return_type::ERROR otherwise.
*/
@@ -151,7 +151,7 @@ class SystemInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI
* \note All starting and stopping interface keys are passed to all components, so the function
* should return return_type::OK by default when given interface keys not relevant for this
* component. \param[in] start_interfaces vector of string identifiers for the command interfaces
- * starting. \param[in] stop_interfaces vector of string identifiers for the command interfacs
+ * starting. \param[in] stop_interfaces vector of string identifiers for the command interfaces
* stopping. \return return_type::OK if the new command interface combination can be switched to,
* or if the interface key is not relevant to this system. Returns return_type::ERROR otherwise.
*/
@@ -191,6 +191,12 @@ class SystemInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI
*/
virtual std::string get_name() const { return info_.name; }
+ /// Get name of the actuator hardware group to which it belongs to.
+ /**
+ * \return group name.
+ */
+ virtual std::string get_group_name() const { return info_.group; }
+
/// Get life-cycle state of the actuator hardware.
/**
* \return state.
diff --git a/hardware_interface/include/hardware_interface/types/hardware_interface_type_values.hpp b/hardware_interface/include/hardware_interface/types/hardware_interface_type_values.hpp
index 27b5207a0f..f6f6e052e3 100644
--- a/hardware_interface/include/hardware_interface/types/hardware_interface_type_values.hpp
+++ b/hardware_interface/include/hardware_interface/types/hardware_interface_type_values.hpp
@@ -25,6 +25,28 @@ constexpr char HW_IF_VELOCITY[] = "velocity";
constexpr char HW_IF_ACCELERATION[] = "acceleration";
/// Constant defining effort interface name
constexpr char HW_IF_EFFORT[] = "effort";
+/// Constant defining torque interface name
+constexpr char HW_IF_TORQUE[] = "torque";
+/// Constant defining force interface name
+constexpr char HW_IF_FORCE[] = "force";
+/// Constant defining current interface name
+constexpr char HW_IF_CURRENT[] = "current";
+/// Constant defining temperature interface name
+constexpr char HW_IF_TEMPERATURE[] = "temperature";
+
+/// Gains interface constants
+/// Constant defining proportional gain interface name
+constexpr char HW_IF_PROPORTIONAL_GAIN[] = "proportional";
+/// Constant defining integral gain interface name
+constexpr char HW_IF_INTEGRAL_GAIN[] = "integral";
+/// Constant defining derivative gain interface name
+constexpr char HW_IF_DERIVATIVE_GAIN[] = "derivative";
+/// Constant defining integral clamp interface name
+constexpr char HW_IF_INTEGRAL_CLAMP_MAX[] = "integral_clamp_max";
+/// Constant defining integral clamp interface name
+constexpr char HW_IF_INTEGRAL_CLAMP_MIN[] = "integral_clamp_min";
+/// Constant defining the feedforward interface name
+constexpr char HW_IF_FEEDFORWARD[] = "feedforward";
} // namespace hardware_interface
#endif // HARDWARE_INTERFACE__TYPES__HARDWARE_INTERFACE_TYPE_VALUES_HPP_
diff --git a/hardware_interface/package.xml b/hardware_interface/package.xml
index 41daac50cd..f3aaad3306 100644
--- a/hardware_interface/package.xml
+++ b/hardware_interface/package.xml
@@ -1,7 +1,7 @@
hardware_interface
- 4.8.0
+ 4.11.0ros2_control hardware interfaceBence MagyarDenis Štogl
@@ -16,6 +16,7 @@
rclcpp_lifecyclercpputilstinyxml2_vendor
+ joint_limitsurdfrcutils
diff --git a/hardware_interface/src/actuator.cpp b/hardware_interface/src/actuator.cpp
index 6b58e365dc..b80f76ebf5 100644
--- a/hardware_interface/src/actuator.cpp
+++ b/hardware_interface/src/actuator.cpp
@@ -214,6 +214,8 @@ return_type Actuator::perform_command_mode_switch(
std::string Actuator::get_name() const { return impl_->get_name(); }
+std::string Actuator::get_group_name() const { return impl_->get_group_name(); }
+
const rclcpp_lifecycle::State & Actuator::get_state() const { return impl_->get_state(); }
return_type Actuator::read(const rclcpp::Time & time, const rclcpp::Duration & period)
diff --git a/hardware_interface/src/component_parser.cpp b/hardware_interface/src/component_parser.cpp
index 0841265e81..ef585c971b 100644
--- a/hardware_interface/src/component_parser.cpp
+++ b/hardware_interface/src/component_parser.cpp
@@ -26,6 +26,8 @@
#include "hardware_interface/component_parser.hpp"
#include "hardware_interface/hardware_info.hpp"
#include "hardware_interface/lexical_casts.hpp"
+#include "hardware_interface/types/hardware_interface_type_values.hpp"
+#include "joint_limits/joint_limits_urdf.hpp"
namespace
{
@@ -34,6 +36,7 @@ constexpr const auto kROS2ControlTag = "ros2_control";
constexpr const auto kHardwareTag = "hardware";
constexpr const auto kPluginNameTag = "plugin";
constexpr const auto kParamTag = "param";
+constexpr const auto kGroupTag = "group";
constexpr const auto kActuatorTag = "actuator";
constexpr const auto kJointTag = "joint";
constexpr const auto kSensorTag = "sensor";
@@ -43,6 +46,8 @@ constexpr const auto kCommandInterfaceTag = "command_interface";
constexpr const auto kStateInterfaceTag = "state_interface";
constexpr const auto kMinTag = "min";
constexpr const auto kMaxTag = "max";
+constexpr const auto kLimitsTag = "limits";
+constexpr const auto kEnableAttribute = "enable";
constexpr const auto kInitialValueTag = "initial_value";
constexpr const auto kMimicAttribute = "mimic";
constexpr const auto kDataTypeAttribute = "data_type";
@@ -289,6 +294,15 @@ hardware_interface::InterfaceInfo parse_interfaces_from_xml(
interface.max = interface_param->second;
}
+ // Option enable or disable the interface limits, by default they are enabled
+ interface.enable_limits = true;
+ const auto * limits_it = interfaces_it->FirstChildElement(kLimitsTag);
+ if (limits_it)
+ {
+ interface.enable_limits =
+ parse_bool(get_attribute_value(limits_it, kEnableAttribute, limits_it->Name()));
+ }
+
// Optional initial_value attribute
interface_param = interface_params.find(kInitialValueTag);
if (interface_param != interface_params.end())
@@ -333,11 +347,21 @@ ComponentInfo parse_component_from_xml(const tinyxml2::XMLElement * component_it
}
}
+ // Option enable or disable the interface limits, by default they are enabled
+ bool enable_limits = true;
+ const auto * limits_it = component_it->FirstChildElement(kLimitsTag);
+ if (limits_it)
+ {
+ enable_limits = parse_bool(get_attribute_value(limits_it, kEnableAttribute, limits_it->Name()));
+ }
+
// Parse all command interfaces
const auto * command_interfaces_it = component_it->FirstChildElement(kCommandInterfaceTag);
while (command_interfaces_it)
{
- component.command_interfaces.push_back(parse_interfaces_from_xml(command_interfaces_it));
+ InterfaceInfo cmd_info = parse_interfaces_from_xml(command_interfaces_it);
+ cmd_info.enable_limits &= enable_limits;
+ component.command_interfaces.push_back(cmd_info);
command_interfaces_it = command_interfaces_it->NextSiblingElement(kCommandInterfaceTag);
}
@@ -345,7 +369,9 @@ ComponentInfo parse_component_from_xml(const tinyxml2::XMLElement * component_it
const auto * state_interfaces_it = component_it->FirstChildElement(kStateInterfaceTag);
while (state_interfaces_it)
{
- component.state_interfaces.push_back(parse_interfaces_from_xml(state_interfaces_it));
+ InterfaceInfo state_info = parse_interfaces_from_xml(state_interfaces_it);
+ state_info.enable_limits &= enable_limits;
+ component.state_interfaces.push_back(state_info);
state_interfaces_it = state_interfaces_it->NextSiblingElement(kStateInterfaceTag);
}
@@ -553,6 +579,11 @@ HardwareInfo parse_resource_from_xml(
const auto * type_it = ros2_control_child_it->FirstChildElement(kPluginNameTag);
hardware.hardware_plugin_name =
get_text_for_element(type_it, std::string("hardware ") + kPluginNameTag);
+ const auto * group_it = ros2_control_child_it->FirstChildElement(kGroupTag);
+ if (group_it)
+ {
+ hardware.group = get_text_for_element(group_it, std::string("hardware.") + kGroupTag);
+ }
const auto * params_it = ros2_control_child_it->FirstChildElement(kParamTag);
if (params_it)
{
@@ -589,6 +620,171 @@ HardwareInfo parse_resource_from_xml(
return hardware;
}
+/**
+ * @brief Retrieve the min and max values from the interface tag.
+ * @param itf The interface tag to retrieve the values from.
+ * @param min The minimum value to be retrieved.
+ * @param max The maximum value to be retrieved.
+ * @return true if the values are retrieved, false otherwise.
+ */
+bool retrieve_min_max_interface_values(const InterfaceInfo & itf, double & min, double & max)
+{
+ try
+ {
+ if (itf.min.empty() && itf.max.empty())
+ {
+ // If the limits don't exist then return false as they are not retrieved
+ return false;
+ }
+ if (!itf.min.empty())
+ {
+ min = hardware_interface::stod(itf.min);
+ }
+ if (!itf.max.empty())
+ {
+ max = hardware_interface::stod(itf.max);
+ }
+ return true;
+ }
+ catch (const std::invalid_argument & err)
+ {
+ std::cerr << "Error parsing the limits for the interface: " << itf.name << " from the tags ["
+ << kMinTag << ": '" << itf.min << "' and " << kMaxTag << ": '" << itf.max
+ << "'] within " << kROS2ControlTag << " tag inside the URDF. Skipping it"
+ << std::endl;
+ return false;
+ }
+}
+
+/**
+ * @brief Set custom values for acceleration and jerk limits (no URDF standard)
+ * @param itr The interface tag to retrieve the values from.
+ * @param limits The joint limits to be set.
+ */
+void set_custom_interface_values(const InterfaceInfo & itr, joint_limits::JointLimits & limits)
+{
+ if (itr.name == hardware_interface::HW_IF_ACCELERATION)
+ {
+ double max_decel(std::numeric_limits::quiet_NaN()),
+ max_accel(std::numeric_limits::quiet_NaN());
+ if (detail::retrieve_min_max_interface_values(itr, max_decel, max_accel))
+ {
+ if (std::isfinite(max_decel))
+ {
+ limits.max_deceleration = std::fabs(max_decel);
+ if (!std::isfinite(max_accel))
+ {
+ limits.max_acceleration = std::fabs(limits.max_deceleration);
+ }
+ limits.has_deceleration_limits = itr.enable_limits;
+ }
+ if (std::isfinite(max_accel))
+ {
+ limits.max_acceleration = max_accel;
+
+ if (!std::isfinite(limits.max_deceleration))
+ {
+ limits.max_deceleration = std::fabs(limits.max_acceleration);
+ }
+ limits.has_acceleration_limits = itr.enable_limits;
+ }
+ }
+ }
+ else if (itr.name == "jerk")
+ {
+ if (!itr.min.empty())
+ {
+ std::cerr << "Error parsing the limits for the interface: " << itr.name
+ << " from the tag: " << kMinTag << " within " << kROS2ControlTag
+ << " tag inside the URDF. Jerk only accepts max limits." << std::endl;
+ }
+ double min_jerk(std::numeric_limits::quiet_NaN()),
+ max_jerk(std::numeric_limits::quiet_NaN());
+ if (
+ !itr.max.empty() && detail::retrieve_min_max_interface_values(itr, min_jerk, max_jerk) &&
+ std::isfinite(max_jerk))
+ {
+ limits.max_jerk = std::abs(max_jerk);
+ limits.has_jerk_limits = itr.enable_limits;
+ }
+ }
+ else
+ {
+ if (!itr.min.empty() || !itr.max.empty())
+ {
+ std::cerr << "Unable to parse the limits for the interface: " << itr.name
+ << " from the tags [" << kMinTag << " and " << kMaxTag << "] within "
+ << kROS2ControlTag
+ << " tag inside the URDF. Supported interfaces for joint limits are: "
+ "position, velocity, effort, acceleration and jerk."
+ << std::endl;
+ }
+ }
+}
+
+/**
+ * @brief Retrieve the limits from ros2_control command interface tags and override URDF limits if
+ * restrictive
+ * @param interfaces The interfaces to retrieve the limits from.
+ * @param limits The joint limits to be set.
+ */
+void update_interface_limits(
+ const std::vector & interfaces, joint_limits::JointLimits & limits)
+{
+ for (auto & itr : interfaces)
+ {
+ if (itr.name == hardware_interface::HW_IF_POSITION)
+ {
+ limits.min_position = limits.has_position_limits && itr.enable_limits
+ ? limits.min_position
+ : -std::numeric_limits::max();
+ limits.max_position = limits.has_position_limits && itr.enable_limits
+ ? limits.max_position
+ : std::numeric_limits::max();
+ double min_pos(limits.min_position), max_pos(limits.max_position);
+ if (itr.enable_limits && detail::retrieve_min_max_interface_values(itr, min_pos, max_pos))
+ {
+ limits.min_position = std::max(min_pos, limits.min_position);
+ limits.max_position = std::min(max_pos, limits.max_position);
+ limits.has_position_limits = true;
+ }
+ limits.has_position_limits &= itr.enable_limits;
+ }
+ else if (itr.name == hardware_interface::HW_IF_VELOCITY)
+ {
+ limits.max_velocity =
+ limits.has_velocity_limits ? limits.max_velocity : std::numeric_limits::max();
+ // Apply the most restrictive one in the case
+ double min_vel(-limits.max_velocity), max_vel(limits.max_velocity);
+ if (itr.enable_limits && detail::retrieve_min_max_interface_values(itr, min_vel, max_vel))
+ {
+ max_vel = std::min(std::abs(min_vel), max_vel);
+ limits.max_velocity = std::min(max_vel, limits.max_velocity);
+ limits.has_velocity_limits = true;
+ }
+ limits.has_velocity_limits &= itr.enable_limits;
+ }
+ else if (itr.name == hardware_interface::HW_IF_EFFORT)
+ {
+ limits.max_effort =
+ limits.has_effort_limits ? limits.max_effort : std::numeric_limits::max();
+ // Apply the most restrictive one in the case
+ double min_eff(-limits.max_effort), max_eff(limits.max_effort);
+ if (itr.enable_limits && detail::retrieve_min_max_interface_values(itr, min_eff, max_eff))
+ {
+ max_eff = std::min(std::abs(min_eff), max_eff);
+ limits.max_effort = std::min(max_eff, limits.max_effort);
+ limits.has_effort_limits = true;
+ }
+ limits.has_effort_limits &= itr.enable_limits;
+ }
+ else
+ {
+ detail::set_custom_interface_values(itr, limits);
+ }
+ }
+}
+
} // namespace detail
std::vector parse_control_resources_from_urdf(const std::string & urdf)
@@ -716,6 +912,37 @@ std::vector parse_control_resources_from_urdf(const std::string &
hw_info.mimic_joints.push_back(mimic_joint);
}
}
+ // TODO(christophfroehlich) remove this code if deprecated mimic attribute - branch
+ // from above is removed (double check here, but throws already above if not found in URDF)
+ auto urdf_joint = model.getJoint(joint.name);
+ if (!urdf_joint)
+ {
+ std::cerr << "Joint: '" + joint.name + "' not found in URDF. Skipping limits parsing!"
+ << std::endl;
+ continue;
+ }
+ if (urdf_joint->type == urdf::Joint::FIXED)
+ {
+ throw std::runtime_error(
+ "Joint '" + joint.name +
+ "' is of type 'fixed'. "
+ "Fixed joints do not make sense in ros2_control.");
+ }
+ joint_limits::JointLimits limits;
+ getJointLimits(urdf_joint, limits);
+ // Take the most restricted one. Also valid for continuous-joint type only
+ detail::update_interface_limits(joint.command_interfaces, limits);
+ hw_info.limits[joint.name] = limits;
+ joint_limits::SoftJointLimits soft_limits;
+ if (getSoftJointLimits(urdf_joint, soft_limits))
+ {
+ if (limits.has_position_limits)
+ {
+ soft_limits.min_position = std::max(soft_limits.min_position, limits.min_position);
+ soft_limits.max_position = std::min(soft_limits.max_position, limits.max_position);
+ }
+ hw_info.soft_limits[joint.name] = soft_limits;
+ }
}
}
diff --git a/hardware_interface/src/mock_components/generic_system.cpp b/hardware_interface/src/mock_components/generic_system.cpp
index 2d8a01a34f..162c3aa60d 100644
--- a/hardware_interface/src/mock_components/generic_system.cpp
+++ b/hardware_interface/src/mock_components/generic_system.cpp
@@ -459,7 +459,8 @@ return_type GenericSystem::read(const rclcpp::Time & /*time*/, const rclcpp::Dur
return return_type::OK;
}
- auto mirror_command_to_state = [](auto & states_, auto commands_, size_t start_index = 0)
+ auto mirror_command_to_state =
+ [](auto & states_, auto commands_, size_t start_index = 0) -> return_type
{
for (size_t i = start_index; i < states_.size(); ++i)
{
@@ -469,8 +470,13 @@ return_type GenericSystem::read(const rclcpp::Time & /*time*/, const rclcpp::Dur
{
states_[i][j] = commands_[i][j];
}
+ if (std::isinf(commands_[i][j]))
+ {
+ return return_type::ERROR;
+ }
}
}
+ return return_type::OK;
};
for (size_t j = 0; j < joint_states_[POSITION_INTERFACE_INDEX].size(); ++j)
@@ -556,13 +562,11 @@ return_type GenericSystem::read(const rclcpp::Time & /*time*/, const rclcpp::Dur
// do loopback on all other interfaces - starts from 1 or 3 because 0, 1, 3 are position,
// velocity, and acceleration interface
- if (calculate_dynamics_)
- {
- mirror_command_to_state(joint_states_, joint_commands_, 3);
- }
- else
+ if (
+ mirror_command_to_state(joint_states_, joint_commands_, calculate_dynamics_ ? 3 : 1) !=
+ return_type::OK)
{
- mirror_command_to_state(joint_states_, joint_commands_, 1);
+ return return_type::ERROR;
}
for (const auto & mimic_joint : info_.mimic_joints)
diff --git a/hardware_interface/src/resource_manager.cpp b/hardware_interface/src/resource_manager.cpp
index 2e8ccc7b1f..d77f915eee 100644
--- a/hardware_interface/src/resource_manager.cpp
+++ b/hardware_interface/src/resource_manager.cpp
@@ -67,6 +67,26 @@ auto trigger_and_print_hardware_state_transition =
return result;
};
+std::string interfaces_to_string(
+ const std::vector & start_interfaces,
+ const std::vector & stop_interfaces)
+{
+ std::stringstream ss;
+ ss << "Start interfaces: " << std::endl << "[" << std::endl;
+ for (const auto & start_if : start_interfaces)
+ {
+ ss << " " << start_if << std::endl;
+ }
+ ss << "]" << std::endl;
+ ss << "Stop interfaces: " << std::endl << "[" << std::endl;
+ for (const auto & stop_if : stop_interfaces)
+ {
+ ss << " " << stop_if << std::endl;
+ }
+ ss << "]" << std::endl;
+ return ss.str();
+};
+
class ResourceStorage
{
static constexpr const char * pkg_name = "hardware_interface";
@@ -90,27 +110,64 @@ class ResourceStorage
}
template
- void load_hardware(
+ [[nodiscard]] bool load_hardware(
const HardwareInfo & hardware_info, pluginlib::ClassLoader & loader,
std::vector & container)
{
- RCUTILS_LOG_INFO_NAMED(
- "resource_manager", "Loading hardware '%s' ", hardware_info.name.c_str());
- // hardware_plugin_name has to match class name in plugin xml description
- auto interface = std::unique_ptr(
- loader.createUnmanagedInstance(hardware_info.hardware_plugin_name));
- HardwareT hardware(std::move(interface));
- container.emplace_back(std::move(hardware));
- // initialize static data about hardware component to reduce later calls
- HardwareComponentInfo component_info;
- component_info.name = hardware_info.name;
- component_info.type = hardware_info.type;
- component_info.plugin_name = hardware_info.hardware_plugin_name;
- component_info.is_async = hardware_info.is_async;
-
- hardware_info_map_.insert(std::make_pair(component_info.name, component_info));
- hardware_used_by_controllers_.insert(
- std::make_pair(component_info.name, std::vector()));
+ bool is_loaded = false;
+ try
+ {
+ RCUTILS_LOG_INFO_NAMED(
+ "resource_manager", "Loading hardware '%s' ", hardware_info.name.c_str());
+ // hardware_plugin_name has to match class name in plugin xml description
+ auto interface = std::unique_ptr(
+ loader.createUnmanagedInstance(hardware_info.hardware_plugin_name));
+ if (interface)
+ {
+ RCUTILS_LOG_INFO_NAMED(
+ "resource_manager", "Loaded hardware '%s' from plugin '%s'", hardware_info.name.c_str(),
+ hardware_info.hardware_plugin_name.c_str());
+ HardwareT hardware(std::move(interface));
+ container.emplace_back(std::move(hardware));
+ // initialize static data about hardware component to reduce later calls
+ HardwareComponentInfo component_info;
+ component_info.name = hardware_info.name;
+ component_info.type = hardware_info.type;
+ component_info.group = hardware_info.group;
+ component_info.plugin_name = hardware_info.hardware_plugin_name;
+ component_info.is_async = hardware_info.is_async;
+
+ hardware_info_map_.insert(std::make_pair(component_info.name, component_info));
+ hw_group_state_.insert(std::make_pair(component_info.group, return_type::OK));
+ hardware_used_by_controllers_.insert(
+ std::make_pair(component_info.name, std::vector()));
+ is_loaded = true;
+ }
+ else
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Failed to load hardware '%s' from plugin '%s'",
+ hardware_info.name.c_str(), hardware_info.hardware_plugin_name.c_str());
+ }
+ }
+ catch (const pluginlib::PluginlibException & ex)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Exception while loading hardware: %s", ex.what());
+ }
+ catch (const std::exception & ex)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Exception occurred while loading hardware '%s': %s",
+ hardware_info.name.c_str(), ex.what());
+ }
+ catch (...)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Unknown exception occurred while loading hardware '%s'",
+ hardware_info.name.c_str());
+ }
+ return is_loaded;
}
template
@@ -119,30 +176,62 @@ class ResourceStorage
RCUTILS_LOG_INFO_NAMED(
"resource_manager", "Initialize hardware '%s' ", hardware_info.name.c_str());
- const rclcpp_lifecycle::State new_state = hardware.initialize(hardware_info);
-
- bool result = new_state.id() == lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED;
+ bool result = false;
+ try
+ {
+ const rclcpp_lifecycle::State new_state = hardware.initialize(hardware_info);
+ result = new_state.id() == lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED;
- if (result)
+ if (result)
+ {
+ RCUTILS_LOG_INFO_NAMED(
+ "resource_manager", "Successful initialization of hardware '%s'",
+ hardware_info.name.c_str());
+ }
+ else
+ {
+ RCUTILS_LOG_INFO_NAMED(
+ "resource_manager", "Failed to initialize hardware '%s'", hardware_info.name.c_str());
+ }
+ }
+ catch (const std::exception & ex)
{
- RCUTILS_LOG_INFO_NAMED(
- "resource_manager", "Successful initialization of hardware '%s'",
- hardware_info.name.c_str());
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Exception occurred while initializing hardware '%s': %s",
+ hardware_info.name.c_str(), ex.what());
}
- else
+ catch (...)
{
- RCUTILS_LOG_INFO_NAMED(
- "resource_manager", "Failed to initialize hardware '%s'", hardware_info.name.c_str());
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Unknown exception occurred while initializing hardware '%s'",
+ hardware_info.name.c_str());
}
+
return result;
}
template
bool configure_hardware(HardwareT & hardware)
{
- bool result = trigger_and_print_hardware_state_transition(
- std::bind(&HardwareT::configure, &hardware), "configure", hardware.get_name(),
- lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE);
+ bool result = false;
+ try
+ {
+ result = trigger_and_print_hardware_state_transition(
+ std::bind(&HardwareT::configure, &hardware), "configure", hardware.get_name(),
+ lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE);
+ }
+ catch (const std::exception & ex)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Exception occurred while configuring hardware '%s': %s",
+ hardware.get_name().c_str(), ex.what());
+ }
+ catch (...)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Unknown exception occurred while configuring hardware '%s'",
+ hardware.get_name().c_str());
+ }
if (result)
{
@@ -206,9 +295,15 @@ class ResourceStorage
{
async_component_threads_.emplace(
std::piecewise_construct, std::forward_as_tuple(hardware.get_name()),
- std::forward_as_tuple(&hardware, cm_update_rate_, clock_interface_));
+ std::forward_as_tuple(cm_update_rate_, clock_interface_));
+
+ async_component_threads_.at(hardware.get_name()).register_component(&hardware);
}
}
+ if (!hardware.get_group_name().empty())
+ {
+ hw_group_state_[hardware.get_group_name()] = return_type::OK;
+ }
return result;
}
@@ -267,23 +362,59 @@ class ResourceStorage
template
bool cleanup_hardware(HardwareT & hardware)
{
- bool result = trigger_and_print_hardware_state_transition(
- std::bind(&HardwareT::cleanup, &hardware), "cleanup", hardware.get_name(),
- lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED);
+ bool result = false;
+ try
+ {
+ result = trigger_and_print_hardware_state_transition(
+ std::bind(&HardwareT::cleanup, &hardware), "cleanup", hardware.get_name(),
+ lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED);
+ }
+ catch (const std::exception & ex)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Exception occurred while cleaning up hardware '%s': %s",
+ hardware.get_name().c_str(), ex.what());
+ }
+ catch (...)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Unknown exception occurred while cleaning up hardware '%s'",
+ hardware.get_name().c_str());
+ }
if (result)
{
remove_all_hardware_interfaces_from_available_list(hardware.get_name());
}
+ if (!hardware.get_group_name().empty())
+ {
+ hw_group_state_[hardware.get_group_name()] = return_type::OK;
+ }
return result;
}
template
bool shutdown_hardware(HardwareT & hardware)
{
- bool result = trigger_and_print_hardware_state_transition(
- std::bind(&HardwareT::shutdown, &hardware), "shutdown", hardware.get_name(),
- lifecycle_msgs::msg::State::PRIMARY_STATE_FINALIZED);
+ bool result = false;
+ try
+ {
+ result = trigger_and_print_hardware_state_transition(
+ std::bind(&HardwareT::shutdown, &hardware), "shutdown", hardware.get_name(),
+ lifecycle_msgs::msg::State::PRIMARY_STATE_FINALIZED);
+ }
+ catch (const std::exception & ex)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Exception occurred while shutting down hardware '%s': %s",
+ hardware.get_name().c_str(), ex.what());
+ }
+ catch (...)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Unknown exception occurred while shutting down hardware '%s'",
+ hardware.get_name().c_str());
+ }
if (result)
{
@@ -293,6 +424,10 @@ class ResourceStorage
// deimport_non_movement_command_interfaces(hardware);
// deimport_state_interfaces(hardware);
// use remove_command_interfaces(hardware);
+ if (!hardware.get_group_name().empty())
+ {
+ hw_group_state_[hardware.get_group_name()] = return_type::OK;
+ }
}
return result;
}
@@ -300,9 +435,25 @@ class ResourceStorage
template
bool activate_hardware(HardwareT & hardware)
{
- bool result = trigger_and_print_hardware_state_transition(
- std::bind(&HardwareT::activate, &hardware), "activate", hardware.get_name(),
- lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE);
+ bool result = false;
+ try
+ {
+ result = trigger_and_print_hardware_state_transition(
+ std::bind(&HardwareT::activate, &hardware), "activate", hardware.get_name(),
+ lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE);
+ }
+ catch (const std::exception & ex)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Exception occurred while activating hardware '%s': %s",
+ hardware.get_name().c_str(), ex.what());
+ }
+ catch (...)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Unknown exception occurred while activating hardware '%s'",
+ hardware.get_name().c_str());
+ }
if (result)
{
@@ -319,9 +470,25 @@ class ResourceStorage
template
bool deactivate_hardware(HardwareT & hardware)
{
- bool result = trigger_and_print_hardware_state_transition(
- std::bind(&HardwareT::deactivate, &hardware), "deactivate", hardware.get_name(),
- lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE);
+ bool result = false;
+ try
+ {
+ result = trigger_and_print_hardware_state_transition(
+ std::bind(&HardwareT::deactivate, &hardware), "deactivate", hardware.get_name(),
+ lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE);
+ }
+ catch (const std::exception & ex)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Exception occurred while deactivating hardware '%s': %s",
+ hardware.get_name().c_str(), ex.what());
+ }
+ catch (...)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Unknown exception occurred while deactivating hardware '%s'",
+ hardware.get_name().c_str());
+ }
if (result)
{
@@ -433,25 +600,60 @@ class ResourceStorage
template
void import_state_interfaces(HardwareT & hardware)
{
- auto interfaces = hardware.export_state_interfaces();
- std::vector interface_names;
- interface_names.reserve(interfaces.size());
- for (auto & interface : interfaces)
+ try
{
- auto key = interface.get_name();
- state_interface_map_.emplace(std::make_pair(key, std::move(interface)));
- interface_names.push_back(key);
+ auto interfaces = hardware.export_state_interfaces();
+ std::vector interface_names;
+ interface_names.reserve(interfaces.size());
+ for (auto & interface : interfaces)
+ {
+ auto key = interface.get_name();
+ state_interface_map_.emplace(std::make_pair(key, std::move(interface)));
+ interface_names.push_back(key);
+ }
+ hardware_info_map_[hardware.get_name()].state_interfaces = interface_names;
+ available_state_interfaces_.reserve(
+ available_state_interfaces_.capacity() + interface_names.size());
+ }
+ catch (const std::exception & e)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager",
+ "Exception occurred while importing state interfaces for the hardware '%s' : %s",
+ hardware.get_name().c_str(), e.what());
+ }
+ catch (...)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager",
+ "Unknown exception occurred while importing state interfaces for the hardware '%s'",
+ hardware.get_name().c_str());
}
- hardware_info_map_[hardware.get_name()].state_interfaces = interface_names;
- available_state_interfaces_.reserve(
- available_state_interfaces_.capacity() + interface_names.size());
}
template
void import_command_interfaces(HardwareT & hardware)
{
- auto interfaces = hardware.export_command_interfaces();
- hardware_info_map_[hardware.get_name()].command_interfaces = add_command_interfaces(interfaces);
+ try
+ {
+ auto interfaces = hardware.export_command_interfaces();
+ hardware_info_map_[hardware.get_name()].command_interfaces =
+ add_command_interfaces(interfaces);
+ }
+ catch (const std::exception & ex)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager",
+ "Exception occurred while importing command interfaces for the hardware '%s' : %s",
+ hardware.get_name().c_str(), ex.what());
+ }
+ catch (...)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager",
+ "Unknown exception occurred while importing command interfaces for the hardware '%s'",
+ hardware.get_name().c_str());
+ }
}
/// Adds exported command interfaces into internal storage.
@@ -514,7 +716,10 @@ class ResourceStorage
auto load_and_init_actuators = [&](auto & container)
{
check_for_duplicates(hardware_info);
- load_hardware(hardware_info, actuator_loader_, container);
+ if (!load_hardware(hardware_info, actuator_loader_, container))
+ {
+ return;
+ }
if (initialize_hardware(hardware_info, container.back()))
{
import_state_interfaces(container.back());
@@ -544,7 +749,10 @@ class ResourceStorage
auto load_and_init_sensors = [&](auto & container)
{
check_for_duplicates(hardware_info);
- load_hardware(hardware_info, sensor_loader_, container);
+ if (!load_hardware(hardware_info, sensor_loader_, container))
+ {
+ return;
+ }
if (initialize_hardware(hardware_info, container.back()))
{
import_state_interfaces(container.back());
@@ -573,7 +781,10 @@ class ResourceStorage
auto load_and_init_systems = [&](auto & container)
{
check_for_duplicates(hardware_info);
- load_hardware(hardware_info, system_loader_, container);
+ if (!load_hardware(hardware_info, system_loader_, container))
+ {
+ return;
+ }
if (initialize_hardware(hardware_info, container.back()))
{
import_state_interfaces(container.back());
@@ -687,6 +898,27 @@ class ResourceStorage
}
}
+ /**
+ * Returns the return type of the hardware component group state, if the return type is other
+ * than OK, then updates the return type of the group to the respective one
+ */
+ return_type update_hardware_component_group_state(
+ const std::string & group_name, const return_type & value)
+ {
+ // This is for the components that has no configured group
+ if (group_name.empty())
+ {
+ return value;
+ }
+ // If it is anything other than OK, change the return type of the hardware group state
+ // to the respective return type
+ if (value != return_type::OK)
+ {
+ hw_group_state_.at(group_name) = value;
+ }
+ return hw_group_state_.at(group_name);
+ }
+
// hardware plugins
pluginlib::ClassLoader actuator_loader_;
pluginlib::ClassLoader sensor_loader_;
@@ -701,6 +933,7 @@ class ResourceStorage
std::vector async_systems_;
std::unordered_map hardware_info_map_;
+ std::unordered_map hw_group_state_;
/// Mapping between hardware and controllers that are using it (accessing data from it)
std::unordered_map> hardware_used_by_controllers_;
@@ -1083,24 +1316,6 @@ bool ResourceManager::prepare_command_mode_switch(
return true;
}
- auto interfaces_to_string = [&]()
- {
- std::stringstream ss;
- ss << "Start interfaces: " << std::endl << "[" << std::endl;
- for (const auto & start_if : start_interfaces)
- {
- ss << " " << start_if << std::endl;
- }
- ss << "]" << std::endl;
- ss << "Stop interfaces: " << std::endl << "[" << std::endl;
- for (const auto & stop_if : stop_interfaces)
- {
- ss << " " << stop_if << std::endl;
- }
- ss << "]" << std::endl;
- return ss.str();
- };
-
// Check if interface exists
std::stringstream ss_not_existing;
ss_not_existing << "Not existing: " << std::endl << "[" << std::endl;
@@ -1122,7 +1337,8 @@ bool ResourceManager::prepare_command_mode_switch(
ss_not_existing << "]" << std::endl;
RCUTILS_LOG_ERROR_NAMED(
"resource_manager", "Not acceptable command interfaces combination: \n%s%s",
- interfaces_to_string().c_str(), ss_not_existing.str().c_str());
+ interfaces_to_string(start_interfaces, stop_interfaces).c_str(),
+ ss_not_existing.str().c_str());
return false;
}
@@ -1147,12 +1363,12 @@ bool ResourceManager::prepare_command_mode_switch(
ss_not_available << "]" << std::endl;
RCUTILS_LOG_ERROR_NAMED(
"resource_manager", "Not acceptable command interfaces combination: \n%s%s",
- interfaces_to_string().c_str(), ss_not_available.str().c_str());
+ interfaces_to_string(start_interfaces, stop_interfaces).c_str(),
+ ss_not_available.str().c_str());
return false;
}
- auto call_prepare_mode_switch =
- [&start_interfaces, &stop_interfaces, &interfaces_to_string](auto & components)
+ auto call_prepare_mode_switch = [&start_interfaces, &stop_interfaces](auto & components)
{
bool ret = true;
for (auto & component : components)
@@ -1161,14 +1377,38 @@ bool ResourceManager::prepare_command_mode_switch(
component.get_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE ||
component.get_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE)
{
- if (
- return_type::OK !=
- component.prepare_command_mode_switch(start_interfaces, stop_interfaces))
+ try
+ {
+ if (
+ return_type::OK !=
+ component.prepare_command_mode_switch(start_interfaces, stop_interfaces))
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager",
+ "Component '%s' did not accept command interfaces combination: \n%s",
+ component.get_name().c_str(),
+ interfaces_to_string(start_interfaces, stop_interfaces).c_str());
+ ret = false;
+ }
+ }
+ catch (const std::exception & e)
{
RCUTILS_LOG_ERROR_NAMED(
"resource_manager",
- "Component '%s' did not accept command interfaces combination: \n%s",
- component.get_name().c_str(), interfaces_to_string().c_str());
+ "Exception occurred while preparing command mode switch for component '%s' for the "
+ "interfaces: \n %s : %s",
+ component.get_name().c_str(),
+ interfaces_to_string(start_interfaces, stop_interfaces).c_str(), e.what());
+ ret = false;
+ }
+ catch (...)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager",
+ "Unknown exception occurred while preparing command mode switch for component '%s' for "
+ "the interfaces: \n %s",
+ component.get_name().c_str(),
+ interfaces_to_string(start_interfaces, stop_interfaces).c_str());
ret = false;
}
}
@@ -1202,13 +1442,37 @@ bool ResourceManager::perform_command_mode_switch(
component.get_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE ||
component.get_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE)
{
- if (
- return_type::OK !=
- component.perform_command_mode_switch(start_interfaces, stop_interfaces))
+ try
+ {
+ if (
+ return_type::OK !=
+ component.perform_command_mode_switch(start_interfaces, stop_interfaces))
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Component '%s' could not perform switch",
+ component.get_name().c_str());
+ ret = false;
+ }
+ }
+ catch (const std::exception & e)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager",
+ "Exception occurred while performing command mode switch for component '%s' for the "
+ "interfaces: \n %s : %s",
+ component.get_name().c_str(),
+ interfaces_to_string(start_interfaces, stop_interfaces).c_str(), e.what());
+ ret = false;
+ }
+ catch (...)
{
RCUTILS_LOG_ERROR_NAMED(
- "resource_manager", "Component '%s' could not perform switch",
- component.get_name().c_str());
+ "resource_manager",
+ "Unknown exception occurred while performing command mode switch for component '%s' "
+ "for "
+ "the interfaces: \n %s",
+ component.get_name().c_str(),
+ interfaces_to_string(start_interfaces, stop_interfaces).c_str());
ret = false;
}
}
@@ -1324,6 +1588,13 @@ return_type ResourceManager::set_component_state(
return result;
}
+void ResourceManager::shutdown_async_components()
+{
+ resource_storage_->async_component_threads_.erase(
+ resource_storage_->async_component_threads_.begin(),
+ resource_storage_->async_component_threads_.end());
+}
+
// CM API: Called in "update"-thread
HardwareReadWriteStatus ResourceManager::read(
const rclcpp::Time & time, const rclcpp::Duration & period)
@@ -1336,7 +1607,28 @@ HardwareReadWriteStatus ResourceManager::read(
{
for (auto & component : components)
{
- auto ret_val = component.read(time, period);
+ auto ret_val = return_type::OK;
+ try
+ {
+ ret_val = component.read(time, period);
+ const auto component_group = component.get_group_name();
+ ret_val =
+ resource_storage_->update_hardware_component_group_state(component_group, ret_val);
+ }
+ catch (const std::exception & e)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Exception thrown durind read of the component '%s': %s",
+ component.get_name().c_str(), e.what());
+ ret_val = return_type::ERROR;
+ }
+ catch (...)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Unknown exception thrown during read of the component '%s'",
+ component.get_name().c_str());
+ ret_val = return_type::ERROR;
+ }
if (ret_val == return_type::ERROR)
{
read_write_status.ok = false;
@@ -1376,7 +1668,28 @@ HardwareReadWriteStatus ResourceManager::write(
{
for (auto & component : components)
{
- auto ret_val = component.write(time, period);
+ auto ret_val = return_type::OK;
+ try
+ {
+ ret_val = component.write(time, period);
+ const auto component_group = component.get_group_name();
+ ret_val =
+ resource_storage_->update_hardware_component_group_state(component_group, ret_val);
+ }
+ catch (const std::exception & e)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Exception thrown during write of the component '%s': %s",
+ component.get_name().c_str(), e.what());
+ ret_val = return_type::ERROR;
+ }
+ catch (...)
+ {
+ RCUTILS_LOG_ERROR_NAMED(
+ "resource_manager", "Unknown exception thrown during write of the component '%s'",
+ component.get_name().c_str());
+ ret_val = return_type::ERROR;
+ }
if (ret_val == return_type::ERROR)
{
read_write_status.ok = false;
diff --git a/hardware_interface/src/sensor.cpp b/hardware_interface/src/sensor.cpp
index 2e53e447b9..2da627f892 100644
--- a/hardware_interface/src/sensor.cpp
+++ b/hardware_interface/src/sensor.cpp
@@ -191,6 +191,8 @@ std::vector Sensor::export_state_interfaces()
std::string Sensor::get_name() const { return impl_->get_name(); }
+std::string Sensor::get_group_name() const { return impl_->get_group_name(); }
+
const rclcpp_lifecycle::State & Sensor::get_state() const { return impl_->get_state(); }
return_type Sensor::read(const rclcpp::Time & time, const rclcpp::Duration & period)
diff --git a/hardware_interface/src/system.cpp b/hardware_interface/src/system.cpp
index ee942d6581..8e950faa89 100644
--- a/hardware_interface/src/system.cpp
+++ b/hardware_interface/src/system.cpp
@@ -210,6 +210,8 @@ return_type System::perform_command_mode_switch(
std::string System::get_name() const { return impl_->get_name(); }
+std::string System::get_group_name() const { return impl_->get_group_name(); }
+
const rclcpp_lifecycle::State & System::get_state() const { return impl_->get_state(); }
return_type System::read(const rclcpp::Time & time, const rclcpp::Duration & period)
diff --git a/hardware_interface/test/mock_components/test_generic_system.cpp b/hardware_interface/test/mock_components/test_generic_system.cpp
index ac89dc1553..c7777b3e21 100644
--- a/hardware_interface/test/mock_components/test_generic_system.cpp
+++ b/hardware_interface/test/mock_components/test_generic_system.cpp
@@ -96,6 +96,7 @@ class TestGenericSystem : public ::testing::Test
mock_components/GenericSystem
+ Hardware Group
@@ -121,6 +122,7 @@ class TestGenericSystem : public ::testing::Test
mock_components/GenericSystem
+ Hardware Group
@@ -289,6 +291,7 @@ class TestGenericSystem : public ::testing::Test
mock_components/GenericSystem
+ Hardware Group
-3
actual_position
@@ -351,6 +354,7 @@ class TestGenericSystem : public ::testing::Test
mock_components/GenericSystem
+ Hardware Group
2
2
@@ -579,6 +583,70 @@ class TestGenericSystem : public ::testing::Test
)";
+
+ hardware_system_2dof_standard_interfaces_with_same_hardware_group_ =
+ R"(
+
+
+ mock_components/GenericSystem
+ Hardware Group
+
+
+
+
+
+ 3.45
+
+
+
+
+
+
+ mock_components/GenericSystem
+ Hardware Group
+
+
+
+
+
+ 2.78
+
+
+
+
+)";
+
+ hardware_system_2dof_standard_interfaces_with_two_diff_hw_groups_ =
+ R"(
+
+
+ mock_components/GenericSystem
+ Hardware Group 1
+
+
+
+
+
+ 3.45
+
+
+
+
+
+
+ mock_components/GenericSystem
+ Hardware Group 2
+
+
+
+
+
+ 2.78
+
+
+
+
+)";
}
std::string hardware_system_2dof_;
@@ -600,6 +668,8 @@ class TestGenericSystem : public ::testing::Test
std::string hardware_system_2dof_standard_interfaces_with_different_control_modes_;
std::string valid_hardware_system_2dof_standard_interfaces_with_different_control_modes_;
std::string disabled_commands_;
+ std::string hardware_system_2dof_standard_interfaces_with_same_hardware_group_;
+ std::string hardware_system_2dof_standard_interfaces_with_two_diff_hw_groups_;
};
// Forward declaration
@@ -814,7 +884,7 @@ void generic_system_functional_test(
ASSERT_EQ(0.44, j2v_c.get_value());
// write() does not change values
- rm.write(TIME, PERIOD);
+ ASSERT_TRUE(rm.write(TIME, PERIOD).ok);
ASSERT_EQ(3.45, j1p_s.get_value());
ASSERT_EQ(0.0, j1v_s.get_value());
ASSERT_EQ(2.78, j2p_s.get_value());
@@ -825,7 +895,7 @@ void generic_system_functional_test(
ASSERT_EQ(0.44, j2v_c.get_value());
// read() mirrors commands + offset to states
- rm.read(TIME, PERIOD);
+ ASSERT_TRUE(rm.read(TIME, PERIOD).ok);
ASSERT_EQ(0.11 + offset, j1p_s.get_value());
ASSERT_EQ(0.22, j1v_s.get_value());
ASSERT_EQ(0.33 + offset, j2p_s.get_value());
@@ -857,6 +927,158 @@ void generic_system_functional_test(
status_map[component_name].state.label(), hardware_interface::lifecycle_state_names::INACTIVE);
}
+void generic_system_error_group_test(
+ const std::string & urdf, const std::string component_prefix, bool validate_same_group)
+{
+ TestableResourceManager rm(urdf);
+ const std::string component1 = component_prefix + "1";
+ const std::string component2 = component_prefix + "2";
+ // check is hardware is configured
+ auto status_map = rm.get_components_status();
+ for (auto component : {component1, component2})
+ {
+ EXPECT_EQ(
+ status_map[component].state.label(), hardware_interface::lifecycle_state_names::UNCONFIGURED);
+ configure_components(rm, {component});
+ status_map = rm.get_components_status();
+ EXPECT_EQ(
+ status_map[component].state.label(), hardware_interface::lifecycle_state_names::INACTIVE);
+ activate_components(rm, {component});
+ status_map = rm.get_components_status();
+ EXPECT_EQ(
+ status_map[component].state.label(), hardware_interface::lifecycle_state_names::ACTIVE);
+ }
+
+ // Check initial values
+ hardware_interface::LoanedStateInterface j1p_s = rm.claim_state_interface("joint1/position");
+ hardware_interface::LoanedStateInterface j1v_s = rm.claim_state_interface("joint1/velocity");
+ hardware_interface::LoanedStateInterface j2p_s = rm.claim_state_interface("joint2/position");
+ hardware_interface::LoanedStateInterface j2v_s = rm.claim_state_interface("joint2/velocity");
+ hardware_interface::LoanedCommandInterface j1p_c = rm.claim_command_interface("joint1/position");
+ hardware_interface::LoanedCommandInterface j1v_c = rm.claim_command_interface("joint1/velocity");
+ hardware_interface::LoanedCommandInterface j2p_c = rm.claim_command_interface("joint2/position");
+ hardware_interface::LoanedCommandInterface j2v_c = rm.claim_command_interface("joint2/velocity");
+
+ // State interfaces without initial value are set to 0
+ ASSERT_EQ(3.45, j1p_s.get_value());
+ ASSERT_EQ(0.0, j1v_s.get_value());
+ ASSERT_EQ(2.78, j2p_s.get_value());
+ ASSERT_EQ(0.0, j2v_s.get_value());
+ ASSERT_TRUE(std::isnan(j1p_c.get_value()));
+ ASSERT_TRUE(std::isnan(j1v_c.get_value()));
+ ASSERT_TRUE(std::isnan(j2p_c.get_value()));
+ ASSERT_TRUE(std::isnan(j2v_c.get_value()));
+
+ // set some new values in commands
+ j1p_c.set_value(0.11);
+ j1v_c.set_value(0.22);
+ j2p_c.set_value(0.33);
+ j2v_c.set_value(0.44);
+
+ // State values should not be changed
+ ASSERT_EQ(3.45, j1p_s.get_value());
+ ASSERT_EQ(0.0, j1v_s.get_value());
+ ASSERT_EQ(2.78, j2p_s.get_value());
+ ASSERT_EQ(0.0, j2v_s.get_value());
+ ASSERT_EQ(0.11, j1p_c.get_value());
+ ASSERT_EQ(0.22, j1v_c.get_value());
+ ASSERT_EQ(0.33, j2p_c.get_value());
+ ASSERT_EQ(0.44, j2v_c.get_value());
+
+ // write() does not change values
+ ASSERT_TRUE(rm.write(TIME, PERIOD).ok);
+ ASSERT_EQ(3.45, j1p_s.get_value());
+ ASSERT_EQ(0.0, j1v_s.get_value());
+ ASSERT_EQ(2.78, j2p_s.get_value());
+ ASSERT_EQ(0.0, j2v_s.get_value());
+ ASSERT_EQ(0.11, j1p_c.get_value());
+ ASSERT_EQ(0.22, j1v_c.get_value());
+ ASSERT_EQ(0.33, j2p_c.get_value());
+ ASSERT_EQ(0.44, j2v_c.get_value());
+
+ // read() mirrors commands to states
+ ASSERT_TRUE(rm.read(TIME, PERIOD).ok);
+ ASSERT_EQ(0.11, j1p_s.get_value());
+ ASSERT_EQ(0.22, j1v_s.get_value());
+ ASSERT_EQ(0.33, j2p_s.get_value());
+ ASSERT_EQ(0.44, j2v_s.get_value());
+ ASSERT_EQ(0.11, j1p_c.get_value());
+ ASSERT_EQ(0.22, j1v_c.get_value());
+ ASSERT_EQ(0.33, j2p_c.get_value());
+ ASSERT_EQ(0.44, j2v_c.get_value());
+
+ // set some new values in commands
+ j1p_c.set_value(0.55);
+ j1v_c.set_value(0.66);
+ j2p_c.set_value(0.77);
+ j2v_c.set_value(0.88);
+
+ // state values should not be changed
+ ASSERT_EQ(0.11, j1p_s.get_value());
+ ASSERT_EQ(0.22, j1v_s.get_value());
+ ASSERT_EQ(0.33, j2p_s.get_value());
+ ASSERT_EQ(0.44, j2v_s.get_value());
+ ASSERT_EQ(0.55, j1p_c.get_value());
+ ASSERT_EQ(0.66, j1v_c.get_value());
+ ASSERT_EQ(0.77, j2p_c.get_value());
+ ASSERT_EQ(0.88, j2v_c.get_value());
+
+ // Error testing
+ j1p_c.set_value(std::numeric_limits::infinity());
+ j1v_c.set_value(std::numeric_limits::infinity());
+ // read() should now bring error in the first component
+ auto read_result = rm.read(TIME, PERIOD);
+ ASSERT_FALSE(read_result.ok);
+ if (validate_same_group)
+ {
+ // If they belong to the same group, show the error in all hardware components of same group
+ EXPECT_THAT(read_result.failed_hardware_names, ::testing::ElementsAre(component1, component2));
+ }
+ else
+ {
+ // If they don't belong to the same group, show the error in only that hardware component
+ EXPECT_THAT(read_result.failed_hardware_names, ::testing::ElementsAre(component1));
+ }
+
+ // Check initial values
+ ASSERT_FALSE(rm.state_interface_is_available("joint1/position"));
+ ASSERT_FALSE(rm.state_interface_is_available("joint1/velocity"));
+ ASSERT_FALSE(rm.command_interface_is_available("joint1/position"));
+ ASSERT_FALSE(rm.command_interface_is_available("joint1/velocity"));
+
+ if (validate_same_group)
+ {
+ ASSERT_FALSE(rm.state_interface_is_available("joint2/position"));
+ ASSERT_FALSE(rm.state_interface_is_available("joint2/velocity"));
+ ASSERT_FALSE(rm.command_interface_is_available("joint2/position"));
+ ASSERT_FALSE(rm.command_interface_is_available("joint2/velocity"));
+ }
+ else
+ {
+ ASSERT_TRUE(rm.state_interface_is_available("joint2/position"));
+ ASSERT_TRUE(rm.state_interface_is_available("joint2/velocity"));
+ ASSERT_TRUE(rm.command_interface_is_available("joint2/position"));
+ ASSERT_TRUE(rm.command_interface_is_available("joint2/velocity"));
+ }
+
+ // Error should be recoverable only after reactivating the hardware component
+ j1p_c.set_value(0.0);
+ j1v_c.set_value(0.0);
+ ASSERT_FALSE(rm.read(TIME, PERIOD).ok);
+
+ // Now it should be recoverable
+ deactivate_components(rm, {component1});
+ activate_components(rm, {component1});
+ ASSERT_TRUE(rm.read(TIME, PERIOD).ok);
+
+ deactivate_components(rm, {component1, component2});
+ status_map = rm.get_components_status();
+ EXPECT_EQ(
+ status_map[component1].state.label(), hardware_interface::lifecycle_state_names::INACTIVE);
+ EXPECT_EQ(
+ status_map[component2].state.label(), hardware_interface::lifecycle_state_names::INACTIVE);
+}
+
TEST_F(TestGenericSystem, generic_system_2dof_functionality)
{
auto urdf = ros2_control_test_assets::urdf_head + hardware_system_2dof_standard_interfaces_ +
@@ -865,6 +1087,24 @@ TEST_F(TestGenericSystem, generic_system_2dof_functionality)
generic_system_functional_test(urdf, {"MockHardwareSystem"});
}
+TEST_F(TestGenericSystem, generic_system_2dof_error_propagation_different_group)
+{
+ auto urdf = ros2_control_test_assets::urdf_head +
+ hardware_system_2dof_standard_interfaces_with_two_diff_hw_groups_ +
+ ros2_control_test_assets::urdf_tail;
+
+ generic_system_error_group_test(urdf, {"MockHardwareSystem"}, false);
+}
+
+TEST_F(TestGenericSystem, generic_system_2dof_error_propagation_same_group)
+{
+ auto urdf = ros2_control_test_assets::urdf_head +
+ hardware_system_2dof_standard_interfaces_with_same_hardware_group_ +
+ ros2_control_test_assets::urdf_tail;
+
+ generic_system_error_group_test(urdf, {"MockHardwareSystem"}, true);
+}
+
TEST_F(TestGenericSystem, generic_system_2dof_other_interfaces)
{
auto urdf = ros2_control_test_assets::urdf_head + hardware_system_2dof_with_other_interface_ +
diff --git a/hardware_interface/test/test_component_parser.cpp b/hardware_interface/test/test_component_parser.cpp
index 968d41ed97..2e2cae9807 100644
--- a/hardware_interface/test/test_component_parser.cpp
+++ b/hardware_interface/test/test_component_parser.cpp
@@ -32,6 +32,7 @@ class TestComponentParser : public Test
void SetUp() override {}
};
+using hardware_interface::HW_IF_ACCELERATION;
using hardware_interface::HW_IF_EFFORT;
using hardware_interface::HW_IF_POSITION;
using hardware_interface::HW_IF_VELOCITY;
@@ -112,6 +113,7 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_one_interface)
EXPECT_EQ(hardware_info.name, "RRBotSystemPositionOnly");
EXPECT_EQ(hardware_info.type, "system");
+ ASSERT_THAT(hardware_info.group, IsEmpty());
EXPECT_EQ(
hardware_info.hardware_plugin_name,
"ros2_control_demo_hardware/RRBotSystemPositionOnlyHardware");
@@ -137,6 +139,30 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_one_interface)
EXPECT_EQ(hardware_info.joints[1].command_interfaces[0].max, "1");
ASSERT_THAT(hardware_info.joints[1].state_interfaces, SizeIs(1));
EXPECT_EQ(hardware_info.joints[1].state_interfaces[0].name, HW_IF_POSITION);
+
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(2));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(1));
+ char joint2_name[] = "joint2";
+ for (const auto & joint : {"joint1", "joint2"})
+ {
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ // position limits will be limited than original range, as their range is limited in the
+ // ros2_control tags
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(1.0, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ if (strcmp(joint, joint2_name) == 0)
+ {
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).max_position, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_position, DoubleNear(10.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_velocity, DoubleNear(20.0, 1e-5));
+ }
+ }
}
TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_multi_interface)
@@ -151,6 +177,7 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_multi_interface
EXPECT_EQ(hardware_info.name, "RRBotSystemMultiInterface");
EXPECT_EQ(hardware_info.type, "system");
+ ASSERT_THAT(hardware_info.group, IsEmpty());
EXPECT_EQ(
hardware_info.hardware_plugin_name,
"ros2_control_demo_hardware/RRBotSystemMultiInterfaceHardware");
@@ -174,6 +201,31 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_multi_interface
ASSERT_THAT(hardware_info.joints[1].command_interfaces, SizeIs(1));
ASSERT_THAT(hardware_info.joints[1].state_interfaces, SizeIs(3));
EXPECT_EQ(hardware_info.joints[1].state_interfaces[2].name, HW_IF_EFFORT);
+
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(2));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(1));
+ char joint2_name[] = "joint2";
+ for (const auto & joint : {"joint1", "joint2"})
+ {
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ // position limits will be limited than original range, as their range is limited in the
+ // ros2_control tags
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(1.0, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ // effort and velocity limits won't change as they are above the main URDF hard limits
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ if (strcmp(joint, joint2_name) == 0)
+ {
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).max_position, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_position, DoubleNear(10.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_velocity, DoubleNear(20.0, 1e-5));
+ }
+ }
}
TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_robot_with_sensor)
@@ -188,6 +240,7 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_robot_with_sens
EXPECT_EQ(hardware_info.name, "RRBotSystemWithSensor");
EXPECT_EQ(hardware_info.type, "system");
+ ASSERT_THAT(hardware_info.group, IsEmpty());
EXPECT_EQ(
hardware_info.hardware_plugin_name, "ros2_control_demo_hardware/RRBotSystemWithSensorHardware");
ASSERT_THAT(hardware_info.hardware_parameters, SizeIs(2));
@@ -218,6 +271,30 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_robot_with_sens
EXPECT_EQ(hardware_info.sensors[0].parameters.at("frame_id"), "kuka_tcp");
EXPECT_EQ(hardware_info.sensors[0].parameters.at("lower_limits"), "-100");
EXPECT_EQ(hardware_info.sensors[0].parameters.at("upper_limits"), "100");
+
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(2));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(1));
+ char joint2_name[] = "joint2";
+ for (const auto & joint : {"joint1", "joint2"})
+ {
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ // position limits will be limited than original range, as their range is limited in the
+ // ros2_control tags
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(1.0, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ if (strcmp(joint, joint2_name) == 0)
+ {
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).max_position, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_position, DoubleNear(10.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_velocity, DoubleNear(20.0, 1e-5));
+ }
+ }
}
TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_robot_with_external_sensor)
@@ -232,6 +309,7 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_robot_with_exte
EXPECT_EQ(hardware_info.name, "RRBotSystemPositionOnlyWithExternalSensor");
EXPECT_EQ(hardware_info.type, "system");
+ ASSERT_THAT(hardware_info.group, IsEmpty());
EXPECT_EQ(
hardware_info.hardware_plugin_name,
"ros2_control_demo_hardware/RRBotSystemPositionOnlyHardware");
@@ -248,6 +326,30 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_robot_with_exte
ASSERT_THAT(hardware_info.sensors, SizeIs(0));
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(2));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(1));
+ char joint2_name[] = "joint2";
+ for (const auto & joint : {"joint1", "joint2"})
+ {
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ // position limits will be limited than original range, as their range is limited in the
+ // ros2_control tags
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(1.0, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ if (strcmp(joint, joint2_name) == 0)
+ {
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).max_position, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_position, DoubleNear(10.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_velocity, DoubleNear(20.0, 1e-5));
+ }
+ }
+
hardware_info = control_hardware.at(1);
EXPECT_EQ(hardware_info.name, "RRBotForceTorqueSensor2D");
@@ -259,6 +361,8 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_robot_with_exte
EXPECT_EQ(hardware_info.sensors[0].name, "tcp_fts_sensor");
EXPECT_EQ(hardware_info.sensors[0].type, "sensor");
EXPECT_EQ(hardware_info.sensors[0].parameters.at("frame_id"), "kuka_tcp");
+ ASSERT_THAT(hardware_info.limits, SizeIs(0));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(0));
}
TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_modular_robot)
@@ -272,6 +376,7 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_modular_robot
auto hardware_info = control_hardware.at(0);
EXPECT_EQ(hardware_info.name, "RRBotModularJoint1");
+ EXPECT_EQ(hardware_info.group, "Hardware Group");
EXPECT_EQ(hardware_info.type, "actuator");
EXPECT_EQ(
hardware_info.hardware_plugin_name, "ros2_control_demo_hardware/PositionActuatorHardware");
@@ -281,10 +386,26 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_modular_robot
ASSERT_THAT(hardware_info.joints, SizeIs(1));
EXPECT_EQ(hardware_info.joints[0].name, "joint1");
EXPECT_EQ(hardware_info.joints[0].type, "joint");
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(1));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(0));
+ for (const auto & joint : {"joint1"})
+ {
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ // position limits will be limited than original range, as their range is limited in the
+ // ros2_control tags
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(1.0, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ }
hardware_info = control_hardware.at(1);
EXPECT_EQ(hardware_info.name, "RRBotModularJoint2");
+ EXPECT_EQ(hardware_info.group, "Hardware Group");
EXPECT_EQ(hardware_info.type, "actuator");
EXPECT_EQ(
hardware_info.hardware_plugin_name, "ros2_control_demo_hardware/PositionActuatorHardware");
@@ -294,6 +415,29 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_modular_robot
ASSERT_THAT(hardware_info.joints, SizeIs(1));
EXPECT_EQ(hardware_info.joints[0].name, "joint2");
EXPECT_EQ(hardware_info.joints[0].type, "joint");
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(1));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(1));
+ char joint2_name[] = "joint2";
+ for (const auto & joint : {"joint2"})
+ {
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ // position limits will be limited than original range, as their range is limited in the
+ // ros2_control tags
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(1.0, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ if (strcmp(joint, joint2_name) == 0)
+ {
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).max_position, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_position, DoubleNear(10.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_velocity, DoubleNear(20.0, 1e-5));
+ }
+ }
}
TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_modular_robot_with_sensors)
@@ -307,6 +451,7 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_modular_robot
auto hardware_info = control_hardware.at(0);
EXPECT_EQ(hardware_info.name, "RRBotModularJoint1");
+ EXPECT_EQ(hardware_info.group, "Hardware Group 1");
EXPECT_EQ(hardware_info.type, "actuator");
EXPECT_EQ(
hardware_info.hardware_plugin_name, "ros2_control_demo_hardware/VelocityActuatorHardware");
@@ -328,10 +473,25 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_modular_robot
hardware_info.transmissions[0].joints[0].mechanical_reduction, DoubleNear(1024.0 / M_PI, 0.01));
ASSERT_THAT(hardware_info.transmissions[0].actuators, SizeIs(1));
EXPECT_THAT(hardware_info.transmissions[0].actuators[0].name, "actuator1");
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(1));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(0));
+ for (const auto & joint : {"joint1"})
+ {
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(M_PI, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-M_PI, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ // effort and velocity limits won't change as they are above the main URDF hard limits
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ }
hardware_info = control_hardware.at(1);
EXPECT_EQ(hardware_info.name, "RRBotModularJoint2");
+ EXPECT_EQ(hardware_info.group, "Hardware Group 2");
EXPECT_EQ(hardware_info.type, "actuator");
EXPECT_EQ(
hardware_info.hardware_plugin_name, "ros2_control_demo_hardware/VelocityActuatorHardware");
@@ -345,10 +505,33 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_modular_robot
EXPECT_EQ(hardware_info.joints[0].command_interfaces[0].name, HW_IF_VELOCITY);
EXPECT_EQ(hardware_info.joints[0].command_interfaces[0].min, "-1");
EXPECT_EQ(hardware_info.joints[0].command_interfaces[0].max, "1");
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(1));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(1));
+ char joint2_name[] = "joint2";
+ for (const auto & joint : {"joint2"})
+ {
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(M_PI, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-M_PI, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ // effort and velocity limits won't change as they are above the main URDF hard limits
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ if (strcmp(joint, joint2_name) == 0)
+ {
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).max_position, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).min_position, DoubleNear(-1.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_position, DoubleNear(10.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_velocity, DoubleNear(20.0, 1e-5));
+ }
+ }
hardware_info = control_hardware.at(2);
EXPECT_EQ(hardware_info.name, "RRBotModularPositionSensorJoint1");
+ EXPECT_EQ(hardware_info.group, "Hardware Group 1");
EXPECT_EQ(hardware_info.type, "sensor");
EXPECT_EQ(
hardware_info.hardware_plugin_name, "ros2_control_demo_hardware/PositionSensorHardware");
@@ -362,10 +545,25 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_modular_robot
ASSERT_THAT(hardware_info.joints[0].command_interfaces, IsEmpty());
ASSERT_THAT(hardware_info.joints[0].state_interfaces, SizeIs(1));
EXPECT_EQ(hardware_info.joints[0].state_interfaces[0].name, HW_IF_POSITION);
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(1));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(0));
+ for (const auto & joint : {"joint1"})
+ {
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(M_PI, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-M_PI, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ // effort and velocity limits won't change as they are above the main URDF hard limits
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ }
hardware_info = control_hardware.at(3);
EXPECT_EQ(hardware_info.name, "RRBotModularPositionSensorJoint2");
+ EXPECT_EQ(hardware_info.group, "Hardware Group 2");
EXPECT_EQ(hardware_info.type, "sensor");
EXPECT_EQ(
hardware_info.hardware_plugin_name, "ros2_control_demo_hardware/PositionSensorHardware");
@@ -379,6 +577,27 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_modular_robot
ASSERT_THAT(hardware_info.joints[0].command_interfaces, IsEmpty());
ASSERT_THAT(hardware_info.joints[0].state_interfaces, SizeIs(1));
EXPECT_EQ(hardware_info.joints[0].state_interfaces[0].name, HW_IF_POSITION);
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(1));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(1));
+ for (const auto & joint : {"joint2"})
+ {
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(M_PI, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-M_PI, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ // effort and velocity limits won't change as they are above the main URDF hard limits
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ if (strcmp(joint, joint2_name) == 0)
+ {
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).max_position, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).min_position, DoubleNear(-1.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_position, DoubleNear(10.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_velocity, DoubleNear(20.0, 1e-5));
+ }
+ }
}
TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_multi_joints_transmission)
@@ -393,6 +612,7 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_multi_joints_tr
EXPECT_EQ(hardware_info.name, "RRBotModularWrist");
EXPECT_EQ(hardware_info.type, "system");
+ ASSERT_THAT(hardware_info.group, IsEmpty());
EXPECT_EQ(
hardware_info.hardware_plugin_name, "ros2_control_demo_hardware/ActuatorHardwareMultiDOF");
ASSERT_THAT(hardware_info.hardware_parameters, SizeIs(2));
@@ -435,6 +655,7 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_sensor_only)
EXPECT_EQ(hardware_info.name, "CameraWithIMU");
EXPECT_EQ(hardware_info.type, "sensor");
+ ASSERT_THAT(hardware_info.group, IsEmpty());
EXPECT_EQ(hardware_info.hardware_plugin_name, "ros2_control_demo_hardware/CameraWithIMUSensor");
ASSERT_THAT(hardware_info.hardware_parameters, SizeIs(1));
EXPECT_EQ(hardware_info.hardware_parameters.at("example_param_read_for_sec"), "2");
@@ -451,6 +672,9 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_sensor_only)
EXPECT_EQ(hardware_info.sensors[1].type, "sensor");
ASSERT_THAT(hardware_info.sensors[1].state_interfaces, SizeIs(1));
EXPECT_EQ(hardware_info.sensors[1].state_interfaces[0].name, "image");
+ // There will be no limits as the ros2_control tag has only sensor info
+ ASSERT_THAT(hardware_info.limits, SizeIs(0));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(0));
}
TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_only)
@@ -500,6 +724,18 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_actuator_only)
EXPECT_THAT(actuator.offset, DoubleEq(0.0));
ASSERT_THAT(transmission.parameters, SizeIs(1));
EXPECT_EQ(transmission.parameters.at("additional_special_parameter"), "1337");
+
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(1));
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_position_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_position, DoubleNear(M_PI, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").min_position, DoubleNear(-M_PI, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_effort_limits);
+ // effort and velocity limits won't change as they are above the main URDF hard limits
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_effort, DoubleNear(0.1, 1e-5));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(0));
}
TEST_F(TestComponentParser, successfully_parse_locale_independent_double)
@@ -568,6 +804,29 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_robot_with_gpio
EXPECT_EQ(hardware_info.gpios[1].command_interfaces[0].name, "vacuum");
EXPECT_THAT(hardware_info.transmissions, IsEmpty());
+
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(2));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(1));
+ char joint2_name[] = "joint2";
+ for (const auto & joint : {"joint1", "joint2"})
+ {
+ // Position limits are limited in the ros2_control tag
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(1.0, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ if (strcmp(joint, joint2_name) == 0)
+ {
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).max_position, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_position, DoubleNear(10.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at(joint).k_velocity, DoubleNear(20.0, 1e-5));
+ }
+ }
}
TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_with_size_and_data_type)
@@ -616,6 +875,366 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_with_size_and_d
EXPECT_EQ(hardware_info.gpios[0].state_interfaces[1].size, 1);
}
+TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_and_disabled_interfaces)
+{
+ std::string urdf_to_test =
+ std::string(ros2_control_test_assets::urdf_head) +
+ ros2_control_test_assets::
+ valid_urdf_ros2_control_system_robot_with_gpio_and_disabled_interface_limits +
+ ros2_control_test_assets::urdf_tail;
+ const auto control_hardware = parse_control_resources_from_urdf(urdf_to_test);
+ ASSERT_THAT(control_hardware, SizeIs(1));
+ auto hardware_info = control_hardware.front();
+
+ EXPECT_EQ(hardware_info.name, "RRBotSystemWithGPIO");
+ EXPECT_EQ(hardware_info.type, "system");
+ EXPECT_EQ(
+ hardware_info.hardware_plugin_name, "ros2_control_demo_hardware/RRBotSystemWithGPIOHardware");
+
+ ASSERT_THAT(hardware_info.joints, SizeIs(3));
+
+ EXPECT_EQ(hardware_info.joints[0].name, "joint1");
+ EXPECT_EQ(hardware_info.joints[0].type, "joint");
+ EXPECT_THAT(hardware_info.joints[0].command_interfaces, SizeIs(5));
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[0].name, HW_IF_POSITION);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[0].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[0].size, 1);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[1].name, HW_IF_VELOCITY);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[1].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[1].size, 1);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[2].name, HW_IF_EFFORT);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[2].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[2].size, 1);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[3].name, HW_IF_ACCELERATION);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[3].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[3].size, 1);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[4].name, "jerk");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[4].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[4].size, 1);
+ EXPECT_THAT(hardware_info.joints[0].state_interfaces, SizeIs(1));
+ EXPECT_EQ(hardware_info.joints[0].state_interfaces[0].name, HW_IF_POSITION);
+ EXPECT_EQ(hardware_info.joints[0].state_interfaces[0].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].state_interfaces[0].size, 1);
+
+ EXPECT_EQ(hardware_info.joints[1].name, "joint2");
+ EXPECT_EQ(hardware_info.joints[1].type, "joint");
+ EXPECT_THAT(hardware_info.joints[1].command_interfaces, SizeIs(5));
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[0].name, HW_IF_POSITION);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[0].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[0].size, 1);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[1].name, HW_IF_VELOCITY);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[1].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[1].size, 1);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[2].name, HW_IF_EFFORT);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[2].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[2].size, 1);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[3].name, HW_IF_ACCELERATION);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[3].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[3].size, 1);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[4].name, "jerk");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[4].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[4].size, 1);
+ EXPECT_THAT(hardware_info.joints[1].state_interfaces, SizeIs(1));
+ EXPECT_EQ(hardware_info.joints[1].state_interfaces[0].name, HW_IF_POSITION);
+ EXPECT_EQ(hardware_info.joints[1].state_interfaces[0].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].state_interfaces[0].size, 1);
+
+ EXPECT_EQ(hardware_info.joints[2].name, "joint3");
+ EXPECT_EQ(hardware_info.joints[2].type, "joint");
+ EXPECT_THAT(hardware_info.joints[2].command_interfaces, SizeIs(1));
+ EXPECT_EQ(hardware_info.joints[2].command_interfaces[0].name, HW_IF_ACCELERATION);
+ EXPECT_EQ(hardware_info.joints[2].command_interfaces[0].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[2].command_interfaces[0].size, 1);
+
+ ASSERT_THAT(hardware_info.gpios, SizeIs(1));
+
+ EXPECT_EQ(hardware_info.gpios[0].name, "flange_IOS");
+ EXPECT_EQ(hardware_info.gpios[0].type, "gpio");
+ EXPECT_THAT(hardware_info.gpios[0].command_interfaces, SizeIs(1));
+ EXPECT_EQ(hardware_info.gpios[0].command_interfaces[0].name, "digital_output");
+ EXPECT_EQ(hardware_info.gpios[0].command_interfaces[0].data_type, "bool");
+ EXPECT_EQ(hardware_info.gpios[0].command_interfaces[0].size, 2);
+ EXPECT_THAT(hardware_info.gpios[0].state_interfaces, SizeIs(2));
+ EXPECT_EQ(hardware_info.gpios[0].state_interfaces[0].name, "analog_input");
+ EXPECT_EQ(hardware_info.gpios[0].state_interfaces[0].data_type, "double");
+ EXPECT_EQ(hardware_info.gpios[0].state_interfaces[0].size, 3);
+ EXPECT_EQ(hardware_info.gpios[0].state_interfaces[1].name, "image");
+ EXPECT_EQ(hardware_info.gpios[0].state_interfaces[1].data_type, "cv::Mat");
+ EXPECT_EQ(hardware_info.gpios[0].state_interfaces[1].size, 1);
+
+ EXPECT_FALSE(hardware_info.limits.at("joint1").has_position_limits);
+ EXPECT_THAT(
+ hardware_info.limits.at("joint1").max_position,
+ DoubleNear(std::numeric_limits::max(), 1e-5));
+ EXPECT_THAT(
+ hardware_info.limits.at("joint1").min_position,
+ DoubleNear(-std::numeric_limits::max(), 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_effort_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_acceleration_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_deceleration_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_jerk_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_velocity, DoubleNear(0.05, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_effort, DoubleNear(0.1, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_acceleration, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_deceleration, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_jerk, DoubleNear(5.0, 1e-5));
+
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_position_limits);
+ EXPECT_THAT(
+ hardware_info.limits.at("joint2").max_position,
+ DoubleNear(std::numeric_limits::max(), 1e-5));
+ EXPECT_THAT(
+ hardware_info.limits.at("joint2").min_position,
+ DoubleNear(-std::numeric_limits::max(), 1e-5));
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_velocity_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_effort_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_acceleration_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_deceleration_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_jerk_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint2").max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint2").max_effort, DoubleNear(0.1, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at("joint2").max_position, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at("joint2").min_position, DoubleNear(-1.5, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at("joint2").k_position, DoubleNear(10.0, 1e-5));
+ EXPECT_THAT(hardware_info.soft_limits.at("joint2").k_velocity, DoubleNear(20.0, 1e-5));
+
+ EXPECT_TRUE(hardware_info.limits.at("joint3").has_position_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint3").min_position, DoubleNear(-M_PI, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint3").max_position, DoubleNear(M_PI, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at("joint3").has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint3").has_effort_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint3").has_acceleration_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint3").has_deceleration_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint3").has_jerk_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint3").max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint3").max_effort, DoubleNear(0.1, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint3").max_acceleration, DoubleNear(1.0, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint3").max_deceleration, DoubleNear(1.0, 1e-5));
+}
+
+TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_and_unavailable_interfaces)
+{
+ std::string urdf_to_test =
+ std::string(ros2_control_test_assets::urdf_head) +
+ ros2_control_test_assets::valid_urdf_ros2_control_system_robot_with_unavailable_interfaces +
+ ros2_control_test_assets::urdf_tail;
+ const auto control_hardware = parse_control_resources_from_urdf(urdf_to_test);
+ ASSERT_THAT(control_hardware, SizeIs(1));
+ auto hardware_info = control_hardware.front();
+
+ EXPECT_EQ(hardware_info.name, "RRBotSystemWithGPIO");
+ EXPECT_EQ(hardware_info.type, "system");
+ EXPECT_EQ(
+ hardware_info.hardware_plugin_name, "ros2_control_demo_hardware/RRBotSystemWithGPIOHardware");
+
+ ASSERT_THAT(hardware_info.joints, SizeIs(3));
+
+ EXPECT_EQ(hardware_info.joints[0].name, "joint1");
+ EXPECT_EQ(hardware_info.joints[0].type, "joint");
+ EXPECT_THAT(hardware_info.joints[0].command_interfaces, SizeIs(5));
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[0].name, HW_IF_POSITION);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[0].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[0].size, 1);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[1].name, HW_IF_VELOCITY);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[1].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[1].size, 1);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[2].name, HW_IF_EFFORT);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[2].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[2].size, 1);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[3].name, HW_IF_ACCELERATION);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[3].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[3].size, 1);
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[4].name, "jerk");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[4].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].command_interfaces[4].size, 1);
+ EXPECT_THAT(hardware_info.joints[0].state_interfaces, SizeIs(1));
+ EXPECT_EQ(hardware_info.joints[0].state_interfaces[0].name, HW_IF_POSITION);
+ EXPECT_EQ(hardware_info.joints[0].state_interfaces[0].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[0].state_interfaces[0].size, 1);
+
+ EXPECT_EQ(hardware_info.joints[1].name, "joint2");
+ EXPECT_EQ(hardware_info.joints[1].type, "joint");
+ EXPECT_THAT(hardware_info.joints[1].command_interfaces, SizeIs(5));
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[0].name, HW_IF_POSITION);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[0].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[0].size, 1);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[1].name, HW_IF_VELOCITY);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[1].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[1].size, 1);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[2].name, HW_IF_EFFORT);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[2].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[2].size, 1);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[3].name, HW_IF_ACCELERATION);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[3].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[3].size, 1);
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[4].name, "jerk");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[4].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].command_interfaces[4].size, 1);
+ EXPECT_THAT(hardware_info.joints[1].state_interfaces, SizeIs(1));
+ EXPECT_EQ(hardware_info.joints[1].state_interfaces[0].name, HW_IF_POSITION);
+ EXPECT_EQ(hardware_info.joints[1].state_interfaces[0].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[1].state_interfaces[0].size, 1);
+
+ EXPECT_EQ(hardware_info.joints[2].name, "joint3");
+ EXPECT_EQ(hardware_info.joints[2].type, "joint");
+ EXPECT_THAT(hardware_info.joints[2].command_interfaces, SizeIs(2));
+ EXPECT_EQ(hardware_info.joints[2].command_interfaces[0].name, HW_IF_ACCELERATION);
+ EXPECT_EQ(hardware_info.joints[2].command_interfaces[0].data_type, "double");
+ EXPECT_EQ(hardware_info.joints[2].command_interfaces[0].size, 1);
+
+ ASSERT_THAT(hardware_info.gpios, SizeIs(0));
+
+ EXPECT_FALSE(hardware_info.limits.at("joint1").has_position_limits);
+ EXPECT_THAT(
+ hardware_info.limits.at("joint1").max_position,
+ DoubleNear(std::numeric_limits::max(), 1e-5));
+ EXPECT_THAT(
+ hardware_info.limits.at("joint1").min_position,
+ DoubleNear(-std::numeric_limits::max(), 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_effort_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_acceleration_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_deceleration_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_jerk_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_velocity, DoubleNear(0.05, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_effort, DoubleNear(0.1, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_acceleration, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_deceleration, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_jerk, DoubleNear(5.0, 1e-5));
+
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_position_limits);
+ EXPECT_THAT(
+ hardware_info.limits.at("joint2").max_position,
+ DoubleNear(std::numeric_limits::max(), 1e-5));
+ EXPECT_THAT(
+ hardware_info.limits.at("joint2").min_position,
+ DoubleNear(-std::numeric_limits::max(), 1e-5));
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_velocity_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_effort_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_acceleration_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_deceleration_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_jerk_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint2").max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint2").max_effort, DoubleNear(0.1, 1e-5));
+
+ EXPECT_TRUE(hardware_info.limits.at("joint3").has_position_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint3").min_position, DoubleNear(-M_PI, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint3").max_position, DoubleNear(M_PI, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at("joint3").has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint3").has_effort_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint3").has_acceleration_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint3").has_deceleration_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint3").has_jerk_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint3").max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint3").max_effort, DoubleNear(0.1, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint3").max_acceleration, DoubleNear(1.0, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint3").max_deceleration, DoubleNear(1.0, 1e-5));
+}
+
+TEST_F(TestComponentParser, throw_on_parse_invalid_urdf_system_missing_limits)
+{
+ std::string urdf_to_test =
+ std::string(ros2_control_test_assets::urdf_head_revolute_missing_limits) +
+ ros2_control_test_assets::valid_urdf_ros2_control_system_one_interface +
+ ros2_control_test_assets::urdf_tail;
+ EXPECT_THROW(parse_control_resources_from_urdf(urdf_to_test), std::runtime_error);
+
+ urdf_to_test = std::string(ros2_control_test_assets::urdf_head_prismatic_missing_limits) +
+ ros2_control_test_assets::valid_urdf_ros2_control_system_one_interface +
+ ros2_control_test_assets::urdf_tail;
+ EXPECT_THROW(parse_control_resources_from_urdf(urdf_to_test), std::runtime_error);
+}
+
+TEST_F(TestComponentParser, throw_on_parse_urdf_system_with_command_fixed_joint)
+{
+ std::string urdf_to_test =
+ std::string(ros2_control_test_assets::urdf_head) +
+ ros2_control_test_assets::invalid_urdf_ros2_control_system_with_command_fixed_joint +
+ ros2_control_test_assets::urdf_tail;
+ EXPECT_THROW(parse_control_resources_from_urdf(urdf_to_test), std::runtime_error);
+}
+
+TEST_F(TestComponentParser, successfully_parse_urdf_system_continuous_missing_limits)
+{
+ std::string urdf_to_test =
+ std::string(ros2_control_test_assets::urdf_head_continuous_missing_limits) +
+ ros2_control_test_assets::valid_urdf_ros2_control_system_robot_with_all_interfaces +
+ ros2_control_test_assets::urdf_tail;
+ EXPECT_NO_THROW(parse_control_resources_from_urdf(urdf_to_test));
+
+ const auto control_hardware = parse_control_resources_from_urdf(urdf_to_test);
+ ASSERT_THAT(control_hardware, SizeIs(1));
+ auto hardware_info = control_hardware.front();
+
+ ASSERT_GT(hardware_info.limits.count("joint1"), 0);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_position_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint1").min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_position, DoubleNear(1.0, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_effort_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_acceleration_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_deceleration_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_jerk_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_velocity, DoubleNear(0.05, 1e-5))
+ << "velocity constraint from ros2_control_tag";
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_effort, DoubleNear(0.2, 1e-5))
+ << "effort constraint from ros2_control_tag";
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_acceleration, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_deceleration, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_jerk, DoubleNear(5.0, 1e-5));
+
+ ASSERT_GT(hardware_info.limits.count("joint2"), 0);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_position_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_velocity_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_effort_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_acceleration_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_deceleration_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_jerk_limits);
+}
+
+TEST_F(TestComponentParser, successfully_parse_urdf_system_continuous_with_limits)
+{
+ std::string urdf_to_test =
+ std::string(ros2_control_test_assets::urdf_head_continuous_with_limits) +
+ ros2_control_test_assets::valid_urdf_ros2_control_system_robot_with_all_interfaces +
+ ros2_control_test_assets::urdf_tail;
+ EXPECT_NO_THROW(parse_control_resources_from_urdf(urdf_to_test));
+
+ const auto control_hardware = parse_control_resources_from_urdf(urdf_to_test);
+ ASSERT_THAT(control_hardware, SizeIs(1));
+ auto hardware_info = control_hardware.front();
+
+ ASSERT_GT(hardware_info.limits.count("joint1"), 0);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_position_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint1").min_position, DoubleNear(-1.0, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_position, DoubleNear(1.0, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_effort_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_acceleration_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_deceleration_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint1").has_jerk_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_velocity, DoubleNear(0.05, 1e-5))
+ << "velocity URDF constraint overridden by ros2_control tag";
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_effort, DoubleNear(0.1, 1e-5))
+ << "effort constraint from URDF";
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_acceleration, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_deceleration, DoubleNear(0.5, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at("joint1").max_jerk, DoubleNear(5.0, 1e-5));
+
+ ASSERT_GT(hardware_info.limits.count("joint2"), 0);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_position_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint2").has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at("joint2").has_effort_limits);
+ EXPECT_THAT(hardware_info.limits.at("joint2").max_velocity, DoubleNear(0.2, 1e-5))
+ << "velocity constraint from URDF";
+ EXPECT_THAT(hardware_info.limits.at("joint2").max_effort, DoubleNear(0.1, 1e-5))
+ << "effort constraint from URDF";
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_acceleration_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_deceleration_limits);
+ EXPECT_FALSE(hardware_info.limits.at("joint2").has_jerk_limits);
+}
+
TEST_F(TestComponentParser, successfully_parse_parameter_empty)
{
const std::string urdf_to_test =
@@ -640,6 +1259,20 @@ TEST_F(TestComponentParser, successfully_parse_parameter_empty)
EXPECT_EQ(hardware_info.hardware_parameters.at("example_param_write_for_sec"), "");
EXPECT_EQ(hardware_info.hardware_parameters.at("example_param_read_for_sec"), "2");
+
+ // Verify limits parsed from the URDF
+ ASSERT_THAT(hardware_info.limits, SizeIs(1));
+ ASSERT_THAT(hardware_info.soft_limits, SizeIs(0));
+ for (const auto & joint : {"joint1"})
+ {
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_position_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_position, DoubleNear(M_PI, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).min_position, DoubleNear(-M_PI, 1e-5));
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_velocity_limits);
+ EXPECT_TRUE(hardware_info.limits.at(joint).has_effort_limits);
+ EXPECT_THAT(hardware_info.limits.at(joint).max_velocity, DoubleNear(0.2, 1e-5));
+ EXPECT_THAT(hardware_info.limits.at(joint).max_effort, DoubleNear(0.1, 1e-5));
+ }
}
TEST_F(TestComponentParser, negative_size_throws_error)
diff --git a/hardware_interface_testing/CHANGELOG.rst b/hardware_interface_testing/CHANGELOG.rst
index 58f6edbcdb..66d8794675 100644
--- a/hardware_interface_testing/CHANGELOG.rst
+++ b/hardware_interface_testing/CHANGELOG.rst
@@ -2,6 +2,17 @@
Changelog for package hardware_interface_testing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+4.11.0 (2024-05-14)
+-------------------
+
+4.10.0 (2024-05-08)
+-------------------
+
+4.9.0 (2024-04-30)
+------------------
+* Component parser: Get mimic information from URDF (`#1256 `_)
+* Contributors: Christoph Fröhlich
+
4.8.0 (2024-03-27)
------------------
diff --git a/hardware_interface_testing/package.xml b/hardware_interface_testing/package.xml
index 49968cfd56..b63e76ec6d 100644
--- a/hardware_interface_testing/package.xml
+++ b/hardware_interface_testing/package.xml
@@ -1,7 +1,7 @@
hardware_interface_testing
- 4.8.0
+ 4.11.0ros2_control hardware interface testingBence MagyarDenis Štogl
diff --git a/joint_limits/CHANGELOG.rst b/joint_limits/CHANGELOG.rst
index e318e1d3c0..12cfce3bd6 100644
--- a/joint_limits/CHANGELOG.rst
+++ b/joint_limits/CHANGELOG.rst
@@ -2,6 +2,18 @@
Changelog for package joint_limits
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+4.11.0 (2024-05-14)
+-------------------
+* Fix dependencies for source build (`#1533 `_)
+* Add find_package for ament_cmake_gen_version_h (`#1534 `_)
+* Contributors: Christoph Fröhlich
+
+4.10.0 (2024-05-08)
+-------------------
+
+4.9.0 (2024-04-30)
+------------------
+
4.8.0 (2024-03-27)
------------------
* generate version.h file per package using the ament_generate_version_header (`#1449 `_)
diff --git a/joint_limits/CMakeLists.txt b/joint_limits/CMakeLists.txt
index 1139a5248e..316a1ec7a2 100644
--- a/joint_limits/CMakeLists.txt
+++ b/joint_limits/CMakeLists.txt
@@ -13,6 +13,7 @@ set(THIS_PACKAGE_INCLUDE_DEPENDS
)
find_package(ament_cmake REQUIRED)
+find_package(ament_cmake_gen_version_h REQUIRED)
foreach(Dependency IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS})
find_package(${Dependency} REQUIRED)
endforeach()
@@ -61,4 +62,5 @@ install(TARGETS joint_limits
ament_export_targets(export_joint_limits HAS_LIBRARY_TARGET)
ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS})
ament_package()
-ament_generate_version_header(${PROJECT_NAME})
+# TODO(anyone) uncomment if https://github.com/ament/ament_cmake/pull/526 is merged
+# ament_generate_version_header(${PROJECT_NAME})
diff --git a/joint_limits/include/joint_limits/joint_limits.hpp b/joint_limits/include/joint_limits/joint_limits.hpp
index f9944a85b1..809bfd777b 100644
--- a/joint_limits/include/joint_limits/joint_limits.hpp
+++ b/joint_limits/include/joint_limits/joint_limits.hpp
@@ -128,11 +128,14 @@ struct SoftJointLimits
{
std::stringstream ss_output;
- ss_output << " soft position limits: " << "[" << min_position << ", " << max_position << "]\n";
+ ss_output << " soft position limits: "
+ << "[" << min_position << ", " << max_position << "]\n";
- ss_output << " k-position: " << "[" << k_position << "]\n";
+ ss_output << " k-position: "
+ << "[" << k_position << "]\n";
- ss_output << " k-velocity: " << "[" << k_velocity << "]\n";
+ ss_output << " k-velocity: "
+ << "[" << k_velocity << "]\n";
return ss_output.str();
}
diff --git a/joint_limits/package.xml b/joint_limits/package.xml
index a9a1d54dfd..aec1c44b76 100644
--- a/joint_limits/package.xml
+++ b/joint_limits/package.xml
@@ -1,6 +1,6 @@
joint_limits
- 4.8.0
+ 4.11.0Interfaces for handling of joint limits for controllers or hardware.Bence Magyar
@@ -13,12 +13,14 @@
https://github.com/ros-controls/ros2_controlament_cmake
- ament_cmake_gen_version_h
+
+
rclcpprclcpp_lifecycleurdf
+ launch_roslaunch_testing_ament_cmakeament_cmake_gtest
diff --git a/ros2_control-not-released.jazzy.repos b/ros2_control-not-released.jazzy.repos
new file mode 100644
index 0000000000..56f46b6f79
--- /dev/null
+++ b/ros2_control-not-released.jazzy.repos
@@ -0,0 +1 @@
+repositories:
diff --git a/ros2_control.jazzy.repos b/ros2_control.jazzy.repos
new file mode 100644
index 0000000000..c93d8f4ef6
--- /dev/null
+++ b/ros2_control.jazzy.repos
@@ -0,0 +1,9 @@
+repositories:
+ ros-controls/realtime_tools:
+ type: git
+ url: https://github.com/ros-controls/realtime_tools.git
+ version: master
+ ros-controls/control_msgs:
+ type: git
+ url: https://github.com/ros-controls/control_msgs.git
+ version: master
diff --git a/ros2_control/CHANGELOG.rst b/ros2_control/CHANGELOG.rst
index 1ee75204b3..c036c1c914 100644
--- a/ros2_control/CHANGELOG.rst
+++ b/ros2_control/CHANGELOG.rst
@@ -2,6 +2,15 @@
Changelog for package ros2_control
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+4.11.0 (2024-05-14)
+-------------------
+
+4.10.0 (2024-05-08)
+-------------------
+
+4.9.0 (2024-04-30)
+------------------
+
4.8.0 (2024-03-27)
------------------
diff --git a/ros2_control/package.xml b/ros2_control/package.xml
index e6716e478d..3941d9a17e 100644
--- a/ros2_control/package.xml
+++ b/ros2_control/package.xml
@@ -1,7 +1,7 @@
ros2_control
- 4.8.0
+ 4.11.0Metapackage for ROS2 control related packagesBence MagyarDenis Štogl
diff --git a/ros2_control_test_assets/CHANGELOG.rst b/ros2_control_test_assets/CHANGELOG.rst
index 43250ac2d4..1b475c5c40 100644
--- a/ros2_control_test_assets/CHANGELOG.rst
+++ b/ros2_control_test_assets/CHANGELOG.rst
@@ -2,6 +2,21 @@
Changelog for package ros2_control_test_assets
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+4.11.0 (2024-05-14)
+-------------------
+* Parse URDF soft_limits into the HardwareInfo structure (`#1488 `_)
+* Contributors: adriaroig
+
+4.10.0 (2024-05-08)
+-------------------
+* Parse URDF joint hard limits into the HardwareInfo structure (`#1472 `_)
+* Contributors: Sai Kishor Kothakota
+
+4.9.0 (2024-04-30)
+------------------
+* Component parser: Get mimic information from URDF (`#1256 `_)
+* Contributors: Christoph Fröhlich
+
4.8.0 (2024-03-27)
------------------
diff --git a/ros2_control_test_assets/include/ros2_control_test_assets/components_urdfs.hpp b/ros2_control_test_assets/include/ros2_control_test_assets/components_urdfs.hpp
index 7b2dd46e7a..eba16c1e71 100644
--- a/ros2_control_test_assets/include/ros2_control_test_assets/components_urdfs.hpp
+++ b/ros2_control_test_assets/include/ros2_control_test_assets/components_urdfs.hpp
@@ -172,6 +172,7 @@ const auto valid_urdf_ros2_control_actuator_modular_robot =
ros2_control_demo_hardware/PositionActuatorHardware
+ Hardware Group
1.23
3
@@ -186,6 +187,7 @@ const auto valid_urdf_ros2_control_actuator_modular_robot =
ros2_control_demo_hardware/PositionActuatorHardware
+ Hardware Group
1.23
3
@@ -206,6 +208,7 @@ const auto valid_urdf_ros2_control_actuator_modular_robot_sensors =
ros2_control_demo_hardware/VelocityActuatorHardware
+ Hardware Group 1
1.23
3
@@ -226,6 +229,7 @@ const auto valid_urdf_ros2_control_actuator_modular_robot_sensors =
ros2_control_demo_hardware/VelocityActuatorHardware
+ Hardware Group 2
1.23
3
@@ -240,6 +244,7 @@ const auto valid_urdf_ros2_control_actuator_modular_robot_sensors =
ros2_control_demo_hardware/PositionSensorHardware
+ Hardware Group 1
2
@@ -249,6 +254,7 @@ const auto valid_urdf_ros2_control_actuator_modular_robot_sensors =
ros2_control_demo_hardware/PositionSensorHardware
+ Hardware Group 2
2
@@ -400,6 +406,166 @@ const auto valid_urdf_ros2_control_system_robot_with_size_and_data_type =
)";
+// 12. Industrial Robots with integrated GPIO with few disabled limits in joints
+const auto valid_urdf_ros2_control_system_robot_with_gpio_and_disabled_interface_limits =
+ R"(
+
+
+ ros2_control_demo_hardware/RRBotSystemWithGPIOHardware
+ 2
+ 2
+
+
+
+
+ -1
+ 1
+
+
+ -0.05
+ 0.1
+
+
+ -0.2
+ 0.2
+
+
+ -0.5
+ 0.5
+
+
+ 5.0
+
+
+
+
+
+
+ -1
+ 1
+
+
+
+
+ -0.3
+ 0.3
+
+
+ -2.0
+ 2.0
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+)";
+const auto valid_urdf_ros2_control_system_robot_with_unavailable_interfaces =
+ R"(
+
+
+ ros2_control_demo_hardware/RRBotSystemWithGPIOHardware
+ 2
+ 2
+
+
+
+
+ -1
+ 1
+
+
+ -0.05
+ 0.1
+
+
+ -0.2
+ 0.2
+
+
+ -0.5
+ 0.5
+
+
+ 5.0
+
+
+
+
+
+
+ -1
+ 1
+
+
+
+
+ -0.3
+ 0.3
+
+
+
+
+
+
+ 1.0
+
+
+ -0.3
+ 0.3
+
+
+
+)";
+const auto valid_urdf_ros2_control_system_robot_with_all_interfaces =
+ R"(
+
+
+ ros2_control_demo_hardware/RRBotSystemWithGPIOHardware
+ 2
+ 2
+
+
+
+ -1
+ 1
+
+
+ -0.05
+ 0.1
+
+
+ -0.2
+ 0.2
+
+
+ -0.5
+ 0.5
+
+
+ 5.0
+
+
+
+
+
+
+
+
+
+
+
+
+)";
+
const auto valid_urdf_ros2_control_parameter_empty =
R"(
@@ -567,6 +733,21 @@ const auto invalid_urdf2_transmission_given_too_many_joints =
)";
+const auto invalid_urdf_ros2_control_system_with_command_fixed_joint =
+ R"(
+
+
+ ros2_control_demo_hardware/2DOF_System_Hardware_Position_Only
+ 2
+ 2
+
+
+
+
+
+
+)";
+
} // namespace ros2_control_test_assets
#endif // ROS2_CONTROL_TEST_ASSETS__COMPONENTS_URDFS_HPP_
diff --git a/ros2_control_test_assets/include/ros2_control_test_assets/descriptions.hpp b/ros2_control_test_assets/include/ros2_control_test_assets/descriptions.hpp
index 308751e0d8..31e630059b 100644
--- a/ros2_control_test_assets/include/ros2_control_test_assets/descriptions.hpp
+++ b/ros2_control_test_assets/include/ros2_control_test_assets/descriptions.hpp
@@ -23,10 +23,6 @@ namespace ros2_control_test_assets
const auto urdf_head =
R"(
-
-
-
-
@@ -90,6 +86,7 @@ const auto urdf_head =
+
@@ -149,13 +146,370 @@ const auto urdf_head =
)";
+
+const auto urdf_head_revolute_missing_limits =
+ R"(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)";
+
+const auto urdf_head_continuous_missing_limits =
+ R"(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)";
+
+const auto urdf_head_continuous_with_limits =
+ R"(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)";
+
+const auto urdf_head_prismatic_missing_limits =
+ R"(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)";
+
const auto urdf_head_mimic =
R"(
-
-
-
-
@@ -503,6 +857,7 @@ const auto diffbot_urdf =
+
@@ -538,6 +893,7 @@ const auto diffbot_urdf =
+
diff --git a/ros2_control_test_assets/package.xml b/ros2_control_test_assets/package.xml
index ecf8d57490..4ed6912f83 100644
--- a/ros2_control_test_assets/package.xml
+++ b/ros2_control_test_assets/package.xml
@@ -2,7 +2,7 @@
ros2_control_test_assets
- 4.8.0
+ 4.11.0The package provides shared test resources for ros2_control stack
Bence Magyar
diff --git a/ros2controlcli/CHANGELOG.rst b/ros2controlcli/CHANGELOG.rst
index 19ff351c64..52ad767224 100644
--- a/ros2controlcli/CHANGELOG.rst
+++ b/ros2controlcli/CHANGELOG.rst
@@ -2,6 +2,18 @@
Changelog for package ros2controlcli
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+4.11.0 (2024-05-14)
+-------------------
+
+4.10.0 (2024-05-08)
+-------------------
+
+4.9.0 (2024-04-30)
+------------------
+* [CI] Specify runner/container images and codecov for joint_limits (`#1504 `_)
+* [CLI] Add `set_hardware_component_state` verb (`#1248 `_)
+* Contributors: Christoph Fröhlich
+
4.8.0 (2024-03-27)
------------------
diff --git a/ros2controlcli/package.xml b/ros2controlcli/package.xml
index c8c9ae3dc7..d37c053413 100644
--- a/ros2controlcli/package.xml
+++ b/ros2controlcli/package.xml
@@ -2,7 +2,7 @@
ros2controlcli
- 4.8.0
+ 4.11.0
The ROS 2 command line tools for ROS2 Control.
diff --git a/ros2controlcli/ros2controlcli/api/__init__.py b/ros2controlcli/ros2controlcli/api/__init__.py
index c1a3d8cb5d..2b4b805980 100644
--- a/ros2controlcli/ros2controlcli/api/__init__.py
+++ b/ros2controlcli/ros2controlcli/api/__init__.py
@@ -28,7 +28,7 @@ def service_caller(service_name, service_type, request):
try:
rclpy.init()
- node = rclpy.create_node(f"ros2controlcli_{ service_name.replace('/', '') }_requester")
+ node = rclpy.create_node(f"ros2controlcli_{service_name.replace('/', '')}_requester")
cli = node.create_client(service_type, service_name)
@@ -38,14 +38,14 @@ def service_caller(service_name, service_type, request):
if not cli.wait_for_service(2.0):
raise RuntimeError(f"Could not contact service {service_name}")
- node.get_logger().debug(f"requester: making request: { repr(request) }\n")
+ node.get_logger().debug(f"requester: making request: {repr(request)}\n")
future = cli.call_async(request)
rclpy.spin_until_future_complete(node, future)
if future.result() is not None:
return future.result()
else:
future_exception = future.exception()
- raise RuntimeError(f"Exception while calling service: { repr(future_exception) }")
+ raise RuntimeError(f"Exception while calling service: {repr(future_exception)}")
finally:
node.destroy_node()
rclpy.shutdown()
diff --git a/ros2controlcli/ros2controlcli/verb/load_controller.py b/ros2controlcli/ros2controlcli/verb/load_controller.py
index babd6d2307..b5a155de94 100644
--- a/ros2controlcli/ros2controlcli/verb/load_controller.py
+++ b/ros2controlcli/ros2controlcli/verb/load_controller.py
@@ -59,6 +59,6 @@ def main(self, *, args):
print(
f"Successfully loaded controller {args.controller_name} into "
- f'state { "inactive" if args.set_state == "inactive" else "active" }'
+ f'state {"inactive" if args.set_state == "inactive" else "active"}'
)
return 0
diff --git a/ros2controlcli/setup.py b/ros2controlcli/setup.py
index 441b7ea601..a0ee818309 100644
--- a/ros2controlcli/setup.py
+++ b/ros2controlcli/setup.py
@@ -19,7 +19,7 @@
setup(
name=package_name,
- version="4.8.0",
+ version="4.11.0",
packages=find_packages(exclude=["test"]),
data_files=[
("share/" + package_name, ["package.xml"]),
diff --git a/rqt_controller_manager/CHANGELOG.rst b/rqt_controller_manager/CHANGELOG.rst
index a96091ae2a..a82b6e708f 100644
--- a/rqt_controller_manager/CHANGELOG.rst
+++ b/rqt_controller_manager/CHANGELOG.rst
@@ -2,6 +2,15 @@
Changelog for package rqt_controller_manager
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+4.11.0 (2024-05-14)
+-------------------
+
+4.10.0 (2024-05-08)
+-------------------
+
+4.9.0 (2024-04-30)
+------------------
+
4.8.0 (2024-03-27)
------------------
* Fix rqt_controller_manager for non-humble (`#1454 `_)
diff --git a/rqt_controller_manager/package.xml b/rqt_controller_manager/package.xml
index 61b92653a9..e2866496ab 100644
--- a/rqt_controller_manager/package.xml
+++ b/rqt_controller_manager/package.xml
@@ -2,7 +2,7 @@
rqt_controller_manager
- 4.8.0
+ 4.11.0Graphical frontend for interacting with the controller manager.Bence MagyarDenis Štogl
diff --git a/rqt_controller_manager/setup.py b/rqt_controller_manager/setup.py
index c314d30353..1f23378eba 100644
--- a/rqt_controller_manager/setup.py
+++ b/rqt_controller_manager/setup.py
@@ -20,7 +20,7 @@
setup(
name=package_name,
- version="4.8.0",
+ version="4.11.0",
packages=[package_name],
data_files=[
("share/ament_index/resource_index/packages", ["resource/" + package_name]),
diff --git a/transmission_interface/CHANGELOG.rst b/transmission_interface/CHANGELOG.rst
index 70ac550955..44c85099dd 100644
--- a/transmission_interface/CHANGELOG.rst
+++ b/transmission_interface/CHANGELOG.rst
@@ -2,6 +2,20 @@
Changelog for package transmission_interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+4.11.0 (2024-05-14)
+-------------------
+* Add find_package for ament_cmake_gen_version_h (`#1534 `_)
+* Contributors: Christoph Fröhlich
+
+4.10.0 (2024-05-08)
+-------------------
+
+4.9.0 (2024-04-30)
+------------------
+* rosdoc2 for transmission_interface (`#1496 `_)
+* Component parser: Get mimic information from URDF (`#1256 `_)
+* Contributors: Christoph Fröhlich
+
4.8.0 (2024-03-27)
------------------
* generate version.h file per package using the ament_generate_version_header (`#1449 `_)
diff --git a/transmission_interface/CMakeLists.txt b/transmission_interface/CMakeLists.txt
index 98dcdcd192..185fb32d3a 100644
--- a/transmission_interface/CMakeLists.txt
+++ b/transmission_interface/CMakeLists.txt
@@ -13,6 +13,7 @@ set(THIS_PACKAGE_INCLUDE_DEPENDS
)
find_package(ament_cmake REQUIRED)
+find_package(ament_cmake_gen_version_h REQUIRED)
foreach(Dependency IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS})
find_package(${Dependency} REQUIRED)
endforeach()
diff --git a/transmission_interface/Doxyfile b/transmission_interface/Doxyfile
new file mode 100644
index 0000000000..db5b9c7e6f
--- /dev/null
+++ b/transmission_interface/Doxyfile
@@ -0,0 +1,2661 @@
+# Doxyfile 1.9.1
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the configuration
+# file that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# https://www.gnu.org/software/libiconv/ for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME = "ros2_control transmission_interface"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF =
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO =
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY = doc/_build/
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE = English
+
+# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all generated output in the proper direction.
+# Possible values are: None, LTR, RTL and Context.
+# The default value is: None.
+
+OUTPUT_TEXT_DIRECTION = None
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF = "The $name class" \
+ "The $name widget" \
+ "The $name file" \
+ is \
+ provides \
+ specifies \
+ contains \
+ represents \
+ a \
+ an \
+ the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF = NO
+
+# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
+# such as
+# /***************
+# as being the beginning of a Javadoc-style comment "banner". If set to NO, the
+# Javadoc-style will behave just like regular comments and it will not be
+# interpreted by doxygen.
+# The default value is: NO.
+
+JAVADOC_BANNER = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# By default Python docstrings are displayed as preformatted text and doxygen's
+# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the
+# doxygen's special commands can be used and the contents of the docstring
+# documentation blocks is shown as doxygen documentation.
+# The default value is: YES.
+
+PYTHON_DOCSTRING = YES
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE = 4
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines (in the resulting output). You can put ^^ in the value part of an
+# alias to insert a newline as if a physical newline was in the original file.
+# When you need a literal { or } or , in the value part of an alias you have to
+# escape them by means of a backslash (\), this can lead to conflicts with the
+# commands \{ and \} for these it is advised to use the version @{ and @} or use
+# a double escape (\\{ and \\})
+
+ALIASES =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL = NO
+
+# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice
+# sources only. Doxygen will then generate output that is more tailored for that
+# language. For instance, namespaces will be presented as modules, types will be
+# separated into more groups, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_SLICE = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
+# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL,
+# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
+# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
+# tries to guess whether the code is fixed or free formatted code, this is the
+# default for Fortran type files). For instance to make doxygen treat .inc files
+# as Fortran files (default is PHP), and .f files as C (default is Fortran),
+# use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen. When specifying no_extension you should add
+# * to the FILE_PATTERNS.
+#
+# Note see also the list of default file extension mappings.
+
+EXTENSION_MAPPING =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See https://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 5.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS = 5
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE = 0
+
+# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use
+# during processing. When set to 0 doxygen will based this on the number of
+# cores available in the system. You can set it explicitly to a value larger
+# than 0 to get more control over the balance between CPU load and processing
+# speed. At this moment only the input processing can be done using multiple
+# threads. Since this is still an experimental feature the default is set to 1,
+# which efficively disables parallel processing. Please report any issues you
+# encounter. Generating dot graphs in parallel is controlled by the
+# DOT_NUM_THREADS setting.
+# Minimum value: 0, maximum value: 32, default value: 1.
+
+NUM_PROC_THREADS = 1
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE = NO
+
+# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual
+# methods of a class will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIV_VIRTUAL = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE = NO
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES = NO
+
+# If this flag is set to YES, the name of an unnamed parameter in a declaration
+# will be determined by the corresponding definition. By default unnamed
+# parameters remain unnamed in the output.
+# The default value is: YES.
+
+RESOLVE_UNNAMED_PARAMS = YES
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# declarations. If set to NO, these declarations will be included in the
+# documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS = NO
+
+# With the correct setting of option CASE_SENSE_NAMES doxygen will better be
+# able to match the capabilities of the underlying filesystem. In case the
+# filesystem is case sensitive (i.e. it supports files in the same directory
+# whose names only differ in casing), the option must be set to YES to properly
+# deal with such files in case they appear in the input. For filesystems that
+# are not case sensitive the option should be be set to NO to properly deal with
+# output files written for symbols that only differ in casing, such as for two
+# classes, one named CLASS and the other named Class, and to also support
+# references to files without having to specify the exact matching casing. On
+# Windows (including Cygwin) and MacOS, users should typically set this option
+# to NO, whereas on Linux or other Unix flavors it should typically be set to
+# YES.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if ... \endif and \cond
+# ... \endcond blocks.
+
+ENABLED_SECTIONS =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation. If
+# EXTRACT_ALL is set to YES then this flag will automatically be disabled.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC = NO
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
+# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
+# at the end of the doxygen process doxygen will return with a non-zero status.
+# Possible values are: NO, YES and FAIL_ON_WARNINGS.
+# The default value is: NO.
+
+WARN_AS_ERROR = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT = README.md \
+ .
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see:
+# https://www.gnu.org/software/libiconv/) for the list of possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# Note the list of default checked file patterns might differ from the list of
+# default file extension mappings.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),
+# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl,
+# *.ucf, *.qsf and *.ice.
+
+FILE_PATTERNS = *.c \
+ *.cc \
+ *.cxx \
+ *.cpp \
+ *.c++ \
+ *.java \
+ *.ii \
+ *.ixx \
+ *.ipp \
+ *.i++ \
+ *.inl \
+ *.idl \
+ *.ddl \
+ *.odl \
+ *.h \
+ *.hh \
+ *.hxx \
+ *.hpp \
+ *.h++ \
+ *.cs \
+ *.d \
+ *.php \
+ *.php4 \
+ *.php5 \
+ *.phtml \
+ *.inc \
+ *.m \
+ *.markdown \
+ *.md \
+ *.mm \
+ *.dox \
+ *.doc \
+ *.txt \
+ *.py \
+ *.pyw \
+ *.f90 \
+ *.f95 \
+ *.f03 \
+ *.f08 \
+ *.f \
+ *.for \
+ *.tcl \
+ *.vhd \
+ *.vhdl \
+ *.ucf \
+ *.qsf \
+ *.ice
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS = */test/* */doc/*
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS = *
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH = images
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+#
+#
+# where is the value of the INPUT_FILTER tag, and is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE = README.md
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# entity all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see https://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS = YES
+
+# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the
+# clang parser (see:
+# http://clang.llvm.org/) for more accurate parsing at the cost of reduced
+# performance. This can be particularly helpful with template rich C++ code for
+# which doxygen's built-in parser lacks the necessary type information.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse_libclang=ON option for CMake.
+# The default value is: NO.
+
+CLANG_ASSISTED_PARSING = NO
+
+# If clang assisted parsing is enabled and the CLANG_ADD_INC_PATHS tag is set to
+# YES then doxygen will add the directory of each input to the include path.
+# The default value is: YES.
+
+CLANG_ADD_INC_PATHS = YES
+
+# If clang assisted parsing is enabled you can provide the compiler with command
+# line options that you would normally use when invoking the compiler. Note that
+# the include paths will already be set by doxygen for the files and directories
+# specified with INPUT and INCLUDE_PATH.
+# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.
+
+CLANG_OPTIONS =
+
+# If clang assisted parsing is enabled you can provide the clang parser with the
+# path to the directory containing a file called compile_commands.json. This
+# file is the compilation database (see:
+# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the
+# options used when the source files were built. This is equivalent to
+# specifying the -p option to a clang tool, such as clang-check. These options
+# will then be passed to the parser. Any options specified with CLANG_OPTIONS
+# will be added as well.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse_libclang=ON option for CMake.
+
+CLANG_DATABASE_PATH =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX = YES
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP = NO
+
+# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
+# documentation will contain a main index with vertical navigation menus that
+# are dynamically created via JavaScript. If disabled, the navigation index will
+# consists of multiple levels of tabs that are statically embedded in every HTML
+# page. Disable this option to support browsers that do not have JavaScript,
+# like the Qt help browser.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_MENUS = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see:
+# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To
+# create a documentation set, doxygen will generate a Makefile in the HTML
+# output directory. Running make will produce the docset in that directory and
+# running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy
+# genXcode/_index.html for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see:
+# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the main .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS =
+
+# The QHG_LOCATION tag can be used to specify the location (absolute path
+# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to
+# run qhelpgenerator on the generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW = NO
+
+# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
+# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
+# https://inkscape.org) to generate formulas as SVG images instead of PNGs for
+# the HTML output. These images will generally look nicer at scaled resolutions.
+# Possible values are: png (the default) and svg (looks nicer but requires the
+# pdf2svg or inkscape tool).
+# The default value is: png.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FORMULA_FORMAT = png
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE = 10
+
+# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT = YES
+
+# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
+# to create new LaTeX commands to be used in formulas as building blocks. See
+# the section "Including formulas" for details.
+
+FORMULA_MACROFILE =
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# https://www.mathjax.org) which uses client side JavaScript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from https://www.mathjax.org before deployment.
+# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH = https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see:
+# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use + S
+# (what the is depends on the OS and browser, but it is typically
+# , /