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.0 Description of controller_interface Bence Magyar Denis Š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.0 Description of controller_manager Bence Magyar Denis Š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.0 Messages and services for the controller manager. Bence Magyar Denis Š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.0 ros2_control hardware interface Bence Magyar Denis Štogl @@ -16,6 +16,7 @@ rclcpp_lifecycle rcpputils tinyxml2_vendor + joint_limits urdf rcutils 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.0 ros2_control hardware interface testing Bence Magyar Denis Š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.0 Interfaces for handling of joint limits for controllers or hardware. Bence Magyar @@ -13,12 +13,14 @@ https://github.com/ros-controls/ros2_control ament_cmake - ament_cmake_gen_version_h + + rclcpp rclcpp_lifecycle urdf + launch_ros launch_testing_ament_cmake ament_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.0 Metapackage for ROS2 control related packages Bence Magyar Denis Š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.0 The 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.0 Graphical frontend for interacting with the controller manager. Bence Magyar Denis Š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 +# , /