diff --git a/.github/reviewer-lottery.yml b/.github/reviewer-lottery.yml deleted file mode 100644 index 2da79efd9a..0000000000 --- a/.github/reviewer-lottery.yml +++ /dev/null @@ -1,32 +0,0 @@ -groups: - # Default reviewers for all pull requests. - # Usually, at least on of the maintainers should approve PR before merging. - # The best is if two maintainers do that. - - name: maintainers # name of the group - reviewers: 2 # how many reviewers do you want to assign? - internal_reviewers: 1 # how many reviewers do you want to assign when the PR author belongs to this group? - usernames: # github usernames of the reviewers - - bmagyar - - destogl - - # Reviewers group to get broader feedback. - - name: reviewers - reviewers: 5 - usernames: - - aprotyas - - arne48 - - bijoua29 - - christophfroehlich - - DasRoteSkelett - - erickisos - - fmauch - - jaron-l - - livanov93 - - mcbed - - moriarty - - olivier-stasse - - peterdavidfagan - - progtologist - - saikishor - - VanshGehlot - - VX792 diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 5e8c8ef0fd..62007ffc2d 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,6 +1,7 @@ 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-main.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-binary-build-main.yml?branch=master)
[![Rolling Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-binary-build-testing.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-binary-build-testing.yml?branch=master)
[![Rolling Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-semi-binary-build-main.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-semi-binary-build-main.yml?branch=master)
[![Rolling Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-semi-binary-build-testing.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-semi-binary-build-testing.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) | [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) -**Iron** | [`master`](https://github.com/ros-controls/ros2_control/tree/master) | [![Iron Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-binary-build-main.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-binary-build-main.yml?branch=master)
[![Iron Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-binary-build-testing.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-binary-build-testing.yml?branch=master)
[![Iron Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-semi-binary-build-main.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-semi-binary-build-main.yml?branch=master)
[![Iron Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-semi-binary-build-testing.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-semi-binary-build-testing.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) | [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/#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-main.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-binary-build-main.yml?branch=master)
[![Humble Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/humble-binary-build-testing.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-binary-build-testing.yml?branch=master)
[![Humble Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/humble-semi-binary-build-main.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-semi-binary-build-main.yml?branch=master)
[![Humble Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/humble-semi-binary-build-testing.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-semi-binary-build-testing.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) | [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) +**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/ci-coverage-build.yml b/.github/workflows/ci-coverage-build.yml deleted file mode 100644 index 171a313fc0..0000000000 --- a/.github/workflows/ci-coverage-build.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Coverage Build -on: - workflow_dispatch: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - coverage: - name: coverage build - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - env: - ROS_DISTRO: rolling - steps: - - uses: ros-tooling/setup-ros@0.7.1 - with: - required-ros-distributions: ${{ env.ROS_DISTRO }} - - uses: actions/checkout@v4 - - uses: ros-tooling/action-ros-ci@0.3.6 - with: - target-ros2-distro: ${{ env.ROS_DISTRO }} - import-token: ${{ secrets.GITHUB_TOKEN }} - # build all packages listed in the meta package - package-name: - controller_interface - controller_manager - hardware_interface - hardware_interface_testing - transmission_interface - - vcs-repo-file-url: | - https://raw.githubusercontent.com/${{ github.repository }}/${{ github.sha }}/ros2_control-not-released.${{ env.ROS_DISTRO }}.repos?token=${{ secrets.GITHUB_TOKEN }} - colcon-defaults: | - { - "build": { - "mixin": ["coverage-gcc"] - } - } - colcon-mixin-repository: https://raw.githubusercontent.com/colcon/colcon-mixin-repository/master/index.yaml - - uses: codecov/codecov-action@v3.1.5 - with: - file: ros_ws/lcov/total_coverage.info - flags: unittests - name: codecov-umbrella - - uses: actions/upload-artifact@v4.3.0 - with: - name: colcon-logs-ubuntu-22.04-coverage-rolling - path: ros_ws/log diff --git a/.github/workflows/ci-format.yml b/.github/workflows/ci-format.yml deleted file mode 100644 index c7199b88c5..0000000000 --- a/.github/workflows/ci-format.yml +++ /dev/null @@ -1,23 +0,0 @@ -# This is a format job. Pre-commit has a first-party GitHub action, so we use -# that: https://github.com/pre-commit/action - -name: Format - -on: - workflow_dispatch: - pull_request: - -jobs: - pre-commit: - name: Format - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5.0.0 - with: - python-version: '3.10' - - name: Install system hooks - run: sudo apt install -qq cppcheck - - uses: pre-commit/action@v3.0.0 - with: - extra_args: --all-files --hook-stage manual diff --git a/.github/workflows/ci-ros-lint.yml b/.github/workflows/ci-ros-lint.yml deleted file mode 100644 index 52da5edfe4..0000000000 --- a/.github/workflows/ci-ros-lint.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: ROS Lint -on: - pull_request: - -jobs: - ament_lint: - name: ament_${{ matrix.linter }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - linter: [cppcheck, copyright, lint_cmake] - env: - AMENT_CPPCHECK_ALLOW_SLOW_VERSIONS: true - steps: - - uses: actions/checkout@v4 - - uses: ros-tooling/setup-ros@0.7.1 - - uses: ros-tooling/action-ros-lint@v0.1 - with: - distribution: rolling - linter: ${{ matrix.linter }} - package-name: - controller_interface - controller_manager - controller_manager_msgs - hardware_interface - hardware_interface_testing - ros2controlcli - ros2_control - ros2_control_test_assets - transmission_interface - - ament_lint_100: - name: ament_${{ matrix.linter }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - linter: [cpplint] - steps: - - uses: actions/checkout@v4 - - uses: ros-tooling/setup-ros@0.7.1 - - uses: ros-tooling/action-ros-lint@v0.1 - with: - distribution: rolling - linter: cpplint - arguments: "--linelength=100 --filter=-whitespace/newline" - package-name: - controller_interface - controller_manager - controller_manager_msgs - hardware_interface - hardware_interface_testing - ros2controlcli - ros2_control - ros2_control_test_assets - transmission_interface diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f5015d8a90..70366d4033 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -42,7 +42,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} - name: Build and push Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . push: true @@ -82,7 +82,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} - name: Build and push Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . push: true 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-main.yml b/.github/workflows/humble-binary-build-main.yml deleted file mode 100644 index a438d7a9e5..0000000000 --- a/.github/workflows/humble-binary-build-main.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Humble Binary Build - main -# author: Denis Štogl -# description: 'Build & test all dependencies from released (binary) packages.' - -on: - workflow_dispatch: - branches: - - humble - pull_request: - branches: - - humble - push: - branches: - - humble - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '03 1 * * *' - -jobs: - binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: humble - ros_repo: main - upstream_workspace: ros2_control-not-released.humble.repos - ref_for_scheduled_build: humble diff --git a/.github/workflows/humble-binary-build-testing.yml b/.github/workflows/humble-binary-build-testing.yml deleted file mode 100644 index 5a61534d4e..0000000000 --- a/.github/workflows/humble-binary-build-testing.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Humble Binary Build - testing -# author: Denis Štogl -# description: 'Build & test all dependencies from released (binary) packages.' - -on: - workflow_dispatch: - branches: - - humble - pull_request: - branches: - - humble - push: - branches: - - humble - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '03 1 * * *' - -jobs: - binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: humble - ros_repo: testing - upstream_workspace: ros2_control-not-released.humble.repos - ref_for_scheduled_build: humble diff --git a/.github/workflows/humble-binary-build.yml b/.github/workflows/humble-binary-build.yml new file mode 100644 index 0000000000..6392ba8e68 --- /dev/null +++ b/.github/workflows/humble-binary-build.yml @@ -0,0 +1,47 @@ +name: Humble Binary Build +# author: Denis Štogl +# description: 'Build & test all dependencies from released (binary) packages.' + +on: + workflow_dispatch: + 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 * * *' + +jobs: + binary: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-industrial-ci-with-cache.yml@master + strategy: + fail-fast: false + matrix: + ROS_DISTRO: [humble] + 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: humble 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 new file mode 100644 index 0000000000..209c931d4e --- /dev/null +++ b/.github/workflows/humble-coverage-build.yml @@ -0,0 +1,36 @@ +name: Coverage Build - Humble +on: + workflow_dispatch: + 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: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-build-coverage.yml@master + secrets: inherit + with: + ros_distro: humble diff --git a/.github/workflows/humble-debian-build.yml b/.github/workflows/humble-debian-build.yml index 0db1f58210..85a1b38241 100644 --- a/.github/workflows/humble-debian-build.yml +++ b/.github/workflows/humble-debian-build.yml @@ -1,30 +1,33 @@ -name: Debian Humble Build +name: Debian Humble Source Build on: workflow_dispatch: 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 * * *' jobs: - humble_debian: - name: Humble debian build - runs-on: ubuntu-latest - env: - ROS_DISTRO: humble - container: ghcr.io/ros-controls/ros:humble-debian - steps: - - uses: actions/checkout@v4 - with: - path: src/ros2_control - - name: Build and test - shell: bash - run: | - source /opt/ros2_ws/install/setup.bash - vcs import src < src/ros2_control/ros2_control.${{ env.ROS_DISTRO }}.repos - colcon build --packages-skip rqt_controller_manager - colcon test --packages-skip rqt_controller_manager control_msgs controller_manager_msgs - colcon test-result --verbose + debian_source_build: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-debian-build.yml@master + strategy: + fail-fast: false + matrix: + ROS_DISTRO: [humble] + with: + ros_distro: ${{ matrix.ROS_DISTRO }} + upstream_workspace: ros2_control.${{ matrix.ROS_DISTRO }}.repos + ref_for_scheduled_build: master + skip_packages: rqt_controller_manager + skip_packages_test: controller_manager_msgs diff --git a/.github/workflows/humble-pre-commit.yml b/.github/workflows/humble-pre-commit.yml new file mode 100644 index 0000000000..5bb2408578 --- /dev/null +++ b/.github/workflows/humble-pre-commit.yml @@ -0,0 +1,13 @@ +name: Pre-Commit - Humble + +on: + workflow_dispatch: + pull_request: + branches: + - humble + +jobs: + pre-commit: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-pre-commit.yml@master + with: + ros_distro: humble diff --git a/.github/workflows/humble-rhel-binary-build.yml b/.github/workflows/humble-rhel-binary-build.yml index ed37092520..62c5cd62ba 100644 --- a/.github/workflows/humble-rhel-binary-build.yml +++ b/.github/workflows/humble-rhel-binary-build.yml @@ -1,31 +1,31 @@ -name: RHEL Humble Binary Build +name: RHEL Humble Semi-Binary Build on: workflow_dispatch: 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 * * *' jobs: - humble_rhel_binary: - name: Humble RHEL binary build - runs-on: ubuntu-latest - env: - ROS_DISTRO: humble - container: ghcr.io/ros-controls/ros:humble-rhel - steps: - - uses: actions/checkout@v4 - with: - path: src/ros2_control - - name: Install dependencies - run: | - rosdep update - rosdep install -iyr --from-path src/ros2_control || true - - name: Build and test - run: | - source /opt/ros/${{ env.ROS_DISTRO }}/setup.bash - colcon build --packages-skip rqt_controller_manager - colcon test --packages-skip rqt_controller_manager ros2controlcli - colcon test-result --verbose + 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: [humble] + with: + ros_distro: ${{ matrix.ROS_DISTRO }} + upstream_workspace: ros2_control.${{ matrix.ROS_DISTRO }}.repos + ref_for_scheduled_build: humble + skip_packages: rqt_controller_manager diff --git a/.github/workflows/humble-semi-binary-build-main.yml b/.github/workflows/humble-semi-binary-build-main.yml deleted file mode 100644 index 539477a302..0000000000 --- a/.github/workflows/humble-semi-binary-build-main.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Humble Semi-Binary Build - main -# description: 'Build & test that compiles the main dependencies from source.' - -on: - workflow_dispatch: - branches: - - humble - pull_request: - branches: - - humble - push: - branches: - - humble - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '33 1 * * *' - -jobs: - semi_binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: humble - ros_repo: main - upstream_workspace: ros2_control.humble.repos - ref_for_scheduled_build: humble diff --git a/.github/workflows/humble-semi-binary-build-testing.yml b/.github/workflows/humble-semi-binary-build-testing.yml deleted file mode 100644 index 889822e7f2..0000000000 --- a/.github/workflows/humble-semi-binary-build-testing.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Humble Semi-Binary Build - testing -# description: 'Build & test that compiles the main dependencies from source.' - -on: - workflow_dispatch: - branches: - - humble - pull_request: - branches: - - humble - push: - branches: - - humble - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '33 1 * * *' - -jobs: - semi_binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: humble - ros_repo: testing - upstream_workspace: ros2_control.humble.repos - ref_for_scheduled_build: humble diff --git a/.github/workflows/humble-semi-binary-build.yml b/.github/workflows/humble-semi-binary-build.yml new file mode 100644 index 0000000000..560ac37cff --- /dev/null +++ b/.github/workflows/humble-semi-binary-build.yml @@ -0,0 +1,47 @@ +name: Humble Semi-Binary Build +# author: Denis Štogl +# description: 'Build & test all dependencies from released (binary) packages.' + +on: + workflow_dispatch: + 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 * * *' + +jobs: + binary: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-industrial-ci-with-cache.yml@master + strategy: + fail-fast: false + matrix: + ROS_DISTRO: [humble] + 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: humble diff --git a/.github/workflows/humble-source-build.yml b/.github/workflows/humble-source-build.yml index ff0fd62e05..878ad92677 100644 --- a/.github/workflows/humble-source-build.yml +++ b/.github/workflows/humble-source-build.yml @@ -1,19 +1,27 @@ name: Humble Source Build on: workflow_dispatch: - branches: - - humble 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 * * *' jobs: source: - uses: ./.github/workflows/reusable-ros-tooling-source-build.yml + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-ros-tooling-source-build.yml@master with: 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-main.yml b/.github/workflows/iron-binary-build-main.yml deleted file mode 100644 index bc2dbd96cc..0000000000 --- a/.github/workflows/iron-binary-build-main.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Iron Binary Build - main -# author: Denis Štogl -# description: 'Build & test all dependencies from released (binary) packages.' - -on: - workflow_dispatch: - branches: - - iron - pull_request: - branches: - - iron - push: - branches: - - iron - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '03 1 * * *' - -jobs: - binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: iron - ros_repo: main - upstream_workspace: ros2_control-not-released.iron.repos - ref_for_scheduled_build: iron diff --git a/.github/workflows/iron-binary-build-testing.yml b/.github/workflows/iron-binary-build-testing.yml deleted file mode 100644 index 512c4b55e4..0000000000 --- a/.github/workflows/iron-binary-build-testing.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Iron Binary Build - testing -# author: Denis Štogl -# description: 'Build & test all dependencies from released (binary) packages.' - -on: - workflow_dispatch: - branches: - - iron - pull_request: - branches: - - iron - push: - branches: - - iron - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '03 1 * * *' - -jobs: - binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: iron - ros_repo: testing - upstream_workspace: ros2_control-not-released.iron.repos - ref_for_scheduled_build: iron diff --git a/.github/workflows/iron-binary-build.yml b/.github/workflows/iron-binary-build.yml new file mode 100644 index 0000000000..ef90e256a0 --- /dev/null +++ b/.github/workflows/iron-binary-build.yml @@ -0,0 +1,47 @@ +name: Iron Binary Build +# author: Denis Štogl +# description: 'Build & test all dependencies from released (binary) packages.' + +on: + workflow_dispatch: + 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 * * *' + +jobs: + binary: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-industrial-ci-with-cache.yml@master + strategy: + fail-fast: false + matrix: + ROS_DISTRO: [iron] + ROS_REPO: [main, testing] + with: + ros_distro: ${{ matrix.ROS_DISTRO }} + ros_repo: ${{ matrix.ROS_REPO }} + upstream_workspace: ros2_control-not-released.iron.repos + ref_for_scheduled_build: iron 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 new file mode 100644 index 0000000000..ff5be81d7d --- /dev/null +++ b/.github/workflows/iron-coverage-build.yml @@ -0,0 +1,36 @@ +name: Coverage Build - Iron +on: + workflow_dispatch: + 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: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-build-coverage.yml@master + secrets: inherit + with: + ros_distro: iron diff --git a/.github/workflows/iron-debian-build.yml b/.github/workflows/iron-debian-build.yml index 405e4f9135..3cbe0c5127 100644 --- a/.github/workflows/iron-debian-build.yml +++ b/.github/workflows/iron-debian-build.yml @@ -1,30 +1,33 @@ -name: Debian Iron Build +name: Debian Iron Source Build on: workflow_dispatch: 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 * * *' jobs: - iron_debian: - name: Iron debian build - runs-on: ubuntu-latest - env: - ROS_DISTRO: iron - container: ghcr.io/ros-controls/ros:iron-debian - steps: - - uses: actions/checkout@v4 - with: - path: src/ros2_control - - name: Build and test - shell: bash - run: | - source /opt/ros2_ws/install/setup.bash - vcs import src < src/ros2_control/ros2_control.${{ env.ROS_DISTRO }}.repos - colcon build --packages-skip rqt_controller_manager - colcon test --packages-skip rqt_controller_manager control_msgs controller_manager_msgs - colcon test-result --verbose + debian_source_build: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-debian-build.yml@master + strategy: + fail-fast: false + matrix: + ROS_DISTRO: [iron] + with: + ros_distro: ${{ matrix.ROS_DISTRO }} + upstream_workspace: ros2_control.${{ matrix.ROS_DISTRO }}.repos + ref_for_scheduled_build: master + skip_packages: rqt_controller_manager + skip_packages_test: controller_manager_msgs diff --git a/.github/workflows/iron-pre-commit.yml b/.github/workflows/iron-pre-commit.yml new file mode 100644 index 0000000000..a128958031 --- /dev/null +++ b/.github/workflows/iron-pre-commit.yml @@ -0,0 +1,13 @@ +name: Pre-Commit - Iron + +on: + workflow_dispatch: + pull_request: + branches: + - iron + +jobs: + pre-commit: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-pre-commit.yml@master + with: + ros_distro: iron diff --git a/.github/workflows/iron-rhel-binary-build.yml b/.github/workflows/iron-rhel-binary-build.yml index fc48bd80ea..f308c495f3 100644 --- a/.github/workflows/iron-rhel-binary-build.yml +++ b/.github/workflows/iron-rhel-binary-build.yml @@ -1,32 +1,31 @@ -name: RHEL Iron Binary Build +name: RHEL Iron Semi-Binary Build on: workflow_dispatch: 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 * * *' - jobs: - iron_rhel_binary: - name: Iron RHEL binary build - runs-on: ubuntu-latest - env: - ROS_DISTRO: iron - container: ghcr.io/ros-controls/ros:iron-rhel - steps: - - uses: actions/checkout@v4 - with: - path: src/ros2_control - - name: Install dependencies - run: | - rosdep update - rosdep install -iyr --from-path src/ros2_control || true - - name: Build and test - run: | - source /opt/ros/${{ env.ROS_DISTRO }}/setup.bash - colcon build --packages-skip rqt_controller_manager - colcon test --packages-skip rqt_controller_manager ros2controlcli - colcon test-result --verbose + 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: [iron] + with: + ros_distro: ${{ matrix.ROS_DISTRO }} + upstream_workspace: ros2_control.${{ matrix.ROS_DISTRO }}.repos + ref_for_scheduled_build: iron + skip_packages: rqt_controller_manager diff --git a/.github/workflows/iron-semi-binary-build-main.yml b/.github/workflows/iron-semi-binary-build-main.yml deleted file mode 100644 index 1399e8b32b..0000000000 --- a/.github/workflows/iron-semi-binary-build-main.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Iron Semi-Binary Build - main -# description: 'Build & test that compiles the main dependencies from source.' - -on: - workflow_dispatch: - branches: - - iron - pull_request: - branches: - - iron - push: - branches: - - iron - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '33 1 * * *' - -jobs: - semi_binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: iron - ros_repo: main - upstream_workspace: ros2_control.iron.repos - ref_for_scheduled_build: iron diff --git a/.github/workflows/iron-semi-binary-build-testing.yml b/.github/workflows/iron-semi-binary-build-testing.yml deleted file mode 100644 index b29f798931..0000000000 --- a/.github/workflows/iron-semi-binary-build-testing.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Iron Semi-Binary Build - testing -# description: 'Build & test that compiles the main dependencies from source.' - -on: - workflow_dispatch: - branches: - - iron - pull_request: - branches: - - iron - push: - branches: - - iron - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '33 1 * * *' - -jobs: - semi_binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: iron - ros_repo: testing - upstream_workspace: ros2_control.iron.repos - ref_for_scheduled_build: iron diff --git a/.github/workflows/iron-semi-binary-build.yml b/.github/workflows/iron-semi-binary-build.yml new file mode 100644 index 0000000000..30a83e5367 --- /dev/null +++ b/.github/workflows/iron-semi-binary-build.yml @@ -0,0 +1,47 @@ +name: Iron Semi-Binary Build +# author: Denis Štogl +# description: 'Build & test all dependencies from released (binary) packages.' + +on: + workflow_dispatch: + 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 * * *' + +jobs: + binary: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-industrial-ci-with-cache.yml@master + strategy: + fail-fast: false + matrix: + ROS_DISTRO: [iron] + ROS_REPO: [main, testing] + with: + ros_distro: ${{ matrix.ROS_DISTRO }} + ros_repo: ${{ matrix.ROS_REPO }} + upstream_workspace: ros2_control.iron.repos + ref_for_scheduled_build: iron diff --git a/.github/workflows/iron-source-build.yml b/.github/workflows/iron-source-build.yml index 1e9d865c49..3b7c53f6ff 100644 --- a/.github/workflows/iron-source-build.yml +++ b/.github/workflows/iron-source-build.yml @@ -1,19 +1,27 @@ name: Iron Source Build on: workflow_dispatch: - branches: - - iron 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 * * *' jobs: source: - uses: ./.github/workflows/reusable-ros-tooling-source-build.yml + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-ros-tooling-source-build.yml@master with: 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/reusable-industrial-ci-with-cache.yml b/.github/workflows/reusable-industrial-ci-with-cache.yml deleted file mode 100644 index acefeebfac..0000000000 --- a/.github/workflows/reusable-industrial-ci-with-cache.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Reusable industrial_ci Workflow with Cache -# Reusable action to simplify dealing with ROS/ROS2 industrial_ci builds with cache -# author: Denis Štogl - -on: - workflow_call: - inputs: - ref_for_scheduled_build: - description: 'Reference on which the repo should be checkout for scheduled build. Usually is this name of a branch or a tag.' - default: '' - required: false - type: string - - upstream_workspace: - description: 'UPSTREAM_WORKSPACE variable for industrial_ci. Usually path to local .repos file.' - required: true - type: string - ros_distro: - description: 'ROS_DISTRO variable for industrial_ci' - required: true - type: string - ros_repo: - description: 'ROS_REPO to run for industrial_ci. Possible values: "main", "testing"' - default: 'main' - required: false - type: string - os_code_name: - description: 'OS_CODE_NAME variable for industrial_ci' - default: '' - required: false - type: string - before_install_upstream_dependencies: - description: 'BEFORE_INSTALL_UPSTREAM_DEPENDENCIES variable for industrial_ci' - default: '' - required: false - type: string - - ccache_dir: - description: 'Local path to store cache (from "github.workspace"). For standard industrial_ci configuration do not have to be changed' - default: '.ccache' - required: false - type: string - basedir: - description: 'Local path to workspace base directory to cache (from "github.workspace"). For standard industrial_ci configuration do not have to be changed' - default: '.work' - required: false - type: string - - -jobs: - reusable_industrial_ci_with_cache: - name: ${{ inputs.ros_distro }} ${{ inputs.ros_repo }} ${{ inputs.os_code_name }} - runs-on: ubuntu-latest - env: - CCACHE_DIR: ${{ github.workspace }}/${{ inputs.ccache_dir }} - BASEDIR: ${{ github.workspace }}/${{ inputs.basedir }} - CACHE_PREFIX: ${{ inputs.ros_distro }}-${{ inputs.upstream_workspace }}-${{ inputs.ros_repo }}-${{ github.job }} - steps: - - name: Checkout ${{ inputs.ref }} when build is not scheduled - if: ${{ github.event_name != 'schedule' }} - uses: actions/checkout@v4 - - name: Checkout ${{ inputs.ref }} on scheduled build - if: ${{ github.event_name == 'schedule' }} - uses: actions/checkout@v4 - with: - ref: ${{ inputs.ref_for_scheduled_build }} - - name: cache target_ws - if: ${{ ! matrix.env.CCOV }} - uses: pat-s/always-upload-cache@v3.0.11 - with: - path: ${{ env.BASEDIR }}/target_ws - key: target_ws-${{ env.CACHE_PREFIX }}-${{ hashFiles('**/CMakeLists.txt', '**/package.xml') }}-${{ github.run_id }} - restore-keys: | - target_ws-${{ env.CACHE_PREFIX }}-${{ hashFiles('**/CMakeLists.txt', '**/package.xml') }} - - name: cache ccache - uses: pat-s/always-upload-cache@v3.0.11 - with: - path: ${{ env.CCACHE_DIR }} - key: ccache-${{ env.CACHE_PREFIX }}-${{ github.sha }}-${{ github.run_id }} - restore-keys: | - ccache-${{ env.CACHE_PREFIX }}-${{ github.sha }} - ccache-${{ env.CACHE_PREFIX }} - - uses: 'ros-industrial/industrial_ci@master' - env: - UPSTREAM_WORKSPACE: ${{ inputs.upstream_workspace }} - ROS_DISTRO: ${{ inputs.ros_distro }} - ROS_REPO: ${{ inputs.ros_repo }} - OS_CODE_NAME: ${{ inputs.os_code_name }} - BEFORE_INSTALL_UPSTREAM_DEPENDENCIES: ${{ inputs.before_install_upstream_dependencies }} - - name: prepare target_ws for cache - if: ${{ always() && ! matrix.env.CCOV }} - run: | - du -sh ${{ env.BASEDIR }}/target_ws - sudo find ${{ env.BASEDIR }}/target_ws -wholename '*/test_results/*' -delete - sudo rm -rf ${{ env.BASEDIR }}/target_ws/src - du -sh ${{ env.BASEDIR }}/target_ws diff --git a/.github/workflows/reusable-ros-tooling-source-build.yml b/.github/workflows/reusable-ros-tooling-source-build.yml deleted file mode 100644 index fa96b7288c..0000000000 --- a/.github/workflows/reusable-ros-tooling-source-build.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Reusable industrial_ci Workflow with Cache -# Reusable action to simplify dealing with ROS/ROS2 industrial_ci builds with cache -# author: Denis Štogl - -on: - workflow_call: - inputs: - ros_distro: - description: 'ROS2 distribution name' - required: true - type: string - ref: - description: 'Reference on which the repo should be checkout. Usually is this name of a branch or a tag.' - required: true - type: string - ros2_repo_branch: - description: 'Branch in the ros2/ros2 repository from which ".repos" should be used. Possible values: master (Rolling), humble.' - default: 'master' - required: false - type: string - -jobs: - reusable_ros_tooling_source_build: - name: ${{ inputs.ros_distro }} ubuntu-22.04 - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - steps: - - uses: ros-tooling/setup-ros@0.7.1 - with: - required-ros-distributions: ${{ inputs.ros_distro }} - - uses: actions/checkout@v4 - with: - ref: ${{ inputs.ref }} - - uses: ros-tooling/action-ros-ci@0.3.6 - with: - target-ros2-distro: ${{ inputs.ros_distro }} - # build all packages listed in the meta package - ref: ${{ inputs.ref }} # otherwise the default branch is used for scheduled workflows - package-name: - controller_interface - controller_manager - controller_manager_msgs - hardware_interface - ros2controlcli - ros2_control - ros2_control_test_assets - transmission_interface - vcs-repo-file-url: | - https://raw.githubusercontent.com/ros2/ros2/${{ inputs.ros2_repo_branch }}/ros2.repos - https://raw.githubusercontent.com/${{ github.repository }}/${{ github.sha }}/ros2_control.${{ inputs.ros_distro }}.repos?token=${{ secrets.GITHUB_TOKEN }} - colcon-mixin-repository: https://raw.githubusercontent.com/colcon/colcon-mixin-repository/master/index.yaml - - uses: actions/upload-artifact@v4.3.0 - with: - name: colcon-logs-ubuntu-22.04 - path: ros_ws/log diff --git a/.github/workflows/reviewer_lottery.yml b/.github/workflows/reviewer_lottery.yml index ed28964e01..0584f4a7f3 100644 --- a/.github/workflows/reviewer_lottery.yml +++ b/.github/workflows/reviewer_lottery.yml @@ -1,14 +1,10 @@ name: Reviewer lottery +# pull_request_target takes the same events as pull_request, +# but it runs on the base branch instead of the head branch. on: pull_request_target: types: [opened, ready_for_review, reopened] jobs: - test: - runs-on: ubuntu-latest - if: github.actor != 'dependabot[bot]' && github.actor != 'mergify[bot]' - steps: - - uses: actions/checkout@v4 - - uses: uesteibar/reviewer-lottery@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + assign_reviewers: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-reviewer-lottery.yml@master 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-main.yml b/.github/workflows/rolling-binary-build-main.yml deleted file mode 100644 index 05a9b9c0b2..0000000000 --- a/.github/workflows/rolling-binary-build-main.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Rolling Binary Build - main -# author: Denis Štogl -# description: 'Build & test all dependencies from released (binary) packages.' - -on: - workflow_dispatch: - branches: - - master - pull_request: - branches: - - master - push: - branches: - - master - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '03 1 * * *' - -jobs: - binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: rolling - ros_repo: main - upstream_workspace: ros2_control-not-released.rolling.repos - ref_for_scheduled_build: master diff --git a/.github/workflows/rolling-binary-build-testing.yml b/.github/workflows/rolling-binary-build-testing.yml deleted file mode 100644 index 811c96fce4..0000000000 --- a/.github/workflows/rolling-binary-build-testing.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Rolling Binary Build - testing -# author: Denis Štogl -# description: 'Build & test all dependencies from released (binary) packages.' - -on: - workflow_dispatch: - branches: - - master - pull_request: - branches: - - master - push: - branches: - - master - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '03 1 * * *' - -jobs: - binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: rolling - ros_repo: testing - upstream_workspace: ros2_control-not-released.rolling.repos - ref_for_scheduled_build: master diff --git a/.github/workflows/rolling-binary-build.yml b/.github/workflows/rolling-binary-build.yml new file mode 100644 index 0000000000..24a28f16ae --- /dev/null +++ b/.github/workflows/rolling-binary-build.yml @@ -0,0 +1,47 @@ +name: Rolling 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/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 * * *' + +jobs: + binary: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-industrial-ci-with-cache.yml@master + strategy: + fail-fast: false + matrix: + ROS_DISTRO: [rolling] + 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/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 new file mode 100644 index 0000000000..45b10876e7 --- /dev/null +++ b/.github/workflows/rolling-coverage-build.yml @@ -0,0 +1,36 @@ +name: Coverage Build - Rolling +on: + workflow_dispatch: + 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: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-build-coverage.yml@master + secrets: inherit + with: + ros_distro: rolling diff --git a/.github/workflows/rolling-debian-build.yml b/.github/workflows/rolling-debian-build.yml index 098af45029..00d4ad844b 100644 --- a/.github/workflows/rolling-debian-build.yml +++ b/.github/workflows/rolling-debian-build.yml @@ -1,30 +1,32 @@ -name: Debian Rolling Build +name: Debian Rolling Source Build on: workflow_dispatch: 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 * * *' jobs: - rolling_debian: - name: Rolling debian build - runs-on: ubuntu-latest - env: - ROS_DISTRO: rolling - container: ghcr.io/ros-controls/ros:rolling-debian - steps: - - uses: actions/checkout@v4 - with: - path: src/ros2_control - - name: Build and test - shell: bash - run: | - source /opt/ros2_ws/install/setup.bash - vcs import src < src/ros2_control/ros2_control.${{ env.ROS_DISTRO }}.repos - colcon build --packages-skip rqt_controller_manager - colcon test --packages-skip rqt_controller_manager - colcon test-result --verbose + debian_source_build: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-debian-build.yml@master + strategy: + fail-fast: false + matrix: + ROS_DISTRO: [rolling] + 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/rolling-pre-commit.yml b/.github/workflows/rolling-pre-commit.yml new file mode 100644 index 0000000000..7bc07ba802 --- /dev/null +++ b/.github/workflows/rolling-pre-commit.yml @@ -0,0 +1,13 @@ +name: Pre-Commit - Rolling + +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: rolling diff --git a/.github/workflows/rolling-rhel-binary-build.yml b/.github/workflows/rolling-rhel-binary-build.yml index 06a5411c24..c8939d6015 100644 --- a/.github/workflows/rolling-rhel-binary-build.yml +++ b/.github/workflows/rolling-rhel-binary-build.yml @@ -1,32 +1,31 @@ -name: RHEL Rolling Binary Build +name: RHEL Rolling Semi-Binary Build on: workflow_dispatch: 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 * * *' - jobs: - rolling_rhel_binary: - name: Rolling RHEL binary build - runs-on: ubuntu-latest - env: - ROS_DISTRO: rolling - container: ghcr.io/ros-controls/ros:rolling-rhel - steps: - - uses: actions/checkout@v4 - with: - path: src/ros2_control - - name: Install dependencies - run: | - rosdep update - rosdep install -iyr --from-path src/ros2_control || true - - name: Build and test - run: | - source /opt/ros/${{ env.ROS_DISTRO }}/setup.bash - colcon build --packages-skip rqt_controller_manager - colcon test --packages-skip rqt_controller_manager ros2controlcli - colcon test-result --verbose + 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: [rolling] + 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/rolling-semi-binary-build-main.yml b/.github/workflows/rolling-semi-binary-build-main.yml deleted file mode 100644 index 1033dd1e6c..0000000000 --- a/.github/workflows/rolling-semi-binary-build-main.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Rolling Semi-Binary Build - main -# description: 'Build & test that compiles the main dependencies from source.' - -on: - workflow_dispatch: - branches: - - master - pull_request: - branches: - - master - push: - branches: - - master - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '33 1 * * *' - -jobs: - semi_binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: rolling - ros_repo: main - upstream_workspace: ros2_control.rolling.repos - ref_for_scheduled_build: master diff --git a/.github/workflows/rolling-semi-binary-build-testing.yml b/.github/workflows/rolling-semi-binary-build-testing.yml deleted file mode 100644 index 4ddfcf5057..0000000000 --- a/.github/workflows/rolling-semi-binary-build-testing.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Rolling Semi-Binary Build - testing -# description: 'Build & test that compiles the main dependencies from source.' - -on: - workflow_dispatch: - branches: - - master - pull_request: - branches: - - master - push: - branches: - - master - schedule: - # Run every morning to detect flakiness and broken dependencies - - cron: '33 1 * * *' - -jobs: - semi_binary: - uses: ./.github/workflows/reusable-industrial-ci-with-cache.yml - with: - ros_distro: rolling - ros_repo: testing - upstream_workspace: ros2_control.rolling.repos - ref_for_scheduled_build: master diff --git a/.github/workflows/rolling-semi-binary-build.yml b/.github/workflows/rolling-semi-binary-build.yml new file mode 100644 index 0000000000..4cdb7ab585 --- /dev/null +++ b/.github/workflows/rolling-semi-binary-build.yml @@ -0,0 +1,47 @@ +name: Rolling 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/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 * * *' + +jobs: + binary: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-industrial-ci-with-cache.yml@master + strategy: + fail-fast: false + matrix: + ROS_DISTRO: [rolling] + 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/rolling-source-build.yml b/.github/workflows/rolling-source-build.yml index b96ad0298e..9bbf09cda4 100644 --- a/.github/workflows/rolling-source-build.yml +++ b/.github/workflows/rolling-source-build.yml @@ -1,19 +1,27 @@ name: Rolling Source Build on: workflow_dispatch: - branches: - - master 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 * * *' jobs: source: - uses: ./.github/workflows/reusable-ros-tooling-source-build.yml + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-ros-tooling-source-build.yml@master with: ros_distro: rolling ref: master ros2_repo_branch: master + container: ubuntu:24.04 diff --git a/.github/workflows/rosdoc2.yml b/.github/workflows/rosdoc2.yml new file mode 100644 index 0000000000..07a9e2904a --- /dev/null +++ b/.github/workflows/rosdoc2.yml @@ -0,0 +1,14 @@ +name: rosdoc2 + +on: + workflow_dispatch: + pull_request: + paths: + - ros2_control/doc/** + - ros2_control/rosdoc2.yaml + - ros2_control/package.xml + + +jobs: + check: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-rosdoc2.yml@master diff --git a/.github/workflows/update-pre-commit.yml b/.github/workflows/update-pre-commit.yml new file mode 100644 index 0000000000..6bedaa0c97 --- /dev/null +++ b/.github/workflows/update-pre-commit.yml @@ -0,0 +1,12 @@ +name: Auto Update pre-commit +# Update pre-commit config and create PR if changes are detected +# author: Christoph Fröhlich + +on: + workflow_dispatch: + schedule: + - cron: '0 0 1 * *' # Runs at 00:00, on day 1 of the month + +jobs: + auto_update_and_create_pr: + uses: ros-controls/ros2_control_ci/.github/workflows/reusable-update-pre-commit.yml@master diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e6d41cc9c..29862f70e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: # Standard hooks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-ast @@ -26,6 +26,7 @@ repos: - id: check-symlinks - id: check-xml - id: check-yaml + args: ["--allow-multiple-documents"] - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending @@ -36,7 +37,7 @@ repos: # Python hooks - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.16.0 hooks: - id: pyupgrade args: [--py36-plus] @@ -49,40 +50,38 @@ repos: args: ["--ignore=D100,D101,D102,D103,D104,D105,D106,D107,D203,D212,D404"] - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 24.4.2 hooks: - id: black args: ["--line-length=99"] - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + rev: 7.1.0 hooks: - id: flake8 args: ["--extend-ignore=E501"] # CPP hooks - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v15.0.6 + rev: v18.1.8 hooks: - id: clang-format + args: ['-fallback-style=none', '-i'] - repo: local hooks: - id: ament_cppcheck name: ament_cppcheck description: Static code analysis of C/C++ files. - stages: [commit] - entry: ament_cppcheck + entry: env AMENT_CPPCHECK_ALLOW_SLOW_VERSIONS=1 ament_cppcheck language: system files: \.(h\+\+|h|hh|hxx|hpp|cuh|c|cc|cpp|cu|c\+\+|cxx|tpp|txx)$ - # Maybe use https://github.com/cpplint/cpplint instead - repo: local hooks: - id: ament_cpplint name: ament_cpplint description: Static code analysis of C/C++ files. - stages: [commit] entry: ament_cpplint language: system files: \.(h\+\+|h|hh|hxx|hpp|cuh|c|cc|cpp|cu|c\+\+|cxx|tpp|txx)$ @@ -94,7 +93,6 @@ repos: - id: ament_lint_cmake name: ament_lint_cmake description: Check format of CMakeLists.txt files. - stages: [commit] entry: ament_lint_cmake language: system files: CMakeLists\.txt$ @@ -105,9 +103,9 @@ repos: - id: ament_copyright name: ament_copyright description: Check if copyright notice is available in all files. - stages: [commit] entry: ament_copyright language: system + exclude: .*/conf\.py$ # Docs - RestructuredText hooks - repo: https://github.com/PyCQA/doc8 @@ -128,8 +126,18 @@ repos: # Spellcheck in comments and docs # skipping of *.svg files is not working... - repo: https://github.com/codespell-project/codespell - rev: v2.2.4 + rev: v2.3.0 hooks: - id: codespell - args: ['--write-changes'] - exclude: CHANGELOG\.rst|\.(svg|pyc)$ + 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.6 + hooks: + - id: check-github-workflows + args: ["--verbose"] + - id: check-github-actions + args: ["--verbose"] + - id: check-dependabot + args: ["--verbose"] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9cdc27041..df91cfbf20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,7 +54,9 @@ As this project, by default, uses the default GitHub issue labels ## Licensing -Any contribution that you make to this repository will be under the Apache 2 License, as dictated by that [license]: +Any contribution that you make to this repository will +be under the Apache 2 License, as dictated by that +[license](http://www.apache.org/licenses/LICENSE-2.0.html): ~~~ 5. Submission of Contributions. Unless You explicitly state otherwise, @@ -69,4 +71,3 @@ Any contribution that you make to this repository will be under the Apache 2 Lic [issues]: https://github.com/ros-controls/ros2_control/issues [closed-issues]: https://github.com/ros-controls/ros2_control/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20 [help-wanted]: https://github.com/ros-controls/ros2_control/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22 -[license]: http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/README.md b/README.md index 0c26afd184..40d2a3c189 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,10 @@ 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-main.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-binary-build-main.yml?branch=master)
[![Rolling Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-semi-binary-build-main.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/rolling-semi-binary-build-main.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) -**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-main.yml/badge.svg?branch=iron)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-binary-build-main.yml?branch=iron)
[![Iron Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/iron-semi-binary-build-main.yml/badge.svg?branch=iron)](https://github.com/ros-controls/ros2_control/actions/workflows/iron-semi-binary-build-main.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-main.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-binary-build-main.yml?branch=master)
[![Humble Semi-Binary Build](https://github.com/ros-controls/ros2_control/actions/workflows/humble-semi-binary-build-main.yml/badge.svg?branch=master)](https://github.com/ros-controls/ros2_control/actions/workflows/humble-semi-binary-build-main.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) +**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) [Detailed build status](.github/workflows/README.md) 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 c6471c90f6..c4f9c56800 100644 --- a/controller_interface/CHANGELOG.rst +++ b/controller_interface/CHANGELOG.rst @@ -2,6 +2,52 @@ Changelog for package controller_interface ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.13.0 (2024-07-08) +------------------- +* [ControllerChaining] Export state interfaces from chainable controllers (`#1021 `_) +* Contributors: Sai Kishor Kothakota + +4.12.0 (2024-07-01) +------------------- + +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 `_) +* Use ament_cmake generated rclcpp version header (`#1448 `_) +* Contributors: Sai Kishor Kothakota + +4.7.0 (2024-03-22) +------------------ +* add missing compiler definitions of RCLCPP_VERSION_MAJOR (`#1440 `_) +* Contributors: Sai Kishor Kothakota + +4.6.0 (2024-03-02) +------------------ +* Add -Werror=missing-braces to compile options (`#1423 `_) +* added conditioning to have rolling tags compilable in older versions (`#1422 `_) +* Contributors: Sai Kishor Kothakota + +4.5.0 (2024-02-12) +------------------ +* A method to get node options to setup the controller node #api-breaking (`#1169 `_) +* Contributors: Sai Kishor Kothakota + 4.4.0 (2024-01-31) ------------------ diff --git a/controller_interface/CMakeLists.txt b/controller_interface/CMakeLists.txt index 19a501dc62..3dc3bc4d0a 100644 --- a/controller_interface/CMakeLists.txt +++ b/controller_interface/CMakeLists.txt @@ -2,7 +2,8 @@ cmake_minimum_required(VERSION 3.16) project(controller_interface LANGUAGES CXX) if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") - add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow) + add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow + -Werror=missing-braces) endif() set(THIS_PACKAGE_INCLUDE_DEPENDS @@ -11,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() @@ -88,3 +90,4 @@ install(TARGETS controller_interface ament_export_targets(export_controller_interface HAS_LIBRARY_TARGET) ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS}) ament_package() +ament_generate_version_header(${PROJECT_NAME}) 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/include/controller_interface/chainable_controller_interface.hpp b/controller_interface/include/controller_interface/chainable_controller_interface.hpp index 2bdccefdc5..2e39f038b1 100644 --- a/controller_interface/include/controller_interface/chainable_controller_interface.hpp +++ b/controller_interface/include/controller_interface/chainable_controller_interface.hpp @@ -15,6 +15,7 @@ #ifndef CONTROLLER_INTERFACE__CHAINABLE_CONTROLLER_INTERFACE_HPP_ #define CONTROLLER_INTERFACE__CHAINABLE_CONTROLLER_INTERFACE_HPP_ +#include #include #include "controller_interface/controller_interface_base.hpp" @@ -55,6 +56,9 @@ class ChainableControllerInterface : public ControllerInterfaceBase CONTROLLER_INTERFACE_PUBLIC bool is_chainable() const final; + CONTROLLER_INTERFACE_PUBLIC + std::vector export_state_interfaces() final; + CONTROLLER_INTERFACE_PUBLIC std::vector export_reference_interfaces() final; @@ -65,8 +69,19 @@ class ChainableControllerInterface : public ControllerInterfaceBase bool is_in_chained_mode() const final; protected: - /// Virtual method that each chainable controller should implement to export its chainable - /// interfaces. + /// Virtual method that each chainable controller should implement to export its read-only + /// chainable interfaces. + /** + * Each chainable controller implements this methods where all its state(read only) interfaces are + * exported. The method has the same meaning as `export_state_interfaces` method from + * hardware_interface::SystemInterface or hardware_interface::ActuatorInterface. + * + * \returns list of StateInterfaces that other controller can use as their inputs. + */ + virtual std::vector on_export_state_interfaces(); + + /// Virtual method that each chainable controller should implement to export its read/write + /// chainable interfaces. /** * Each chainable controller implements this methods where all input (command) interfaces are * exported. The method has the same meaning as `export_command_interface` method from @@ -74,7 +89,7 @@ class ChainableControllerInterface : public ControllerInterfaceBase * * \returns list of CommandInterfaces that other controller can use as their outputs. */ - virtual std::vector on_export_reference_interfaces() = 0; + virtual std::vector on_export_reference_interfaces(); /// Virtual method that each chainable controller should implement to switch chained mode. /** @@ -114,7 +129,12 @@ class ChainableControllerInterface : public ControllerInterfaceBase virtual return_type update_and_write_commands( const rclcpp::Time & time, const rclcpp::Duration & period) = 0; + /// Storage of values for state interfaces + std::vector exported_state_interface_names_; + std::vector state_interfaces_values_; + /// Storage of values for reference interfaces + std::vector exported_reference_interface_names_; std::vector reference_interfaces_; private: diff --git a/controller_interface/include/controller_interface/controller_interface.hpp b/controller_interface/include/controller_interface/controller_interface.hpp index a3d006755f..17f39c8478 100644 --- a/controller_interface/include/controller_interface/controller_interface.hpp +++ b/controller_interface/include/controller_interface/controller_interface.hpp @@ -42,6 +42,14 @@ class ControllerInterface : public controller_interface::ControllerInterfaceBase CONTROLLER_INTERFACE_PUBLIC bool is_chainable() const final; + /** + * A non-chainable controller doesn't export any state interfaces. + * + * \returns empty list. + */ + CONTROLLER_INTERFACE_PUBLIC + std::vector export_state_interfaces() final; + /** * Controller has no reference interfaces. * diff --git a/controller_interface/include/controller_interface/controller_interface_base.hpp b/controller_interface/include/controller_interface/controller_interface_base.hpp index 6ce483d1ff..1b5fd2e059 100644 --- a/controller_interface/include/controller_interface/controller_interface_base.hpp +++ b/controller_interface/include/controller_interface/controller_interface_base.hpp @@ -26,6 +26,7 @@ #include "hardware_interface/loaned_state_interface.hpp" #include "rclcpp/rclcpp.hpp" +#include "rclcpp/version.h" #include "rclcpp_lifecycle/lifecycle_node.hpp" namespace controller_interface @@ -144,7 +145,7 @@ class ControllerInterfaceBase : public rclcpp_lifecycle::node_interfaces::Lifecy std::shared_ptr get_node(); CONTROLLER_INTERFACE_PUBLIC - std::shared_ptr get_node() const; + std::shared_ptr get_node() const; CONTROLLER_INTERFACE_PUBLIC const rclcpp_lifecycle::State & get_state() const; @@ -172,7 +173,14 @@ class ControllerInterfaceBase : public rclcpp_lifecycle::node_interfaces::Lifecy CONTROLLER_INTERFACE_PUBLIC virtual rclcpp::NodeOptions define_custom_node_options() const { +// \note The versions conditioning is added here to support the source-compatibility with Humble +#if RCLCPP_VERSION_MAJOR >= 21 return rclcpp::NodeOptions().enable_logger_service(true); +#else + return rclcpp::NodeOptions() + .allow_undeclared_parameters(true) + .automatically_declare_parameters_from_overrides(true); +#endif } /// Declare and initialize a parameter with a type. @@ -216,11 +224,20 @@ class ControllerInterfaceBase : public rclcpp_lifecycle::node_interfaces::Lifecy CONTROLLER_INTERFACE_PUBLIC virtual std::vector export_reference_interfaces() = 0; + /** + * Export interfaces for a chainable controller that can be used as state interface by other + * controllers. + * + * \returns list of state interfaces for preceding controllers. + */ + CONTROLLER_INTERFACE_PUBLIC + virtual std::vector export_state_interfaces() = 0; + /** * Set chained mode of a chainable controller. This method triggers internal processes to switch * a chainable controller to "chained" mode and vice-versa. Setting controller to "chained" mode - * usually involves disabling of subscribers and other external interfaces to avoid potential - * concurrency in input commands. + * usually involves the usage of the controller's reference interfaces by the other + * controllers * * \returns true if mode is switched successfully and false if not. */ diff --git a/controller_interface/package.xml b/controller_interface/package.xml index 919fdf8d20..1ee83bd019 100644 --- a/controller_interface/package.xml +++ b/controller_interface/package.xml @@ -2,13 +2,14 @@ controller_interface - 4.4.0 + 4.13.0 Description of controller_interface Bence Magyar Denis Štogl Apache License 2.0 ament_cmake + ament_cmake_gen_version_h hardware_interface rclcpp_lifecycle diff --git a/controller_interface/src/chainable_controller_interface.cpp b/controller_interface/src/chainable_controller_interface.cpp index 2f7c67741e..9f4a171ec3 100644 --- a/controller_interface/src/chainable_controller_interface.cpp +++ b/controller_interface/src/chainable_controller_interface.cpp @@ -44,26 +44,35 @@ return_type ChainableControllerInterface::update( return ret; } -std::vector -ChainableControllerInterface::export_reference_interfaces() +std::vector +ChainableControllerInterface::export_state_interfaces() { - auto reference_interfaces = on_export_reference_interfaces(); + auto state_interfaces = on_export_state_interfaces(); - // check if the "reference_interfaces_" variable is resized to number of interfaces - if (reference_interfaces_.size() != reference_interfaces.size()) + // check if the names of the controller state interfaces begin with the controller's name + for (const auto & interface : state_interfaces) { - // TODO(destogl): Should here be "FATAL"? It is fatal in terms of controller but not for the - // framework - RCLCPP_FATAL( - get_node()->get_logger(), - "The internal storage for reference values 'reference_interfaces_' variable has size '%zu', " - "but it is expected to have the size '%zu' equal to the number of exported reference " - "interfaces. No reference interface will be exported. Please correct and recompile " - "the controller with name '%s' and try again.", - reference_interfaces_.size(), reference_interfaces.size(), get_node()->get_name()); - reference_interfaces.clear(); + if (interface.get_prefix_name() != get_node()->get_name()) + { + RCLCPP_FATAL( + get_node()->get_logger(), + "The name of the interface '%s' does not begin with the controller's name. This is " + "mandatory for state interfaces. No state interface will be exported. Please " + "correct and recompile the controller with name '%s' and try again.", + interface.get_name().c_str(), get_node()->get_name()); + state_interfaces.clear(); + break; + } } + return state_interfaces; +} + +std::vector +ChainableControllerInterface::export_reference_interfaces() +{ + auto reference_interfaces = on_export_reference_interfaces(); + // check if the names of the reference interfaces begin with the controller's name for (const auto & interface : reference_interfaces) { @@ -113,4 +122,30 @@ bool ChainableControllerInterface::is_in_chained_mode() const { return in_chaine bool ChainableControllerInterface::on_set_chained_mode(bool /*chained_mode*/) { return true; } +std::vector +ChainableControllerInterface::on_export_state_interfaces() +{ + state_interfaces_values_.resize(exported_state_interface_names_.size(), 0.0); + std::vector state_interfaces; + for (size_t i = 0; i < exported_state_interface_names_.size(); ++i) + { + state_interfaces.emplace_back(hardware_interface::StateInterface( + get_node()->get_name(), exported_state_interface_names_[i], &state_interfaces_values_[i])); + } + return state_interfaces; +} + +std::vector +ChainableControllerInterface::on_export_reference_interfaces() +{ + reference_interfaces_.resize(exported_reference_interface_names_.size(), 0.0); + std::vector reference_interfaces; + for (size_t i = 0; i < exported_reference_interface_names_.size(); ++i) + { + reference_interfaces.emplace_back(hardware_interface::CommandInterface( + get_node()->get_name(), exported_reference_interface_names_[i], &reference_interfaces_[i])); + } + return reference_interfaces; +} + } // namespace controller_interface diff --git a/controller_interface/src/controller_interface.cpp b/controller_interface/src/controller_interface.cpp index 392a48ffa4..0f11bba71c 100644 --- a/controller_interface/src/controller_interface.cpp +++ b/controller_interface/src/controller_interface.cpp @@ -28,6 +28,11 @@ ControllerInterface::ControllerInterface() : ControllerInterfaceBase() {} bool ControllerInterface::is_chainable() const { return false; } +std::vector ControllerInterface::export_state_interfaces() +{ + return {}; +} + std::vector ControllerInterface::export_reference_interfaces() { return {}; diff --git a/controller_interface/src/controller_interface_base.cpp b/controller_interface/src/controller_interface_base.cpp index e48e1d21b3..6b87ba5cda 100644 --- a/controller_interface/src/controller_interface_base.cpp +++ b/controller_interface/src/controller_interface_base.cpp @@ -121,7 +121,7 @@ std::shared_ptr ControllerInterfaceBase::get_no return node_; } -std::shared_ptr ControllerInterfaceBase::get_node() const +std::shared_ptr ControllerInterfaceBase::get_node() const { if (!node_.get()) { diff --git a/controller_interface/test/test_chainable_controller_interface.cpp b/controller_interface/test/test_chainable_controller_interface.cpp index 47487e019c..2f04273f3e 100644 --- a/controller_interface/test/test_chainable_controller_interface.cpp +++ b/controller_interface/test/test_chainable_controller_interface.cpp @@ -16,6 +16,9 @@ #include +using ::testing::IsEmpty; +using ::testing::SizeIs; + TEST_F(ChainableControllerInterfaceTest, default_returns) { TestableChainableControllerInterface controller; @@ -31,7 +34,7 @@ TEST_F(ChainableControllerInterfaceTest, default_returns) EXPECT_FALSE(controller.is_in_chained_mode()); } -TEST_F(ChainableControllerInterfaceTest, export_reference_interfaces) +TEST_F(ChainableControllerInterfaceTest, export_state_interfaces) { TestableChainableControllerInterface controller; @@ -42,16 +45,16 @@ TEST_F(ChainableControllerInterfaceTest, export_reference_interfaces) controller_interface::return_type::OK); ASSERT_NO_THROW(controller.get_node()); - auto reference_interfaces = controller.export_reference_interfaces(); + auto exported_state_interfaces = controller.export_state_interfaces(); - ASSERT_EQ(reference_interfaces.size(), 1u); - EXPECT_EQ(reference_interfaces[0].get_prefix_name(), TEST_CONTROLLER_NAME); - EXPECT_EQ(reference_interfaces[0].get_interface_name(), "test_itf"); + ASSERT_THAT(exported_state_interfaces, SizeIs(1)); + EXPECT_EQ(exported_state_interfaces[0].get_prefix_name(), TEST_CONTROLLER_NAME); + EXPECT_EQ(exported_state_interfaces[0].get_interface_name(), "test_state"); - EXPECT_EQ(reference_interfaces[0].get_value(), INTERFACE_VALUE); + EXPECT_EQ(exported_state_interfaces[0].get_value(), EXPORTED_STATE_INTERFACE_VALUE); } -TEST_F(ChainableControllerInterfaceTest, reference_interfaces_storage_not_correct_size) +TEST_F(ChainableControllerInterfaceTest, export_reference_interfaces) { TestableChainableControllerInterface controller; @@ -62,13 +65,16 @@ TEST_F(ChainableControllerInterfaceTest, reference_interfaces_storage_not_correc controller_interface::return_type::OK); ASSERT_NO_THROW(controller.get_node()); - // expect empty return because storage is not resized - controller.reference_interfaces_.clear(); auto reference_interfaces = controller.export_reference_interfaces(); - ASSERT_TRUE(reference_interfaces.empty()); + + ASSERT_THAT(reference_interfaces, SizeIs(1)); + EXPECT_EQ(reference_interfaces[0].get_prefix_name(), TEST_CONTROLLER_NAME); + EXPECT_EQ(reference_interfaces[0].get_interface_name(), "test_itf"); + + EXPECT_EQ(reference_interfaces[0].get_value(), INTERFACE_VALUE); } -TEST_F(ChainableControllerInterfaceTest, reference_interfaces_prefix_is_not_node_name) +TEST_F(ChainableControllerInterfaceTest, interfaces_prefix_is_not_node_name) { TestableChainableControllerInterface controller; @@ -83,7 +89,10 @@ TEST_F(ChainableControllerInterfaceTest, reference_interfaces_prefix_is_not_node // expect empty return because interface prefix is not equal to the node name auto reference_interfaces = controller.export_reference_interfaces(); - ASSERT_TRUE(reference_interfaces.empty()); + ASSERT_THAT(reference_interfaces, IsEmpty()); + // expect empty return because interface prefix is not equal to the node name + auto exported_state_interfaces = controller.export_state_interfaces(); + ASSERT_THAT(exported_state_interfaces, IsEmpty()); } TEST_F(ChainableControllerInterfaceTest, setting_chained_mode) @@ -98,12 +107,15 @@ TEST_F(ChainableControllerInterfaceTest, setting_chained_mode) ASSERT_NO_THROW(controller.get_node()); auto reference_interfaces = controller.export_reference_interfaces(); - ASSERT_EQ(reference_interfaces.size(), 1u); + ASSERT_THAT(reference_interfaces, SizeIs(1)); + auto exported_state_interfaces = controller.export_state_interfaces(); + ASSERT_THAT(exported_state_interfaces, SizeIs(1)); EXPECT_FALSE(controller.is_in_chained_mode()); // Fail setting chained mode EXPECT_EQ(reference_interfaces[0].get_value(), INTERFACE_VALUE); + EXPECT_EQ(exported_state_interfaces[0].get_value(), EXPORTED_STATE_INTERFACE_VALUE); EXPECT_FALSE(controller.set_chained_mode(true)); EXPECT_FALSE(controller.is_in_chained_mode()); @@ -116,6 +128,7 @@ TEST_F(ChainableControllerInterfaceTest, setting_chained_mode) EXPECT_TRUE(controller.set_chained_mode(true)); EXPECT_TRUE(controller.is_in_chained_mode()); + EXPECT_EQ(exported_state_interfaces[0].get_value(), EXPORTED_STATE_INTERFACE_VALUE_IN_CHAINMODE); controller.configure(); EXPECT_TRUE(controller.set_chained_mode(false)); @@ -147,6 +160,7 @@ TEST_F(ChainableControllerInterfaceTest, test_update_logic) controller_interface::return_type::OK); ASSERT_NO_THROW(controller.get_node()); + EXPECT_FALSE(controller.set_chained_mode(false)); EXPECT_FALSE(controller.is_in_chained_mode()); // call update and update it from subscriber because not in chained mode @@ -154,6 +168,7 @@ TEST_F(ChainableControllerInterfaceTest, test_update_logic) controller.update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)), controller_interface::return_type::OK); ASSERT_EQ(controller.reference_interfaces_[0], INTERFACE_VALUE_INITIAL_REF - 1); + ASSERT_EQ(controller.state_interfaces_values_[0], EXPORTED_STATE_INTERFACE_VALUE + 1); // Provoke error in update from subscribers - return ERROR and update_and_write_commands not exec. controller.set_new_reference_interface_value(INTERFACE_VALUE_SUBSCRIBER_ERROR); @@ -161,6 +176,7 @@ TEST_F(ChainableControllerInterfaceTest, test_update_logic) controller.update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)), controller_interface::return_type::ERROR); ASSERT_EQ(controller.reference_interfaces_[0], INTERFACE_VALUE_INITIAL_REF - 1); + ASSERT_EQ(controller.state_interfaces_values_[0], EXPORTED_STATE_INTERFACE_VALUE + 1); // Provoke error from update - return ERROR, but reference interface is updated and not reduced controller.set_new_reference_interface_value(INTERFACE_VALUE_UPDATE_ERROR); @@ -168,6 +184,7 @@ TEST_F(ChainableControllerInterfaceTest, test_update_logic) controller.update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)), controller_interface::return_type::ERROR); ASSERT_EQ(controller.reference_interfaces_[0], INTERFACE_VALUE_UPDATE_ERROR); + ASSERT_EQ(controller.state_interfaces_values_[0], EXPORTED_STATE_INTERFACE_VALUE + 1); controller.reference_interfaces_[0] = 0.0; @@ -181,6 +198,8 @@ TEST_F(ChainableControllerInterfaceTest, test_update_logic) controller.update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)), controller_interface::return_type::OK); ASSERT_EQ(controller.reference_interfaces_[0], -1.0); + ASSERT_EQ( + controller.state_interfaces_values_[0], EXPORTED_STATE_INTERFACE_VALUE_IN_CHAINMODE + 1); // Provoke error from update - return ERROR, but reference interface is updated directly controller.set_new_reference_interface_value(INTERFACE_VALUE_SUBSCRIBER_ERROR); @@ -189,4 +208,6 @@ TEST_F(ChainableControllerInterfaceTest, test_update_logic) controller.update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)), controller_interface::return_type::ERROR); ASSERT_EQ(controller.reference_interfaces_[0], INTERFACE_VALUE_UPDATE_ERROR); + ASSERT_EQ( + controller.state_interfaces_values_[0], EXPORTED_STATE_INTERFACE_VALUE_IN_CHAINMODE + 1); } diff --git a/controller_interface/test/test_chainable_controller_interface.hpp b/controller_interface/test/test_chainable_controller_interface.hpp index 28401f13d5..f9684c27fd 100644 --- a/controller_interface/test/test_chainable_controller_interface.hpp +++ b/controller_interface/test/test_chainable_controller_interface.hpp @@ -29,24 +29,28 @@ constexpr double INTERFACE_VALUE = 1989.0; constexpr double INTERFACE_VALUE_SUBSCRIBER_ERROR = 12345.0; constexpr double INTERFACE_VALUE_UPDATE_ERROR = 67890.0; constexpr double INTERFACE_VALUE_INITIAL_REF = 1984.0; +constexpr double EXPORTED_STATE_INTERFACE_VALUE = 21833.0; +constexpr double EXPORTED_STATE_INTERFACE_VALUE_IN_CHAINMODE = 82802.0; class TestableChainableControllerInterface : public controller_interface::ChainableControllerInterface { public: - FRIEND_TEST(ChainableControllerInterfaceTest, reference_interfaces_storage_not_correct_size); + FRIEND_TEST(ChainableControllerInterfaceTest, interfaces_storage_not_correct_size); FRIEND_TEST(ChainableControllerInterfaceTest, test_update_logic); TestableChainableControllerInterface() { reference_interfaces_.reserve(1); reference_interfaces_.push_back(INTERFACE_VALUE); + state_interfaces_values_.reserve(1); + state_interfaces_values_.push_back(EXPORTED_STATE_INTERFACE_VALUE); } controller_interface::CallbackReturn on_init() override { // set default value - name_prefix_of_reference_interfaces_ = get_node()->get_name(); + name_prefix_of_interfaces_ = get_node()->get_name(); return controller_interface::CallbackReturn::SUCCESS; } @@ -63,13 +67,24 @@ class TestableChainableControllerInterface controller_interface::interface_configuration_type::NONE}; } + // Implementation of ChainableController virtual methods + std::vector on_export_state_interfaces() override + { + std::vector state_interfaces; + + state_interfaces.push_back(hardware_interface::StateInterface( + name_prefix_of_interfaces_, "test_state", &state_interfaces_values_[0])); + + return state_interfaces; + } + // Implementation of ChainableController virtual methods std::vector on_export_reference_interfaces() override { std::vector command_interfaces; command_interfaces.push_back(hardware_interface::CommandInterface( - name_prefix_of_reference_interfaces_, "test_itf", &reference_interfaces_[0])); + name_prefix_of_interfaces_, "test_itf", &reference_interfaces_[0])); return command_interfaces; } @@ -78,6 +93,7 @@ class TestableChainableControllerInterface { if (reference_interfaces_[0] == 0.0) { + state_interfaces_values_[0] = EXPORTED_STATE_INTERFACE_VALUE_IN_CHAINMODE; return true; } else @@ -107,13 +123,14 @@ class TestableChainableControllerInterface } reference_interfaces_[0] -= 1; + state_interfaces_values_[0] += 1; return controller_interface::return_type::OK; } void set_name_prefix_of_reference_interfaces(const std::string & prefix) { - name_prefix_of_reference_interfaces_ = prefix; + name_prefix_of_interfaces_ = prefix; } void set_new_reference_interface_value(const double ref_itf_value) @@ -121,7 +138,7 @@ class TestableChainableControllerInterface reference_interface_value_ = ref_itf_value; } - std::string name_prefix_of_reference_interfaces_; + std::string name_prefix_of_interfaces_; double reference_interface_value_ = INTERFACE_VALUE_INITIAL_REF; }; 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_interface/test/test_force_torque_sensor.hpp b/controller_interface/test/test_force_torque_sensor.hpp index 2d61f665c3..b26517a90c 100644 --- a/controller_interface/test/test_force_torque_sensor.hpp +++ b/controller_interface/test/test_force_torque_sensor.hpp @@ -68,13 +68,13 @@ class ForceTorqueSensorTest : public ::testing::Test protected: const size_t size_ = 6; const std::string sensor_name_ = "test_FTS"; - std::array force_values_ = {1.1, 2.2, 3.3}; - std::array torque_values_ = {4.4, 5.5, 6.6}; + std::array force_values_ = {{1.1, 2.2, 3.3}}; + std::array torque_values_ = {{4.4, 5.5, 6.6}}; std::unique_ptr force_torque_sensor_; std::vector full_interface_names_; - const std::vector fts_interface_names_ = {"force.x", "force.y", "force.z", - "torque.x", "torque.y", "torque.z"}; + const std::vector fts_interface_names_ = { + {"force.x", "force.y", "force.z", "torque.x", "torque.y", "torque.z"}}; }; #endif // TEST_FORCE_TORQUE_SENSOR_HPP_ diff --git a/controller_interface/test/test_imu_sensor.hpp b/controller_interface/test/test_imu_sensor.hpp index 9d0a39e7e5..801a425546 100644 --- a/controller_interface/test/test_imu_sensor.hpp +++ b/controller_interface/test/test_imu_sensor.hpp @@ -56,9 +56,9 @@ class IMUSensorTest : public ::testing::Test protected: const size_t size_ = 10; const std::string sensor_name_ = "test_IMU"; - std::array orientation_values_ = {1.1, 2.2, 3.3, 4.4}; - std::array angular_velocity_values_ = {4.4, 5.5, 6.6}; - std::array linear_acceleration_values_ = {4.4, 5.5, 6.6}; + std::array orientation_values_ = {{1.1, 2.2, 3.3, 4.4}}; + std::array angular_velocity_values_ = {{4.4, 5.5, 6.6}}; + std::array linear_acceleration_values_ = {{4.4, 5.5, 6.6}}; std::unique_ptr imu_sensor_; std::vector full_interface_names_; diff --git a/controller_manager/CHANGELOG.rst b/controller_manager/CHANGELOG.rst index f5bc17f0f3..1ef5eef03b 100644 --- a/controller_manager/CHANGELOG.rst +++ b/controller_manager/CHANGELOG.rst @@ -2,6 +2,74 @@ Changelog for package controller_manager ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.13.0 (2024-07-08) +------------------- +* Change the spamming checking interface ERROR to DEBUG (`#1605 `_) +* [ResourceManager] Propagate access to logger and clock interfaces to HardwareComponent (`#1585 `_) +* [ControllerChaining] Export state interfaces from chainable controllers (`#1021 `_) +* Contributors: Sai Kishor Kothakota + +4.12.0 (2024-07-01) +------------------- +* [rqt_controller_manager] Add hardware components (`#1455 `_) +* [RM] Rename `load_urdf` method to `load_and_initialize_components` and add error handling there to avoid stack crashing when error happens. (`#1354 `_) +* Fix update `period` for the first update after activation (`#1551 `_) +* Bump version of pre-commit hooks (`#1556 `_) +* Contributors: Christoph Fröhlich, Dr. Denis, github-actions[bot] + +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 `_) +* Use ament_cmake generated rclcpp version header (`#1448 `_) +* Replace random_shuffle with shuffle. (`#1446 `_) +* Contributors: Chris Lalancette, Sai Kishor Kothakota + +4.7.0 (2024-03-22) +------------------ +* add missing compiler definitions of RCLCPP_VERSION_MAJOR (`#1440 `_) +* Codeformat from new pre-commit config (`#1433 `_) +* add conditioning to get_parameter_value method import (`#1428 `_) +* Change the controller sorting with an approach similar to directed acyclic graphs (`#1384 `_) +* Contributors: Christoph Fröhlich, Sai Kishor Kothakota + +4.6.0 (2024-03-02) +------------------ +* Add -Werror=missing-braces to compile options (`#1423 `_) +* added conditioning to have rolling tags compilable in older versions (`#1422 `_) +* [CM] Remove deprecated parameters for initial component states. (`#1357 `_) +* [BREAKING CHANGE] Use `robot_description` topic instead of `~/robot_description` and update docs regarding this (`#1410 `_) +* [CI] Code coverage + pre-commit (`#1413 `_) +* Fix multiple chainable controller activation bug (`#1401 `_) +* Contributors: Christoph Fröhlich, Dr. Denis, Felix Exner (fexner), Sai Kishor Kothakota + +4.5.0 (2024-02-12) +------------------ +* check for state of the controller node before cleanup (`#1363 `_) +* [CM] Use explicit constants in controller tests. (`#1356 `_) +* [CM] Optimized debug output about interfaces when switching controllers. (`#1355 `_) +* A method to get node options to setup the controller node #api-breaking (`#1169 `_) +* Contributors: Dr. Denis, Sai Kishor Kothakota + 4.4.0 (2024-01-31) ------------------ * Move `test_components` to own package (`#1325 `_) diff --git a/controller_manager/CMakeLists.txt b/controller_manager/CMakeLists.txt index 0d1b88a55a..7c437d35e0 100644 --- a/controller_manager/CMakeLists.txt +++ b/controller_manager/CMakeLists.txt @@ -2,7 +2,8 @@ cmake_minimum_required(VERSION 3.16) project(controller_manager LANGUAGES CXX) if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") - add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow) + add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow + -Werror=missing-braces) endif() set(THIS_PACKAGE_INCLUDE_DEPENDS @@ -20,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 +192,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 ) @@ -225,3 +233,4 @@ ament_python_install_package(controller_manager ament_export_targets(export_controller_manager HAS_LIBRARY_TARGET) ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS}) ament_package() +ament_generate_version_header(${PROJECT_NAME}) 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 cf78442856..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, @@ -32,7 +33,13 @@ from rcl_interfaces.msg import Parameter from rclpy.duration import Duration from rclpy.node import Node -from rclpy.parameter import get_parameter_value + +# @note: The versions conditioning is added here to support the source-compatibility with Humble +# The `get_parameter_value` function is moved to `rclpy.parameter` module from `ros2param.api` module from version 3.6.0 +try: + from rclpy.parameter import get_parameter_value +except ImportError: + from ros2param.api import get_parameter_value from rclpy.signals import SignalHandlerOptions from ros2param.api import call_set_parameters @@ -129,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() @@ -164,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, ) @@ -188,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) @@ -195,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) @@ -220,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 @@ -231,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" @@ -295,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/controller_chaining.rst b/controller_manager/doc/controller_chaining.rst index c83ed97ac0..1103a7ae5a 100644 --- a/controller_manager/doc/controller_chaining.rst +++ b/controller_manager/doc/controller_chaining.rst @@ -27,7 +27,7 @@ To describe the intent of this document, lets focus on the simple yet sufficient :alt: Example2 -In this example, we want to chain 'position_tracking' controller with 'diff_drive_controller' and two PID controllers. +In this example, we want to chain 'position_tracking' controller with 'diff_drive_controller' and two PID controllers as well as the 'robot_localization' controller. Let's now imagine a use-case where we don't only want to run all those controllers as a group, but also flexibly add preceding steps. This means the following: @@ -37,9 +37,19 @@ This means the following: 2. Then "diff_drive_controller" is activated and attaches itself to the virtual input interfaces of PID controllers. PID controllers also get informed that they are working in chained mode and therefore disable their external interface through subscriber. Now we check if kinematics of differential robot is running properly. - 3. After that, "position_tracking" can be activated and attached to "diff_drive_controller" that disables its external interfaces. - 4. If any of the controllers is deactivated, also all preceding controllers are deactivated. + 3. Once the 'diff_drive_controller' is activated, it exposes the 'odom' state interfaces that is used by 'odom_publisher' as well as 'sensor_fusion' controllers. + The 'odom_publisher' controller is activated and attaches itself to the exported 'odom' state interfaces of 'diff_drive_controller'. + The 'sensor_fusion' controller is activated and attaches itself to the exported 'odom' state interfaces of 'diff_drive_controller' along with the 'imu' state interfaces. + 4. Once the 'sensor_fusion' controller is activated, it exposes the 'odom' state interfaces that is used by 'robot_localization' controller. + The 'robot_localization' controller is activated and attaches itself to the 'odom' state interfaces of 'sensor_fusion' controller. + Once activated, the 'robot_localization' controller exposes the 'actual_pose' state interfaces that is used by 'position_tracking' controller. + 5. After that, "position_tracking" can be activated and attached to "diff_drive_controller" that disables its external interfaces and to the 'robot_localization' controller which provides the 'actual_pose' state interface. + 6. If any of the controllers is deactivated, also all preceding controllers needs to be deactivated. +.. note:: + + Only the controllers that exposes the reference interfaces are switched to chained mode, when their reference interfaces are used by other controllers. When their reference interfaces are not used by the other controllers, they are switched back to get references from the subscriber. + However, the controllers that exposes the state interfaces are not triggered to chained mode, when their state interfaces are used by other controllers. Implementation -------------- @@ -47,19 +57,34 @@ Implementation A Controller Base-Class: ChainableController ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A ``ChainableController`` extends ``ControllerInterface`` class with ``virtual InterfaceConfiguration input_interface_configuration() const = 0`` method. -This method should implement for each controller that **can be preceded** by another controller exporting all the input interfaces. -For simplicity reasons, it is assumed for now that controller's all input interfaces are used. -Therefore, do not try to implement any exclusive combinations of input interfaces, but rather write multiple controllers if you need exclusivity. +A ``ChainableController`` extends ``ControllerInterface`` class with ``virtual std::vector export_reference_interfaces() = 0`` method as well as ``virtual std::vector export_state_interfaces() = 0`` method. +This method should be implemented for each controller that **can be preceded** by another controller exporting all the reference/state interfaces. +For simplicity reasons, it is assumed for now that controller's all reference interfaces are used by other controller. However, the state interfaces exported by the controller, can be used by multiple controllers at the same time and with the combination they want. +Therefore, do not try to implement any exclusive combinations of reference interfaces, but rather write multiple controllers if you need exclusivity. -The ``ChainableController`` base class implements ``void set_chained_mode(bool activate)`` that sets an internal flag that a controller is used by another controller (in chained mode) and calls ``virtual void on_set_chained_mode(bool activate) = 0`` that implements controller's specific actions when chained modes is activated or deactivated, e.g., deactivating subscribers. +The ``ChainableController`` base class implements ``void set_chained_mode(bool activate)`` that sets an internal flag that a controller is used by another controller (in chained mode) and calls ``virtual void on_set_chained_mode(bool activate) = 0`` that implements controller's specific actions when chained mode is activated or deactivated, e.g., deactivating subscribers. As an example, PID controllers export one virtual interface ``pid_reference`` and stop their subscriber ``/pid_reference`` when used in chained mode. 'diff_drive_controller' controller exports list of virtual interfaces ``/v_x``, ``/v_y``, and ``/w_z``, and stops subscribers from topics ``/cmd_vel`` and ``/cmd_vel_unstamped``. Its publishers can continue running. +Nomenclature +^^^^^^^^^^^^^^ + +There are two types of interfaces within the context of ``ros2_control``: ``CommandInterface`` and ``StateInterface``. + +- The ``CommandInterface`` is a Read-Write type of interface that can be used to get and set values. Its typical use-case is to set command values to the hardware. +- The ``StateInterface`` is a Read-Only type of interface that can be used to get values. Its typical use-case is to get actual state values from the hardware. + +These interfaces are utilized in different places within the controller in order to have a functional controller or controller chain that commands the hardware. + +- The ``virtual InterfaceConfiguration command_interface_configuration() const`` method defined in the ``ControllerInterface`` class is used to define the command interfaces used by the controller. These interfaces are used to command the hardware or the exposed reference interfaces from another controller. The ``controller_manager`` uses this configuration to claim the command interfaces from the ``ResourceManager``. +- The ``virtual InterfaceConfiguration state_interface_configuration() const`` method defined in the ``ControllerInterface`` class is used to define the state interfaces used by the controller. These interfaces are used to get the actual state values from the hardware or the exposed state interfaces from another controller. The ``controller_manager`` uses this configuration to claim the state interfaces from the ``ResourceManager``. +- The ``std::vector export_reference_interfaces()`` method defined in the ``ChainableController`` class is used to define the reference interfaces exposed by the controller. These interfaces are used to command the controller from other controllers. +- The ``std::vector export_state_interfaces()`` method defined in the ``ChainableController`` class is used to define the state interfaces exposed by the controller. These interfaces are used to get the actual state values from the controller by other controllers. + Inner Resource Management ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -After configuring a chainable controller, controller manager calls ``input_interface_configuration`` method and takes ownership over controller's input interfaces. +After configuring a chainable controller, controller manager calls ``export_reference_interfaces`` and ``export_state_interfaces`` method and takes ownership over controller's exported reference/state interfaces. This is the same process as done by ``ResourceManager`` and hardware interfaces. Controller manager maintains "claimed" status of interface in a vector (the same as done in ``ResourceManager``). @@ -71,6 +96,8 @@ Chained controllers must be activated and deactivated together or in the proper This means you must first activate all following controllers to have the preceding one activated. For the deactivation there is the inverse rule - all preceding controllers have to be deactivated before the following controller is deactivated. One can also think of it as an actual chain, you can not add a chain link or break the chain in the middle. +The chained controllers can also be activated when parsed as in a single list through the fields ``activate_controllers`` or ``deactivate_controllers`` in the ``switch_controllers`` service provided by the controller_manager. +The controller_manager ``spawner`` can also be used to activate all the controllers of the chain in a single call, by parsing the argument ``--activate-as-group``. Debugging outputs @@ -84,4 +111,4 @@ Debugging outputs Closing remarks ---------------------------- -- Maybe addition of the new controller's type ``ChainableController`` is not necessary. It would also be feasible to add implementation of ``input_interface_configuration()`` method into ``ControllerInterface`` class with default result ``interface_configuration_type::NONE``. +- Maybe addition of the new controller's type ``ChainableController`` is not necessary. It would also be feasible to add implementation of ``export_reference_interfaces()`` and ``export_state_interfaces()`` method into ``ControllerInterface`` class with default result ``interface_configuration_type::NONE``. diff --git a/controller_manager/doc/images/chaining_example2.png b/controller_manager/doc/images/chaining_example2.png index 1ba49a116e..c21ab88d72 100644 Binary files a/controller_manager/doc/images/chaining_example2.png and b/controller_manager/doc/images/chaining_example2.png differ diff --git a/controller_manager/doc/images/rqt_controller_manager.png b/controller_manager/doc/images/rqt_controller_manager.png new file mode 100644 index 0000000000..01c4f55bdf Binary files /dev/null and b/controller_manager/doc/images/rqt_controller_manager.png differ diff --git a/controller_manager/doc/userdoc.rst b/controller_manager/doc/userdoc.rst index 46c46fa028..02e532866d 100644 --- a/controller_manager/doc/userdoc.rst +++ b/controller_manager/doc/userdoc.rst @@ -45,6 +45,15 @@ Alternatives to the standard kernel include Though installing a realtime-kernel will definitely get the best results when it comes to low jitter, using a lowlatency kernel can improve things a lot with being really easy to install. +Subscribers +----------- + +robot_description [std_msgs::msg::String] + String with the URDF xml, e.g., from ``robot_state_publisher``. + Reloading of the URDF is not supported yet. + All joints defined in the ````-tag have to be present in the URDF. + + Parameters ----------- @@ -70,19 +79,56 @@ hardware_components_initial_state.unconfigured (optional; list; default: hardware_components_initial_state.inactive (optional; list; default: empty) Defines which hardware components will be configured immediately when controller manager is started. -robot_description (mandatory; string) - String with the URDF string as robot description. - This is usually result of the parsed description files by ``xacro`` command. - update_rate (mandatory; integer) The frequency of controller manager's real-time update loop. This loop reads states from hardware, updates controller and writes commands to hardware. - .type Name of a plugin exported using ``pluginlib`` for a controller. This is a class from which controller's instance with name "``controller_name``" is created. +Handling Multiple Controller Managers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When dealing with multiple controller managers, you have two options for managing different robot descriptions: + +1. **Using Namespaces:** You can place both the ``robot_state_publisher`` and the ``controller_manager`` nodes into the same namespace. + +.. code-block:: python + + control_node = Node( + package="controller_manager", + executable="ros2_control_node", + parameters=[robot_controllers], + output="both", + namespace="rrbot", + ) + robot_state_pub_node = Node( + package="robot_state_publisher", + executable="robot_state_publisher", + output="both", + parameters=[robot_description], + namespace="rrbot", + ) + +2. **Using Remappings:** You can use remappings to handle different robot descriptions. This involves relaying topics using the ``remappings`` tag, allowing you to specify custom topics for each controller manager. + +.. code-block:: python + + control_node = Node( + package="controller_manager", + executable="ros2_control_node", + parameters=[robot_controllers], + output="both", + remappings=[('robot_description', '/rrbot/robot_description')] + ) + robot_state_pub_node = Node( + package="robot_state_publisher", + executable="robot_state_publisher", + output="both", + parameters=[robot_description], + namespace="rrbot", + ) Helper scripts -------------- @@ -138,6 +184,21 @@ There are two scripts to interact with controller manager from launch files: -c CONTROLLER_MANAGER, --controller-manager CONTROLLER_MANAGER Name of the controller manager ROS node +rqt_controller_manager +---------------------- +A GUI tool to interact with the controller manager services to be able to switch the lifecycle states of the controllers as well as the hardware components. + +.. image:: images/rqt_controller_manager.png + +It can be launched independently using the following command or as rqt plugin. + +.. code-block:: console + + ros2 run rqt_controller_manager rqt_controller_manager + + * Double-click on a controller or hardware component to show the additional info. + * Right-click on a controller or hardware component to show a context menu with options for lifecycle management. + Using the Controller Manager in a Process ----------------------------------------- @@ -179,6 +240,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 c0a22fe5bd..393c0e64f9 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" @@ -81,6 +82,13 @@ class ControllerManager : public rclcpp::Node const std::string & node_namespace = "", const rclcpp::NodeOptions & options = get_cm_node_options()); + CONTROLLER_MANAGER_PUBLIC + ControllerManager( + std::shared_ptr executor, const std::string & urdf, + bool activate_all_hw_components, const std::string & manager_node_name = "controller_manager", + const std::string & node_namespace = "", + const rclcpp::NodeOptions & options = get_cm_node_options()); + CONTROLLER_MANAGER_PUBLIC virtual ~ControllerManager() = default; @@ -192,10 +200,36 @@ class ControllerManager : public rclcpp::Node // the executor (see issue #260). // rclcpp::CallbackGroup::SharedPtr deterministic_callback_group_; - // Per controller update rate support + /// Interface for external components to check if Resource Manager is initialized. + /** + * Checks if components in Resource Manager are loaded and initialized. + * \returns true if they are initialized, false otherwise. + */ + CONTROLLER_MANAGER_PUBLIC + bool is_resource_manager_initialized() const + { + return resource_manager_ && resource_manager_->are_components_initialized(); + } + + /// Update rate of the main control loop in the controller manager. + /** + * Update rate of the main control loop in the controller manager. + * The method is used for per-controller update rate support. + * + * \returns update rate of the controller manager. + */ 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(); @@ -390,28 +424,28 @@ class ControllerManager : public rclcpp::Node const std::vector & controllers, int strictness, const ControllersListIterator controller_it); - /// A method to be used in the std::sort method to sort the controllers to be able to - /// execute them in a proper order /** - * Compares the controllers ctrl_a and ctrl_b and then returns which comes first in the sequence - * - * @note The following conditions needs to be handled while ordering the controller list - * 1. The controllers that do not use any state or command interfaces are updated first - * 2. The controllers that use only the state system interfaces only are updated next - * 3. The controllers that use any of an another controller's reference interface are updated - * before the preceding controller - * 4. The controllers that use the controller's estimated interfaces are updated after the - * preceding controller - * 5. The controllers that only use the hardware command interfaces are updated last - * 6. All inactive controllers go at the end of the list + * @brief Inserts a controller into an ordered list based on dependencies to compute the + * controller chain. * - * \param[in] controllers list of controllers to compare their names to interface's prefix. + * This method computes the controller chain by inserting the provided controller name into an + * ordered list of controllers based on dependencies. It ensures that controllers are inserted in + * the correct order so that dependencies are satisfied. * - * @return true, if ctrl_a needs to execute first, else false + * @param ctrl_name The name of the controller to be inserted into the chain. + * @param controller_iterator An iterator pointing to the position in the ordered list where the + * controller should be inserted. + * @param append_to_controller Flag indicating whether the controller should be appended or + * prepended to the parsed iterator. + * @note The specification of controller dependencies is in the ControllerChainSpec, + * containing information about following and preceding controllers. This struct should include + * the neighboring controllers with their relationships to the provided controller. + * `following_controllers` specify controllers that come after the provided controller. + * `preceding_controllers` specify controllers that come before the provided controller. */ - bool controller_sorting( - const ControllerSpec & ctrl_a, const ControllerSpec & ctrl_b, - const std::vector & controllers); + void update_list_with_controller_chain( + const std::string & ctrl_name, std::vector::iterator controller_iterator, + bool append_to_controller); void controller_activity_diagnostic_callback(diagnostic_updater::DiagnosticStatusWrapper & stat); @@ -515,6 +549,8 @@ class ControllerManager : public rclcpp::Node }; RTControllerListWrapper rt_controllers_wrapper_; + std::unordered_map controller_chain_spec_; + std::vector ordered_controllers_names_; /// mutex copied from ROS1 Control, protects service callbacks /// not needed if we're guaranteed that the callbacks don't come from multiple threads std::mutex services_lock_; @@ -544,6 +580,9 @@ class ControllerManager : public rclcpp::Node std::vector activate_command_interface_request_, deactivate_command_interface_request_; + std::map> controller_chained_reference_interfaces_cache_; + std::map> controller_chained_state_interfaces_cache_; + std::string robot_description_; rclcpp::Subscription::SharedPtr robot_description_subscription_; rclcpp::TimerBase::SharedPtr robot_description_notification_timer_; @@ -562,65 +601,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/include/controller_manager/controller_spec.hpp b/controller_manager/include/controller_manager/controller_spec.hpp index 6f7483f3ec..d13e9c56bd 100644 --- a/controller_manager/include/controller_manager/controller_spec.hpp +++ b/controller_manager/include/controller_manager/controller_spec.hpp @@ -41,5 +41,10 @@ struct ControllerSpec std::shared_ptr next_update_cycle_time; }; +struct ControllerChainSpec +{ + std::vector following_controllers; + std::vector preceding_controllers; +}; } // namespace controller_manager #endif // CONTROLLER_MANAGER__CONTROLLER_SPEC_HPP_ diff --git a/controller_manager/package.xml b/controller_manager/package.xml index e58ff84c3d..c8da38364a 100644 --- a/controller_manager/package.xml +++ b/controller_manager/package.xml @@ -2,13 +2,14 @@ controller_manager - 4.4.0 + 4.13.0 Description of controller_manager Bence Magyar Denis Štogl Apache License 2.0 ament_cmake + ament_cmake_gen_version_h ament_cmake_python ament_index_cpp diff --git a/controller_manager/src/controller_manager.cpp b/controller_manager/src/controller_manager.cpp index 932dfd392b..fa1fd067da 100644 --- a/controller_manager/src/controller_manager.cpp +++ b/controller_manager/src/controller_manager.cpp @@ -25,6 +25,7 @@ #include "hardware_interface/types/lifecycle_state_names.hpp" #include "lifecycle_msgs/msg/state.hpp" #include "rclcpp/rclcpp.hpp" +#include "rclcpp/version.h" #include "rclcpp_lifecycle/state.hpp" namespace // utility @@ -36,10 +37,24 @@ static constexpr const char * kChainableControllerInterfaceClassName = "controller_interface::ChainableControllerInterface"; // Changed services history QoS to keep all so we don't lose any client service calls +// \note The versions conditioning is added here to support the source-compatibility with Humble +#if RCLCPP_VERSION_MAJOR >= 17 rclcpp::QoS qos_services = rclcpp::QoS(rclcpp::QoSInitialization(RMW_QOS_POLICY_HISTORY_KEEP_ALL, 1)) .reliable() .durability_volatile(); +#else +static const rmw_qos_profile_t qos_services = { + RMW_QOS_POLICY_HISTORY_KEEP_ALL, + 1, // message queue depth + RMW_QOS_POLICY_RELIABILITY_RELIABLE, + RMW_QOS_POLICY_DURABILITY_VOLATILE, + RMW_QOS_DEADLINE_DEFAULT, + RMW_QOS_LIFESPAN_DEFAULT, + RMW_QOS_POLICY_LIVELINESS_SYSTEM_DEFAULT, + RMW_QOS_LIVELINESS_LEASE_DURATION_DEFAULT, + false}; +#endif inline bool is_controller_inactive(const controller_interface::ControllerInterfaceBase & controller) { @@ -68,11 +83,11 @@ bool controller_name_compare(const controller_manager::ControllerSpec & a, const return a.info.name == name; } -/// Checks if a command interface belongs to a controller based on its prefix. +/// Checks if an interface belongs to a controller based on its prefix. /** - * A command interface can be provided by a controller in which case is called "reference" - * interface. - * This means that the @interface_name starts with the name of a controller. + * A State/Command interface can be provided by a controller in which case is called + * "state/reference" interface. This means that the @interface_name starts with the name of a + * controller. * * \param[in] interface_name to be found in the map. * \param[in] controllers list of controllers to compare their names to interface's prefix. @@ -80,7 +95,7 @@ bool controller_name_compare(const controller_manager::ControllerSpec & a, const * @interface_name belongs to. * \return true if interface has a controller name as prefix, false otherwise. */ -bool command_interface_is_reference_interface_of_controller( +bool is_interface_a_chained_interface( const std::string interface_name, const std::vector & controllers, controller_manager::ControllersListIterator & following_controller_it) @@ -110,136 +125,50 @@ bool command_interface_is_reference_interface_of_controller( { RCLCPP_DEBUG( rclcpp::get_logger("ControllerManager::utils"), - "Required command interface '%s' with prefix '%s' is not reference interface.", - interface_name.c_str(), interface_prefix.c_str()); + "Required interface '%s' with prefix '%s' is not a chain interface.", interface_name.c_str(), + interface_prefix.c_str()); return false; } return true; } -/** - * A method to retrieve the names of all it's following controllers given a controller name - * For instance, for the following case - * A -> B -> C -> D - * When called with B, returns C and D - * NOTE: A -> B signifies that the controller A is utilizing the reference interfaces exported from - * the controller B (or) the controller B is utilizing the expected interfaces exported from the - * controller A - * - * @param controller_name - Name of the controller for checking the tree - * \param[in] controllers list of controllers to compare their names to interface's prefix. - * @return list of controllers that are following the given controller in a chain. If none, return - * empty. - */ -std::vector get_following_controller_names( - const std::string controller_name, - const std::vector & controllers) +template +void add_element_to_list(std::vector & list, const T & element) { - std::vector following_controllers; - auto controller_it = std::find_if( - controllers.begin(), controllers.end(), - std::bind(controller_name_compare, std::placeholders::_1, controller_name)); - if (controller_it == controllers.end()) + if (std::find(list.begin(), list.end(), element) == list.end()) { - RCLCPP_DEBUG( - rclcpp::get_logger("ControllerManager::utils"), - "Required controller : '%s' is not found in the controller list ", controller_name.c_str()); - - return following_controllers; + // Only add to the list if it doesn't exist + list.push_back(element); } - // If the controller is not configured, return empty - if (!(is_controller_active(controller_it->c) || is_controller_inactive(controller_it->c))) +} + +template +void remove_element_from_list(std::vector & list, const T & element) +{ + auto itr = std::find(list.begin(), list.end(), element); + if (itr != list.end()) { - return following_controllers; + list.erase(itr); } - const auto cmd_itfs = controller_it->c->command_interface_configuration().names; - for (const auto & itf : cmd_itfs) - { - controller_manager::ControllersListIterator ctrl_it; - if (command_interface_is_reference_interface_of_controller(itf, controllers, ctrl_it)) - { - RCLCPP_DEBUG( - rclcpp::get_logger("ControllerManager::utils"), - "The interface is a reference interface of controller : %s", ctrl_it->info.name.c_str()); - following_controllers.push_back(ctrl_it->info.name); - const std::vector ctrl_names = - get_following_controller_names(ctrl_it->info.name, controllers); - for (const std::string & controller : ctrl_names) - { - if ( - std::find(following_controllers.begin(), following_controllers.end(), controller) == - following_controllers.end()) - { - // Only add to the list if it doesn't exist - following_controllers.push_back(controller); - } - } - } - } - return following_controllers; } -/** - * A method to retrieve the names of all it's preceding controllers given a controller name - * For instance, for the following case - * A -> B -> C -> D - * When called with C, returns A and B - * NOTE: A -> B signifies that the controller A is utilizing the reference interfaces exported from - * the controller B (or) the controller B is utilizing the expected interfaces exported from the - * controller A - * - * @param controller_name - Name of the controller for checking the tree - * \param[in] controllers list of controllers to compare their names to interface's prefix. - * @return list of controllers that are preceding the given controller in a chain. If none, return - * empty. - */ -std::vector get_preceding_controller_names( - const std::string controller_name, - const std::vector & controllers) +void controller_chain_spec_cleanup( + std::unordered_map & ctrl_chain_spec, + const std::string & controller) { - std::vector preceding_controllers; - auto controller_it = std::find_if( - controllers.begin(), controllers.end(), - std::bind(controller_name_compare, std::placeholders::_1, controller_name)); - if (controller_it == controllers.end()) + const auto following_controllers = ctrl_chain_spec[controller].following_controllers; + const auto preceding_controllers = ctrl_chain_spec[controller].preceding_controllers; + for (const auto & flwg_ctrl : following_controllers) { - RCLCPP_DEBUG( - rclcpp::get_logger("ControllerManager::utils"), - "Required controller : '%s' is not found in the controller list ", controller_name.c_str()); - return preceding_controllers; + remove_element_from_list(ctrl_chain_spec[flwg_ctrl].preceding_controllers, controller); } - for (const auto & ctrl : controllers) + for (const auto & preced_ctrl : preceding_controllers) { - // If the controller is not configured, then continue - if (!(is_controller_active(ctrl.c) || is_controller_inactive(ctrl.c))) - { - continue; - } - auto cmd_itfs = ctrl.c->command_interface_configuration().names; - for (const auto & itf : cmd_itfs) - { - auto split_pos = itf.find_first_of('/'); - if ((split_pos != std::string::npos) && (itf.substr(0, split_pos) == controller_name)) - { - preceding_controllers.push_back(ctrl.info.name); - auto ctrl_names = get_preceding_controller_names(ctrl.info.name, controllers); - for (const std::string & controller : ctrl_names) - { - if ( - std::find(preceding_controllers.begin(), preceding_controllers.end(), controller) == - preceding_controllers.end()) - { - // Only add to the list if it doesn't exist - preceding_controllers.push_back(controller); - } - } - } - } + remove_element_from_list(ctrl_chain_spec[preced_ctrl].following_controllers, controller); } - return preceding_controllers; + ctrl_chain_spec.erase(controller); } - } // namespace namespace controller_manager @@ -250,6 +179,10 @@ rclcpp::NodeOptions get_cm_node_options() // Required for getting types of controllers to be loaded via service call node_options.allow_undeclared_parameters(true); node_options.automatically_declare_parameters_from_overrides(true); +// \note The versions conditioning is added here to support the source-compatibility until Humble +#if RCLCPP_VERSION_MAJOR >= 21 + node_options.enable_logger_service(true); +#endif return node_options; } @@ -258,7 +191,7 @@ ControllerManager::ControllerManager( const std::string & node_namespace, const rclcpp::NodeOptions & options) : rclcpp::Node(manager_node_name, node_namespace, options), resource_manager_(std::make_unique( - update_rate_, this->get_node_clock_interface())), + this->get_node_clock_interface(), this->get_node_logging_interface())), diagnostics_updater_(this), executor_(executor), loader_(std::make_shared>( @@ -270,6 +203,39 @@ ControllerManager::ControllerManager( init_controller_manager(); } +ControllerManager::ControllerManager( + std::shared_ptr executor, const std::string & urdf, + bool activate_all_hw_components, const std::string & manager_node_name, + const std::string & node_namespace, const rclcpp::NodeOptions & options) +: rclcpp::Node(manager_node_name, node_namespace, options), + update_rate_(get_parameter_or("update_rate", 100)), + resource_manager_(std::make_unique( + urdf, this->get_node_clock_interface(), this->get_node_logging_interface(), + activate_all_hw_components, update_rate_)), + diagnostics_updater_(this), + executor_(executor), + loader_(std::make_shared>( + kControllerInterfaceNamespace, kControllerInterfaceClassName)), + chainable_loader_( + std::make_shared>( + kControllerInterfaceNamespace, kChainableControllerInterfaceClassName)) +{ + if (!get_parameter("update_rate", update_rate_)) + { + RCLCPP_WARN( + get_logger(), "'update_rate' parameter not set, using default value of %d Hz.", update_rate_); + } + + if (is_resource_manager_initialized()) + { + init_services(); + } + + diagnostics_updater_.setHardwareID("ros2_control"); + diagnostics_updater_.add( + "Controllers Activity", this, &ControllerManager::controller_activity_diagnostic_callback); +} + ControllerManager::ControllerManager( std::unique_ptr resource_manager, std::shared_ptr executor, const std::string & manager_node_name, @@ -292,7 +258,8 @@ void ControllerManager::init_controller_manager() // Get parameters needed for RT "update" loop to work if (!get_parameter("update_rate", update_rate_)) { - RCLCPP_WARN(get_logger(), "'update_rate' parameter not set, using default value."); + RCLCPP_WARN( + get_logger(), "'update_rate' parameter not set, using default value of %d Hz.", update_rate_); } robot_description_notification_timer_ = create_wall_timer( @@ -300,13 +267,17 @@ void ControllerManager::init_controller_manager() [&]() { RCLCPP_WARN( - get_logger(), "Waiting for data on '~/robot_description' topic to finish initialization"); + get_logger(), "Waiting for data on 'robot_description' topic to finish initialization"); }); + if (is_resource_manager_initialized()) + { + init_services(); + } // set QoS to transient local to get messages that have already been published // (if robot state publisher starts before controller manager) robot_description_subscription_ = create_subscription( - "~/robot_description", rclcpp::QoS(1).transient_local(), + "robot_description", rclcpp::QoS(1).transient_local(), std::bind(&ControllerManager::robot_description_callback, this, std::placeholders::_1)); RCLCPP_INFO( get_logger(), "Subscribing to '%s' topic for robot description.", @@ -323,27 +294,30 @@ void ControllerManager::robot_description_callback(const std_msgs::msg::String & RCLCPP_INFO(get_logger(), "Received robot description from topic."); RCLCPP_DEBUG( get_logger(), "'Content of robot description file: %s", robot_description.data.c_str()); - if (resource_manager_->is_urdf_already_loaded()) + robot_description_ = robot_description.data; + if (is_resource_manager_initialized()) { RCLCPP_WARN( get_logger(), - "ResourceManager has already loaded an urdf file. Ignoring attempt to reload a robot " - "description file."); + "ResourceManager has already loaded a urdf. Ignoring attempt to reload a robot description."); return; } - robot_description_ = robot_description.data; init_resource_manager(robot_description_); + if (is_resource_manager_initialized()) + { + init_services(); + } } void ControllerManager::init_resource_manager(const std::string & robot_description) { - if (!resource_manager_->load_urdf(robot_description)) + if (!resource_manager_->load_and_initialize_components(robot_description, update_rate_)) { RCLCPP_WARN( get_logger(), - "URDF validation went wrong check the previous output. This might only mean that interfaces " - "defined in URDF and exported by the hardware do not match. Therefore continue initializing " - "controller manager..."); + "Could not load and initialize hardware. Please check previous output for more details. " + "After you have corrected your URDF, try to publish robot description again."); + return; } // Get all components and if they are not defined in parameters activate them automatically @@ -388,56 +362,17 @@ void ControllerManager::init_resource_manager(const std::string & robot_descript State::PRIMARY_STATE_UNCONFIGURED, hardware_interface::lifecycle_state_names::UNCONFIGURED)); // inactive (configured) - // BEGIN: Keep old functionality on for backwards compatibility (Remove at the end of 2023) - std::vector configure_components_on_start = std::vector({}); - get_parameter("configure_components_on_start", configure_components_on_start); - if (!configure_components_on_start.empty()) - { - RCLCPP_WARN( - get_logger(), - "Parameter 'configure_components_on_start' is deprecated. " - "Use 'hardware_components_initial_state.inactive' instead, to set component's initial " - "state to 'inactive'. Don't use this parameters in combination with the new " - "'hardware_components_initial_state' parameter structure."); - set_components_to_state( - "configure_components_on_start", - rclcpp_lifecycle::State( - State::PRIMARY_STATE_INACTIVE, hardware_interface::lifecycle_state_names::INACTIVE)); - } - // END: Keep old functionality on humble backwards compatibility (Remove at the end of 2023) - else - { - set_components_to_state( - "hardware_components_initial_state.inactive", - rclcpp_lifecycle::State( - State::PRIMARY_STATE_INACTIVE, hardware_interface::lifecycle_state_names::INACTIVE)); - } + set_components_to_state( + "hardware_components_initial_state.inactive", + rclcpp_lifecycle::State( + State::PRIMARY_STATE_INACTIVE, hardware_interface::lifecycle_state_names::INACTIVE)); - // BEGIN: Keep old functionality on for backwards compatibility (Remove at the end of 2023) - std::vector activate_components_on_start = std::vector({}); - get_parameter("activate_components_on_start", activate_components_on_start); - rclcpp_lifecycle::State active_state( - State::PRIMARY_STATE_ACTIVE, hardware_interface::lifecycle_state_names::ACTIVE); - if (!activate_components_on_start.empty()) + // activate all other components + for (const auto & [component, state] : components_to_activate) { - RCLCPP_WARN( - get_logger(), - "Parameter 'activate_components_on_start' is deprecated. " - "Components are activated per default. Don't use this parameters in combination with the new " - "'hardware_components_initial_state' parameter structure."); - for (const auto & component : activate_components_on_start) - { - resource_manager_->set_component_state(component, active_state); - } - } - // END: Keep old functionality on humble for backwards compatibility (Remove at the end of 2023) - else - { - // activate all other components - for (const auto & [component, state] : components_to_activate) - { - resource_manager_->set_component_state(component, active_state); - } + rclcpp_lifecycle::State active_state( + State::PRIMARY_STATE_ACTIVE, hardware_interface::lifecycle_state_names::ACTIVE); + resource_manager_->set_component_state(component, active_state); } // Init CM services first after the URDF is loaded an components are set @@ -569,6 +504,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); } @@ -640,9 +586,38 @@ controller_interface::return_type ControllerManager::unload_controller( } RCLCPP_DEBUG(get_logger(), "Cleanup controller"); + controller_chain_spec_cleanup(controller_chain_spec_, controller_name); // TODO(destogl): remove reference interface if chainable; i.e., add a separate method for // cleaning-up controllers? - controller.c->get_node()->cleanup(); + if (is_controller_inactive(*controller.c)) + { + RCLCPP_DEBUG( + 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? + try + { + 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()); + } + } executor_->remove_node(controller.c->get_node()->get_node_base_interface()); to.erase(found_it); @@ -704,22 +679,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(), "Caught exception while configuring controller '%s': %s", + controller_name.c_str(), e.what()); + return controller_interface::return_type::ERROR; + } + catch (...) { 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 unknown exception while configuring controller '%s'", + controller_name.c_str()); return controller_interface::return_type::ERROR; } @@ -727,7 +729,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(); @@ -759,16 +762,51 @@ controller_interface::return_type ControllerManager::configure_controller( get_logger(), "Controller '%s' is chainable. Interfaces are being exported to resource manager.", controller_name.c_str()); - auto interfaces = controller->export_reference_interfaces(); - if (interfaces.empty()) + auto state_interfaces = controller->export_state_interfaces(); + auto ref_interfaces = controller->export_reference_interfaces(); + if (ref_interfaces.empty() && state_interfaces.empty()) { // TODO(destogl): Add test for this! RCLCPP_ERROR( - get_logger(), "Controller '%s' is chainable, but does not export any reference interfaces.", + get_logger(), + "Controller '%s' is chainable, but does not export any state or reference interfaces.", controller_name.c_str()); return controller_interface::return_type::ERROR; } - resource_manager_->import_controller_reference_interfaces(controller_name, interfaces); + resource_manager_->import_controller_reference_interfaces(controller_name, ref_interfaces); + resource_manager_->import_controller_exported_state_interfaces( + controller_name, state_interfaces); + } + + // let's update the list of following and preceding controllers + const auto cmd_itfs = controller->command_interface_configuration().names; + const auto state_itfs = controller->state_interface_configuration().names; + for (const auto & cmd_itf : cmd_itfs) + { + controller_manager::ControllersListIterator ctrl_it; + if (is_interface_a_chained_interface(cmd_itf, controllers, ctrl_it)) + { + add_element_to_list( + controller_chain_spec_[controller_name].following_controllers, ctrl_it->info.name); + add_element_to_list( + controller_chain_spec_[ctrl_it->info.name].preceding_controllers, controller_name); + add_element_to_list( + controller_chained_reference_interfaces_cache_[ctrl_it->info.name], controller_name); + } + } + // This is needed when we start exporting the state interfaces from the controllers + for (const auto & state_itf : state_itfs) + { + controller_manager::ControllersListIterator ctrl_it; + if (is_interface_a_chained_interface(state_itf, controllers, ctrl_it)) + { + add_element_to_list( + controller_chain_spec_[controller_name].preceding_controllers, ctrl_it->info.name); + add_element_to_list( + controller_chain_spec_[ctrl_it->info.name].following_controllers, controller_name); + add_element_to_list( + controller_chained_state_interfaces_cache_[ctrl_it->info.name], controller_name); + } } // Now let's reorder the controllers @@ -779,14 +817,32 @@ controller_interface::return_type ControllerManager::configure_controller( // Copy all controllers from the 'from' list to the 'to' list to = from; + std::vector sorted_list; - // Reordering the controllers - std::stable_sort( - to.begin(), to.end(), - std::bind( - &ControllerManager::controller_sorting, this, std::placeholders::_1, std::placeholders::_2, - to)); + // clear the list before reordering it again + ordered_controllers_names_.clear(); + for (const auto & [ctrl_name, chain_spec] : controller_chain_spec_) + { + auto it = + std::find(ordered_controllers_names_.begin(), ordered_controllers_names_.end(), ctrl_name); + if (it == ordered_controllers_names_.end()) + { + update_list_with_controller_chain(ctrl_name, ordered_controllers_names_.end(), false); + } + } + std::vector new_list; + for (const auto & ctrl : ordered_controllers_names_) + { + auto controller_it = std::find_if( + to.begin(), to.end(), std::bind(controller_name_compare, std::placeholders::_1, ctrl)); + if (controller_it != to.end()) + { + new_list.push_back(*controller_it); + } + } + + to = new_list; RCLCPP_DEBUG(get_logger(), "Reordered controllers list is:"); for (const auto & ctrl : to) { @@ -805,6 +861,13 @@ void ControllerManager::clear_requests() { deactivate_request_.clear(); activate_request_.clear(); + // Set these interfaces as unavailable when clearing requests to avoid leaving them in available + // state without the controller being in active state + for (const auto & controller_name : to_chained_mode_request_) + { + resource_manager_->make_controller_exported_state_interfaces_unavailable(controller_name); + resource_manager_->make_controller_reference_interfaces_unavailable(controller_name); + } to_chained_mode_request_.clear(); from_chained_mode_request_.clear(); activate_command_interface_request_.clear(); @@ -816,12 +879,12 @@ controller_interface::return_type ControllerManager::switch_controller( const std::vector & deactivate_controllers, int strictness, bool activate_asap, const rclcpp::Duration & timeout) { - if (!resource_manager_->is_urdf_already_loaded()) + if (!is_resource_manager_initialized()) { RCLCPP_ERROR( get_logger(), "Resource Manager is not initialized yet! Please provide robot description on " - "'~/robot_description' topic before trying to switch controllers."); + "'robot_description' topic before trying to switch controllers."); return controller_interface::return_type::ERROR; } @@ -864,14 +927,15 @@ controller_interface::return_type ControllerManager::switch_controller( strictness = controller_manager_msgs::srv::SwitchController::Request::BEST_EFFORT; } - RCLCPP_DEBUG(get_logger(), "Switching controllers:"); + RCLCPP_DEBUG(get_logger(), "Activating controllers:"); for (const auto & controller : activate_controllers) { - RCLCPP_DEBUG(get_logger(), "- Activating controller '%s'", controller.c_str()); + RCLCPP_DEBUG(get_logger(), " - %s", controller.c_str()); } + RCLCPP_DEBUG(get_logger(), "Deactivating controllers:"); for (const auto & controller : deactivate_controllers) { - RCLCPP_DEBUG(get_logger(), "- Deactivating controller '%s'", controller.c_str()); + RCLCPP_DEBUG(get_logger(), " - %s", controller.c_str()); } const auto list_controllers = [this, strictness]( @@ -1040,6 +1104,14 @@ controller_interface::return_type ControllerManager::switch_controller( } } + // Check after the check if the activate and deactivate list is empty or not + if (activate_request_.empty() && deactivate_request_.empty()) + { + RCLCPP_INFO(get_logger(), "Empty activate and deactivate list, not requesting switch"); + clear_requests(); + return controller_interface::return_type::OK; + } + for (const auto & controller : controllers) { auto to_chained_mode_list_it = std::find( @@ -1209,6 +1281,17 @@ controller_interface::return_type ControllerManager::switch_controller( return controller_interface::return_type::OK; } + RCLCPP_DEBUG(get_logger(), "Request for command interfaces from activating controllers:"); + for (const auto & interface : activate_command_interface_request_) + { + RCLCPP_DEBUG(get_logger(), " - %s", interface.c_str()); + } + RCLCPP_DEBUG(get_logger(), "Release of command interfaces from deactivating controllers:"); + for (const auto & interface : deactivate_command_interface_request_) + { + RCLCPP_DEBUG(get_logger(), " - %s", interface.c_str()); + } + if ( !activate_command_interface_request_.empty() || !deactivate_command_interface_request_.empty()) { @@ -1305,17 +1388,44 @@ 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(), "Caught exception while initializing controller '%s': %s", + controller.info.name.c_str(), e.what()); + return nullptr; + } + catch (...) { to.clear(); RCLCPP_ERROR( - get_logger(), "Could not initialize the controller named '%s'", controller.info.name.c_str()); + get_logger(), "Caught unknown exception while initializing controller '%s'", + controller.info.name.c_str()); return nullptr; } + // initialize the data for the controller chain spec once it is loaded. It is needed, so when we + // sort the controllers later, they will be added to the list + controller_chain_spec_[controller.info.name] = ControllerChainSpec(); + controller_chained_state_interfaces_cache_[controller.info.name] = {}; + controller_chained_reference_interfaces_cache_[controller.info.name] = {}; + executor_->add_node(controller.c->get_node()->get_node_base_interface()); to.emplace_back(controller); @@ -1351,18 +1461,38 @@ 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 { - resource_manager_->make_controller_reference_interfaces_unavailable(controller_name); + 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_exported_state_interfaces_unavailable(controller_name); + 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()); + } } - if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE) + catch (const std::exception & e) + { + RCLCPP_ERROR( + get_logger(), "Caught exception while deactivating the controller '%s': %s", + controller_name.c_str(), e.what()); + continue; + } + 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; } } } @@ -1391,9 +1521,6 @@ void ControllerManager::switch_chained_mode( auto controller = found_it->c; if (!is_controller_active(*controller)) { - // if it is a chainable controller, make the reference interfaces available on preactivation - // (This is needed when you activate a couple of chainable controller altogether) - resource_manager_->make_controller_reference_interfaces_available(controller_name); if (!controller->set_chained_mode(to_chained_mode)) { RCLCPP_ERROR( @@ -1521,20 +1648,39 @@ 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 if (controller->is_chainable()) { + // make all the exported interfaces of the controller available + resource_manager_->make_controller_exported_state_interfaces_available(controller_name); resource_manager_->make_controller_reference_interfaces_available(controller_name); } @@ -1629,13 +1775,23 @@ void ControllerManager::list_controllers_srv_cb( { auto references = resource_manager_->get_controller_reference_interface_names(controllers[i].info.name); + auto exported_state_interfaces = + resource_manager_->get_controller_exported_state_interface_names( + controllers[i].info.name); controller_state.reference_interfaces.reserve(references.size()); + controller_state.exported_state_interfaces.reserve(exported_state_interfaces.size()); for (const auto & reference : references) { const std::string prefix_name = controllers[i].c->get_node()->get_name(); const std::string interface_name = reference.substr(prefix_name.size() + 1); controller_state.reference_interfaces.push_back(interface_name); } + for (const auto & state_interface : exported_state_interfaces) + { + const std::string prefix_name = controllers[i].c->get_node()->get_name(); + const std::string interface_name = state_interface.substr(prefix_name.size() + 1); + controller_state.exported_state_interfaces.push_back(interface_name); + } } } response->controller.push_back(controller_state); @@ -2041,6 +2197,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 @@ -2053,6 +2210,17 @@ controller_interface::return_type ControllerManager::update( run_controller_at_cm_rate ? period : rclcpp::Duration::from_seconds((1.0 / controller_update_rate)); + if ( + *loaded_controller.next_update_cycle_time == + rclcpp::Time(0, 0, this->get_node_clock_interface()->get_clock()->get_clock_type())) + { + // it is zero after activation + RCLCPP_DEBUG( + get_logger(), "Setting next_update_cycle_time to %fs for the controller : %s", + time.seconds(), loaded_controller.info.name.c_str()); + *loaded_controller.next_update_cycle_time = time; + } + bool controller_go = (time == rclcpp::Time(0, 0, this->get_node_clock_interface()->get_clock()->get_clock_type())) || @@ -2067,23 +2235,50 @@ 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); - - if ( - *loaded_controller.next_update_cycle_time == - rclcpp::Time(0, 0, this->get_node_clock_interface()->get_clock()->get_clock_type())) + 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) { - *loaded_controller.next_update_cycle_time = time; + 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; + } + *loaded_controller.next_update_cycle_time += controller_period; 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) @@ -2191,6 +2386,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) { @@ -2215,12 +2417,16 @@ void ControllerManager::propagate_deactivation_of_chained_mode( break; } - for (const auto & cmd_itf_name : controller.c->command_interface_configuration().names) + const auto ctrl_cmd_itf_names = controller.c->command_interface_configuration().names; + const auto ctrl_state_itf_names = controller.c->state_interface_configuration().names; + auto ctrl_itf_names = ctrl_cmd_itf_names; + ctrl_itf_names.insert( + ctrl_itf_names.end(), ctrl_state_itf_names.begin(), ctrl_state_itf_names.end()); + for (const auto & ctrl_itf_name : ctrl_itf_names) { // controller that 'cmd_tf_name' belongs to ControllersListIterator following_ctrl_it; - if (command_interface_is_reference_interface_of_controller( - cmd_itf_name, controllers, following_ctrl_it)) + if (is_interface_a_chained_interface(ctrl_itf_name, controllers, following_ctrl_it)) { // currently iterated "controller" is preceding controller --> add following controller // with matching interface name to "from" chained mode list (if not already in it) @@ -2249,12 +2455,21 @@ controller_interface::return_type ControllerManager::check_following_controllers get_logger(), "Checking following controllers of preceding controller with name '%s'.", controller_it->info.name.c_str()); - for (const auto & cmd_itf_name : controller_it->c->command_interface_configuration().names) + const auto controller_cmd_interfaces = controller_it->c->command_interface_configuration().names; + const auto controller_state_interfaces = controller_it->c->state_interface_configuration().names; + // get all interfaces of the controller + auto controller_interfaces = controller_cmd_interfaces; + controller_interfaces.insert( + controller_interfaces.end(), controller_state_interfaces.begin(), + controller_state_interfaces.end()); + for (const auto & ctrl_itf_name : controller_interfaces) { + RCLCPP_DEBUG( + get_logger(), "Checking interface '%s' of controller '%s'.", ctrl_itf_name.c_str(), + controller_it->info.name.c_str()); ControllersListIterator following_ctrl_it; // Check if interface if reference interface and following controller exist. - if (!command_interface_is_reference_interface_of_controller( - cmd_itf_name, controllers, following_ctrl_it)) + if (!is_interface_a_chained_interface(ctrl_itf_name, controllers, following_ctrl_it)) { continue; } @@ -2274,9 +2489,9 @@ controller_interface::return_type ControllerManager::check_following_controllers { RCLCPP_WARN( get_logger(), - "No reference interface '%s' exist, since the following controller with name '%s' " - "is not chainable.", - cmd_itf_name.c_str(), following_ctrl_it->info.name.c_str()); + "No state/reference interface '%s' exist, since the following controller with name " + "'%s' is not chainable.", + ctrl_itf_name.c_str(), following_ctrl_it->info.name.c_str()); return controller_interface::return_type::ERROR; } @@ -2329,10 +2544,23 @@ controller_interface::return_type ControllerManager::check_following_controllers following_ctrl_it->info.name); if (found_it == to_chained_mode_request_.end()) { - to_chained_mode_request_.push_back(following_ctrl_it->info.name); - RCLCPP_DEBUG( - get_logger(), "Adding controller '%s' in 'to chained mode' request.", - following_ctrl_it->info.name.c_str()); + // if it is a chainable controller, make the reference interfaces available on preactivation + // (This is needed when you activate a couple of chainable controller altogether) + // make all the exported interfaces of the controller available + resource_manager_->make_controller_exported_state_interfaces_available( + following_ctrl_it->info.name); + if ( + std::find( + controller_cmd_interfaces.begin(), controller_cmd_interfaces.end(), ctrl_itf_name) != + controller_cmd_interfaces.end()) + { + resource_manager_->make_controller_reference_interfaces_available( + following_ctrl_it->info.name); + to_chained_mode_request_.push_back(following_ctrl_it->info.name); + RCLCPP_DEBUG( + get_logger(), "Adding controller '%s' in 'to chained mode' request.", + following_ctrl_it->info.name.c_str()); + } } } else @@ -2365,225 +2593,163 @@ controller_interface::return_type ControllerManager::check_preceeding_controller return controller_interface::return_type::OK; } - if (!controller_it->c->is_in_chained_mode()) - { - RCLCPP_DEBUG( - get_logger(), - "Controller with name '%s' is chainable but not in chained mode. " - "No need to do any checks of preceding controllers when stopping it.", - controller_it->info.name.c_str()); - return controller_interface::return_type::OK; - } - RCLCPP_DEBUG( get_logger(), "Checking preceding controller of following controller with name '%s'.", controller_it->info.name.c_str()); - for (const auto & ref_itf_name : - resource_manager_->get_controller_reference_interface_names(controller_it->info.name)) + auto preceeding_controllers_list = + controller_chained_state_interfaces_cache_[controller_it->info.name]; + preceeding_controllers_list.insert( + preceeding_controllers_list.end(), + controller_chained_reference_interfaces_cache_[controller_it->info.name].cbegin(), + controller_chained_reference_interfaces_cache_[controller_it->info.name].cend()); + + for (const auto & preceeding_controller : preceeding_controllers_list) { - std::vector preceding_controllers_using_ref_itf; + RCLCPP_DEBUG(get_logger(), "\t Preceding controller : '%s'.", preceeding_controller.c_str()); + auto found_it = std::find_if( + controllers.begin(), controllers.end(), + std::bind(controller_name_compare, std::placeholders::_1, preceeding_controller)); - // TODO(destogl): This data could be cached after configuring controller into a map for faster - // access here - for (auto preceding_ctrl_it = controllers.begin(); preceding_ctrl_it != controllers.end(); - ++preceding_ctrl_it) + if (found_it != controllers.end()) { - const auto preceding_ctrl_cmd_itfs = - preceding_ctrl_it->c->command_interface_configuration().names; - - // if controller is not preceding go the next one if ( - std::find(preceding_ctrl_cmd_itfs.begin(), preceding_ctrl_cmd_itfs.end(), ref_itf_name) == - preceding_ctrl_cmd_itfs.end()) - { - continue; - } - - // check if preceding controller will be activated - if ( - is_controller_inactive(preceding_ctrl_it->c) && - std::find( - activate_request_.begin(), activate_request_.end(), preceding_ctrl_it->info.name) != + is_controller_inactive(found_it->c) && + std::find(activate_request_.begin(), activate_request_.end(), preceeding_controller) != activate_request_.end()) { RCLCPP_WARN( get_logger(), "Could not deactivate controller with name '%s' because " - "preceding controller with name '%s' will be activated. ", - controller_it->info.name.c_str(), preceding_ctrl_it->info.name.c_str()); + "preceding controller with name '%s' is inactive and will be activated.", + controller_it->info.name.c_str(), preceeding_controller.c_str()); return controller_interface::return_type::ERROR; } - // check if preceding controller will not be deactivated - else if ( - is_controller_active(preceding_ctrl_it->c) && - std::find( - deactivate_request_.begin(), deactivate_request_.end(), preceding_ctrl_it->info.name) == + if ( + is_controller_active(found_it->c) && + std::find(deactivate_request_.begin(), deactivate_request_.end(), preceeding_controller) == deactivate_request_.end()) { RCLCPP_WARN( get_logger(), "Could not deactivate controller with name '%s' because " "preceding controller with name '%s' is active and will not be deactivated.", - controller_it->info.name.c_str(), preceding_ctrl_it->info.name.c_str()); + controller_it->info.name.c_str(), preceeding_controller.c_str()); return controller_interface::return_type::ERROR; } - // TODO(destogl): this should be discussed how to it the best - just a placeholder for now - // else if ( - // strictness == - // controller_manager_msgs::srv::SwitchController::Request::MANIPULATE_CONTROLLERS_CHAIN) - // { - // // insert to the begin of activate request list to be activated before preceding controller - // activate_request_.insert(activate_request_.begin(), preceding_ctrl_name); - // } } } + + // TODO(destogl): this should be discussed how to it the best - just a placeholder for now + // else if ( + // strictness == + // controller_manager_msgs::srv::SwitchController::Request::MANIPULATE_CONTROLLERS_CHAIN) + // { + // // insert to the begin of activate request list to be activated before preceding + // controller + // activate_request_.insert(activate_request_.begin(), preceding_ctrl_name); + // } + return controller_interface::return_type::OK; } -bool ControllerManager::controller_sorting( - const ControllerSpec & ctrl_a, const ControllerSpec & ctrl_b, - const std::vector & controllers) +void ControllerManager::controller_activity_diagnostic_callback( + diagnostic_updater::DiagnosticStatusWrapper & stat) { - // If the neither of the controllers are configured, then return false - if (!((is_controller_active(ctrl_a.c) || is_controller_inactive(ctrl_a.c)) && - (is_controller_active(ctrl_b.c) || is_controller_inactive(ctrl_b.c)))) + // lock controllers + std::lock_guard guard(rt_controllers_wrapper_.controllers_lock_); + const std::vector & controllers = rt_controllers_wrapper_.get_updated_list(guard); + bool all_active = true; + for (size_t i = 0; i < controllers.size(); ++i) { - if (is_controller_active(ctrl_a.c) || is_controller_inactive(ctrl_a.c)) + if (!is_controller_active(controllers[i].c)) { - return true; + all_active = false; } - return false; + stat.add(controllers[i].info.name, controllers[i].c->get_state().label()); } - const std::vector cmd_itfs = ctrl_a.c->command_interface_configuration().names; - const std::vector state_itfs = ctrl_a.c->state_interface_configuration().names; - if (cmd_itfs.empty() || !ctrl_a.c->is_chainable()) - { - // The case of the controllers that don't have any command interfaces. For instance, - // joint_state_broadcaster - // If the controller b is also under the same condition, then maintain their initial order - const auto command_interfaces_exist = - !ctrl_b.c->command_interface_configuration().names.empty(); - return ctrl_b.c->is_chainable() && command_interfaces_exist; - } - else if (ctrl_b.c->command_interface_configuration().names.empty() || !ctrl_b.c->is_chainable()) + if (all_active) { - // If only the controller b is a broadcaster or non chainable type , then swap the controllers - return false; + stat.summary(diagnostic_msgs::msg::DiagnosticStatus::OK, "All controllers are active"); } else { - auto following_ctrls = get_following_controller_names(ctrl_a.info.name, controllers); - if (following_ctrls.empty()) - { - return false; - } - // If the ctrl_b is any of the following controllers of ctrl_a, then place ctrl_a before ctrl_b - if ( - std::find(following_ctrls.begin(), following_ctrls.end(), ctrl_b.info.name) != - following_ctrls.end()) - { - return true; - } - else - { - auto ctrl_a_preceding_ctrls = get_preceding_controller_names(ctrl_a.info.name, controllers); - // This is to check that the ctrl_b is in the preceding controllers list of ctrl_a - This - // check is useful when there is a chained controller branching, but they belong to same - // branch - if ( - std::find(ctrl_a_preceding_ctrls.begin(), ctrl_a_preceding_ctrls.end(), ctrl_b.info.name) != - ctrl_a_preceding_ctrls.end()) - { - return false; - } + stat.summary(diagnostic_msgs::msg::DiagnosticStatus::OK, "Not all controllers are active"); + } +} - // This is to handle the cases where, the parsed ctrl_a and ctrl_b are not directly related - // but might have a common parent - happens in branched chained controller - auto ctrl_b_preceding_ctrls = get_preceding_controller_names(ctrl_b.info.name, controllers); - std::sort(ctrl_a_preceding_ctrls.begin(), ctrl_a_preceding_ctrls.end()); - std::sort(ctrl_b_preceding_ctrls.begin(), ctrl_b_preceding_ctrls.end()); - std::list intersection; - std::set_intersection( - ctrl_a_preceding_ctrls.begin(), ctrl_a_preceding_ctrls.end(), - ctrl_b_preceding_ctrls.begin(), ctrl_b_preceding_ctrls.end(), - std::back_inserter(intersection)); - if (!intersection.empty()) - { - // If there is an intersection, then there is a common parent controller for both ctrl_a and - // ctrl_b - return true; - } +void ControllerManager::update_list_with_controller_chain( + const std::string & ctrl_name, std::vector::iterator controller_iterator, + bool append_to_controller) +{ + auto new_ctrl_it = + std::find(ordered_controllers_names_.begin(), ordered_controllers_names_.end(), ctrl_name); + if (new_ctrl_it == ordered_controllers_names_.end()) + { + RCLCPP_DEBUG(get_logger(), "Adding controller chain : %s", ctrl_name.c_str()); - // If there is no common parent, then they belong to 2 different sets - auto following_ctrls_b = get_following_controller_names(ctrl_b.info.name, controllers); - if (following_ctrls_b.empty()) + auto iterator = controller_iterator; + for (const auto & ctrl : controller_chain_spec_[ctrl_name].following_controllers) + { + auto it = + std::find(ordered_controllers_names_.begin(), ordered_controllers_names_.end(), ctrl); + if (it != ordered_controllers_names_.end()) { - return true; + if ( + std::distance(ordered_controllers_names_.begin(), it) < + std::distance(ordered_controllers_names_.begin(), iterator)) + { + iterator = it; + } } - auto find_first_element = [&](const auto & controllers_list) -> size_t + } + for (const auto & ctrl : controller_chain_spec_[ctrl_name].preceding_controllers) + { + auto it = + std::find(ordered_controllers_names_.begin(), ordered_controllers_names_.end(), ctrl); + if (it != ordered_controllers_names_.end()) { - auto it = std::find_if( - controllers.begin(), controllers.end(), - std::bind(controller_name_compare, std::placeholders::_1, controllers_list.back())); - if (it != controllers.end()) + if ( + std::distance(ordered_controllers_names_.begin(), it) > + std::distance(ordered_controllers_names_.begin(), iterator)) { - return std::distance(controllers.begin(), it); + iterator = it; } - return 0; - }; - const auto ctrl_a_chain_first_controller = find_first_element(following_ctrls); - const auto ctrl_b_chain_first_controller = find_first_element(following_ctrls_b); - if (ctrl_a_chain_first_controller < ctrl_b_chain_first_controller) - { - return true; } } - // If the ctrl_a's state interface is the one exported by the ctrl_b then ctrl_b should be - // infront of ctrl_a - // TODO(saikishor): deal with the state interface chaining in the sorting algorithm - auto state_it = std::find_if( - state_itfs.begin(), state_itfs.end(), - [ctrl_b](auto itf) - { - auto index = itf.find_first_of('/'); - return ((index != std::string::npos) && (itf.substr(0, index) == ctrl_b.info.name)); - }); - if (state_it != state_itfs.end()) + if (append_to_controller) { - return false; + ordered_controllers_names_.insert(iterator + 1, ctrl_name); } - - // The rest of the cases, basically end up at the end of the list - return false; - } -}; - -void ControllerManager::controller_activity_diagnostic_callback( - diagnostic_updater::DiagnosticStatusWrapper & stat) -{ - // lock controllers - std::lock_guard guard(rt_controllers_wrapper_.controllers_lock_); - const std::vector & controllers = rt_controllers_wrapper_.get_updated_list(guard); - bool all_active = true; - for (size_t i = 0; i < controllers.size(); ++i) - { - if (!is_controller_active(controllers[i].c)) + else { - all_active = false; + ordered_controllers_names_.insert(iterator, ctrl_name); } - stat.add(controllers[i].info.name, controllers[i].c->get_state().label()); - } - if (all_active) - { - stat.summary(diagnostic_msgs::msg::DiagnosticStatus::OK, "All controllers are active"); - } - else - { - stat.summary(diagnostic_msgs::msg::DiagnosticStatus::OK, "Not all controllers are active"); + RCLCPP_DEBUG_EXPRESSION( + get_logger(), !controller_chain_spec_[ctrl_name].following_controllers.empty(), + "\t[%s] Following controllers : %ld", ctrl_name.c_str(), + controller_chain_spec_[ctrl_name].following_controllers.size()); + for (const std::string & flwg_ctrl : controller_chain_spec_[ctrl_name].following_controllers) + { + new_ctrl_it = + std::find(ordered_controllers_names_.begin(), ordered_controllers_names_.end(), ctrl_name); + RCLCPP_DEBUG(get_logger(), "\t\t[%s] : %s", ctrl_name.c_str(), flwg_ctrl.c_str()); + update_list_with_controller_chain(flwg_ctrl, new_ctrl_it, true); + } + RCLCPP_DEBUG_EXPRESSION( + get_logger(), !controller_chain_spec_[ctrl_name].preceding_controllers.empty(), + "\t[%s] Preceding controllers : %ld", ctrl_name.c_str(), + controller_chain_spec_[ctrl_name].preceding_controllers.size()); + for (const std::string & preced_ctrl : controller_chain_spec_[ctrl_name].preceding_controllers) + { + new_ctrl_it = + std::find(ordered_controllers_names_.begin(), ordered_controllers_names_.end(), ctrl_name); + RCLCPP_DEBUG(get_logger(), "\t\t[%s]: %s", ctrl_name.c_str(), preced_ctrl.c_str()); + update_list_with_controller_chain(preced_ctrl, new_ctrl_it, false); + } } } 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/controller_manager_test_common.hpp b/controller_manager/test/controller_manager_test_common.hpp index 8b6608bbb3..8b6bd91376 100644 --- a/controller_manager/test/controller_manager_test_common.hpp +++ b/controller_manager/test/controller_manager_test_common.hpp @@ -70,7 +70,9 @@ class ControllerManagerFixture : public ::testing::Test { executor_ = std::make_shared(); cm_ = std::make_shared( - std::make_unique(), executor_, TEST_CM_NAME); + std::make_unique( + rm_node_->get_node_clock_interface(), rm_node_->get_node_logging_interface()), + executor_, TEST_CM_NAME); // We want to be able to not pass robot description immediately if (!robot_description_.empty()) { @@ -144,6 +146,9 @@ class ControllerManagerFixture : public ::testing::Test bool run_updater_; const std::string robot_description_; rclcpp::Time time_; + +protected: + rclcpp::Node::SharedPtr rm_node_ = std::make_shared("ResourceManager"); }; class TestControllerManagerSrvs diff --git a/controller_manager/test/test_chainable_controller/test_chainable_controller.cpp b/controller_manager/test/test_chainable_controller/test_chainable_controller.cpp index d21957a0b4..e43f2a13a1 100644 --- a/controller_manager/test/test_chainable_controller/test_chainable_controller.cpp +++ b/controller_manager/test/test_chainable_controller/test_chainable_controller.cpp @@ -52,6 +52,13 @@ TestChainableController::state_interface_configuration() const get_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE || get_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) { + auto state_iface_cfg = state_iface_cfg_; + if (imu_sensor_) + { + auto imu_interfaces = imu_sensor_->get_state_interface_names(); + state_iface_cfg.names.insert( + state_iface_cfg.names.end(), imu_interfaces.begin(), imu_interfaces.end()); + } return state_iface_cfg_; } else @@ -96,6 +103,20 @@ controller_interface::return_type TestChainableController::update_and_write_comm { command_interfaces_[i].set_value(reference_interfaces_[i] - state_interfaces_[i].get_value()); } + // If there is a command interface then integrate and set it to the exported state interface data + for (size_t i = 0; i < exported_state_interface_names_.size() && i < command_interfaces_.size(); + ++i) + { + state_interfaces_values_[i] = command_interfaces_[i].get_value() * CONTROLLER_DT; + } + // If there is no command interface and if there is a state interface then just forward the same + // value as in the state interface + for (size_t i = 0; i < exported_state_interface_names_.size() && i < state_interfaces_.size() && + command_interfaces_.empty(); + ++i) + { + state_interfaces_values_[i] = state_interfaces_[i].get_value(); + } return controller_interface::return_type::OK; } @@ -150,6 +171,20 @@ CallbackReturn TestChainableController::on_cleanup( return CallbackReturn::SUCCESS; } +std::vector +TestChainableController::on_export_state_interfaces() +{ + std::vector state_interfaces; + + for (size_t i = 0; i < exported_state_interface_names_.size(); ++i) + { + state_interfaces.push_back(hardware_interface::StateInterface( + get_node()->get_name(), exported_state_interface_names_[i], &state_interfaces_values_[i])); + } + + return state_interfaces; +} + std::vector TestChainableController::on_export_reference_interfaces() { @@ -184,6 +219,31 @@ void TestChainableController::set_reference_interface_names( reference_interfaces_.resize(reference_interface_names.size(), 0.0); } +void TestChainableController::set_exported_state_interface_names( + const std::vector & state_interface_names) +{ + exported_state_interface_names_ = state_interface_names; + + state_interfaces_values_.resize(exported_state_interface_names_.size(), 0.0); +} + +void TestChainableController::set_imu_sensor_name(const std::string & name) +{ + if (!name.empty()) + { + imu_sensor_ = std::make_unique(name); + } +} + +std::vector TestChainableController::get_state_interface_data() const +{ + std::vector state_intr_data; + for (const auto & interface : state_interfaces_) + { + state_intr_data.push_back(interface.get_value()); + } + return state_intr_data; +} } // namespace test_chainable_controller #include "pluginlib/class_list_macros.hpp" diff --git a/controller_manager/test/test_chainable_controller/test_chainable_controller.hpp b/controller_manager/test/test_chainable_controller/test_chainable_controller.hpp index 5925ed8d11..f4f59ad9df 100644 --- a/controller_manager/test/test_chainable_controller/test_chainable_controller.hpp +++ b/controller_manager/test/test_chainable_controller/test_chainable_controller.hpp @@ -23,6 +23,7 @@ #include "controller_manager/visibility_control.h" #include "rclcpp/subscription.hpp" #include "realtime_tools/realtime_buffer.h" +#include "semantic_components/imu_sensor.hpp" #include "std_msgs/msg/float64_multi_array.hpp" namespace test_chainable_controller @@ -35,6 +36,7 @@ using CallbackReturn = rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface constexpr char TEST_CONTROLLER_NAME[] = "test_chainable_controller_name"; // corresponds to the name listed within the pluginlib xml constexpr char TEST_CONTROLLER_CLASS_NAME[] = "controller_manager/test_chainable_controller"; +constexpr double CONTROLLER_DT = 0.001; class TestChainableController : public controller_interface::ChainableControllerInterface { public: @@ -60,6 +62,9 @@ class TestChainableController : public controller_interface::ChainableController CONTROLLER_MANAGER_PUBLIC CallbackReturn on_cleanup(const rclcpp_lifecycle::State & previous_state) override; + CONTROLLER_MANAGER_PUBLIC + std::vector on_export_state_interfaces() override; + CONTROLLER_MANAGER_PUBLIC std::vector on_export_reference_interfaces() override; @@ -80,10 +85,21 @@ class TestChainableController : public controller_interface::ChainableController CONTROLLER_MANAGER_PUBLIC void set_reference_interface_names(const std::vector & reference_interface_names); + CONTROLLER_MANAGER_PUBLIC + void set_exported_state_interface_names(const std::vector & state_interface_names); + + CONTROLLER_MANAGER_PUBLIC + void set_imu_sensor_name(const std::string & name); + + CONTROLLER_MANAGER_PUBLIC + std::vector get_state_interface_data() const; + size_t internal_counter; controller_interface::InterfaceConfiguration cmd_iface_cfg_; controller_interface::InterfaceConfiguration state_iface_cfg_; std::vector reference_interface_names_; + std::vector exported_state_interface_names_; + std::unique_ptr imu_sensor_; realtime_tools::RealtimeBuffer> rt_command_ptr_; rclcpp::Subscription::SharedPtr joints_command_subscriber_; diff --git a/controller_manager/test/test_controller/test_controller.cpp b/controller_manager/test/test_controller/test_controller.cpp index 7585ae36e5..625d7ed90f 100644 --- a/controller_manager/test/test_controller/test_controller.cpp +++ b/controller_manager/test/test_controller/test_controller.cpp @@ -62,7 +62,7 @@ controller_interface::InterfaceConfiguration TestController::state_interface_con controller_interface::return_type TestController::update( const rclcpp::Time & /*time*/, const rclcpp::Duration & period) { - update_period_ = period.seconds(); + update_period_ = period; ++internal_counter; // set value to hardware to produce and test different behaviors there @@ -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]); @@ -120,6 +128,16 @@ void TestController::set_state_interface_configuration( state_iface_cfg_ = cfg; } +std::vector TestController::get_state_interface_data() const +{ + std::vector state_intr_data; + for (const auto & interface : state_interfaces_) + { + state_intr_data.push_back(interface.get_value()); + } + return state_intr_data; +} + } // namespace test_controller #include "pluginlib/class_list_macros.hpp" diff --git a/controller_manager/test/test_controller/test_controller.hpp b/controller_manager/test/test_controller/test_controller.hpp index 14ad753803..bf183c7bad 100644 --- a/controller_manager/test/test_controller/test_controller.hpp +++ b/controller_manager/test/test_controller/test_controller.hpp @@ -66,6 +66,9 @@ class TestController : public controller_interface::ControllerInterface CONTROLLER_MANAGER_PUBLIC void set_state_interface_configuration(const controller_interface::InterfaceConfiguration & cfg); + CONTROLLER_MANAGER_PUBLIC + std::vector get_state_interface_data() const; + const std::string & getRobotDescription() const; unsigned int internal_counter = 0; @@ -80,7 +83,7 @@ class TestController : public controller_interface::ControllerInterface // enables external setting of values to command interfaces - used for simulation of hardware // errors double set_first_command_interface_value_to; - double update_period_ = 0; + rclcpp::Duration update_period_ = rclcpp::Duration::from_seconds(0.); }; } // namespace test_controller diff --git a/controller_manager/test/test_controller_manager.cpp b/controller_manager/test/test_controller_manager.cpp index e88b41f222..3e2f91e91a 100644 --- a/controller_manager/test/test_controller_manager.cpp +++ b/controller_manager/test/test_controller_manager.cpp @@ -374,9 +374,12 @@ TEST_P(TestControllerManagerWithUpdateRates, per_controller_equal_and_higher_upd controller_interface::return_type::OK, cm_->update(time_, rclcpp::Duration::from_seconds(0.01))); // In case of a non perfect divisor, the update period should respect the rule - // [controller_update_rate, 2*controller_update_rate) - ASSERT_GE(test_controller->update_period_, 1.0 / cm_update_rate); - ASSERT_LT(test_controller->update_period_, 2.0 / cm_update_rate); + // [cm_update_rate, 2*cm_update_rate) + EXPECT_THAT( + test_controller->update_period_, + testing::AllOf( + testing::Ge(rclcpp::Duration::from_seconds(1.0 / cm_update_rate)), + testing::Lt(rclcpp::Duration::from_seconds(2.0 / cm_update_rate)))); loop_rate.sleep(); } // if we do 2 times of the controller_manager update rate, the internal counter should be @@ -445,6 +448,7 @@ TEST_P(TestControllerUpdateRates, check_the_controller_update_rate) EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, test_controller->get_state().id()); // Start controller, will take effect at the end of the update function + time_ = test_controller->get_node()->now(); // set to something nonzero const auto strictness = controller_manager_msgs::srv::SwitchController::Request::STRICT; std::vector start_controllers = {test_controller::TEST_CONTROLLER_NAME}; std::vector stop_controllers = {}; @@ -472,6 +476,7 @@ TEST_P(TestControllerUpdateRates, check_the_controller_update_rate) const auto controller_update_rate = test_controller->get_update_rate(); const auto initial_counter = test_controller->internal_counter; + // don't start with zero to check if the period is correct if controller is activated anytime rclcpp::Time time = time_; for (size_t update_counter = 0; update_counter <= 10 * cm_update_rate; ++update_counter) { @@ -480,8 +485,12 @@ TEST_P(TestControllerUpdateRates, check_the_controller_update_rate) cm_->update(time, rclcpp::Duration::from_seconds(0.01))); // In case of a non perfect divisor, the update period should respect the rule // [controller_update_rate, 2*controller_update_rate) - ASSERT_GE(test_controller->update_period_, 1.0 / controller_update_rate); - ASSERT_LT(test_controller->update_period_, 2.0 / controller_update_rate); + EXPECT_THAT( + test_controller->update_period_, + testing::AllOf( + testing::Ge(rclcpp::Duration::from_seconds(1.0 / controller_update_rate)), + testing::Lt(rclcpp::Duration::from_seconds(2.0 / controller_update_rate)))) + << "update_counter: " << update_counter; time += rclcpp::Duration::from_seconds(0.01); if (update_counter % cm_update_rate == 0) 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_manager_srvs.cpp b/controller_manager/test/test_controller_manager_srvs.cpp index 2cd5645bd8..4fdfe3fb9f 100644 --- a/controller_manager/test/test_controller_manager_srvs.cpp +++ b/controller_manager/test/test_controller_manager_srvs.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -456,6 +457,8 @@ TEST_F(TestControllerManagerSrvs, unload_controller_srv) result = call_service_and_wait(*client, request, srv_executor, true); ASSERT_TRUE(result->ok); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, test_controller->get_state().id()); EXPECT_EQ(0u, cm_->get_loaded_controllers().size()); } @@ -488,6 +491,8 @@ TEST_F(TestControllerManagerSrvs, robot_description_on_load_and_unload_controlle auto unload_request = std::make_shared(); unload_request->name = test_controller::TEST_CONTROLLER_NAME; auto result = call_service_and_wait(*unload_client, unload_request, srv_executor, true); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, test_controller->get_state().id()); EXPECT_EQ(0u, cm_->get_loaded_controllers().size()); // now load it and check if it got the new robot description @@ -508,6 +513,9 @@ TEST_F(TestControllerManagerSrvs, configure_controller_srv) rclcpp::Client::SharedPtr client = srv_node->create_client( "test_controller_manager/configure_controller"); + rclcpp::Client::SharedPtr unload_client = + srv_node->create_client( + "test_controller_manager/unload_controller"); auto request = std::make_shared(); request->name = test_controller::TEST_CONTROLLER_NAME; @@ -526,6 +534,15 @@ TEST_F(TestControllerManagerSrvs, configure_controller_srv) EXPECT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, cm_->get_loaded_controllers()[0].c->get_state().id()); + EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, test_controller->get_state().id()); + + // now unload the controller and check the state + auto unload_request = std::make_shared(); + unload_request->name = test_controller::TEST_CONTROLLER_NAME; + ASSERT_TRUE(call_service_and_wait(*unload_client, unload_request, srv_executor, true)->ok); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, test_controller->get_state().id()); + EXPECT_EQ(0u, cm_->get_loaded_controllers().size()); } TEST_F(TestControllerManagerSrvs, list_sorted_chained_controllers) @@ -816,8 +833,7 @@ TEST_F(TestControllerManagerSrvs, list_sorted_complex_chained_controllers) auto get_ctrl_pos = [result](const std::string & controller_name) -> int64_t { auto it = std::find_if( - result->controller.begin(), result->controller.end(), - [controller_name](auto itf) + result->controller.begin(), result->controller.end(), [controller_name](auto itf) { return (itf.name.find(controller_name) != std::string::npos); }); return std::distance(result->controller.begin(), it); }; @@ -849,7 +865,7 @@ TEST_F(TestControllerManagerSrvs, list_sorted_independent_chained_controllers) /// && /// test_controller_name_2 -> chain_ctrl_6 -> chain_ctrl_5 -> chain_ctrl_4 /// && - /// test_controller_name_7 -> test_controller_name_8 + /// test_controller_name_8 -> test_controller_name_7 /// /// NOTE: A -> B signifies that the controller A is utilizing the reference interfaces exported /// from the controller B (or) the controller B is utilizing the expected interfaces exported from @@ -964,34 +980,37 @@ TEST_F(TestControllerManagerSrvs, list_sorted_independent_chained_controllers) // add controllers /// @todo add controllers in random order /// For now, adding the ordered case to see that current sorting doesn't change order - cm_->add_controller( - test_chained_controller_2, TEST_CHAINED_CONTROLLER_2, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_6, TEST_CHAINED_CONTROLLER_6, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_1, TEST_CHAINED_CONTROLLER_1, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_7, TEST_CHAINED_CONTROLLER_7, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_controller_1, TEST_CONTROLLER_1, test_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_5, TEST_CHAINED_CONTROLLER_5, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_3, TEST_CHAINED_CONTROLLER_3, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_4, TEST_CHAINED_CONTROLLER_4, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_controller_2, TEST_CONTROLLER_2, test_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_8, TEST_CHAINED_CONTROLLER_8, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + { + ControllerManagerRunner cm_runner(this); + cm_->add_controller( + test_chained_controller_2, TEST_CHAINED_CONTROLLER_2, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_6, TEST_CHAINED_CONTROLLER_6, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_1, TEST_CHAINED_CONTROLLER_1, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_7, TEST_CHAINED_CONTROLLER_7, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_controller_1, TEST_CONTROLLER_1, test_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_5, TEST_CHAINED_CONTROLLER_5, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_3, TEST_CHAINED_CONTROLLER_3, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_4, TEST_CHAINED_CONTROLLER_4, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_controller_2, TEST_CONTROLLER_2, test_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_8, TEST_CHAINED_CONTROLLER_8, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + } // get controller list before configure auto result = call_service_and_wait(*client, request, srv_executor); @@ -1028,8 +1047,7 @@ TEST_F(TestControllerManagerSrvs, list_sorted_independent_chained_controllers) auto get_ctrl_pos = [result](const std::string & controller_name) -> int64_t { auto it = std::find_if( - result->controller.begin(), result->controller.end(), - [controller_name](auto itf) + result->controller.begin(), result->controller.end(), [controller_name](auto itf) { return (itf.name.find(controller_name) != std::string::npos); }); return std::distance(result->controller.begin(), it); }; @@ -1218,40 +1236,40 @@ TEST_F(TestControllerManagerSrvs, list_large_number_of_controllers_with_chains) // add controllers /// @todo add controllers in random order /// For now, adding the ordered case to see that current sorting doesn't change order - cm_->add_controller( - test_chained_controller_2, TEST_CHAINED_CONTROLLER_2, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_6, TEST_CHAINED_CONTROLLER_6, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_1, TEST_CHAINED_CONTROLLER_1, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_7, TEST_CHAINED_CONTROLLER_7, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_controller_1, TEST_CONTROLLER_1, test_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_5, TEST_CHAINED_CONTROLLER_5, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_3, TEST_CHAINED_CONTROLLER_3, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_4, TEST_CHAINED_CONTROLLER_4, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_controller_2, TEST_CONTROLLER_2, test_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_8, TEST_CHAINED_CONTROLLER_8, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - cm_->add_controller( - test_chained_controller_9, TEST_CHAINED_CONTROLLER_9, - test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); - { ControllerManagerRunner cm_runner(this); + cm_->add_controller( + test_chained_controller_2, TEST_CHAINED_CONTROLLER_2, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_6, TEST_CHAINED_CONTROLLER_6, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_1, TEST_CHAINED_CONTROLLER_1, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_7, TEST_CHAINED_CONTROLLER_7, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_controller_1, TEST_CONTROLLER_1, test_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_5, TEST_CHAINED_CONTROLLER_5, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_3, TEST_CHAINED_CONTROLLER_3, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_4, TEST_CHAINED_CONTROLLER_4, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_controller_2, TEST_CONTROLLER_2, test_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_8, TEST_CHAINED_CONTROLLER_8, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_9, TEST_CHAINED_CONTROLLER_9, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + for (auto random_ctrl : random_controllers_list) { cm_->add_controller( @@ -1305,8 +1323,7 @@ TEST_F(TestControllerManagerSrvs, list_large_number_of_controllers_with_chains) auto get_ctrl_pos = [result](const std::string & controller_name) -> int64_t { auto it = std::find_if( - result->controller.begin(), result->controller.end(), - [controller_name](auto itf) + result->controller.begin(), result->controller.end(), [controller_name](auto itf) { return (itf.name.find(controller_name) != std::string::npos); }); return std::distance(result->controller.begin(), it); }; @@ -1490,7 +1507,9 @@ TEST_F(TestControllerManagerSrvs, list_sorted_large_chained_controller_tree) } // Now shuffle the list to be able to configure controller later randomly - std::random_shuffle(controllers_list.begin(), controllers_list.end()); + std::random_device rnd; + std::mt19937 mers(rnd()); + std::shuffle(controllers_list.begin(), controllers_list.end(), mers); { ControllerManagerRunner cm_runner(this); @@ -1530,8 +1549,7 @@ TEST_F(TestControllerManagerSrvs, list_sorted_large_chained_controller_tree) auto get_ctrl_pos = [result](const std::string & controller_name) -> int64_t { auto it = std::find_if( - result->controller.begin(), result->controller.end(), - [controller_name](auto itf) + result->controller.begin(), result->controller.end(), [controller_name](auto itf) { return (itf.name.find(controller_name) != std::string::npos); }); return std::distance(result->controller.begin(), it); }; @@ -1713,3 +1731,200 @@ TEST_F(TestControllerManagerSrvs, list_hardware_interfaces_srv) } } } + +TEST_F(TestControllerManagerSrvs, activate_chained_controllers_one_by_one) +{ + /// The simulated controller chaining is: + /// test_controller_name -> chain_ctrl_2 -> chain_ctrl_1 + /// + /// NOTE: A -> B signifies that the controller A is utilizing the reference interfaces exported + /// from the controller B (or) the controller B is utilizing the expected interfaces exported from + /// the controller A + + // create server client and request + rclcpp::executors::SingleThreadedExecutor srv_executor; + rclcpp::Node::SharedPtr srv_node = std::make_shared("srv_client"); + srv_executor.add_node(srv_node); + rclcpp::Client::SharedPtr client = + srv_node->create_client("test_controller_manager/list_controllers"); + auto request = std::make_shared(); + + // create set of chained controllers + static constexpr char TEST_CHAINED_CONTROLLER_1[] = "test_chainable_controller_name_1"; + static constexpr char TEST_CHAINED_CONTROLLER_2[] = "test_chainable_controller_name_2"; + auto test_chained_controller_1 = std::make_shared(); + controller_interface::InterfaceConfiguration chained_cmd_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, {"joint1/position"}}; + controller_interface::InterfaceConfiguration chained_state_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, + {"joint1/position", "joint1/velocity"}}; + test_chained_controller_1->set_command_interface_configuration(chained_cmd_cfg); + test_chained_controller_1->set_state_interface_configuration(chained_state_cfg); + test_chained_controller_1->set_reference_interface_names({"joint1/position", "joint1/velocity"}); + + auto test_chained_controller_2 = std::make_shared(); + chained_cmd_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, + {std::string(TEST_CHAINED_CONTROLLER_1) + "/joint1/position"}}; + test_chained_controller_2->set_command_interface_configuration(chained_cmd_cfg); + test_chained_controller_2->set_state_interface_configuration(chained_state_cfg); + test_chained_controller_2->set_reference_interface_names({"joint1/position", "joint1/velocity"}); + + // create non-chained controller + auto test_controller = std::make_shared(); + controller_interface::InterfaceConfiguration cmd_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, + {std::string(TEST_CHAINED_CONTROLLER_2) + "/joint1/position", + std::string(TEST_CHAINED_CONTROLLER_2) + "/joint1/velocity", "joint2/velocity"}}; + controller_interface::InterfaceConfiguration state_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, + {"joint1/position", "joint1/velocity"}}; + test_controller->set_command_interface_configuration(cmd_cfg); + test_controller->set_state_interface_configuration(state_cfg); + // add controllers + cm_->add_controller( + test_chained_controller_1, TEST_CHAINED_CONTROLLER_1, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_controller, test_controller::TEST_CONTROLLER_NAME, + test_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_2, TEST_CHAINED_CONTROLLER_2, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + // get controller list before configure + auto result = call_service_and_wait(*client, request, srv_executor); + // check chainable controller + ASSERT_EQ(3u, result->controller.size()); + ASSERT_EQ(result->controller[0].name, TEST_CHAINED_CONTROLLER_1); + ASSERT_EQ(result->controller[2].name, TEST_CHAINED_CONTROLLER_2); + // check test controller + ASSERT_EQ(result->controller[1].name, "test_controller_name"); + + // configure controllers + for (const auto & controller : + {TEST_CHAINED_CONTROLLER_1, test_controller::TEST_CONTROLLER_NAME, + TEST_CHAINED_CONTROLLER_2}) + { + cm_->configure_controller(controller); + } + + // get controller list after configure + result = call_service_and_wait(*client, request, srv_executor); + ASSERT_EQ(3u, result->controller.size()); + + // reordered controllers + ASSERT_EQ(result->controller[0].name, "test_controller_name"); + ASSERT_EQ(result->controller[1].name, TEST_CHAINED_CONTROLLER_2); + ASSERT_EQ(result->controller[2].name, TEST_CHAINED_CONTROLLER_1); + + // activate controllers one by one + auto res1 = cm_->switch_controller( + {TEST_CHAINED_CONTROLLER_1}, {}, + controller_manager_msgs::srv::SwitchController::Request::STRICT, true, rclcpp::Duration(0, 0)); + ASSERT_EQ(res1, controller_interface::return_type::OK); + auto res2 = cm_->switch_controller( + {TEST_CHAINED_CONTROLLER_2}, {}, + controller_manager_msgs::srv::SwitchController::Request::STRICT, true, rclcpp::Duration(0, 0)); + ASSERT_EQ(res2, controller_interface::return_type::OK); + auto res3 = cm_->switch_controller( + {test_controller::TEST_CONTROLLER_NAME}, {}, + controller_manager_msgs::srv::SwitchController::Request::STRICT, true, rclcpp::Duration(0, 0)); + ASSERT_EQ(res3, controller_interface::return_type::OK); + + RCLCPP_ERROR(srv_node->get_logger(), "Check successful!"); +} + +TEST_F(TestControllerManagerSrvs, activate_chained_controllers_all_at_once) +{ + /// The simulated controller chaining is: + /// test_controller_name -> chain_ctrl_2 -> chain_ctrl_1 + /// + /// NOTE: A -> B signifies that the controller A is utilizing the reference interfaces exported + /// from the controller B (or) the controller B is utilizing the expected interfaces exported from + /// the controller A + + // create server client and request + rclcpp::executors::SingleThreadedExecutor srv_executor; + rclcpp::Node::SharedPtr srv_node = std::make_shared("srv_client"); + srv_executor.add_node(srv_node); + rclcpp::Client::SharedPtr client = + srv_node->create_client("test_controller_manager/list_controllers"); + auto request = std::make_shared(); + + // create set of chained controllers + static constexpr char TEST_CHAINED_CONTROLLER_1[] = "test_chainable_controller_name_1"; + static constexpr char TEST_CHAINED_CONTROLLER_2[] = "test_chainable_controller_name_2"; + auto test_chained_controller_1 = std::make_shared(); + controller_interface::InterfaceConfiguration chained_cmd_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, {"joint1/position"}}; + controller_interface::InterfaceConfiguration chained_state_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, + {"joint1/position", "joint1/velocity"}}; + test_chained_controller_1->set_command_interface_configuration(chained_cmd_cfg); + test_chained_controller_1->set_state_interface_configuration(chained_state_cfg); + test_chained_controller_1->set_reference_interface_names({"joint1/position", "joint1/velocity"}); + + auto test_chained_controller_2 = std::make_shared(); + chained_cmd_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, + {std::string(TEST_CHAINED_CONTROLLER_1) + "/joint1/position"}}; + test_chained_controller_2->set_command_interface_configuration(chained_cmd_cfg); + test_chained_controller_2->set_state_interface_configuration(chained_state_cfg); + test_chained_controller_2->set_reference_interface_names({"joint1/position", "joint1/velocity"}); + + // create non-chained controller + auto test_controller = std::make_shared(); + controller_interface::InterfaceConfiguration cmd_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, + {std::string(TEST_CHAINED_CONTROLLER_2) + "/joint1/position", + std::string(TEST_CHAINED_CONTROLLER_2) + "/joint1/velocity", "joint2/velocity"}}; + controller_interface::InterfaceConfiguration state_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, + {"joint1/position", "joint1/velocity"}}; + test_controller->set_command_interface_configuration(cmd_cfg); + test_controller->set_state_interface_configuration(state_cfg); + // add controllers + cm_->add_controller( + test_chained_controller_1, TEST_CHAINED_CONTROLLER_1, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_controller, test_controller::TEST_CONTROLLER_NAME, + test_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + test_chained_controller_2, TEST_CHAINED_CONTROLLER_2, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + // get controller list before configure + auto result = call_service_and_wait(*client, request, srv_executor); + // check chainable controller + ASSERT_EQ(3u, result->controller.size()); + ASSERT_EQ(result->controller[0].name, TEST_CHAINED_CONTROLLER_1); + ASSERT_EQ(result->controller[2].name, TEST_CHAINED_CONTROLLER_2); + // check test controller + ASSERT_EQ(result->controller[1].name, "test_controller_name"); + + // configure controllers + for (const auto & controller : + {TEST_CHAINED_CONTROLLER_1, test_controller::TEST_CONTROLLER_NAME, + TEST_CHAINED_CONTROLLER_2}) + { + cm_->configure_controller(controller); + } + + // get controller list after configure + result = call_service_and_wait(*client, request, srv_executor); + ASSERT_EQ(3u, result->controller.size()); + + // reordered controllers + ASSERT_EQ(result->controller[0].name, "test_controller_name"); + ASSERT_EQ(result->controller[1].name, TEST_CHAINED_CONTROLLER_2); + ASSERT_EQ(result->controller[2].name, TEST_CHAINED_CONTROLLER_1); + + // activate controllers all at once + auto res = cm_->switch_controller( + {TEST_CHAINED_CONTROLLER_1, TEST_CHAINED_CONTROLLER_2, test_controller::TEST_CONTROLLER_NAME}, + {}, controller_manager_msgs::srv::SwitchController::Request::STRICT, true, + rclcpp::Duration(0, 0)); + ASSERT_EQ(res, controller_interface::return_type::OK); + + RCLCPP_ERROR(srv_node->get_logger(), "Check successful!"); +} diff --git a/controller_manager/test/test_controller_manager_urdf_passing.cpp b/controller_manager/test/test_controller_manager_urdf_passing.cpp index b5adb257ab..1556211a7f 100644 --- a/controller_manager/test/test_controller_manager_urdf_passing.cpp +++ b/controller_manager/test/test_controller_manager_urdf_passing.cpp @@ -33,18 +33,13 @@ class TestableControllerManager : public controller_manager::ControllerManager { friend TestControllerManagerWithTestableCM; - FRIEND_TEST(TestControllerManagerWithTestableCM, initial_no_load_urdf_called); - FRIEND_TEST(TestControllerManagerWithTestableCM, load_urdf_called_after_callback); - FRIEND_TEST(TestControllerManagerWithTestableCM, load_urdf_called_after_invalid_urdf_passed); FRIEND_TEST( - TestControllerManagerWithTestableCM, - when_starting_broadcaster_expect_error_before_rm_is_initialized_with_robot_description); + TestControllerManagerWithTestableCM, initial_no_load_and_initialize_components_called); FRIEND_TEST( - TestControllerManagerWithTestableCM, - when_starting_controller_expect_error_before_rm_is_initialized_with_robot_description); + TestControllerManagerWithTestableCM, load_and_initialize_components_called_after_callback); FRIEND_TEST( TestControllerManagerWithTestableCM, - when_starting_controller_expect_error_before_rm_is_initialized_after_some_time); + expect_to_failure_when_invalid_urdf_is_given_and_be_able_to_submit_new_robot_description); public: TestableControllerManager( @@ -68,7 +63,7 @@ class TestControllerManagerWithTestableCM void prepare_controller() { - ASSERT_FALSE(cm_->resource_manager_->is_urdf_already_loaded()); + ASSERT_FALSE(cm_->is_resource_manager_initialized()); test_controller_ = std::make_shared(); cm_->add_controller(test_controller_, CONTROLLER_NAME, TEST_CONTROLLER_CLASS_NAME); ASSERT_NE(test_controller_, nullptr); @@ -106,24 +101,41 @@ class TestControllerManagerWithTestableCM std::shared_ptr test_controller_; }; -TEST_P(TestControllerManagerWithTestableCM, initial_no_load_urdf_called) +TEST_P(TestControllerManagerWithTestableCM, initial_no_load_and_initialize_components_called) { - ASSERT_FALSE(cm_->resource_manager_->is_urdf_already_loaded()); + ASSERT_FALSE(cm_->resource_manager_->are_components_initialized()); } -TEST_P(TestControllerManagerWithTestableCM, load_urdf_called_after_callback) +TEST_P(TestControllerManagerWithTestableCM, load_and_initialize_components_called_after_callback) { - ASSERT_FALSE(cm_->resource_manager_->is_urdf_already_loaded()); + ASSERT_FALSE(cm_->is_resource_manager_initialized()); pass_robot_description_to_cm_and_rm(); - ASSERT_TRUE(cm_->resource_manager_->is_urdf_already_loaded()); + ASSERT_TRUE(cm_->is_resource_manager_initialized()); + // mimic callback + auto msg = std_msgs::msg::String(); + msg.data = ros2_control_test_assets::minimal_robot_urdf; + cm_->robot_description_callback(msg); + ASSERT_TRUE(cm_->resource_manager_->are_components_initialized()); } -TEST_P(TestControllerManagerWithTestableCM, load_urdf_called_after_invalid_urdf_passed) +TEST_P( + TestControllerManagerWithTestableCM, + expect_to_failure_when_invalid_urdf_is_given_and_be_able_to_submit_new_robot_description) { - ASSERT_FALSE(cm_->resource_manager_->is_urdf_already_loaded()); + ASSERT_FALSE(cm_->is_resource_manager_initialized()); pass_robot_description_to_cm_and_rm( ros2_control_test_assets::minimal_robot_missing_command_keys_urdf); - ASSERT_TRUE(cm_->resource_manager_->is_urdf_already_loaded()); + ASSERT_FALSE(cm_->is_resource_manager_initialized()); + // wrong urdf + auto msg = std_msgs::msg::String(); + msg.data = ros2_control_test_assets::minimal_uninitializable_robot_urdf; + cm_->robot_description_callback(msg); + ASSERT_FALSE(cm_->resource_manager_->are_components_initialized()); + // correct urdf + msg = std_msgs::msg::String(); + msg.data = ros2_control_test_assets::minimal_robot_urdf; + cm_->robot_description_callback(msg); + ASSERT_TRUE(cm_->resource_manager_->are_components_initialized()); } TEST_P( @@ -148,7 +160,7 @@ TEST_P( configure_and_try_switch_that_returns_error(); pass_robot_description_to_cm_and_rm(); - ASSERT_TRUE(cm_->resource_manager_->is_urdf_already_loaded()); + ASSERT_TRUE(cm_->is_resource_manager_initialized()); try_successful_switch(); } @@ -171,7 +183,7 @@ TEST_P( configure_and_try_switch_that_returns_error(); pass_robot_description_to_cm_and_rm(); - ASSERT_TRUE(cm_->resource_manager_->is_urdf_already_loaded()); + ASSERT_TRUE(cm_->is_resource_manager_initialized()); try_successful_switch(); } @@ -209,7 +221,7 @@ TEST_P( std::this_thread::sleep_for(std::chrono::milliseconds(3000)); pass_robot_description_to_cm_and_rm(); - ASSERT_TRUE(cm_->resource_manager_->is_urdf_already_loaded()); + ASSERT_TRUE(cm_->is_resource_manager_initialized()); try_successful_switch(); } diff --git a/controller_manager/test/test_controller_manager_with_namespace.cpp b/controller_manager/test/test_controller_manager_with_namespace.cpp index 7d3d11c2e0..b83eca0c55 100644 --- a/controller_manager/test/test_controller_manager_with_namespace.cpp +++ b/controller_manager/test/test_controller_manager_with_namespace.cpp @@ -39,7 +39,8 @@ class TestControllerManagerWithNamespace executor_ = std::make_shared(); cm_ = std::make_shared( std::make_unique( - ros2_control_test_assets::minimal_robot_urdf, true, true), + ros2_control_test_assets::minimal_robot_urdf, rm_node_->get_node_clock_interface(), + rm_node_->get_node_logging_interface(), true), executor_, TEST_CM_NAME, TEST_NAMESPACE); run_updater_ = false; } 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_controllers_chaining_with_controller_manager.cpp b/controller_manager/test/test_controllers_chaining_with_controller_manager.cpp index eadca39756..946ffbb4dc 100644 --- a/controller_manager/test/test_controllers_chaining_with_controller_manager.cpp +++ b/controller_manager/test/test_controllers_chaining_with_controller_manager.cpp @@ -111,7 +111,8 @@ class TestControllerChainingWithControllerManager executor_ = std::make_shared(); cm_ = std::make_shared( std::make_unique( - ros2_control_test_assets::diffbot_urdf, true, true), + ros2_control_test_assets::diffbot_urdf, rm_node_->get_node_clock_interface(), + rm_node_->get_node_logging_interface(), true), executor_, TEST_CM_NAME); run_updater_ = false; } @@ -125,6 +126,10 @@ class TestControllerChainingWithControllerManager diff_drive_controller = std::make_shared(); diff_drive_controller_two = std::make_shared(); position_tracking_controller = std::make_shared(); + position_tracking_controller_two = std::make_shared(); + odom_publisher_controller = std::make_shared(); + sensor_fusion_controller = std::make_shared(); + robot_localization_controller = std::make_shared(); // configure Left Wheel controller controller_interface::InterfaceConfiguration pid_left_cmd_ifs_cfg = { @@ -135,7 +140,7 @@ class TestControllerChainingWithControllerManager pid_left_wheel_controller->set_state_interface_configuration(pid_left_state_ifs_cfg); pid_left_wheel_controller->set_reference_interface_names({"velocity"}); - // configure Left Wheel controller + // configure Right Wheel controller controller_interface::InterfaceConfiguration pid_right_cmd_ifs_cfg = { controller_interface::interface_configuration_type::INDIVIDUAL, {"wheel_right/velocity"}}; controller_interface::InterfaceConfiguration pid_right_state_ifs_cfg = { @@ -154,11 +159,13 @@ class TestControllerChainingWithControllerManager diff_drive_controller->set_command_interface_configuration(diff_drive_cmd_ifs_cfg); diff_drive_controller->set_state_interface_configuration(diff_drive_state_ifs_cfg); diff_drive_controller->set_reference_interface_names({"vel_x", "vel_y", "rot_z"}); + diff_drive_controller->set_exported_state_interface_names({"odom_x", "odom_y"}); - // configure Diff Drive Two controller (Has same command interfaces ad Diff Drive controller) + // configure Diff Drive Two controller (Has same command interfaces as Diff Drive controller) diff_drive_controller_two->set_command_interface_configuration(diff_drive_cmd_ifs_cfg); diff_drive_controller_two->set_state_interface_configuration(diff_drive_state_ifs_cfg); diff_drive_controller_two->set_reference_interface_names({"vel_x", "vel_y", "rot_z"}); + diff_drive_controller_two->set_exported_state_interface_names({"odom_x", "odom_y"}); // configure Position Tracking controller controller_interface::InterfaceConfiguration position_tracking_cmd_ifs_cfg = { @@ -168,16 +175,51 @@ class TestControllerChainingWithControllerManager // in this simple example "vel_x" == "velocity left wheel" and "vel_y" == "velocity right wheel" position_tracking_controller->set_command_interface_configuration( position_tracking_cmd_ifs_cfg); + + // configure Odometry Publisher controller + controller_interface::InterfaceConfiguration odom_and_fusion_ifs_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, + {std::string(DIFF_DRIVE_CONTROLLER) + "/odom_x", + std::string(DIFF_DRIVE_CONTROLLER) + "/odom_y"}}; + odom_publisher_controller->set_state_interface_configuration(odom_and_fusion_ifs_cfg); + + // configure sensor fusion controller + sensor_fusion_controller->set_imu_sensor_name("base_imu"); + sensor_fusion_controller->set_state_interface_configuration(odom_and_fusion_ifs_cfg); + sensor_fusion_controller->set_exported_state_interface_names({"odom_x", "odom_y", "yaw"}); + + // configure Robot Localization controller + controller_interface::InterfaceConfiguration odom_ifs_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, + {std::string(SENSOR_FUSION_CONTROLLER) + "/odom_x", + std::string(SENSOR_FUSION_CONTROLLER) + "/odom_y", + std::string(SENSOR_FUSION_CONTROLLER) + "/yaw"}}; + robot_localization_controller->set_state_interface_configuration(odom_ifs_cfg); + robot_localization_controller->set_exported_state_interface_names({"actual_pose"}); + + // configure Position Tracking controller Two + controller_interface::InterfaceConfiguration position_tracking_state_ifs_cfg = { + controller_interface::interface_configuration_type::INDIVIDUAL, + {std::string(ROBOT_LOCALIZATION_CONTROLLER) + "/actual_pose"}}; + // in this simple example "vel_x" == "velocity left wheel" and "vel_y" == "velocity right wheel" + position_tracking_controller_two->set_command_interface_configuration( + position_tracking_cmd_ifs_cfg); + position_tracking_controller_two->set_state_interface_configuration( + position_tracking_state_ifs_cfg); } void CheckIfControllersAreAddedCorrectly() { - EXPECT_EQ(5u, cm_->get_loaded_controllers().size()); + EXPECT_EQ(9u, cm_->get_loaded_controllers().size()); EXPECT_EQ(2, pid_left_wheel_controller.use_count()); EXPECT_EQ(2, pid_right_wheel_controller.use_count()); EXPECT_EQ(2, diff_drive_controller.use_count()); EXPECT_EQ(2, diff_drive_controller_two.use_count()); EXPECT_EQ(2, position_tracking_controller.use_count()); + EXPECT_EQ(2, sensor_fusion_controller.use_count()); + EXPECT_EQ(2, robot_localization_controller.use_count()); + EXPECT_EQ(2, odom_publisher_controller.use_count()); + EXPECT_EQ(2, position_tracking_controller_two.use_count()); EXPECT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, @@ -194,6 +236,18 @@ class TestControllerChainingWithControllerManager EXPECT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, position_tracking_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, + sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, + robot_localization_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, + odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, + position_tracking_controller_two->get_state().id()); } // order or controller configuration is not important therefore we can reuse the same method @@ -201,6 +255,7 @@ class TestControllerChainingWithControllerManager { // Store initial values of command interfaces size_t number_of_cmd_itfs = cm_->resource_manager_->command_interface_keys().size(); + size_t number_of_state_itfs = cm_->resource_manager_->state_interface_keys().size(); // configure chainable controller and check exported interfaces cm_->configure_controller(PID_LEFT_WHEEL); @@ -208,6 +263,7 @@ class TestControllerChainingWithControllerManager pid_left_wheel_controller->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE); EXPECT_EQ(cm_->resource_manager_->command_interface_keys().size(), number_of_cmd_itfs + 1); + EXPECT_EQ(cm_->resource_manager_->state_interface_keys().size(), number_of_state_itfs); for (const auto & interface : {"pid_left_wheel_controller/velocity"}) { EXPECT_TRUE(cm_->resource_manager_->command_interface_exists(interface)); @@ -220,6 +276,7 @@ class TestControllerChainingWithControllerManager pid_right_wheel_controller->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE); EXPECT_EQ(cm_->resource_manager_->command_interface_keys().size(), number_of_cmd_itfs + 2); + EXPECT_EQ(cm_->resource_manager_->state_interface_keys().size(), number_of_state_itfs); for (const auto & interface : {"pid_right_wheel_controller/velocity"}) { EXPECT_TRUE(cm_->resource_manager_->command_interface_exists(interface)); @@ -228,9 +285,14 @@ class TestControllerChainingWithControllerManager } cm_->configure_controller(DIFF_DRIVE_CONTROLLER); + for (auto & x : cm_->resource_manager_->available_state_interfaces()) + { + RCLCPP_ERROR_STREAM(cm_->get_logger(), x); + } EXPECT_EQ( diff_drive_controller->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE); EXPECT_EQ(cm_->resource_manager_->command_interface_keys().size(), number_of_cmd_itfs + 5); + EXPECT_EQ(cm_->resource_manager_->state_interface_keys().size(), number_of_state_itfs + 2); for (const auto & interface : {"diff_drive_controller/vel_x", "diff_drive_controller/vel_y", "diff_drive_controller/rot_z"}) @@ -239,12 +301,18 @@ class TestControllerChainingWithControllerManager EXPECT_FALSE(cm_->resource_manager_->command_interface_is_available(interface)); EXPECT_FALSE(cm_->resource_manager_->command_interface_is_claimed(interface)); } + for (const auto & interface : {"diff_drive_controller/odom_x", "diff_drive_controller/odom_y"}) + { + EXPECT_TRUE(cm_->resource_manager_->state_interface_exists(interface)); + EXPECT_FALSE(cm_->resource_manager_->state_interface_is_available(interface)); + } cm_->configure_controller(DIFF_DRIVE_CONTROLLER_TWO); EXPECT_EQ( diff_drive_controller_two->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE); EXPECT_EQ(cm_->resource_manager_->command_interface_keys().size(), number_of_cmd_itfs + 8); + EXPECT_EQ(cm_->resource_manager_->state_interface_keys().size(), number_of_state_itfs + 4); for (const auto & interface : {"diff_drive_controller/vel_x", "diff_drive_controller/vel_y", "diff_drive_controller/rot_z"}) @@ -253,31 +321,83 @@ class TestControllerChainingWithControllerManager EXPECT_FALSE(cm_->resource_manager_->command_interface_is_available(interface)); EXPECT_FALSE(cm_->resource_manager_->command_interface_is_claimed(interface)); } + for (const auto & interface : {"diff_drive_controller/odom_x", "diff_drive_controller/odom_y"}) + { + EXPECT_TRUE(cm_->resource_manager_->state_interface_exists(interface)); + EXPECT_FALSE(cm_->resource_manager_->state_interface_is_available(interface)); + } cm_->configure_controller(POSITION_TRACKING_CONTROLLER); EXPECT_EQ( position_tracking_controller->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE); EXPECT_EQ(cm_->resource_manager_->command_interface_keys().size(), number_of_cmd_itfs + 8); + EXPECT_EQ(cm_->resource_manager_->state_interface_keys().size(), number_of_state_itfs + 4); + + cm_->configure_controller(SENSOR_FUSION_CONTROLLER); + EXPECT_EQ( + position_tracking_controller->get_state().id(), + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE); + EXPECT_EQ(cm_->resource_manager_->command_interface_keys().size(), number_of_cmd_itfs + 8); + EXPECT_EQ(cm_->resource_manager_->state_interface_keys().size(), number_of_state_itfs + 7); + + cm_->configure_controller(ROBOT_LOCALIZATION_CONTROLLER); + EXPECT_EQ( + position_tracking_controller->get_state().id(), + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE); + EXPECT_EQ(cm_->resource_manager_->command_interface_keys().size(), number_of_cmd_itfs + 8); + EXPECT_EQ(cm_->resource_manager_->state_interface_keys().size(), number_of_state_itfs + 8); + + cm_->configure_controller(ODOM_PUBLISHER_CONTROLLER); + EXPECT_EQ( + position_tracking_controller->get_state().id(), + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE); + EXPECT_EQ(cm_->resource_manager_->command_interface_keys().size(), number_of_cmd_itfs + 8); + EXPECT_EQ(cm_->resource_manager_->state_interface_keys().size(), number_of_state_itfs + 8); + + cm_->configure_controller(POSITION_TRACKING_CONTROLLER_TWO); + EXPECT_EQ( + position_tracking_controller_two->get_state().id(), + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE); + EXPECT_EQ(cm_->resource_manager_->command_interface_keys().size(), number_of_cmd_itfs + 8); + EXPECT_EQ(cm_->resource_manager_->state_interface_keys().size(), number_of_state_itfs + 8); } template < typename T, typename std::enable_if< std::is_convertible::value, T>::type * = nullptr> - void SetToChainedModeAndMakeReferenceInterfacesAvailable( + void SetToChainedModeAndMakeInterfacesAvailable( std::shared_ptr & controller, const std::string & controller_name, + const std::vector & exported_state_interfaces, const std::vector & reference_interfaces) { - controller->set_chained_mode(true); - EXPECT_TRUE(controller->is_in_chained_mode()); - // make reference interface command_interfaces available - cm_->resource_manager_->make_controller_reference_interfaces_available(controller_name); - for (const auto & interface : reference_interfaces) + if (!exported_state_interfaces.empty() || !reference_interfaces.empty()) { - EXPECT_TRUE(cm_->resource_manager_->command_interface_exists(interface)); - EXPECT_TRUE(cm_->resource_manager_->command_interface_is_available(interface)); - EXPECT_FALSE(cm_->resource_manager_->command_interface_is_claimed(interface)); + controller->set_chained_mode(true); + EXPECT_TRUE(controller->is_in_chained_mode()); + if (!reference_interfaces.empty()) + { + // make reference interface command_interfaces available + cm_->resource_manager_->make_controller_reference_interfaces_available(controller_name); + for (const auto & interface : reference_interfaces) + { + EXPECT_TRUE(cm_->resource_manager_->command_interface_exists(interface)); + EXPECT_TRUE(cm_->resource_manager_->command_interface_is_available(interface)); + EXPECT_FALSE(cm_->resource_manager_->command_interface_is_claimed(interface)); + } + } + if (!exported_state_interfaces.empty()) + { + // make state interface state_interfaces available + cm_->resource_manager_->make_controller_exported_state_interfaces_available( + controller_name); + for (const auto & interface : exported_state_interfaces) + { + EXPECT_TRUE(cm_->resource_manager_->state_interface_exists(interface)); + EXPECT_TRUE(cm_->resource_manager_->state_interface_is_available(interface)); + } + } } } @@ -303,11 +423,13 @@ class TestControllerChainingWithControllerManager { if (claimed_interfaces_from_hw) { - EXPECT_TRUE(cm_->resource_manager_->command_interface_is_available(interface)); + EXPECT_TRUE(cm_->resource_manager_->command_interface_is_available(interface)) + << "The interface : " << interface << " should be available but it is not available"; } else { - EXPECT_FALSE(cm_->resource_manager_->command_interface_is_available(interface)); + EXPECT_FALSE(cm_->resource_manager_->command_interface_is_available(interface)) + << "The interface : " << interface << " shouldn't be available but it is available"; } EXPECT_FALSE(cm_->resource_manager_->command_interface_is_claimed(interface)); } @@ -351,10 +473,11 @@ class TestControllerChainingWithControllerManager std::shared_ptr & controller, const std::string & controller_name, const std::vector & claimed_command_itfs, size_t expected_internal_counter = 0u, const bool claimed_interfaces_from_hw = false, - const controller_interface::return_type expected_return = controller_interface::return_type::OK) + const controller_interface::return_type expected_return = controller_interface::return_type::OK, + const std::future_status expected_future_status = std::future_status::timeout) { switch_test_controllers( - {}, {controller_name}, test_param.strictness, std::future_status::timeout, expected_return); + {}, {controller_name}, test_param.strictness, expected_future_status, expected_return); check_after_de_activate( controller, claimed_command_itfs, expected_internal_counter, expected_return, true, claimed_interfaces_from_hw); @@ -382,10 +505,13 @@ class TestControllerChainingWithControllerManager cm_->resource_manager_->read(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); // check if all controllers are updated - ASSERT_EQ(position_tracking_controller->internal_counter, exp_internal_counter_pos_ctrl); - ASSERT_EQ(diff_drive_controller->internal_counter, exp_internal_counter_pos_ctrl + 2); - ASSERT_EQ(pid_left_wheel_controller->internal_counter, exp_internal_counter_pos_ctrl + 6); - ASSERT_EQ(pid_right_wheel_controller->internal_counter, exp_internal_counter_pos_ctrl + 4); + ASSERT_EQ(odom_publisher_controller->internal_counter, exp_internal_counter_pos_ctrl); + ASSERT_EQ(robot_localization_controller->internal_counter, exp_internal_counter_pos_ctrl + 2); + ASSERT_EQ(sensor_fusion_controller->internal_counter, exp_internal_counter_pos_ctrl + 4); + ASSERT_EQ(position_tracking_controller->internal_counter, exp_internal_counter_pos_ctrl + 6); + ASSERT_EQ(diff_drive_controller->internal_counter, exp_internal_counter_pos_ctrl + 8); + ASSERT_EQ(pid_left_wheel_controller->internal_counter, exp_internal_counter_pos_ctrl + 12); + ASSERT_EQ(pid_right_wheel_controller->internal_counter, exp_internal_counter_pos_ctrl + 10); // check if values are set properly in controllers ASSERT_EQ( @@ -401,12 +527,25 @@ class TestControllerChainingWithControllerManager ASSERT_EQ(pid_left_wheel_controller->reference_interfaces_[0], EXP_LEFT_WHEEL_REF); ASSERT_EQ(pid_right_wheel_controller->reference_interfaces_[0], EXP_RIGHT_WHEEL_REF); + EXP_STATE_ODOM_X = chained_estimate_calculation(reference[0], EXP_LEFT_WHEEL_HW_STATE); + EXP_STATE_ODOM_Y = chained_estimate_calculation(reference[1], EXP_RIGHT_WHEEL_HW_STATE); + ASSERT_EQ(sensor_fusion_controller->state_interfaces_values_.size(), 3u); + ASSERT_EQ(robot_localization_controller->get_state_interface_data().size(), 3u); + ASSERT_EQ(robot_localization_controller->get_state_interface_data()[0], EXP_STATE_ODOM_X); + ASSERT_EQ(robot_localization_controller->get_state_interface_data()[1], EXP_STATE_ODOM_Y); + ASSERT_EQ(odom_publisher_controller->get_state_interface_data().size(), 2u); + ASSERT_EQ(odom_publisher_controller->get_state_interface_data()[0], EXP_STATE_ODOM_X); + ASSERT_EQ(odom_publisher_controller->get_state_interface_data()[1], EXP_STATE_ODOM_Y); + EXP_LEFT_WHEEL_CMD = chained_ctrl_calculation(EXP_LEFT_WHEEL_REF, EXP_LEFT_WHEEL_HW_STATE); EXP_LEFT_WHEEL_HW_STATE = hardware_calculation(EXP_LEFT_WHEEL_CMD); ASSERT_EQ(pid_left_wheel_controller->command_interfaces_[0].get_value(), EXP_LEFT_WHEEL_CMD); ASSERT_EQ(pid_left_wheel_controller->state_interfaces_[0].get_value(), EXP_LEFT_WHEEL_HW_STATE); // DiffDrive uses the same state ASSERT_EQ(diff_drive_controller->state_interfaces_[0].get_value(), EXP_LEFT_WHEEL_HW_STATE); + // The state doesn't change wrt to any data from the hardware calculation + ASSERT_EQ(robot_localization_controller->get_state_interface_data()[0], EXP_STATE_ODOM_X); + ASSERT_EQ(odom_publisher_controller->get_state_interface_data()[0], EXP_STATE_ODOM_X); EXP_RIGHT_WHEEL_CMD = chained_ctrl_calculation(EXP_RIGHT_WHEEL_REF, EXP_RIGHT_WHEEL_HW_STATE); EXP_RIGHT_WHEEL_HW_STATE = hardware_calculation(EXP_RIGHT_WHEEL_CMD); @@ -415,10 +554,17 @@ class TestControllerChainingWithControllerManager pid_right_wheel_controller->state_interfaces_[0].get_value(), EXP_RIGHT_WHEEL_HW_STATE); // DiffDrive uses the same state ASSERT_EQ(diff_drive_controller->state_interfaces_[1].get_value(), EXP_RIGHT_WHEEL_HW_STATE); + // The state doesn't change wrt to any data from the hardware calculation + ASSERT_EQ(robot_localization_controller->get_state_interface_data()[1], EXP_STATE_ODOM_Y); + ASSERT_EQ(odom_publisher_controller->get_state_interface_data()[1], EXP_STATE_ODOM_Y); } // check data propagation through controllers and if individual controllers are working double chained_ctrl_calculation(double reference, double state) { return reference - state; } + double chained_estimate_calculation(double reference, double state) + { + return (reference - state) * test_chainable_controller::CONTROLLER_DT; + } double hardware_calculation(double command) { return command / 2.0; } // set controllers' names @@ -427,6 +573,10 @@ class TestControllerChainingWithControllerManager static constexpr char DIFF_DRIVE_CONTROLLER[] = "diff_drive_controller"; static constexpr char DIFF_DRIVE_CONTROLLER_TWO[] = "diff_drive_controller_two"; static constexpr char POSITION_TRACKING_CONTROLLER[] = "position_tracking_controller"; + static constexpr char SENSOR_FUSION_CONTROLLER[] = "sensor_fusion_controller"; + static constexpr char ROBOT_LOCALIZATION_CONTROLLER[] = "robot_localization_controller"; + static constexpr char ODOM_PUBLISHER_CONTROLLER[] = "odometry_publisher_controller"; + static constexpr char POSITION_TRACKING_CONTROLLER_TWO[] = "position_tracking_controller_two"; const std::vector PID_LEFT_WHEEL_REFERENCE_INTERFACES = { "pid_left_wheel_controller/velocity"}; @@ -434,6 +584,11 @@ class TestControllerChainingWithControllerManager "pid_right_wheel_controller/velocity"}; const std::vector DIFF_DRIVE_REFERENCE_INTERFACES = { "diff_drive_controller/vel_x", "diff_drive_controller/vel_y", "diff_drive_controller/rot_z"}; + const std::vector DIFF_DRIVE_STATE_INTERFACES = { + "diff_drive_controller/odom_x", "diff_drive_controller/odom_y"}; + const std::vector SENSOR_FUSION_ESIMTATED_INTERFACES = { + "sensor_fusion_controller/odom_x", "sensor_fusion_controller/odom_y", + "sensor_fusion_controller/yaw"}; const std::vector PID_LEFT_WHEEL_CLAIMED_INTERFACES = {"wheel_left/velocity"}; const std::vector PID_RIGHT_WHEEL_CLAIMED_INTERFACES = {"wheel_right/velocity"}; @@ -447,7 +602,11 @@ class TestControllerChainingWithControllerManager std::shared_ptr pid_right_wheel_controller; std::shared_ptr diff_drive_controller; std::shared_ptr diff_drive_controller_two; + std::shared_ptr sensor_fusion_controller; + std::shared_ptr robot_localization_controller; + std::shared_ptr odom_publisher_controller; std::shared_ptr position_tracking_controller; + std::shared_ptr position_tracking_controller_two; testing::WithParamInterface::ParamType test_param; @@ -458,6 +617,8 @@ class TestControllerChainingWithControllerManager double EXP_RIGHT_WHEEL_HW_STATE = 0.0; double EXP_LEFT_WHEEL_REF = 0.0; double EXP_RIGHT_WHEEL_REF = 0.0; + double EXP_STATE_ODOM_X = 0.0; + double EXP_STATE_ODOM_Y = 0.0; // Expected behaviors struct used in chaining activation/deactivation tests struct ExpectedBehaviorStruct @@ -492,17 +653,32 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers) cm_->add_controller( pid_right_wheel_controller, PID_RIGHT_WHEEL, test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + position_tracking_controller_two, POSITION_TRACKING_CONTROLLER_TWO, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); CheckIfControllersAreAddedCorrectly(); ConfigureAndCheckControllers(); - SetToChainedModeAndMakeReferenceInterfacesAvailable( - pid_left_wheel_controller, PID_LEFT_WHEEL, PID_LEFT_WHEEL_REFERENCE_INTERFACES); - SetToChainedModeAndMakeReferenceInterfacesAvailable( - pid_right_wheel_controller, PID_RIGHT_WHEEL, PID_RIGHT_WHEEL_REFERENCE_INTERFACES); - SetToChainedModeAndMakeReferenceInterfacesAvailable( - diff_drive_controller, DIFF_DRIVE_CONTROLLER, DIFF_DRIVE_REFERENCE_INTERFACES); + SetToChainedModeAndMakeInterfacesAvailable( + pid_left_wheel_controller, PID_LEFT_WHEEL, {}, PID_LEFT_WHEEL_REFERENCE_INTERFACES); + SetToChainedModeAndMakeInterfacesAvailable( + pid_right_wheel_controller, PID_RIGHT_WHEEL, {}, PID_RIGHT_WHEEL_REFERENCE_INTERFACES); + SetToChainedModeAndMakeInterfacesAvailable( + diff_drive_controller, DIFF_DRIVE_CONTROLLER, DIFF_DRIVE_STATE_INTERFACES, + DIFF_DRIVE_REFERENCE_INTERFACES); + SetToChainedModeAndMakeInterfacesAvailable( + sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, SENSOR_FUSION_ESIMTATED_INTERFACES, {}); EXPECT_THROW( cm_->resource_manager_->make_controller_reference_interfaces_available( @@ -512,17 +688,30 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers) // Set ControllerManager into Debug-Mode output to have detailed output on updating controllers cm_->get_logger().set_level(rclcpp::Logger::Level::Debug); + // Before starting check that the internal counters are set to zero + ASSERT_EQ(pid_left_wheel_controller->internal_counter, 0u); + ASSERT_EQ(pid_left_wheel_controller->internal_counter, 0u); + ASSERT_EQ(diff_drive_controller->internal_counter, 0u); + ASSERT_EQ(diff_drive_controller_two->internal_counter, 0u); + ASSERT_EQ(position_tracking_controller->internal_counter, 0u); + ASSERT_EQ(sensor_fusion_controller->internal_counter, 0u); + ASSERT_EQ(odom_publisher_controller->internal_counter, 0u); + ASSERT_EQ(robot_localization_controller->internal_counter, 0u); + // activate controllers - CONTROLLERS HAVE TO ADDED REVERSE EXECUTION ORDER // (otherwise, interface will be missing) ActivateAndCheckController( pid_left_wheel_controller, PID_LEFT_WHEEL, PID_LEFT_WHEEL_CLAIMED_INTERFACES, 1u); + ASSERT_EQ(pid_left_wheel_controller->internal_counter, 1u); ActivateAndCheckController( pid_right_wheel_controller, PID_RIGHT_WHEEL, PID_RIGHT_WHEEL_CLAIMED_INTERFACES, 1u); + ASSERT_EQ(pid_right_wheel_controller->internal_counter, 1u); ASSERT_EQ(pid_left_wheel_controller->internal_counter, 3u); // Diff-Drive Controller claims the reference interfaces of PID controllers ActivateAndCheckController( diff_drive_controller, DIFF_DRIVE_CONTROLLER, DIFF_DRIVE_CLAIMED_INTERFACES, 1u); + ASSERT_EQ(diff_drive_controller->internal_counter, 1u); ASSERT_EQ(pid_right_wheel_controller->internal_counter, 3u); ASSERT_EQ(pid_left_wheel_controller->internal_counter, 5u); @@ -530,6 +719,30 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers) ActivateAndCheckController( position_tracking_controller, POSITION_TRACKING_CONTROLLER, POSITION_CONTROLLER_CLAIMED_INTERFACES, 1u); + ASSERT_EQ(position_tracking_controller->internal_counter, 1u); + ASSERT_EQ(diff_drive_controller->internal_counter, 3u); + ASSERT_EQ(pid_right_wheel_controller->internal_counter, 5u); + ASSERT_EQ(pid_left_wheel_controller->internal_counter, 7u); + + // Sensor Controller uses exported state interfaces of Diff-Drive Controller and IMU + ActivateAndCheckController(sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, {}, 1u); + ASSERT_EQ(sensor_fusion_controller->internal_counter, 1u); + ASSERT_EQ(position_tracking_controller->internal_counter, 3u); + ASSERT_EQ(diff_drive_controller->internal_counter, 5u); + ASSERT_EQ(pid_right_wheel_controller->internal_counter, 7u); + ASSERT_EQ(pid_left_wheel_controller->internal_counter, 9u); + + // Robot localization Controller uses exported state interfaces of Diff-Drive Controller + ActivateAndCheckController(robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, {}, 1u); + ASSERT_EQ(robot_localization_controller->internal_counter, 1u); + ASSERT_EQ(sensor_fusion_controller->internal_counter, 3u); + ASSERT_EQ(position_tracking_controller->internal_counter, 5u); + ASSERT_EQ(diff_drive_controller->internal_counter, 7u); + ASSERT_EQ(pid_right_wheel_controller->internal_counter, 9u); + ASSERT_EQ(pid_left_wheel_controller->internal_counter, 11u); + + // Odometry Publisher Controller uses exported state interfaces of Diff-Drive Controller + ActivateAndCheckController(odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, {}, 1u); // 'rot_z' reference interface is not claimed for (const auto & interface : {"diff_drive_controller/rot_z"}) { @@ -537,9 +750,13 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers) EXPECT_TRUE(cm_->resource_manager_->command_interface_is_available(interface)); EXPECT_FALSE(cm_->resource_manager_->command_interface_is_claimed(interface)); } - ASSERT_EQ(diff_drive_controller->internal_counter, 3u); - ASSERT_EQ(pid_right_wheel_controller->internal_counter, 5u); - ASSERT_EQ(pid_left_wheel_controller->internal_counter, 7u); + ASSERT_EQ(odom_publisher_controller->internal_counter, 1u); + ASSERT_EQ(robot_localization_controller->internal_counter, 3u); + ASSERT_EQ(sensor_fusion_controller->internal_counter, 5u); + ASSERT_EQ(position_tracking_controller->internal_counter, 7u); + ASSERT_EQ(diff_drive_controller->internal_counter, 9u); + ASSERT_EQ(pid_right_wheel_controller->internal_counter, 11u); + ASSERT_EQ(pid_left_wheel_controller->internal_counter, 13u); // update controllers std::vector reference = {32.0, 128.0}; @@ -552,25 +769,43 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers) position_tracking_controller->external_commands_for_testing_[0] = reference[0]; position_tracking_controller->external_commands_for_testing_[1] = reference[1]; position_tracking_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); - ASSERT_EQ(position_tracking_controller->internal_counter, 2u); + ASSERT_EQ(position_tracking_controller->internal_counter, 8u); ASSERT_EQ(diff_drive_controller->reference_interfaces_[0], reference[0]); // position_controller ASSERT_EQ(diff_drive_controller->reference_interfaces_[1], reference[1]); // is pass-through // update 'Diff Drive' Controller diff_drive_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); - ASSERT_EQ(diff_drive_controller->internal_counter, 4u); + ASSERT_EQ(diff_drive_controller->internal_counter, 10u); // default reference values are 0.0 - they should be changed now EXP_LEFT_WHEEL_REF = chained_ctrl_calculation(reference[0], EXP_LEFT_WHEEL_HW_STATE); // 32-0 EXP_RIGHT_WHEEL_REF = chained_ctrl_calculation(reference[1], EXP_RIGHT_WHEEL_HW_STATE); // 128-0 ASSERT_EQ(pid_left_wheel_controller->reference_interfaces_[0], EXP_LEFT_WHEEL_REF); ASSERT_EQ(pid_right_wheel_controller->reference_interfaces_[0], EXP_RIGHT_WHEEL_REF); + // run the update cycles of the robot localization and odom publisher controller + sensor_fusion_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); + ASSERT_EQ(sensor_fusion_controller->internal_counter, 6u); + robot_localization_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); + ASSERT_EQ(robot_localization_controller->internal_counter, 4u); + odom_publisher_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); + ASSERT_EQ(odom_publisher_controller->internal_counter, 2u); + EXP_STATE_ODOM_X = + chained_estimate_calculation(reference[0], EXP_LEFT_WHEEL_HW_STATE); // 32-0 / dt + EXP_STATE_ODOM_Y = + chained_estimate_calculation(reference[1], EXP_RIGHT_WHEEL_HW_STATE); // 128-0 / dt + ASSERT_EQ(robot_localization_controller->get_state_interface_data().size(), 3u); + ASSERT_EQ(robot_localization_controller->get_state_interface_data()[0], EXP_STATE_ODOM_X); + ASSERT_EQ(robot_localization_controller->get_state_interface_data()[1], EXP_STATE_ODOM_Y); + ASSERT_EQ(odom_publisher_controller->get_state_interface_data().size(), 2u); + ASSERT_EQ(odom_publisher_controller->get_state_interface_data()[0], EXP_STATE_ODOM_X); + ASSERT_EQ(odom_publisher_controller->get_state_interface_data()[1], EXP_STATE_ODOM_Y); + // update PID controllers that are writing to hardware pid_left_wheel_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); - ASSERT_EQ(pid_left_wheel_controller->internal_counter, 8u); + ASSERT_EQ(pid_left_wheel_controller->internal_counter, 14u); pid_right_wheel_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); - ASSERT_EQ(pid_right_wheel_controller->internal_counter, 6u); + ASSERT_EQ(pid_right_wheel_controller->internal_counter, 12u); // update hardware ('read' is sufficient for test hardware) cm_->resource_manager_->read(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); @@ -582,6 +817,9 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers) ASSERT_EQ(pid_left_wheel_controller->state_interfaces_[0].get_value(), EXP_LEFT_WHEEL_HW_STATE); // DiffDrive uses the same state ASSERT_EQ(diff_drive_controller->state_interfaces_[0].get_value(), EXP_LEFT_WHEEL_HW_STATE); + // The state doesn't change wrt to any data from the hardware calculation + ASSERT_EQ(robot_localization_controller->get_state_interface_data()[0], EXP_STATE_ODOM_X); + ASSERT_EQ(odom_publisher_controller->get_state_interface_data()[0], EXP_STATE_ODOM_X); // 128 - 0 EXP_RIGHT_WHEEL_CMD = chained_ctrl_calculation(EXP_RIGHT_WHEEL_REF, EXP_RIGHT_WHEEL_HW_STATE); @@ -589,8 +827,14 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers) EXP_RIGHT_WHEEL_HW_STATE = hardware_calculation(EXP_RIGHT_WHEEL_CMD); ASSERT_EQ(pid_right_wheel_controller->command_interfaces_[0].get_value(), EXP_RIGHT_WHEEL_CMD); ASSERT_EQ(pid_right_wheel_controller->state_interfaces_[0].get_value(), EXP_RIGHT_WHEEL_HW_STATE); + ASSERT_EQ(odom_publisher_controller->internal_counter, 2u); + ASSERT_EQ(sensor_fusion_controller->internal_counter, 6u); + ASSERT_EQ(robot_localization_controller->internal_counter, 4u); // DiffDrive uses the same state ASSERT_EQ(diff_drive_controller->state_interfaces_[1].get_value(), EXP_RIGHT_WHEEL_HW_STATE); + // The state doesn't change wrt to any data from the hardware calculation + ASSERT_EQ(robot_localization_controller->get_state_interface_data()[1], EXP_STATE_ODOM_Y); + ASSERT_EQ(odom_publisher_controller->get_state_interface_data()[1], EXP_STATE_ODOM_Y); // update all controllers at once and see that all have expected values --> also checks the order // of controller execution @@ -624,6 +868,18 @@ TEST_P( cm_->add_controller( pid_right_wheel_controller, PID_RIGHT_WHEEL, test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + position_tracking_controller_two, POSITION_TRACKING_CONTROLLER_TWO, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); CheckIfControllersAreAddedCorrectly(); @@ -646,6 +902,7 @@ TEST_P( EXPECT_FALSE(pid_left_wheel_controller->is_in_chained_mode()); EXPECT_FALSE(pid_right_wheel_controller->is_in_chained_mode()); ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); // DiffDrive (preceding) controller is activated --> PID controller in chained mode ActivateAndCheckController( @@ -653,6 +910,7 @@ TEST_P( EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); // PositionController is activated --> all following controller in chained mode ActivateAndCheckController( @@ -661,35 +919,153 @@ TEST_P( EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); ASSERT_TRUE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + for (const auto & interface : {"diff_drive_controller/vel_x", "diff_drive_controller/vel_y"}) + { + EXPECT_TRUE(cm_->resource_manager_->command_interface_exists(interface)); + EXPECT_TRUE(cm_->resource_manager_->command_interface_is_available(interface)); + EXPECT_TRUE(cm_->resource_manager_->command_interface_is_claimed(interface)); + } + for (const auto & interface : {"diff_drive_controller/odom_x", "diff_drive_controller/odom_y"}) + { + EXPECT_TRUE(cm_->resource_manager_->state_interface_exists(interface)); + EXPECT_TRUE(cm_->resource_manager_->state_interface_is_available(interface)); + } + + // Sensor fusion Controller uses exported state interfaces of Diff-Drive Controller and IMU + ActivateAndCheckController(sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, {}, 1u); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + ASSERT_TRUE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + + // Robot localization Controller uses exported state interfaces of sensor fusion Controller + ActivateAndCheckController(robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, {}, 1u); + + // Odometry Publisher Controller uses exported state interfaces of Diff-Drive Controller + ActivateAndCheckController(odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, {}, 1u); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + ASSERT_TRUE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + ASSERT_EQ(robot_localization_controller->internal_counter, 3u); + ASSERT_EQ(odom_publisher_controller->internal_counter, 1u); UpdateAllControllerAndCheck({32.0, 128.0}, 2u); UpdateAllControllerAndCheck({1024.0, 4096.0}, 3u); // Test switch 'from chained mode' when controllers are deactivated - // PositionController is deactivated --> DiffDrive controller is not in chained mode anymore + // PositionController is deactivated --> DiffDrive controller still continues in chained mode + // As the DiffDriveController is in chained mode, right now we tend to also deactivate + // the other controllers that rely on the DiffDriveController exported interfaces DeactivateAndCheckController( position_tracking_controller, POSITION_TRACKING_CONTROLLER, - POSITION_CONTROLLER_CLAIMED_INTERFACES, 4u, true); + POSITION_CONTROLLER_CLAIMED_INTERFACES, 10u, true); EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + // SensorFusionController continues to stay in the chained mode as it is still using the state + // interfaces + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + robot_localization_controller->get_state().id()); - // DiffDrive (preceding) controller is activated --> PID controller in chained mode + // Deactivate the robot localization controller and see that the sensor fusion controller is still + // active but not in the chained mode + DeactivateAndCheckController( + robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, {}, 8u, true); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + robot_localization_controller->get_state().id()); + + // Deactivate the odometry publisher controller + DeactivateAndCheckController(odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, {}, 8u, true); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + robot_localization_controller->get_state().id()); + + // Deactivate the sensor_fusion controller and see that the diff_drive_controller is still active + // but not in the chained mode + DeactivateAndCheckController(sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, {}, 14u, true); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + robot_localization_controller->get_state().id()); + + // Deactivate the diff_drive_controller as all it's following controllers that uses it's + // interfaces are deactivated DeactivateAndCheckController( - diff_drive_controller, DIFF_DRIVE_CONTROLLER, DIFF_DRIVE_CLAIMED_INTERFACES, 8u, true); + diff_drive_controller, DIFF_DRIVE_CONTROLLER, DIFF_DRIVE_CLAIMED_INTERFACES, 20u, true); EXPECT_FALSE(pid_left_wheel_controller->is_in_chained_mode()); EXPECT_FALSE(pid_right_wheel_controller->is_in_chained_mode()); ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + odom_publisher_controller->get_state().id()); // all controllers are deactivated --> chained mode is not changed DeactivateAndCheckController( - pid_left_wheel_controller, PID_LEFT_WHEEL, PID_LEFT_WHEEL_CLAIMED_INTERFACES, 14u, true); + pid_left_wheel_controller, PID_LEFT_WHEEL, PID_LEFT_WHEEL_CLAIMED_INTERFACES, 26u, true); DeactivateAndCheckController( - pid_right_wheel_controller, PID_RIGHT_WHEEL, PID_RIGHT_WHEEL_CLAIMED_INTERFACES, 14u, true); + pid_right_wheel_controller, PID_RIGHT_WHEEL, PID_RIGHT_WHEEL_CLAIMED_INTERFACES, 26u, true); EXPECT_FALSE(pid_left_wheel_controller->is_in_chained_mode()); EXPECT_FALSE(pid_right_wheel_controller->is_in_chained_mode()); ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + robot_localization_controller->get_state().id()); } TEST_P( @@ -713,6 +1089,18 @@ TEST_P( cm_->add_controller( pid_right_wheel_controller, PID_RIGHT_WHEEL, test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + position_tracking_controller_two, POSITION_TRACKING_CONTROLLER_TWO, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); CheckIfControllersAreAddedCorrectly(); @@ -726,6 +1114,7 @@ TEST_P( EXPECT_FALSE(pid_left_wheel_controller->is_in_chained_mode()); EXPECT_FALSE(pid_right_wheel_controller->is_in_chained_mode()); ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); // Test Case 1: Trying to activate a preceding controller when following controller // is not activated --> return error (If STRICT); Preceding controller is still inactive. @@ -746,6 +1135,17 @@ TEST_P( // Check if the controller activated (Should not be activated) ASSERT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, diff_drive_controller->get_state().id()); + ASSERT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + + ActivateController( + SENSOR_FUSION_CONTROLLER, expected.at(test_param.strictness).return_type, + std::future_status::ready); + // Check if the controller activated (Should not be activated) + ASSERT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, diff_drive_controller->get_state().id()); + ASSERT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); // Test Case 2: Try to activate a preceding controller the same time when trying to // deactivate a following controller (using switch_controller function) @@ -759,11 +1159,11 @@ TEST_P( ActivateAndCheckController( pid_right_wheel_controller, PID_RIGHT_WHEEL, PID_RIGHT_WHEEL_CLAIMED_INTERFACES, 1u); - // Attempt to activate a preceding controller (diff-drive controller) + // Attempt to activate a preceding controller (diff-drive controller + remaining) // while trying to deactivate a following controller switch_test_controllers( - {DIFF_DRIVE_CONTROLLER}, {PID_RIGHT_WHEEL}, test_param.strictness, - expected.at(test_param.strictness).future_status, + {DIFF_DRIVE_CONTROLLER, ODOM_PUBLISHER_CONTROLLER, SENSOR_FUSION_CONTROLLER}, {PID_RIGHT_WHEEL}, + test_param.strictness, expected.at(test_param.strictness).future_status, expected.at(test_param.strictness).return_type); // Preceding controller should stay deactivated and following controller @@ -775,6 +1175,11 @@ TEST_P( lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, pid_left_wheel_controller->get_state().id()); ASSERT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, diff_drive_controller->get_state().id()); + ASSERT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + ASSERT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + odom_publisher_controller->get_state().id()); } TEST_P( @@ -802,6 +1207,18 @@ TEST_P( cm_->add_controller( pid_right_wheel_controller, PID_RIGHT_WHEEL, test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + position_tracking_controller_two, POSITION_TRACKING_CONTROLLER_TWO, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); CheckIfControllersAreAddedCorrectly(); @@ -818,17 +1235,69 @@ TEST_P( pid_right_wheel_controller, PID_RIGHT_WHEEL, PID_RIGHT_WHEEL_CLAIMED_INTERFACES, 1u); ActivateAndCheckController( diff_drive_controller, DIFF_DRIVE_CONTROLLER, DIFF_DRIVE_CLAIMED_INTERFACES, 1u); + ActivateAndCheckController(sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, {}, 1u); + ActivateAndCheckController(robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, {}, 1u); + ActivateAndCheckController(odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, {}, 1u); + ActivateAndCheckController( + position_tracking_controller, POSITION_TRACKING_CONTROLLER, + POSITION_CONTROLLER_CLAIMED_INTERFACES, 1u); - // Verify that the other preceding controller is deactivated (diff_drive_controller_two) + // Verify that the other preceding controller is deactivated (diff_drive_controller_two) and other + // depending controllers are active EXPECT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, diff_drive_controller_two->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + robot_localization_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + position_tracking_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + position_tracking_controller_two->get_state().id()); + + // Deactivate position_tracking_controller and activate position_tracking_controller_two + switch_test_controllers( + {POSITION_TRACKING_CONTROLLER_TWO}, {POSITION_TRACKING_CONTROLLER}, test_param.strictness, + std::future_status::timeout, controller_interface::return_type::OK); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + position_tracking_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + position_tracking_controller_two->get_state().id()); + + // Now deactivate the position_tracking_controller_two and it should be in inactive state + switch_test_controllers( + {}, {POSITION_TRACKING_CONTROLLER_TWO}, test_param.strictness, std::future_status::timeout, + controller_interface::return_type::OK); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + position_tracking_controller_two->get_state().id()); + + // Activate it again and deactivate it others to see if we can deactivate it in a group + switch_test_controllers( + {POSITION_TRACKING_CONTROLLER_TWO}, {}, test_param.strictness, std::future_status::timeout, + controller_interface::return_type::OK); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + position_tracking_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + position_tracking_controller_two->get_state().id()); // Deactivate the first preceding controller (diff_drive_controller) and // activate the other preceding controller (diff_drive_controller_two) switch_test_controllers( - {DIFF_DRIVE_CONTROLLER_TWO}, {DIFF_DRIVE_CONTROLLER}, test_param.strictness, - std::future_status::timeout, controller_interface::return_type::OK); + {DIFF_DRIVE_CONTROLLER_TWO}, + {POSITION_TRACKING_CONTROLLER_TWO, DIFF_DRIVE_CONTROLLER, SENSOR_FUSION_CONTROLLER, + ROBOT_LOCALIZATION_CONTROLLER, ODOM_PUBLISHER_CONTROLLER}, + test_param.strictness, std::future_status::timeout, controller_interface::return_type::OK); // Following controllers should stay active EXPECT_EQ( @@ -841,6 +1310,55 @@ TEST_P( lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, diff_drive_controller->get_state().id()); ASSERT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller_two->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + robot_localization_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + position_tracking_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + position_tracking_controller_two->get_state().id()); + + // Activate all the controllers again in group and deactivate the diff_drive_controller_two + switch_test_controllers( + {POSITION_TRACKING_CONTROLLER_TWO, DIFF_DRIVE_CONTROLLER, SENSOR_FUSION_CONTROLLER, + ROBOT_LOCALIZATION_CONTROLLER, ODOM_PUBLISHER_CONTROLLER}, + {DIFF_DRIVE_CONTROLLER_TWO}, test_param.strictness, std::future_status::timeout, + controller_interface::return_type::OK); + // Following controllers should stay active + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, pid_left_wheel_controller->get_state().id()); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, pid_right_wheel_controller->get_state().id()); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + ASSERT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + EXPECT_TRUE(diff_drive_controller->is_in_chained_mode()); + ASSERT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + diff_drive_controller_two->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, sensor_fusion_controller->get_state().id()); + // This is false, because it only uses the state interfaces and exposes state interfaces + EXPECT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + robot_localization_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + position_tracking_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + position_tracking_controller_two->get_state().id()); } TEST_P( @@ -864,6 +1382,18 @@ TEST_P( cm_->add_controller( pid_right_wheel_controller, PID_RIGHT_WHEEL, test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + position_tracking_controller_two, POSITION_TRACKING_CONTROLLER_TWO, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); CheckIfControllersAreAddedCorrectly(); @@ -877,6 +1407,7 @@ TEST_P( EXPECT_FALSE(pid_left_wheel_controller->is_in_chained_mode()); EXPECT_FALSE(pid_right_wheel_controller->is_in_chained_mode()); ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); // Activate following controllers ActivateAndCheckController( @@ -899,11 +1430,28 @@ TEST_P( // Verify preceding controller (diff_drive_controller) is inactive EXPECT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + robot_localization_controller->get_state().id()); // Attempt to deactivate inactive controller (diff_drive_controller) DeactivateController( DIFF_DRIVE_CONTROLLER, expected.at(test_param.strictness).return_type, std::future_status::ready); + DeactivateController( + SENSOR_FUSION_CONTROLLER, expected.at(test_param.strictness).return_type, + std::future_status::ready); + DeactivateController( + ODOM_PUBLISHER_CONTROLLER, expected.at(test_param.strictness).return_type, + std::future_status::ready); + DeactivateController( + ROBOT_LOCALIZATION_CONTROLLER, expected.at(test_param.strictness).return_type, + std::future_status::ready); // Check to see preceding controller (diff_drive_controller) is still inactive and // following controllers (pid_left_wheel_controller) (pid_left_wheel_controller) are still active @@ -913,6 +1461,14 @@ TEST_P( lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, pid_right_wheel_controller->get_state().id()); ASSERT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, diff_drive_controller->get_state().id()); + ASSERT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + odom_publisher_controller->get_state().id()); + ASSERT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + ASSERT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + robot_localization_controller->get_state().id()); // Test Case 6: following controller is deactivated but preceding controller will be activated // --> return error; controllers stay in the same state @@ -979,6 +1535,261 @@ TEST_P( lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); } +TEST_P( + TestControllerChainingWithControllerManager, + test_chained_controllers_deactivation_switching_error_handling) +{ + SetupControllers(); + + // add all controllers - CONTROLLERS HAVE TO ADDED IN EXECUTION ORDER + cm_->add_controller( + position_tracking_controller, POSITION_TRACKING_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + diff_drive_controller, DIFF_DRIVE_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + diff_drive_controller_two, DIFF_DRIVE_CONTROLLER_TWO, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + pid_left_wheel_controller, PID_LEFT_WHEEL, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + pid_right_wheel_controller, PID_RIGHT_WHEEL, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + position_tracking_controller_two, POSITION_TRACKING_CONTROLLER_TWO, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + + CheckIfControllersAreAddedCorrectly(); + + ConfigureAndCheckControllers(); + // Set ControllerManager into Debug-Mode output to have detailed output on updating controllers + cm_->get_logger().set_level(rclcpp::Logger::Level::Debug); + rclcpp::get_logger("ControllerManager::utils").set_level(rclcpp::Logger::Level::Debug); + + // at beginning controllers are not in chained mode + EXPECT_FALSE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_FALSE(pid_right_wheel_controller->is_in_chained_mode()); + EXPECT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + + // Activate the following controller and the preceding controllers + ActivateAndCheckController( + pid_left_wheel_controller, PID_LEFT_WHEEL, PID_LEFT_WHEEL_CLAIMED_INTERFACES, 1u); + ActivateAndCheckController( + pid_right_wheel_controller, PID_RIGHT_WHEEL, PID_RIGHT_WHEEL_CLAIMED_INTERFACES, 1u); + ActivateAndCheckController( + diff_drive_controller, DIFF_DRIVE_CONTROLLER, DIFF_DRIVE_CLAIMED_INTERFACES, 1u); + ActivateAndCheckController(sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, {}, 1u); + ActivateAndCheckController(robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, {}, 1u); + ActivateAndCheckController(odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, {}, 1u); + ActivateAndCheckController( + position_tracking_controller, POSITION_TRACKING_CONTROLLER, + POSITION_CONTROLLER_CLAIMED_INTERFACES, 1u); + + // check once active that they are in chained mode + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + + // Verify that initially all of them are in active state + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, pid_left_wheel_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, pid_right_wheel_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + robot_localization_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, odom_publisher_controller->get_state().id()); + ASSERT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + position_tracking_controller->get_state().id()); + + // There is different error and timeout behavior depending on strictness + static std::unordered_map expected = { + {controller_manager_msgs::srv::SwitchController::Request::STRICT, + {controller_interface::return_type::ERROR, std::future_status::ready, + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE}}, + {controller_manager_msgs::srv::SwitchController::Request::BEST_EFFORT, + {controller_interface::return_type::OK, std::future_status::ready, + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE}}}; + + // Test switch 'from chained mode' when controllers are deactivated and possible combination of + // disabling controllers that use reference/state interfaces of the other controller. This is + // important to check that deactivation is not trigger irrespective of the type + // (reference/state) interface that is shared among the other controllers + + // PositionController is deactivated --> DiffDrive controller still continues in chained mode + // As the DiffDriveController is in chained mode, right now we tend to also deactivate + // the other controllers that rely on the DiffDriveController expected interfaces + DeactivateAndCheckController( + position_tracking_controller, POSITION_TRACKING_CONTROLLER, + POSITION_CONTROLLER_CLAIMED_INTERFACES, 2u, true); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + EXPECT_FALSE(diff_drive_controller->is_in_chained_mode()); + EXPECT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + // SensorFusionController continues to stay in the chained mode as it is still using the state + // interfaces + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + robot_localization_controller->get_state().id()); + + // DiffDrive (preceding) controller is activated --> PID controller in chained mod + // Let's try to deactivate the diff_drive_control, it should fail as there are still other + // controllers that use it's resources + DeactivateController( + DIFF_DRIVE_CONTROLLER, expected.at(test_param.strictness).return_type, + expected.at(test_param.strictness).future_status); + check_after_de_activate( + diff_drive_controller, DIFF_DRIVE_CLAIMED_INTERFACES, 0u, + controller_interface::return_type::ERROR, true, true); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + EXPECT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + robot_localization_controller->get_state().id()); + + // Trying to deactivate the sensor fusion controller, however, it won't be deactivated as the + // robot localization controller is still active + DeactivateAndCheckController( + sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, {}, 0u, true, + expected.at(test_param.strictness).return_type, + expected.at(test_param.strictness).future_status); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + EXPECT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, + robot_localization_controller->get_state().id()); + + // Deactivate the robot localization controller and see that the sensor fusion controller is still + // active but not in the chained mode + DeactivateAndCheckController( + robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, {}, 0u); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + robot_localization_controller->get_state().id()); + + // Deactivate the sensor_fusion controller and this should be successful as there are no other + // controllers using it's interfaces + DeactivateAndCheckController(sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, {}, 0u); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + ASSERT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + robot_localization_controller->get_state().id()); + + // Deactivate the odometry publisher controller and now the diff_drive should continue active but + // not in chained mode + DeactivateAndCheckController(odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, {}, 0u); + EXPECT_TRUE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_TRUE(pid_right_wheel_controller->is_in_chained_mode()); + EXPECT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + robot_localization_controller->get_state().id()); + + // Deactivate the diff_drive_controller as all it's following controllers that uses it's + // interfaces are deactivated + DeactivateAndCheckController( + diff_drive_controller, DIFF_DRIVE_CONTROLLER, DIFF_DRIVE_CLAIMED_INTERFACES, 0u, true); + EXPECT_FALSE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_FALSE(pid_right_wheel_controller->is_in_chained_mode()); + EXPECT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + odom_publisher_controller->get_state().id()); + + // all controllers are deactivated --> chained mode is not changed + DeactivateAndCheckController( + pid_left_wheel_controller, PID_LEFT_WHEEL, PID_LEFT_WHEEL_CLAIMED_INTERFACES, 0u, true); + DeactivateAndCheckController( + pid_right_wheel_controller, PID_RIGHT_WHEEL, PID_RIGHT_WHEEL_CLAIMED_INTERFACES, 0u, true); + EXPECT_FALSE(pid_left_wheel_controller->is_in_chained_mode()); + EXPECT_FALSE(pid_right_wheel_controller->is_in_chained_mode()); + EXPECT_FALSE(diff_drive_controller->is_in_chained_mode()); + ASSERT_FALSE(sensor_fusion_controller->is_in_chained_mode()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, diff_drive_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, sensor_fusion_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + odom_publisher_controller->get_state().id()); + EXPECT_EQ( + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, + robot_localization_controller->get_state().id()); +} + TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers_adding_in_random_order) { SetupControllers(); @@ -987,6 +1798,9 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers_add cm_->add_controller( pid_left_wheel_controller, PID_LEFT_WHEEL, test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); cm_->add_controller( pid_right_wheel_controller, PID_RIGHT_WHEEL, test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); @@ -999,17 +1813,27 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers_add cm_->add_controller( diff_drive_controller_two, DIFF_DRIVE_CONTROLLER_TWO, test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); + cm_->add_controller( + position_tracking_controller_two, POSITION_TRACKING_CONTROLLER_TWO, + test_chainable_controller::TEST_CONTROLLER_CLASS_NAME); CheckIfControllersAreAddedCorrectly(); ConfigureAndCheckControllers(); - SetToChainedModeAndMakeReferenceInterfacesAvailable( - pid_left_wheel_controller, PID_LEFT_WHEEL, PID_LEFT_WHEEL_REFERENCE_INTERFACES); - SetToChainedModeAndMakeReferenceInterfacesAvailable( - pid_right_wheel_controller, PID_RIGHT_WHEEL, PID_RIGHT_WHEEL_REFERENCE_INTERFACES); - SetToChainedModeAndMakeReferenceInterfacesAvailable( - diff_drive_controller, DIFF_DRIVE_CONTROLLER, DIFF_DRIVE_REFERENCE_INTERFACES); + SetToChainedModeAndMakeInterfacesAvailable( + pid_left_wheel_controller, PID_LEFT_WHEEL, {}, PID_LEFT_WHEEL_REFERENCE_INTERFACES); + SetToChainedModeAndMakeInterfacesAvailable( + pid_right_wheel_controller, PID_RIGHT_WHEEL, {}, PID_RIGHT_WHEEL_REFERENCE_INTERFACES); + SetToChainedModeAndMakeInterfacesAvailable( + diff_drive_controller, DIFF_DRIVE_CONTROLLER, DIFF_DRIVE_STATE_INTERFACES, + DIFF_DRIVE_REFERENCE_INTERFACES); EXPECT_THROW( cm_->resource_manager_->make_controller_reference_interfaces_available( @@ -1037,6 +1861,9 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers_add ActivateAndCheckController( position_tracking_controller, POSITION_TRACKING_CONTROLLER, POSITION_CONTROLLER_CLAIMED_INTERFACES, 1u); + ActivateAndCheckController(sensor_fusion_controller, SENSOR_FUSION_CONTROLLER, {}, 1u); + ActivateAndCheckController(robot_localization_controller, ROBOT_LOCALIZATION_CONTROLLER, {}, 1u); + ActivateAndCheckController(odom_publisher_controller, ODOM_PUBLISHER_CONTROLLER, {}, 1u); // 'rot_z' reference interface is not claimed for (const auto & interface : {"diff_drive_controller/rot_z"}) { @@ -1044,13 +1871,17 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers_add EXPECT_TRUE(cm_->resource_manager_->command_interface_is_available(interface)); EXPECT_FALSE(cm_->resource_manager_->command_interface_is_claimed(interface)); } - ASSERT_EQ(diff_drive_controller->internal_counter, 3u); - ASSERT_EQ(pid_right_wheel_controller->internal_counter, 5u); - ASSERT_EQ(pid_left_wheel_controller->internal_counter, 7u); + ASSERT_EQ(diff_drive_controller->internal_counter, 9u); + ASSERT_EQ(pid_right_wheel_controller->internal_counter, 11u); + ASSERT_EQ(pid_left_wheel_controller->internal_counter, 13u); // update controllers std::vector reference = {32.0, 128.0}; + sensor_fusion_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); + robot_localization_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); + odom_publisher_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); + // update 'Position Tracking' controller for (auto & value : diff_drive_controller->reference_interfaces_) { @@ -1059,14 +1890,14 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers_add position_tracking_controller->external_commands_for_testing_[0] = reference[0]; position_tracking_controller->external_commands_for_testing_[1] = reference[1]; position_tracking_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); - ASSERT_EQ(position_tracking_controller->internal_counter, 2u); + ASSERT_EQ(position_tracking_controller->internal_counter, 8u); ASSERT_EQ(diff_drive_controller->reference_interfaces_[0], reference[0]); // position_controller ASSERT_EQ(diff_drive_controller->reference_interfaces_[1], reference[1]); // is pass-through // update 'Diff Drive' Controller diff_drive_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); - ASSERT_EQ(diff_drive_controller->internal_counter, 4u); + ASSERT_EQ(diff_drive_controller->internal_counter, 10u); // default reference values are 0.0 - they should be changed now EXP_LEFT_WHEEL_REF = chained_ctrl_calculation(reference[0], EXP_LEFT_WHEEL_HW_STATE); // 32-0 EXP_RIGHT_WHEEL_REF = chained_ctrl_calculation(reference[1], EXP_RIGHT_WHEEL_HW_STATE); // 128-0 @@ -1075,9 +1906,9 @@ TEST_P(TestControllerChainingWithControllerManager, test_chained_controllers_add // update PID controllers that are writing to hardware pid_left_wheel_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); - ASSERT_EQ(pid_left_wheel_controller->internal_counter, 8u); + ASSERT_EQ(pid_left_wheel_controller->internal_counter, 14u); pid_right_wheel_controller->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); - ASSERT_EQ(pid_right_wheel_controller->internal_counter, 6u); + ASSERT_EQ(pid_right_wheel_controller->internal_counter, 12u); // update hardware ('read' is sufficient for test hardware) cm_->resource_manager_->read(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01)); diff --git a/controller_manager/test/test_hardware_management_srvs.cpp b/controller_manager/test/test_hardware_management_srvs.cpp index 93697d3ca6..2704368789 100644 --- a/controller_manager/test/test_hardware_management_srvs.cpp +++ b/controller_manager/test/test_hardware_management_srvs.cpp @@ -63,8 +63,7 @@ class TestControllerManagerHWManagementSrvs : public TestControllerManagerSrvs void SetUp() override { executor_ = std::make_shared(); - cm_ = std::make_shared( - std::make_unique(), executor_, TEST_CM_NAME); + cm_ = std::make_shared(executor_, TEST_CM_NAME); run_updater_ = false; cm_->set_parameter(rclcpp::Parameter( @@ -197,7 +196,8 @@ class TestControllerManagerHWManagementSrvs : public TestControllerManagerSrvs TEST_F(TestControllerManagerHWManagementSrvs, list_hardware_components) { // Default status after start: - // checks if "configure_components_on_start" and "activate_components_on_start" are correctly read + // checks if "hardware_components_initial_state.unconfigured" and + // "hardware_components_initial_state.inactive" are correctly read list_hardware_components_and_check( // actuator, sensor, system @@ -358,8 +358,7 @@ class TestControllerManagerHWManagementSrvsWithoutParams void SetUp() override { executor_ = std::make_shared(); - cm_ = std::make_shared( - std::make_unique(), executor_, TEST_CM_NAME); + cm_ = std::make_shared(executor_, TEST_CM_NAME); run_updater_ = false; auto msg = std_msgs::msg::String(); @@ -372,8 +371,9 @@ class TestControllerManagerHWManagementSrvsWithoutParams TEST_F(TestControllerManagerHWManagementSrvsWithoutParams, test_default_activation_of_all_hardware) { - // "configure_components_on_start" and "activate_components_on_start" are not set (empty) - // therefore all hardware components are active + // "hardware_components_initial_state.unconfigured" and + // "hardware_components_initial_state.inactive" are not set (empty). Therefore, all hardware + // components are active. list_hardware_components_and_check( // actuator, sensor, system std::vector( @@ -393,64 +393,3 @@ TEST_F(TestControllerManagerHWManagementSrvsWithoutParams, test_default_activati {{false, false, false, false}, {false, false, false, false, false, false, false}}, // system })); } - -// BEGIN: Remove at the end of 2023 -class TestControllerManagerHWManagementSrvsOldParameters -: public TestControllerManagerHWManagementSrvs -{ -public: - void SetUp() override - { - executor_ = std::make_shared(); - cm_ = std::make_shared( - std::make_unique(), executor_, TEST_CM_NAME); - run_updater_ = false; - - cm_->set_parameter( - rclcpp::Parameter("robot_description", ros2_control_test_assets::minimal_robot_urdf)); - cm_->set_parameter(rclcpp::Parameter( - "activate_components_on_start", std::vector({TEST_ACTUATOR_HARDWARE_NAME}))); - cm_->set_parameter(rclcpp::Parameter( - "configure_components_on_start", std::vector({TEST_SENSOR_HARDWARE_NAME}))); - - std::string robot_description = ""; - cm_->get_parameter("robot_description", robot_description); - if (robot_description.empty()) - { - throw std::runtime_error( - "Unable to initialize resource manager, no robot description found."); - } - - auto msg = std_msgs::msg::String(); - msg.data = robot_description_; - cm_->robot_description_callback(msg); - - SetUpSrvsCMExecutor(); - } -}; - -TEST_F(TestControllerManagerHWManagementSrvsOldParameters, list_hardware_components) -{ - // Default status after start: - // checks if "configure_components_on_start" and "activate_components_on_start" are correctly read - - list_hardware_components_and_check( - // actuator, sensor, system - std::vector( - {LFC_STATE::PRIMARY_STATE_ACTIVE, LFC_STATE::PRIMARY_STATE_INACTIVE, - LFC_STATE::PRIMARY_STATE_UNCONFIGURED}), - std::vector({ACTIVE, INACTIVE, UNCONFIGURED}), - std::vector>>({ - // is available - {{true, true}, {true, true, true}}, // actuator - {{}, {true}}, // sensor - {{false, false, false, false}, {false, false, false, false, false, false, false}}, // system - }), - std::vector>>({ - // is claimed - {{false, false}, {false, false, false}}, // actuator - {{}, {false}}, // sensor - {{false, false, false, false}, {false, false, false, false, false, false, false}}, // system - })); -} -// END: Remove at the end of 2023 diff --git a/controller_manager/test/test_load_controller.cpp b/controller_manager/test/test_load_controller.cpp index 390a7365f0..ed443ea3d4 100644 --- a/controller_manager/test/test_load_controller.cpp +++ b/controller_manager/test/test_load_controller.cpp @@ -28,8 +28,8 @@ using test_controller::TEST_CONTROLLER_CLASS_NAME; using ::testing::_; using ::testing::Return; -const auto controller_name1 = "test_controller1"; -const auto controller_name2 = "test_controller2"; +const auto CONTROLLER_NAME_1 = "test_controller1"; +const auto CONTROLLER_NAME_2 = "test_controller2"; using strvec = std::vector; class TestLoadController : public ControllerManagerFixture @@ -53,7 +53,7 @@ TEST_F(TestLoadController, load_controller_failed_init) TEST_F(TestLoadController, configuring_non_loaded_controller_fails) { // try configure non-loaded controller - EXPECT_EQ(cm_->configure_controller(controller_name1), controller_interface::return_type::ERROR); + EXPECT_EQ(cm_->configure_controller(CONTROLLER_NAME_1), controller_interface::return_type::ERROR); } TEST_F(TestLoadController, can_set_and_get_non_default_update_rate) @@ -82,7 +82,7 @@ class TestLoadedController : public TestLoadController { TestLoadController::SetUp(); - controller_if = cm_->load_controller(controller_name1, TEST_CONTROLLER_CLASS_NAME); + controller_if = cm_->load_controller(CONTROLLER_NAME_1, TEST_CONTROLLER_CLASS_NAME); ASSERT_NE(controller_if, nullptr); } @@ -93,7 +93,7 @@ class TestLoadedController : public TestLoadController controller_interface::return_type::OK) { switch_test_controllers( - strvec{controller_name1}, strvec{}, strictness, expected_future_status, + strvec{CONTROLLER_NAME_1}, strvec{}, strictness, expected_future_status, expected_interface_status); } @@ -104,7 +104,7 @@ class TestLoadedController : public TestLoadController controller_interface::return_type::OK) { switch_test_controllers( - strvec{}, strvec{controller_name1}, strictness, expected_future_status, + strvec{}, strvec{CONTROLLER_NAME_1}, strictness, expected_future_status, expected_interface_status); } }; @@ -121,7 +121,7 @@ TEST_P(TestLoadedControllerParametrized, load_and_configure_one_known_controller EXPECT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, controller_if->get_state().id()); - cm_->configure_controller(controller_name1); + cm_->configure_controller(CONTROLLER_NAME_1); EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, controller_if->get_state().id()); } @@ -129,7 +129,7 @@ TEST_P(TestLoadedControllerParametrized, can_start_configured_controller) { const auto test_param = GetParam(); - EXPECT_EQ(cm_->configure_controller(controller_name1), controller_interface::return_type::OK); + EXPECT_EQ(cm_->configure_controller(CONTROLLER_NAME_1), controller_interface::return_type::OK); start_test_controller(test_param.strictness); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, controller_if->get_state().id()); } @@ -138,7 +138,7 @@ TEST_P(TestLoadedControllerParametrized, can_stop_active_controller) { const auto test_param = GetParam(); - EXPECT_EQ(cm_->configure_controller(controller_name1), controller_interface::return_type::OK); + EXPECT_EQ(cm_->configure_controller(CONTROLLER_NAME_1), controller_interface::return_type::OK); start_test_controller(test_param.strictness); @@ -164,7 +164,7 @@ TEST_P(TestLoadedControllerParametrized, starting_and_stopping_a_controller) // Activate configured controller { ControllerManagerRunner cm_runner(this); - cm_->configure_controller(controller_name1); + cm_->configure_controller(CONTROLLER_NAME_1); } start_test_controller(test_param.strictness); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, controller_if->get_state().id()); @@ -180,11 +180,11 @@ TEST_P(TestLoadedControllerParametrized, can_not_configure_active_controller) { const auto test_param = GetParam(); - EXPECT_EQ(cm_->configure_controller(controller_name1), controller_interface::return_type::OK); + EXPECT_EQ(cm_->configure_controller(CONTROLLER_NAME_1), controller_interface::return_type::OK); start_test_controller(test_param.strictness); // Can not configure active controller - EXPECT_EQ(cm_->configure_controller(controller_name1), controller_interface::return_type::ERROR); + EXPECT_EQ(cm_->configure_controller(CONTROLLER_NAME_1), controller_interface::return_type::ERROR); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, controller_if->get_state().id()); } @@ -205,7 +205,7 @@ TEST_P(TestLoadedControllerParametrized, can_not_start_finalized_controller) test_param.strictness, std::future_status::ready, test_param.expected_return); // Can not configure finalize controller - EXPECT_EQ(cm_->configure_controller(controller_name1), controller_interface::return_type::ERROR); + EXPECT_EQ(cm_->configure_controller(CONTROLLER_NAME_1), controller_interface::return_type::ERROR); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_FINALIZED, controller_if->get_state().id()); } @@ -213,7 +213,7 @@ TEST_P(TestLoadedControllerParametrized, inactive_controller_cannot_be_cleaned_u { const auto test_param = GetParam(); - EXPECT_EQ(cm_->configure_controller(controller_name1), controller_interface::return_type::OK); + EXPECT_EQ(cm_->configure_controller(CONTROLLER_NAME_1), controller_interface::return_type::OK); start_test_controller(test_param.strictness); @@ -227,7 +227,7 @@ TEST_P(TestLoadedControllerParametrized, inactive_controller_cannot_be_cleaned_u test_controller->cleanup_calls = &cleanup_calls; // Configure from inactive state: controller can no be cleaned-up test_controller->simulate_cleanup_failure = true; - EXPECT_EQ(cm_->configure_controller(controller_name1), controller_interface::return_type::ERROR); + EXPECT_EQ(cm_->configure_controller(CONTROLLER_NAME_1), controller_interface::return_type::ERROR); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, controller_if->get_state().id()); EXPECT_EQ(0u, cleanup_calls); } @@ -236,7 +236,7 @@ TEST_P(TestLoadedControllerParametrized, inactive_controller_cannot_be_configure { const auto test_param = GetParam(); - EXPECT_EQ(cm_->configure_controller(controller_name1), controller_interface::return_type::OK); + EXPECT_EQ(cm_->configure_controller(CONTROLLER_NAME_1), controller_interface::return_type::OK); start_test_controller(test_param.strictness); @@ -251,7 +251,7 @@ TEST_P(TestLoadedControllerParametrized, inactive_controller_cannot_be_configure test_controller->simulate_cleanup_failure = false; { ControllerManagerRunner cm_runner(this); - EXPECT_EQ(cm_->configure_controller(controller_name1), controller_interface::return_type::OK); + EXPECT_EQ(cm_->configure_controller(CONTROLLER_NAME_1), controller_interface::return_type::OK); } ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, controller_if->get_state().id()); EXPECT_EQ(1u, cleanup_calls); @@ -270,8 +270,8 @@ class SwitchTest const auto UNSPECIFIED = 0; const auto EMPTY_STR_VEC = strvec{}; const auto NONEXISTENT_CONTROLLER = strvec{"nonexistent_controller"}; -const auto VALID_CONTROLLER = strvec{controller_name1}; -const auto VALID_PLUS_NONEXISTENT_CONTROLLERS = strvec{controller_name1, "nonexistent_controller"}; +const auto VALID_CONTROLLER = strvec{CONTROLLER_NAME_1}; +const auto VALID_PLUS_NONEXISTENT_CONTROLLERS = strvec{CONTROLLER_NAME_1, "nonexistent_controller"}; TEST_P(SwitchTest, EmptyListOrNonExistentTest) { @@ -375,10 +375,10 @@ class TestTwoLoadedControllers : public TestLoadController, void SetUp() override { TestLoadController::SetUp(); - controller_if1 = cm_->load_controller(controller_name1, TEST_CONTROLLER_CLASS_NAME); + controller_if1 = cm_->load_controller(CONTROLLER_NAME_1, TEST_CONTROLLER_CLASS_NAME); ASSERT_NE(controller_if1, nullptr); EXPECT_EQ(1u, cm_->get_loaded_controllers().size()); - controller_if2 = cm_->load_controller(controller_name2, TEST_CONTROLLER_CLASS_NAME); + controller_if2 = cm_->load_controller(CONTROLLER_NAME_2, TEST_CONTROLLER_CLASS_NAME); ASSERT_NE(controller_if2, nullptr); EXPECT_EQ(2u, cm_->get_loaded_controllers().size()); ASSERT_EQ( @@ -390,10 +390,10 @@ class TestTwoLoadedControllers : public TestLoadController, TEST_F(TestTwoLoadedControllers, load_and_configure_two_known_controllers) { - cm_->configure_controller(controller_name1); + cm_->configure_controller(CONTROLLER_NAME_1); EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, controller_if1->get_state().id()); - cm_->configure_controller(controller_name2); + cm_->configure_controller(CONTROLLER_NAME_2); EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, controller_if2->get_state().id()); } @@ -401,11 +401,11 @@ TEST_P(TestTwoLoadedControllers, switch_multiple_controllers) { const auto test_param = GetParam(); - cm_->configure_controller(controller_name1); + cm_->configure_controller(CONTROLLER_NAME_1); // Start controller #1 RCLCPP_INFO(cm_->get_logger(), "Starting stopped controller #1"); - switch_test_controllers(strvec{controller_name1}, strvec{}, test_param.strictness); + switch_test_controllers(strvec{CONTROLLER_NAME_1}, strvec{}, test_param.strictness); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, controller_if1->get_state().id()); ASSERT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, controller_if2->get_state().id()); @@ -418,10 +418,10 @@ TEST_P(TestTwoLoadedControllers, switch_multiple_controllers) // TODO(destogl): fix this test for BEST_EFFORT - probably related to: // https://github.com/ros-controls/ros2_control/pull/582#issuecomment-1029931561 // switch_test_controllers( - // strvec{controller_name2}, strvec{controller_name1}, test_param.strictness, + // strvec{CONTROLLER_NAME_2}, strvec{CONTROLLER_NAME_1}, test_param.strictness, // std::future_status::ready, controller_interface::return_type::ERROR); switch_test_controllers( - strvec{controller_name2}, strvec{controller_name1}, STRICT, std::future_status::ready, + strvec{CONTROLLER_NAME_2}, strvec{CONTROLLER_NAME_1}, STRICT, std::future_status::ready, controller_interface::return_type::ERROR); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, controller_if1->get_state().id()); ASSERT_EQ( @@ -429,31 +429,31 @@ TEST_P(TestTwoLoadedControllers, switch_multiple_controllers) { ControllerManagerRunner cm_runner(this); - cm_->configure_controller(controller_name2); + cm_->configure_controller(CONTROLLER_NAME_2); } // Stop controller 1 RCLCPP_INFO(cm_->get_logger(), "Stopping controller #1"); - switch_test_controllers(strvec{}, strvec{controller_name1}, test_param.strictness); + switch_test_controllers(strvec{}, strvec{CONTROLLER_NAME_1}, test_param.strictness); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, controller_if1->get_state().id()); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, controller_if2->get_state().id()); // Start controller 1 again RCLCPP_INFO(cm_->get_logger(), "Starting stopped controller #1"); - switch_test_controllers(strvec{controller_name1}, strvec{}, test_param.strictness); + switch_test_controllers(strvec{CONTROLLER_NAME_1}, strvec{}, test_param.strictness); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, controller_if1->get_state().id()); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, controller_if2->get_state().id()); // Stop controller 1, start controller 2 RCLCPP_INFO(cm_->get_logger(), "Stopping controller #1, starting controller #2"); switch_test_controllers( - strvec{controller_name2}, strvec{controller_name1}, test_param.strictness); + strvec{CONTROLLER_NAME_2}, strvec{CONTROLLER_NAME_1}, test_param.strictness); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, controller_if1->get_state().id()); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, controller_if2->get_state().id()); // Stop controller 2 RCLCPP_INFO(cm_->get_logger(), "Stopping controller #2"); - switch_test_controllers(strvec{}, strvec{controller_name2}, test_param.strictness); + switch_test_controllers(strvec{}, strvec{CONTROLLER_NAME_2}, test_param.strictness); ASSERT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE, controller_if2->get_state().id()); } diff --git a/controller_manager/test/test_spawner_unspawner.cpp b/controller_manager/test/test_spawner_unspawner.cpp index 03917dcd6e..37db345cb7 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::_; @@ -260,6 +261,56 @@ 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 @@ -354,3 +405,58 @@ TEST_F( ASSERT_EQ(ctrl_1.c->get_state().id(), lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE); } } + +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 36245deaad..f6d6fad69e 100644 --- a/controller_manager_msgs/CHANGELOG.rst +++ b/controller_manager_msgs/CHANGELOG.rst @@ -2,6 +2,35 @@ Changelog for package controller_manager_msgs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.13.0 (2024-07-08) +------------------- +* [ControllerChaining] Export state interfaces from chainable controllers (`#1021 `_) +* Contributors: Sai Kishor Kothakota + +4.12.0 (2024-07-01) +------------------- + +4.11.0 (2024-05-14) +------------------- + +4.10.0 (2024-05-08) +------------------- + +4.9.0 (2024-04-30) +------------------ + +4.8.0 (2024-03-27) +------------------ + +4.7.0 (2024-03-22) +------------------ + +4.6.0 (2024-03-02) +------------------ + +4.5.0 (2024-02-12) +------------------ + 4.4.0 (2024-01-31) ------------------ diff --git a/controller_manager_msgs/msg/ControllerState.msg b/controller_manager_msgs/msg/ControllerState.msg index 3b8e032541..65d773bddc 100644 --- a/controller_manager_msgs/msg/ControllerState.msg +++ b/controller_manager_msgs/msg/ControllerState.msg @@ -6,5 +6,6 @@ string[] required_command_interfaces # command interfaces required by con string[] required_state_interfaces # state interfaces required by controller bool is_chainable # specifies whether or not controller can export references for a controller chain bool is_chained # specifies whether or not controller's exported references are claimed by another controller +string[] exported_state_interfaces # state interfaces to be exported (only applicable if is_chainable is true) string[] reference_interfaces # references to be exported (only applicable if is_chainable is true) ChainConnection[] chain_connections # specifies list of controllers and their exported references that the controller is chained to diff --git a/controller_manager_msgs/package.xml b/controller_manager_msgs/package.xml index 1beaf47741..b0bd54a0a7 100644 --- a/controller_manager_msgs/package.xml +++ b/controller_manager_msgs/package.xml @@ -2,7 +2,7 @@ controller_manager_msgs - 4.4.0 + 4.13.0 Messages and services for the controller manager. Bence Magyar Denis Štogl diff --git a/doc/index.rst b/doc/index.rst index 91f965d601..b1d8e21c7d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -16,12 +16,6 @@ API Documentation API documentation is parsed by doxygen and can be found `here <../../api/index.html>`_ -========= -Features -========= - -* :ref:`Command Line Interface (CLI) ` - ======== Concepts ======== @@ -31,5 +25,6 @@ Concepts Controller Manager <../controller_manager/doc/userdoc.rst> Controller Chaining / Cascade Control <../controller_manager/doc/controller_chaining.rst> + Joint Kinematics <../hardware_interface/doc/joints_userdoc.rst> Hardware Components <../hardware_interface/doc/hardware_components_userdoc.rst> Mock Components <../hardware_interface/doc/mock_components_userdoc.rst> diff --git a/doc/migration/Galactic.rst b/doc/migration/Galactic.rst new file mode 100644 index 0000000000..4c6e958951 --- /dev/null +++ b/doc/migration/Galactic.rst @@ -0,0 +1,53 @@ +:github_url: https://github.com/ros-controls/ros2_control/blob/{REPOS_FILE_BRANCH}/doc/release_notes/Galactic.rst + +Foxy to Galactic +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +hardware_interface +************************************** +Between Foxy and Galactic we made substantial changes to the interface of hardware components to enable management of their lifecycle. +The following list shows a set of quick changes to port existing hardware components to Galactic: + +1. Rename ``configure`` to ``on_init`` and change return type to ``CallbackReturn`` + +2. If using BaseInterface as base class then you should remove it. Specifically, change: + + .. code-block:: c++ + + hardware_interface::BaseInterface + + to + + .. code-block:: c++ + + hardware_interface::[Actuator|Sensor|System]Interface + +3. Remove include of headers ``base_interface.hpp`` and ``hardware_interface_status_values.hpp`` + +4. Add include of header ``rclcpp_lifecycle/state.hpp`` although this may not be strictly necessary + +5. replace first three lines in ``on_init`` to + + .. code-block:: c++ + + if (hardware_interface::[Actuator|Sensor|System]Interface::on_init(info) != CallbackReturn::SUCCESS) + { + return CallbackReturn::ERROR; + } + + +6. Change last return of ``on_init`` to ``return CallbackReturn::SUCCESS;`` + +7. Remove all lines with ``status_ = ...`` or ``status::...`` + +8. Rename ``start()`` to ``on_activate(const rclcpp_lifecycle::State & previous_state)`` and + ``stop()`` to ``on_deactivate(const rclcpp_lifecycle::State & previous_state)`` + +9. Change return type of ``on_activate`` and ``on_deactivate`` to ``CallbackReturn`` + +10. Change last return of ``on_activate`` and ``on_deactivate`` to ``return CallbackReturn::SUCCESS;`` + +11. If you have any ``return_type::ERROR`` in ``on_init``, ``on_activate``, or ``in_deactivate`` change to ``CallbackReturn::ERROR`` + +12. If you get link errors with undefined refernences to symbols in ``rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface``, then add + ``rclcpp_lifecyle`` package dependency to ``CMakeLists.txt`` and ``package.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..5d1773afe8 --- /dev/null +++ b/doc/release_notes/Jazzy.rst @@ -0,0 +1,113 @@ +: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 `_) +* Export state interfaces from the chainable controller #api-breaking (`#1021 `_) + + * All chainable controllers must implement the method ``export_state_interfaces`` to export the state interfaces, similar to ``export_reference_interfaces`` method that is exporting the reference interfaces. + +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 `_) +* Access to logger and clock through ``get_logger`` and ``get_clock`` methods in ResourceManager and HardwareComponents ``Actuator``, ``Sensor`` and ``System`` (`#1585 `_) + +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 b5fb894e0c..76ddab43cc 100644 --- a/hardware_interface/CHANGELOG.rst +++ b/hardware_interface/CHANGELOG.rst @@ -2,6 +2,68 @@ Changelog for package hardware_interface ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.13.0 (2024-07-08) +------------------- +* [ResourceManager] Propagate access to logger and clock interfaces to HardwareComponent (`#1585 `_) +* [ControllerChaining] Export state interfaces from chainable controllers (`#1021 `_) +* Remove mimic parameter from ros2_control tag (`#1553 `_) +* Contributors: Christoph Fröhlich, Sai Kishor Kothakota + +4.12.0 (2024-07-01) +------------------- +* Add resources_lock\_ lock_guards to avoid race condition when loading robot_description through topic (`#1451 `_) +* [RM] Rename `load_urdf` method to `load_and_initialize_components` and add error handling there to avoid stack crashing when error happens. (`#1354 `_) +* Small improvements to the error output in component parser to make debugging easier. (`#1580 `_) +* Fix link to gazebosim.org (`#1563 `_) +* Add doc page about joint kinematics (`#1497 `_) +* Bump version of pre-commit hooks (`#1556 `_) +* [Feature] Hardware Components Grouping (`#1458 `_) +* Contributors: Christoph Fröhlich, Dr. Denis, Sai Kishor Kothakota, github-actions[bot] + +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 `_) +* Contributors: Sai Kishor Kothakota + +4.7.0 (2024-03-22) +------------------ +* Codeformat from new pre-commit config (`#1433 `_) +* Contributors: Christoph Fröhlich + +4.6.0 (2024-03-02) +------------------ +* Add -Werror=missing-braces to compile options (`#1423 `_) +* [CI] Code coverage + pre-commit (`#1413 `_) +* Contributors: Christoph Fröhlich, Sai Kishor Kothakota + +4.5.0 (2024-02-12) +------------------ +* Add missing export macros in lexical_casts.hpp (`#1382 `_) +* Move hardware interface README content to sphinx documentation (`#1342 `_) +* [Doc] Add documentation about initial_value regarding mock_hw (`#1352 `_) +* Contributors: Felix Exner (fexner), Mateus Menezes, Silvio Traversaro + 4.4.0 (2024-01-31) ------------------ * Move `test_components` to own package (`#1325 `_) diff --git a/hardware_interface/CMakeLists.txt b/hardware_interface/CMakeLists.txt index 2613ba735a..d2f480e2f3 100644 --- a/hardware_interface/CMakeLists.txt +++ b/hardware_interface/CMakeLists.txt @@ -2,7 +2,8 @@ cmake_minimum_required(VERSION 3.16) project(hardware_interface LANGUAGES CXX) if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") - add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow) + add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow + -Werror=missing-braces) endif() set(THIS_PACKAGE_INCLUDE_DEPENDS @@ -14,9 +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() @@ -82,6 +86,7 @@ if(BUILD_TESTING) add_library(test_hardware_components SHARED test/test_hardware_components/test_single_joint_actuator.cpp test/test_hardware_components/test_force_torque_sensor.cpp + test/test_hardware_components/test_imu_sensor.cpp test/test_hardware_components/test_two_joint_system.cpp test/test_hardware_components/test_system_with_command_modes.cpp ) @@ -121,3 +126,4 @@ install( ament_export_targets(export_hardware_interface HAS_LIBRARY_TARGET) ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS}) ament_package() +ament_generate_version_header(${PROJECT_NAME}) diff --git a/hardware_interface/README.md b/hardware_interface/README.md index 343158b698..76480201cb 100644 --- a/hardware_interface/README.md +++ b/hardware_interface/README.md @@ -1,131 +1,5 @@ -robot agnostic hardware_interface package. -This package will eventually be moved into its own repo. +# Overview -## Best practice for `hardware_interface` implementation -In the following section you can find some advices which will help you implement interface -for your specific hardware. +For detailed information about this package, please see the [ros2_control Documentation]! -### Best practice for having different update rate for each `hardware_interface` by counting loops -Current implementation of [ros2_control main node](https://github.com/ros-controls/ros2_control/blob/master/controller_manager/src/ros2_control_node.cpp) -has one update rate that controls the rate of the [`read()`](https://github.com/ros-controls/ros2_control/blob/fe462926416d527d1da163bc3eabd02ee1de9be9/hardware_interface/include/hardware_interface/system_interface.hpp#L169) and [`write()`](https://github.com/ros-controls/ros2_control/blob/fe462926416d527d1da163bc3eabd02ee1de9be9/hardware_interface/include/hardware_interface/system_interface.hpp#L178) -calls in [`hardware_interface(s)`](https://github.com/ros-controls/ros2_control/blob/master/hardware_interface/include/hardware_interface/system_interface.hpp). -In this section suggestion on how to run each interface implementation on its own update rate is introduced. - -1. Add parameters of main control loop update rate and desired update rate for your hardware interface. -``` - - - - - - - - my_system_interface/MySystemHardware - ${main_loop_update_rate} - ${desired_hw_update_rate} - - ... - - - - - -``` - -2. In you [`on_init()`](https://github.com/ros-controls/ros2_control/blob/fe462926416d527d1da163bc3eabd02ee1de9be9/hardware_interface/include/hardware_interface/system_interface.hpp#L94) specific implementation fetch desired parameters: -``` -namespace my_system_interface -{ -hardware_interface::CallbackReturn MySystemHardware::on_init( - const hardware_interface::HardwareInfo & info) -{ - if ( - hardware_interface::SystemInterface::on_init(info) != - hardware_interface::CallbackReturn::SUCCESS) - { - return hardware_interface::CallbackReturn::ERROR; - } - - // declaration in *.hpp file --> unsigned int main_loop_update_rate_, desired_hw_update_rate_ = 100 ; - main_loop_update_rate_ = stoi(info_.hardware_parameters["main_loop_update_rate"]); - desired_hw_update_rate_ = stoi(info_.hardware_parameters["desired_hw_update_rate"]); - - ... -} -... -} // my_system_interface -``` - -3. In your `on_activate()` specific implementation reset internal loop counter -``` -hardware_interface::CallbackReturn MySystemHardware::on_activate( - const rclcpp_lifecycle::State & /*previous_state*/) -{ - // declaration in *.hpp file --> unsigned int update_loop_counter_ ; - update_loop_counter_ = 0; - ... -} -``` - -4. In your `read(const rclcpp::Time & time, const rclcpp::Duration & period)` and/or - `write(const rclcpp::Time & time, const rclcpp::Duration & period)` - specific implementations decide if you should interfere with your hardware -``` -hardware_interface::return_type MySystemHardware::read(const rclcpp::Time & time, const rclcpp::Duration & period) -{ - - bool hardware_go = main_loop_update_rate_ == 0 || - desired_hw_update_rate_ == 0 || - ((update_loop_counter_ % desired_hw_update_rate_) == 0); - - if (hardware_go){ - // hardware comms and operations - - } - ... - - // update counter - ++update_loop_counter_; - update_loop_counter_ %= main_loop_update_rate_; -} -``` - -### Best practice for having different update rate for each `hardware_interface` by measuring elapsed time -Another way to decide if hardware communication should be executed in the`read(const rclcpp::Time & time, const rclcpp::Duration & period)` and/or -`write(const rclcpp::Time & time, const rclcpp::Duration & period)` implementations is to measure elapsed time since last pass: - -``` -hardware_interface::CallbackReturn MySystemHardware::on_activate( - const rclcpp_lifecycle::State & /*previous_state*/) -{ - // declaration in *.hpp file --> bool first_read_pass_, first_write_pass_ = true ; - first_read_pass_ = first_write_pass_ = true; - ... -} - -hardware_interface::return_type MySystemHardware::read(const rclcpp::Time & time, const rclcpp::Duration & period) -{ - if (first_read_pass_ || (time - last_read_time_ ) > period) - { - first_read_pass_ = false; - // declaration in *.hpp file --> rclcpp::Time last_read_time_ ; - last_read_time_ = time; - // hardware comms and operations - ... - } - ... -} - -hardware_interface::return_type MySystemHardware::write(const rclcpp::Time & time, const rclcpp::Duration & period) -{ - if (first_write_pass_ || (time - last_write_time_ ) > period) - { - first_write_pass_ = false; - // declaration in *.hpp file --> rclcpp::Time last_write_time_ ; - last_write_time_ = time; - // hardware comms and operations - ... - } - ... -} -``` +[ros2_control Documentation]: https://control.ros.org/master/doc/ros2_control/hardware_interface/doc/hardware_components_userdoc.html diff --git a/hardware_interface/doc/different_update_rates_userdoc.rst b/hardware_interface/doc/different_update_rates_userdoc.rst new file mode 100644 index 0000000000..23f5e3564a --- /dev/null +++ b/hardware_interface/doc/different_update_rates_userdoc.rst @@ -0,0 +1,169 @@ +:github_url: https://github.com/ros-controls/ros2_control/blob/{REPOS_FILE_BRANCH}/hardware_interface/doc/different_update_rates_userdoc.rst + +.. _different_update_rates_userdoc: + +Different update rates for Hardware Components +=============================================================================== + +In the following sections you can find some advice which will help you to implement Hardware +Components with update rates different from the main control loop. + +By counting loops +------------------------------------------------------------------------------- + +Current implementation of +`ros2_control main node `_ +has one update rate that controls the rate of the +`read(...) `_ +and `write(...) `_ +calls in `hardware_interface(s) `_. +To achieve different update rates for your hardware component you can use the following steps: + +.. _step-1: + +1. Add parameters of main control loop update rate and desired update rate for your hardware component + + .. code:: xml + + + + + + + + + my_system_interface/MySystemHardware + ${main_loop_update_rate} + ${desired_hw_update_rate} + + ... + + + + + + +.. _step-2: + +2. In you ``on_init()`` specific implementation fetch the desired parameters + + .. code:: cpp + + namespace my_system_interface + { + hardware_interface::CallbackReturn MySystemHardware::on_init( + const hardware_interface::HardwareInfo & info) + { + if ( + hardware_interface::SystemInterface::on_init(info) != + hardware_interface::CallbackReturn::SUCCESS) + { + return hardware_interface::CallbackReturn::ERROR; + } + + // declaration in *.hpp file --> unsigned int main_loop_update_rate_, desired_hw_update_rate_ = 100 ; + main_loop_update_rate_ = stoi(info_.hardware_parameters["main_loop_update_rate"]); + desired_hw_update_rate_ = stoi(info_.hardware_parameters["desired_hw_update_rate"]); + + ... + } + ... + } // my_system_interface + +3. In your ``on_activate`` specific implementation reset internal loop counter + + .. code:: cpp + + hardware_interface::CallbackReturn MySystemHardware::on_activate( + const rclcpp_lifecycle::State & /*previous_state*/) + { + // declaration in *.hpp file --> unsigned int update_loop_counter_ ; + update_loop_counter_ = 0; + ... + } + +4. In your ``read(const rclcpp::Time & time, const rclcpp::Duration & period)`` + and/or ``write(const rclcpp::Time & time, const rclcpp::Duration & period)`` + specific implementations decide if you should interfere with your hardware + + .. code:: cpp + + hardware_interface::return_type MySystemHardware::read(const rclcpp::Time & time, const rclcpp::Duration & period) + { + + bool hardware_go = main_loop_update_rate_ == 0 || + desired_hw_update_rate_ == 0 || + ((update_loop_counter_ % desired_hw_update_rate_) == 0); + + if (hardware_go){ + // hardware comms and operations + ... + } + ... + + // update counter + ++update_loop_counter_; + update_loop_counter_ %= main_loop_update_rate_; + } + +By measuring elapsed time +------------------------------------------------------------------------------- + +Another way to decide if hardware communication should be executed in the +``read(const rclcpp::Time & time, const rclcpp::Duration & period)`` and/or +``write(const rclcpp::Time & time, const rclcpp::Duration & period)`` +implementations is to measure elapsed time since last pass: + +1. In your ``on_activate`` specific implementation reset the flags that indicate + that this is the first pass of the ``read`` and ``write`` methods + + .. code:: cpp + + hardware_interface::CallbackReturn MySystemHardware::on_activate( + const rclcpp_lifecycle::State & /*previous_state*/) + { + // declaration in *.hpp file --> bool first_read_pass_, first_write_pass_ = true ; + first_read_pass_ = first_write_pass_ = true; + ... + } + +2. In your ``read(const rclcpp::Time & time, const rclcpp::Duration & period)`` + and/or ``write(const rclcpp::Time & time, const rclcpp::Duration & period)`` + specific implementations decide if you should interfere with your hardware + + .. code:: cpp + + hardware_interface::return_type MySystemHardware::read(const rclcpp::Time & time, const rclcpp::Duration & period) + { + if (first_read_pass_ || (time - last_read_time_ ) > desired_hw_update_period_) + { + first_read_pass_ = false; + // declaration in *.hpp file --> rclcpp::Time last_read_time_ ; + last_read_time_ = time; + // hardware comms and operations + ... + } + ... + } + + hardware_interface::return_type MySystemHardware::write(const rclcpp::Time & time, const rclcpp::Duration & period) + { + if (first_write_pass_ || (time - last_write_time_ ) > desired_hw_update_period_) + { + first_write_pass_ = false; + // declaration in *.hpp file --> rclcpp::Time last_write_time_ ; + last_write_time_ = time; + // hardware comms and operations + ... + } + ... + } + +.. note:: + + The approach to get the desired update period value from the URDF and assign it to the variable + ``desired_hw_update_period_`` is the same as in the previous section (|step-1|_ and |step-2|_) but + with a different parameter name. + +.. |step-1| replace:: step 1 +.. |step-2| replace:: step 2 diff --git a/hardware_interface/doc/hardware_components_userdoc.rst b/hardware_interface/doc/hardware_components_userdoc.rst index 1a8c5e611b..0a980cad4e 100644 --- a/hardware_interface/doc/hardware_components_userdoc.rst +++ b/hardware_interface/doc/hardware_components_userdoc.rst @@ -16,6 +16,7 @@ Guidelines and Best Practices Hardware Interface Types Writing a Hardware Component + Different Update Rates Handling of errors that happen during read() and write() calls @@ -26,53 +27,3 @@ If ``hardware_interface::return_type::ERROR`` is returned from ``read()`` or ``w Error handling follows the `node lifecycle `_. If successful ``CallbackReturn::SUCCESS`` is returned and hardware is again in ``UNCONFIGURED`` state, if any ``ERROR`` or ``FAILURE`` happens the hardware ends in ``FINALIZED`` state and can not be recovered. The only option is to reload the complete plugin, but there is currently no service for this in the Controller Manager. - -Migration from Foxy to newer versions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Between Foxy and Galactic we made substantial changes to the interface of hardware components to enable management of their lifecycle. -The following list shows a set of quick changes to port existing hardware components to Galactic: - -1. Rename ``configure`` to ``on_init`` and change return type to ``CallbackReturn`` - -2. If using BaseInterface as base class then you should remove it. Specifically, change: - - .. code-block:: c++ - - hardware_interface::BaseInterface - - to - - .. code-block:: c++ - - hardware_interface::[Actuator|Sensor|System]Interface - -3. Remove include of headers ``base_interface.hpp`` and ``hardware_interface_status_values.hpp`` - -4. Add include of header ``rclcpp_lifecycle/state.hpp`` although this may not be strictly necessary - -5. replace first three lines in ``on_init`` to - - .. code-block:: c++ - - if (hardware_interface::[Actuator|Sensor|System]Interface::on_init(info) != CallbackReturn::SUCCESS) - { - return CallbackReturn::ERROR; - } - - -6. Change last return of ``on_init`` to ``return CallbackReturn::SUCCESS;`` - -7. Remove all lines with ``status_ = ...`` or ``status::...`` - -8. Rename ``start()`` to ``on_activate(const rclcpp_lifecycle::State & previous_state)`` and - ``stop()`` to ``on_deactivate(const rclcpp_lifecycle::State & previous_state)`` - -9. Change return type of ``on_activate`` and ``on_deactivate`` to ``CallbackReturn`` - -10. Change last return of ``on_activate`` and ``on_deactivate`` to ``return CallbackReturn::SUCCESS;`` - -11. If you have any ``return_type::ERROR`` in ``on_init``, ``on_activate``, or ``in_deactivate`` change to ``CallbackReturn::ERROR`` - -12. If you get link errors with undefined refernences to symbols in ``rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface``, then add - ``rclcpp_lifecyle`` package dependency to ``CMakeLists.txt`` and ``package.xml`` diff --git a/hardware_interface/doc/hardware_interface_types_userdoc.rst b/hardware_interface/doc/hardware_interface_types_userdoc.rst index cd1e475b20..d8338bf7a6 100644 --- a/hardware_interface/doc/hardware_interface_types_userdoc.rst +++ b/hardware_interface/doc/hardware_interface_types_userdoc.rst @@ -14,6 +14,8 @@ Joints ````-tag groups the interfaces associated with the joints of physical robots and actuators. They have command and state interfaces to set the goal values for hardware and read its current state. +All joints defined in the ````-tag have to be present in the URDF received :ref:`by the controller manager `. + State interfaces of joints can be published as a ROS topic by means of the :ref:`joint_state_broadcaster ` Sensors @@ -39,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. @@ -150,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/joints_userdoc.rst b/hardware_interface/doc/joints_userdoc.rst new file mode 100644 index 0000000000..f4332410d5 --- /dev/null +++ b/hardware_interface/doc/joints_userdoc.rst @@ -0,0 +1,132 @@ +:github_url: https://github.com/ros-controls/ros2_control/blob/{REPOS_FILE_BRANCH}/hardware_interface/doc/joints_userdoc.rst + +.. _joints_userdoc: + + +Joint Kinematics for ros2_control +--------------------------------------------------------- + +This page should give an overview of the joint kinematics in the context of ros2_control. It is intended to give a brief introduction to the topic and to explain the current implementation in ros2_control. + +Nomenclature +############ + +Degrees of Freedom (DoF) + From `wikipedia `__: + + In physics, the degrees of freedom (DoF) of a mechanical system is the number of independent parameters that define its configuration or state. + +Joint + A joint is a connection between two links. In the ROS ecosystem, three types are more typical: Revolute (A hinge joint with position limits), Continuous (A continuous hinge without any position limits) or Prismatic (A sliding joint that moves along the axis). + + In general, a joint can be actuated or non-actuated, also called passive. Passive joints are joints that do not have their own actuation mechanism but instead allow movement by external forces or by being passively moved by other joints. A passive joint can have a DoF of one, such as a pendulum, or it can be part of a parallel kinematic mechanism with zero DoF. + +Serial Kinematics + Serial kinematics refers to the arrangement of joints in a robotic manipulator where each joint is independent of the others, and the number of joints is equal to the DoF of the kinematic chain. + + A typical example is an industrial robot with six revolute joints, having 6-DoF. Each joint can be actuated independently, and the end-effector can be moved to any position and orientation in the workspace. + +Kinematic Loops + On the other hand, kinematic loops, also known as closed-loop mechanisms, involve several joints that are connected in a kinematic chain and being actuated together. This means that the joints are coupled and cannot be moved independently: In general, the number of DoFs is smaller than the number of joints. + This structure is typical for parallel kinematic mechanisms, where the end-effector is connected to the base by several kinematic chains. + + An example is the four-bar linkage, which consists of four links and four joints. It can have one or two actuators and consequently one or two DoFs, despite having four joints. Furthermore, we can say that we have one (two) actuated joint and three (two) passive joints, which must satisfy the kinematic constraints of the mechanism. + +URDF +############# + +URDF is the default format to describe robot kinematics in ROS. However, only serial kinematic chains are supported, except for the so-called mimic joints. See the `URDF specification `__ for more details. + +Mimic joints can be defined in the following way in the URDF + + .. code-block:: xml + + + + + + + + + + + + + + + + + +Mimic joints are an abstraction of the real world. For example, they can be used to describe + +* simple closed-loop kinematics with linear dependencies of the joint positions and velocities +* links connected with belts, like belt and pulley systems or telescope arms +* a simplified model of passive joints, e.g. a pendulum at the end-effector always pointing downwards +* abstract complex groups of actuated joints, where several joints are directly controlled by low-level control loops and move synchronously. Without giving a real-world example, this could be several motors with their individual power electronics but commanded with the same setpoint. + +Mimic joints defined in the URDF are parsed from the resource manager and stored in a class variable of type ``HardwareInfo``, which can be accessed by the hardware components. The mimic joints must not have command interfaces but can have state interfaces. + +.. code-block:: xml + + + + + + + + + + + + + + + +From the officially released packages, the following packages are already using this information: + +* :ref:`mock_components (generic system) ` +* :ref:`gazebo_ros2_control ` +* :ref:`gz_ros2_control ` + +As the URDF specifies only the kinematics, the mimic tag has to be independent of the hardware interface type used in ros2_control. This means that we interpret this info in the following way: + +* **position = multiplier * other_joint_position + offset** +* **velocity = multiplier * other_joint_velocity** + +If someone wants to deactivate the mimic joint behavior for whatever reason without changing the URDF, it can be done by setting the attribute ``mimic=false`` of the joint tag in the ```` section of the XML. + +.. code-block:: xml + + + + + + + +Transmission Interface +####################### +Mechanical transmissions transform effort/flow variables such that their product (power) remains constant. Effort variables for linear and rotational domains are force and torque; while the flow variables are respectively linear velocity and angular velocity. + +In robotics it is customary to place transmissions between actuators and joints. This interface adheres to this naming to identify the input and output spaces of the transformation. The provided interfaces allow bidirectional mappings between actuator and joint spaces for effort, velocity and position. Position is not a power variable, but the mappings can be implemented using the velocity map plus an integration constant representing the offset between actuator and joint zeros. + +The ``transmission_interface`` provides a base class and some implementations for plugins, which can be integrated and loaded by custom hardware components. They are not automatically loaded by any hardware component or the gazebo plugins, each hardware component is responsible for loading the appropriate transmission interface to map the actuator readings to joint readings. + +Currently the following implementations are available: + +* ``SimpleTransmission``: A simple transmission with a constant reduction ratio and no additional dynamics. +* ``DifferentialTransmission``: A differential transmission with two actuators and two joints. +* ``FourBarLinkageTransmission``: A four-bar-linkage transmission with two actuators and two joints. + +For more information, see :ref:`example_8 ` or the `transmission_interface `__ documentation. + +Simulating Closed-Loop Kinematic Chains +####################################### +Depending on the simulation plugin, different approaches can be used to simulate closed-loop kinematic chains. The following list gives an overview of the available simulation plugins and their capabilities: + +gazebo_ros2_control: + * mimic joints + * closed-loop kinematics are supported with ```` tags in the URDF, see, e.g., `here `__. + +gz_ros2_control: + * mimic joints + * closed-loop kinematics are not directly supported yet, but can be implemented by using a ``DetachableJoint`` via custom plugins. Follow `this issue `__ for updates on this topic. diff --git a/hardware_interface/doc/mock_components_userdoc.rst b/hardware_interface/doc/mock_components_userdoc.rst index c075fdeafc..8eca65c52b 100644 --- a/hardware_interface/doc/mock_components_userdoc.rst +++ b/hardware_interface/doc/mock_components_userdoc.rst @@ -19,43 +19,83 @@ For more information about hardware components check :ref:`detailed documentatio Features: - - support for mimic joints + - support for mimic joints, which is parsed from the URDF (see the `URDF wiki `__) - mirroring commands to states with and without offset - fake command interfaces for setting sensor data from an external node (combined with a :ref:`forward controller `) + - fake gpio interfaces for setting sensor data from an external node (combined with a :ref:`forward controller `) 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_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. + 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. position_state_following_offset (optional; double; default: 0.0) - Following offset added to the commanded values when mirrored to states. - - -custom_interface_with_following_offset (optional; string; default: "") - Mapping of offsetted commands to a custom interface. - - -Per-joint Parameters -,,,,,,,,,,,,,,,,,,,, - -mimic (optional; string) - Defined name of the joint to mimic. This is often used concept with parallel grippers. Example: ``joint1``. - - -multiplier (optional; double; default: 1; used if mimic joint is defined) - Multiplier of values for mimicking joint defined in ``mimic`` parameter. Example: ``-2``. + 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..3a1d7a5974 100644 --- a/hardware_interface/include/hardware_interface/actuator.hpp +++ b/hardware_interface/include/hardware_interface/actuator.hpp @@ -24,6 +24,8 @@ #include "hardware_interface/types/hardware_interface_return_values.hpp" #include "hardware_interface/visibility_control.h" #include "rclcpp/duration.hpp" +#include "rclcpp/logging.hpp" +#include "rclcpp/node_interfaces/node_clock_interface.hpp" #include "rclcpp/time.hpp" #include "rclcpp_lifecycle/state.hpp" @@ -44,7 +46,9 @@ class Actuator final ~Actuator() = default; HARDWARE_INTERFACE_PUBLIC - const rclcpp_lifecycle::State & initialize(const HardwareInfo & actuator_info); + const rclcpp_lifecycle::State & initialize( + const HardwareInfo & actuator_info, rclcpp::Logger logger, + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface); HARDWARE_INTERFACE_PUBLIC const rclcpp_lifecycle::State & configure(); @@ -83,6 +87,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..88b36d460b 100644 --- a/hardware_interface/include/hardware_interface/actuator_interface.hpp +++ b/hardware_interface/include/hardware_interface/actuator_interface.hpp @@ -25,6 +25,8 @@ #include "hardware_interface/types/lifecycle_state_names.hpp" #include "lifecycle_msgs/msg/state.hpp" #include "rclcpp/duration.hpp" +#include "rclcpp/logging.hpp" +#include "rclcpp/node_interfaces/node_clock_interface.hpp" #include "rclcpp/time.hpp" #include "rclcpp_lifecycle/node_interfaces/lifecycle_node_interface.hpp" #include "rclcpp_lifecycle/state.hpp" @@ -73,7 +75,8 @@ class ActuatorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNod public: ActuatorInterface() : lifecycle_state_(rclcpp_lifecycle::State( - lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN, lifecycle_state_names::UNKNOWN)) + lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN, lifecycle_state_names::UNKNOWN)), + actuator_logger_(rclcpp::get_logger("actuator_interface")) { } @@ -88,15 +91,33 @@ class ActuatorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNod virtual ~ActuatorInterface() = default; - /// Initialization of the hardware interface from data parsed from the robot's URDF. + /// Initialization of the hardware interface from data parsed from the robot's URDF and also the + /// clock and logger interfaces. /** * \param[in] hardware_info structure with data from URDF. + * \param[in] clock_interface pointer to the clock interface. + * \param[in] logger_interface pointer to the logger interface. * \returns CallbackReturn::SUCCESS if required data are provided and can be parsed. * \returns CallbackReturn::ERROR if any error happens or data are missing. */ - virtual CallbackReturn on_init(const HardwareInfo & hardware_info) + CallbackReturn init( + const HardwareInfo & hardware_info, rclcpp::Logger logger, + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface) { + clock_interface_ = clock_interface; + actuator_logger_ = logger.get_child("hardware_component.actuator." + hardware_info.name); info_ = hardware_info; + return on_init(hardware_info); + }; + + /// Initialization of the hardware interface from data parsed from the robot's URDF. + /** + * \param[in] hardware_info structure with data from URDF. + * \returns CallbackReturn::SUCCESS if required data are provided and can be parsed. + * \returns CallbackReturn::ERROR if any error happens or data are missing. + */ + virtual CallbackReturn on_init(const HardwareInfo & /*hardware_info*/) + { return CallbackReturn::SUCCESS; }; @@ -131,7 +152,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 +171,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 +211,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. @@ -203,8 +230,24 @@ class ActuatorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNod void set_state(const rclcpp_lifecycle::State & new_state) { lifecycle_state_ = new_state; } protected: + /// Get the logger of the ActuatorInterface. + /** + * \return logger of the ActuatorInterface. + */ + rclcpp::Logger get_logger() const { return actuator_logger_; } + + /// Get the clock of the ActuatorInterface. + /** + * \return clock of the ActuatorInterface. + */ + rclcpp::Clock::SharedPtr get_clock() const { return clock_interface_->get_clock(); } + HardwareInfo info_; rclcpp_lifecycle::State lifecycle_state_; + +private: + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface_; + rclcpp::Logger actuator_logger_; }; } // namespace hardware_interface 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 eb6b63cfc3..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,25 @@ 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 +struct MimicJoint +{ + std::size_t joint_index; + std::size_t mimicked_joint_index; + double multiplier = 1.0; + double offset = 0.0; +}; + +/// @brief This enum is used to store the mimic attribute of a joint +enum class MimicAttribute +{ + NOT_SET, + TRUE, + FALSE }; /** @@ -55,6 +76,9 @@ struct ComponentInfo /// Type of the component: sensor, joint, or GPIO. std::string type; + /// Hold the value of the mimic attribute if given, NOT_SET otherwise + MimicAttribute is_mimic = MimicAttribute::NOT_SET; + /** * Name of the command interfaces that can be set, e.g. "position", "velocity", etc. * Used by joints and GPIOs. @@ -109,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. @@ -116,22 +142,26 @@ struct HardwareInfo /// (Optional) Key-value pairs for hardware parameters. std::unordered_map hardware_parameters; /** - * Map of joints provided by the hardware where the key is the joint name. + * Vector of joints provided by the hardware. * Required for Actuator and System Hardware. */ std::vector joints; /** - * Map of sensors provided by the hardware where the key is the joint or link name. + * Vector of mimic joints. + */ + std::vector mimic_joints; + /** + * Vector of sensors provided by the hardware. * Required for Sensor and optional for System Hardware. */ std::vector sensors; /** - * Map of GPIO provided by the hardware where the key is a descriptive name of the GPIO. + * Vector of GPIOs provided by the hardware. * Optional for any hardware components. */ std::vector gpios; /** - * Map of transmissions to calculate ration between joints and physical actuators. + * Vector of transmissions to calculate ratio between joints and physical actuators. * Optional for Actuator and System Hardware. */ std::vector transmissions; @@ -139,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/lexical_casts.hpp b/hardware_interface/include/hardware_interface/lexical_casts.hpp index 846d9f757c..042361e392 100644 --- a/hardware_interface/include/hardware_interface/lexical_casts.hpp +++ b/hardware_interface/include/hardware_interface/lexical_casts.hpp @@ -21,6 +21,8 @@ #include #include +#include "hardware_interface/visibility_control.h" + namespace hardware_interface { @@ -29,8 +31,10 @@ namespace hardware_interface * from https://github.com/ros-planning/srdfdom/blob/ad17b8d25812f752c397a6011cec64aeff090c46/src/model.cpp#L53 */ +HARDWARE_INTERFACE_PUBLIC double stod(const std::string & s); +HARDWARE_INTERFACE_PUBLIC bool parse_bool(const std::string & bool_string); } // 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..5313162c29 100644 --- a/hardware_interface/include/hardware_interface/resource_manager.hpp +++ b/hardware_interface/include/hardware_interface/resource_manager.hpp @@ -49,9 +49,9 @@ class HARDWARE_INTERFACE_PUBLIC ResourceManager { public: /// Default constructor for the Resource Manager. - ResourceManager( - unsigned int update_rate = 100, - rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface = nullptr); + explicit ResourceManager( + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface, + rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr logger_interface); /// Constructor for the Resource Manager. /** @@ -59,19 +59,19 @@ class HARDWARE_INTERFACE_PUBLIC ResourceManager * hardware components listed within as well as populate their respective * state and command interfaces. * - * If the interfaces ought to be validated, the constructor throws an exception - * in case the URDF lists interfaces which are not available. - * * \param[in] urdf string containing the URDF. - * \param[in] validate_interfaces boolean argument indicating whether the exported - * interfaces ought to be validated. Defaults to true. * \param[in] activate_all boolean argument indicating if all resources should be immediately * activated. Currently used only in tests. + * \param[in] update_rate Update rate of the controller manager to calculate calling frequency + * of async components. + * \param[in] clock_interface reference to the clock interface of the CM node for getting time + * used for triggering async components. */ explicit ResourceManager( - const std::string & urdf, bool validate_interfaces = true, bool activate_all = false, - unsigned int update_rate = 100, - rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface = nullptr); + const std::string & urdf, + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface, + rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr logger_interface, + bool activate_all = false, const unsigned int update_rate = 100); ResourceManager(const ResourceManager &) = delete; @@ -79,29 +79,26 @@ class HARDWARE_INTERFACE_PUBLIC ResourceManager /// Load resources from on a given URDF. /** - * The resource manager can be post initialized with a given URDF. + * The resource manager can be post-initialized with a given URDF. * This is mainly used in conjunction with the default constructor * in which the URDF might not be present at first initialization. * * \param[in] urdf string containing the URDF. - * \param[in] validate_interfaces boolean argument indicating whether the exported - * interfaces ought to be validated. Defaults to true. - * \param[in] load_and_initialize_components boolean argument indicating whether to load and - * initialize the components present in the parsed URDF. Defaults to true. + * \param[in] update_rate update rate of the main control loop, i.e., of the controller manager. + * \returns false if URDF validation has failed. */ - void load_urdf( - const std::string & urdf, bool validate_interfaces = true, - bool load_and_initialize_components = true); + virtual bool load_and_initialize_components( + const std::string & urdf, const unsigned int update_rate = 100); /** - * @brief if the resource manager load_urdf(...) function has been called this returns true. - * We want to permit to load the urdf later on but we currently don't want to permit multiple - * calls to load_urdf (reloading/loading different urdf). + * @brief if the resource manager load_and_initialize_components(...) function has been called + * this returns true. We want to permit to loading the urdf later on, but we currently don't want + * to permit multiple calls to load_and_initialize_components (reloading/loading different urdf). * - * @return true if resource manager's load_urdf() has been already called. - * @return false if resource manager's load_urdf() has not been yet called. + * @return true if the resource manager has successfully loaded and initialized the components + * @return false if the resource manager doesn't have any components loaded and initialized. */ - bool is_urdf_already_loaded() const; + bool are_components_initialized() const; /// Claim a state interface given its key. /** @@ -134,6 +131,59 @@ class HARDWARE_INTERFACE_PUBLIC ResourceManager */ bool state_interface_is_available(const std::string & name) const; + /// Add controllers' exported state interfaces to resource manager. + /** + * Interface for transferring management of exported state interfaces to resource manager. + * When chaining controllers, state interfaces are used by the preceding + * controllers. + * Therefore, they should be managed in the same way as state interface of hardware. + * + * \param[in] controller_name name of the controller which state interfaces are imported. + * \param[in] interfaces list of controller's state interfaces as StateInterfaces. + */ + void import_controller_exported_state_interfaces( + const std::string & controller_name, std::vector & interfaces); + + /// Get list of exported tate interface of a controller. + /** + * Returns lists of stored exported state interfaces names for a controller. + * + * \param[in] controller_name for which list of state interface names is returned. + * \returns list of reference interface names. + */ + std::vector get_controller_exported_state_interface_names( + const std::string & controller_name); + + /// Add controller's exported state interfaces to available list. + /** + * Adds state interfacess of a controller with given name to the available list. This method + * should be called when a controller gets activated with chained mode turned on. That means, the + * controller's exported state interfaces can be used by another controllers in chained + * architectures. + * + * \param[in] controller_name name of the controller which interfaces should become available. + */ + void make_controller_exported_state_interfaces_available(const std::string & controller_name); + + /// Remove controller's exported state interface to available list. + /** + * Removes interfaces of a controller with given name from the available list. This method should + * be called when a controller gets deactivated and its reference interfaces cannot be used by + * another controller anymore. + * + * \param[in] controller_name name of the controller which interfaces should become unavailable. + */ + void make_controller_exported_state_interfaces_unavailable(const std::string & controller_name); + + /// Remove controllers exported state interfaces from resource manager. + /** + * Remove exported state interfaces from resource manager, i.e., resource storage. + * The interfaces will be deleted from all internal maps and lists. + * + * \param[in] controller_name list of interface names that will be deleted from resource manager. + */ + void remove_controller_exported_state_interfaces(const std::string & controller_name); + /// Add controllers' reference interfaces to resource manager. /** * Interface for transferring management of reference interfaces to resource manager. @@ -353,7 +403,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 +426,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. @@ -407,23 +463,36 @@ class HARDWARE_INTERFACE_PUBLIC ResourceManager */ bool state_interface_exists(const std::string & key) const; -private: - void validate_storage(const std::vector & hardware_info) const; +protected: + /// Gets the logger for the resource manager + /** + * \return logger of the resource manager + */ + rclcpp::Logger get_logger() const; - void release_command_interface(const std::string & key); + /// Gets the clock for the resource manager + /** + * \return clock of the resource manager + */ + rclcpp::Clock::SharedPtr get_clock() const; - std::unordered_map claimed_command_interface_map_; + bool components_are_loaded_and_initialized_ = false; mutable std::recursive_mutex resource_interfaces_lock_; mutable std::recursive_mutex claimed_command_interfaces_lock_; mutable std::recursive_mutex resources_lock_; +private: + bool validate_storage(const std::vector & hardware_info) const; + + void release_command_interface(const std::string & key); + + std::unordered_map claimed_command_interface_map_; + std::unique_ptr resource_storage_; // Structure to store read and write status so it is not initialized in the real-time loop HardwareReadWriteStatus read_write_status; - - bool is_urdf_loaded__ = false; }; } // namespace hardware_interface diff --git a/hardware_interface/include/hardware_interface/sensor.hpp b/hardware_interface/include/hardware_interface/sensor.hpp index 5d0677c587..d8e55aa4ad 100644 --- a/hardware_interface/include/hardware_interface/sensor.hpp +++ b/hardware_interface/include/hardware_interface/sensor.hpp @@ -25,6 +25,8 @@ #include "hardware_interface/types/hardware_interface_return_values.hpp" #include "hardware_interface/visibility_control.h" #include "rclcpp/duration.hpp" +#include "rclcpp/logging.hpp" +#include "rclcpp/node_interfaces/node_clock_interface.hpp" #include "rclcpp/time.hpp" #include "rclcpp_lifecycle/state.hpp" @@ -45,7 +47,9 @@ class Sensor final ~Sensor() = default; HARDWARE_INTERFACE_PUBLIC - const rclcpp_lifecycle::State & initialize(const HardwareInfo & sensor_info); + const rclcpp_lifecycle::State & initialize( + const HardwareInfo & sensor_info, rclcpp::Logger logger, + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface); HARDWARE_INTERFACE_PUBLIC const rclcpp_lifecycle::State & configure(); @@ -71,6 +75,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..79f689d0c6 100644 --- a/hardware_interface/include/hardware_interface/sensor_interface.hpp +++ b/hardware_interface/include/hardware_interface/sensor_interface.hpp @@ -25,6 +25,8 @@ #include "hardware_interface/types/lifecycle_state_names.hpp" #include "lifecycle_msgs/msg/state.hpp" #include "rclcpp/duration.hpp" +#include "rclcpp/logging.hpp" +#include "rclcpp/node_interfaces/node_clock_interface.hpp" #include "rclcpp/time.hpp" #include "rclcpp_lifecycle/node_interfaces/lifecycle_node_interface.hpp" #include "rclcpp_lifecycle/state.hpp" @@ -73,7 +75,8 @@ class SensorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI public: SensorInterface() : lifecycle_state_(rclcpp_lifecycle::State( - lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN, lifecycle_state_names::UNKNOWN)) + lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN, lifecycle_state_names::UNKNOWN)), + sensor_logger_(rclcpp::get_logger("sensor_interface")) { } @@ -88,15 +91,33 @@ class SensorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI virtual ~SensorInterface() = default; - /// Initialization of the hardware interface from data parsed from the robot's URDF. + /// Initialization of the hardware interface from data parsed from the robot's URDF and also the + /// clock and logger interfaces. /** * \param[in] hardware_info structure with data from URDF. + * \param[in] clock_interface pointer to the clock interface. + * \param[in] logger_interface pointer to the logger interface. * \returns CallbackReturn::SUCCESS if required data are provided and can be parsed. * \returns CallbackReturn::ERROR if any error happens or data are missing. */ - virtual CallbackReturn on_init(const HardwareInfo & hardware_info) + CallbackReturn init( + const HardwareInfo & hardware_info, rclcpp::Logger logger, + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface) { + clock_interface_ = clock_interface; + sensor_logger_ = logger.get_child("hardware_component.sensor." + hardware_info.name); info_ = hardware_info; + return on_init(hardware_info); + }; + + /// Initialization of the hardware interface from data parsed from the robot's URDF. + /** + * \param[in] hardware_info structure with data from URDF. + * \returns CallbackReturn::SUCCESS if required data are provided and can be parsed. + * \returns CallbackReturn::ERROR if any error happens or data are missing. + */ + virtual CallbackReturn on_init(const HardwareInfo & /*hardware_info*/) + { return CallbackReturn::SUCCESS; }; @@ -129,6 +150,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. @@ -142,8 +169,24 @@ class SensorInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI void set_state(const rclcpp_lifecycle::State & new_state) { lifecycle_state_ = new_state; } protected: + /// Get the logger of the SensorInterface. + /** + * \return logger of the SensorInterface. + */ + rclcpp::Logger get_logger() const { return sensor_logger_; } + + /// Get the clock of the SensorInterface. + /** + * \return clock of the SensorInterface. + */ + rclcpp::Clock::SharedPtr get_clock() const { return clock_interface_->get_clock(); } + HardwareInfo info_; rclcpp_lifecycle::State lifecycle_state_; + +private: + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface_; + rclcpp::Logger sensor_logger_; }; } // namespace hardware_interface diff --git a/hardware_interface/include/hardware_interface/system.hpp b/hardware_interface/include/hardware_interface/system.hpp index ece14f814d..1ca4260750 100644 --- a/hardware_interface/include/hardware_interface/system.hpp +++ b/hardware_interface/include/hardware_interface/system.hpp @@ -25,6 +25,8 @@ #include "hardware_interface/types/hardware_interface_return_values.hpp" #include "hardware_interface/visibility_control.h" #include "rclcpp/duration.hpp" +#include "rclcpp/logging.hpp" +#include "rclcpp/node_interfaces/node_clock_interface.hpp" #include "rclcpp/time.hpp" #include "rclcpp_lifecycle/state.hpp" @@ -45,7 +47,9 @@ class System final ~System() = default; HARDWARE_INTERFACE_PUBLIC - const rclcpp_lifecycle::State & initialize(const HardwareInfo & system_info); + const rclcpp_lifecycle::State & initialize( + const HardwareInfo & system_info, rclcpp::Logger logger, + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface); HARDWARE_INTERFACE_PUBLIC const rclcpp_lifecycle::State & configure(); @@ -84,6 +88,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..8e41438ba5 100644 --- a/hardware_interface/include/hardware_interface/system_interface.hpp +++ b/hardware_interface/include/hardware_interface/system_interface.hpp @@ -25,6 +25,8 @@ #include "hardware_interface/types/lifecycle_state_names.hpp" #include "lifecycle_msgs/msg/state.hpp" #include "rclcpp/duration.hpp" +#include "rclcpp/logging.hpp" +#include "rclcpp/node_interfaces/node_clock_interface.hpp" #include "rclcpp/time.hpp" #include "rclcpp_lifecycle/node_interfaces/lifecycle_node_interface.hpp" #include "rclcpp_lifecycle/state.hpp" @@ -74,7 +76,8 @@ class SystemInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI public: SystemInterface() : lifecycle_state_(rclcpp_lifecycle::State( - lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN, lifecycle_state_names::UNKNOWN)) + lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN, lifecycle_state_names::UNKNOWN)), + system_logger_(rclcpp::get_logger("system_interface")) { } @@ -89,15 +92,33 @@ class SystemInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI virtual ~SystemInterface() = default; - /// Initialization of the hardware interface from data parsed from the robot's URDF. + /// Initialization of the hardware interface from data parsed from the robot's URDF and also the + /// clock and logger interfaces. /** * \param[in] hardware_info structure with data from URDF. + * \param[in] clock_interface pointer to the clock interface. + * \param[in] logger_interface pointer to the logger interface. * \returns CallbackReturn::SUCCESS if required data are provided and can be parsed. * \returns CallbackReturn::ERROR if any error happens or data are missing. */ - virtual CallbackReturn on_init(const HardwareInfo & hardware_info) + CallbackReturn init( + const HardwareInfo & hardware_info, rclcpp::Logger logger, + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface) { + clock_interface_ = clock_interface; + system_logger_ = logger.get_child("hardware_component.system." + hardware_info.name); info_ = hardware_info; + return on_init(hardware_info); + }; + + /// Initialization of the hardware interface from data parsed from the robot's URDF. + /** + * \param[in] hardware_info structure with data from URDF. + * \returns CallbackReturn::SUCCESS if required data are provided and can be parsed. + * \returns CallbackReturn::ERROR if any error happens or data are missing. + */ + virtual CallbackReturn on_init(const HardwareInfo & /*hardware_info*/) + { return CallbackReturn::SUCCESS; }; @@ -132,7 +153,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 +172,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 +212,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. @@ -204,8 +231,24 @@ class SystemInterface : public rclcpp_lifecycle::node_interfaces::LifecycleNodeI void set_state(const rclcpp_lifecycle::State & new_state) { lifecycle_state_ = new_state; } protected: + /// Get the logger of the SystemInterface. + /** + * \return logger of the SystemInterface. + */ + rclcpp::Logger get_logger() const { return system_logger_; } + + /// Get the clock of the SystemInterface. + /** + * \return clock of the SystemInterface. + */ + rclcpp::Clock::SharedPtr get_clock() const { return clock_interface_->get_clock(); } + HardwareInfo info_; rclcpp_lifecycle::State lifecycle_state_; + +private: + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface_; + rclcpp::Logger system_logger_; }; } // namespace hardware_interface 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/include/mock_components/generic_system.hpp b/hardware_interface/include/mock_components/generic_system.hpp index e9b38de65d..fbb8547ab1 100644 --- a/hardware_interface/include/mock_components/generic_system.hpp +++ b/hardware_interface/include/mock_components/generic_system.hpp @@ -72,14 +72,6 @@ class HARDWARE_INTERFACE_PUBLIC GenericSystem : public hardware_interface::Syste hardware_interface::HW_IF_POSITION, hardware_interface::HW_IF_VELOCITY, hardware_interface::HW_IF_ACCELERATION, hardware_interface::HW_IF_EFFORT}; - struct MimicJoint - { - std::size_t joint_index; - std::size_t mimicked_joint_index; - double multiplier = 1.0; - }; - std::vector mimic_joints_; - /// The size of this vector is (standard_interfaces_.size() x nr_joints) std::vector> joint_commands_; std::vector> joint_states_; diff --git a/hardware_interface/package.xml b/hardware_interface/package.xml index e3a343c8ae..a3a1d6c77a 100644 --- a/hardware_interface/package.xml +++ b/hardware_interface/package.xml @@ -1,13 +1,14 @@ hardware_interface - 4.4.0 + 4.13.0 ros2_control hardware interface Bence Magyar Denis Štogl Apache License 2.0 ament_cmake + ament_cmake_gen_version_h control_msgs lifecycle_msgs @@ -15,6 +16,8 @@ rclcpp_lifecycle rcpputils tinyxml2_vendor + joint_limits + urdf rcutils rcutils diff --git a/hardware_interface/src/actuator.cpp b/hardware_interface/src/actuator.cpp index 6b58e365dc..7e50c07eb0 100644 --- a/hardware_interface/src/actuator.cpp +++ b/hardware_interface/src/actuator.cpp @@ -36,11 +36,13 @@ using CallbackReturn = rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface Actuator::Actuator(std::unique_ptr impl) : impl_(std::move(impl)) {} -const rclcpp_lifecycle::State & Actuator::initialize(const HardwareInfo & actuator_info) +const rclcpp_lifecycle::State & Actuator::initialize( + const HardwareInfo & actuator_info, rclcpp::Logger logger, + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface) { if (impl_->get_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN) { - switch (impl_->on_init(actuator_info)) + switch (impl_->init(actuator_info, logger, clock_interface)) { case CallbackReturn::SUCCESS: impl_->set_state(rclcpp_lifecycle::State( @@ -214,6 +216,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 4f67d3e8b6..938deeb0fb 100644 --- a/hardware_interface/src/component_parser.cpp +++ b/hardware_interface/src/component_parser.cpp @@ -21,9 +21,13 @@ #include #include +#include "urdf/model.h" + #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 { @@ -32,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"; @@ -41,7 +46,10 @@ 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"; constexpr const auto kSizeAttribute = "size"; constexpr const auto kNameAttribute = "name"; @@ -286,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()) @@ -315,11 +332,36 @@ ComponentInfo parse_component_from_xml(const tinyxml2::XMLElement * component_it component.type = component_it->Name(); component.name = get_attribute_value(component_it, kNameAttribute, component.type); + if (std::string(kJointTag) == component.type) + { + try + { + component.is_mimic = parse_bool(get_attribute_value(component_it, kMimicAttribute, kJointTag)) + ? MimicAttribute::TRUE + : MimicAttribute::FALSE; + } + catch (const std::runtime_error & e) + { + // mimic attribute not set + component.is_mimic = MimicAttribute::NOT_SET; + } + } + + // 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); } @@ -327,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); } @@ -347,7 +391,7 @@ ComponentInfo parse_component_from_xml(const tinyxml2::XMLElement * component_it * and the interface may be an array of a fixed size of the data type. * * \param[in] component_it pointer to the iterator where component - * info should befound + * info should be found * \throws std::runtime_error if a required component attribute or tag is not found. */ ComponentInfo parse_complex_component_from_xml(const tinyxml2::XMLElement * component_it) @@ -530,30 +574,35 @@ HardwareInfo parse_resource_from_xml( const auto * ros2_control_child_it = ros2_control_it->FirstChildElement(); while (ros2_control_child_it) { - if (!std::string(kHardwareTag).compare(ros2_control_child_it->Name())) + if (std::string(kHardwareTag) == ros2_control_child_it->Name()) { 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) { hardware.hardware_parameters = parse_parameters_from_xml(params_it); } } - else if (!std::string(kJointTag).compare(ros2_control_child_it->Name())) + else if (std::string(kJointTag) == ros2_control_child_it->Name()) { hardware.joints.push_back(parse_component_from_xml(ros2_control_child_it)); } - else if (!std::string(kSensorTag).compare(ros2_control_child_it->Name())) + else if (std::string(kSensorTag) == ros2_control_child_it->Name()) { hardware.sensors.push_back(parse_component_from_xml(ros2_control_child_it)); } - else if (!std::string(kGPIOTag).compare(ros2_control_child_it->Name())) + else if (std::string(kGPIOTag) == ros2_control_child_it->Name()) { hardware.gpios.push_back(parse_complex_component_from_xml(ros2_control_child_it)); } - else if (!std::string(kTransmissionTag).compare(ros2_control_child_it->Name())) + else if (std::string(kTransmissionTag) == ros2_control_child_it->Name()) { hardware.transmissions.push_back(parse_transmission_from_xml(ros2_control_child_it)); } @@ -571,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) @@ -583,17 +797,19 @@ std::vector parse_control_resources_from_urdf(const std::string & tinyxml2::XMLDocument doc; if (!doc.Parse(urdf.c_str()) && doc.Error()) { - throw std::runtime_error("invalid URDF passed in to robot parser"); + throw std::runtime_error( + "invalid URDF passed in to robot parser: " + std::string(doc.ErrorStr())); } if (doc.Error()) { - throw std::runtime_error("invalid URDF passed in to robot parser"); + throw std::runtime_error( + "invalid URDF passed in to robot parser: " + std::string(doc.ErrorStr())); } // Find robot tag const tinyxml2::XMLElement * robot_it = doc.RootElement(); - if (std::string(kRobotTag).compare(robot_it->Name())) + if (std::string(kRobotTag) != robot_it->Name()) { throw std::runtime_error("the robot tag is not root element in URDF"); } @@ -611,6 +827,82 @@ std::vector parse_control_resources_from_urdf(const std::string & ros2_control_it = ros2_control_it->NextSiblingElement(kROS2ControlTag); } + // parse full URDF for mimic options + urdf::Model model; + if (!model.initString(urdf)) + { + throw std::runtime_error("Failed to parse URDF file"); + } + for (auto & hw_info : hardware_info) + { + for (auto i = 0u; i < hw_info.joints.size(); ++i) + { + const auto & joint = hw_info.joints.at(i); + auto urdf_joint = model.getJoint(joint.name); + if (!urdf_joint) + { + throw std::runtime_error("Joint '" + joint.name + "' not found in URDF"); + } + if (!urdf_joint->mimic && joint.is_mimic == MimicAttribute::TRUE) + { + throw std::runtime_error( + "Joint '" + joint.name + "' has no mimic information in the URDF."); + } + if (urdf_joint->mimic && joint.is_mimic != MimicAttribute::FALSE) + { + if (joint.command_interfaces.size() > 0) + { + throw std::runtime_error( + "Joint '" + joint.name + + "' has mimic attribute not set to false: Activated mimic joints cannot have command " + "interfaces."); + } + auto find_joint = [&hw_info](const std::string & name) + { + auto it = std::find_if( + hw_info.joints.begin(), hw_info.joints.end(), + [&name](const auto & j) { return j.name == name; }); + if (it == hw_info.joints.end()) + { + throw std::runtime_error("Mimic joint '" + name + "' not found in tag"); + } + return std::distance(hw_info.joints.begin(), it); + }; + + MimicJoint mimic_joint; + mimic_joint.joint_index = i; + mimic_joint.mimicked_joint_index = find_joint(urdf_joint->mimic->joint_name); + mimic_joint.multiplier = urdf_joint->mimic->multiplier; + mimic_joint.offset = urdf_joint->mimic->offset; + hw_info.mimic_joints.push_back(mimic_joint); + } + + 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; + } + } + } + return hardware_info; } diff --git a/hardware_interface/src/mock_components/generic_system.cpp b/hardware_interface/src/mock_components/generic_system.cpp index 22d8aa573c..67f9ed56da 100644 --- a/hardware_interface/src/mock_components/generic_system.cpp +++ b/hardware_interface/src/mock_components/generic_system.cpp @@ -17,7 +17,6 @@ #include "mock_components/generic_system.hpp" #include -#include #include #include #include @@ -137,34 +136,6 @@ CallbackReturn GenericSystem::on_init(const hardware_interface::HardwareInfo & i } } - // Search for mimic joints - for (auto i = 0u; i < info_.joints.size(); ++i) - { - const auto & joint = info_.joints.at(i); - if (joint.parameters.find("mimic") != joint.parameters.cend()) - { - const auto mimicked_joint_it = std::find_if( - info_.joints.begin(), info_.joints.end(), - [&mimicked_joint = - joint.parameters.at("mimic")](const hardware_interface::ComponentInfo & joint_info) - { return joint_info.name == mimicked_joint; }); - if (mimicked_joint_it == info_.joints.cend()) - { - throw std::runtime_error( - std::string("Mimicked joint '") + joint.parameters.at("mimic") + "' not found"); - } - MimicJoint mimic_joint; - mimic_joint.joint_index = i; - mimic_joint.mimicked_joint_index = std::distance(info_.joints.begin(), mimicked_joint_it); - auto param_it = joint.parameters.find("multiplier"); - if (param_it != joint.parameters.end()) - { - mimic_joint.multiplier = hardware_interface::stod(joint.parameters.at("multiplier")); - } - mimic_joints_.push_back(mimic_joint); - } - } - // search for non-standard joint interfaces for (const auto & joint : info_.joints) { @@ -187,15 +158,15 @@ CallbackReturn GenericSystem::on_init(const hardware_interface::HardwareInfo & i { index_custom_interface_with_following_offset_ = std::distance(other_interfaces_.begin(), if_it); - RCUTILS_LOG_INFO_NAMED( - "mock_generic_system", "Custom interface with following offset '%s' found at index: %zu.", + RCLCPP_INFO( + get_logger(), "Custom interface with following offset '%s' found at index: %zu.", custom_interface_with_following_offset_.c_str(), index_custom_interface_with_following_offset_); } else { - RCUTILS_LOG_WARN_NAMED( - "mock_generic_system", + RCLCPP_WARN( + get_logger(), "Custom interface with following offset '%s' does not exist. Offset will not be applied", custom_interface_with_following_offset_.c_str()); } @@ -385,8 +356,8 @@ return_type GenericSystem::prepare_command_mode_switch( { if (!calculate_dynamics_) { - RCUTILS_LOG_WARN_NAMED( - "mock_generic_system", + RCLCPP_WARN( + get_logger(), "Requested velocity mode for joint '%s' without dynamics calculation enabled - this " "might lead to wrong feedback and unexpected behavior.", info_.joints[joint_index].name.c_str()); @@ -397,8 +368,8 @@ return_type GenericSystem::prepare_command_mode_switch( { if (!calculate_dynamics_) { - RCUTILS_LOG_WARN_NAMED( - "mock_generic_system", + RCLCPP_WARN( + get_logger(), "Requested acceleration mode for joint '%s' without dynamics calculation enabled - " "this might lead to wrong feedback and unexpected behavior.", info_.joints[joint_index].name.c_str()); @@ -408,9 +379,8 @@ return_type GenericSystem::prepare_command_mode_switch( } else { - RCUTILS_LOG_DEBUG_NAMED( - "mock_generic_system", "Got interface '%s' that is not joint - nothing to do!", - key.c_str()); + RCLCPP_DEBUG( + get_logger(), "Got interface '%s' that is not joint - nothing to do!", key.c_str()); } } @@ -419,8 +389,8 @@ return_type GenericSystem::prepare_command_mode_switch( // There has to always be at least one control mode from the above three set if (joint_found_in_x_requests_[i] == FOUND_ONCE_FLAG) { - RCUTILS_LOG_ERROR_NAMED( - "mock_generic_system", "Joint '%s' has to have '%s', '%s', or '%s' interface!", + RCLCPP_ERROR( + get_logger(), "Joint '%s' has to have '%s', '%s', or '%s' interface!", info_.joints[i].name.c_str(), hardware_interface::HW_IF_POSITION, hardware_interface::HW_IF_VELOCITY, hardware_interface::HW_IF_ACCELERATION); ret_val = hardware_interface::return_type::ERROR; @@ -429,8 +399,8 @@ return_type GenericSystem::prepare_command_mode_switch( // Currently we don't support multiple interface request if (joint_found_in_x_requests_[i] > (FOUND_ONCE_FLAG + 1)) { - RCUTILS_LOG_ERROR_NAMED( - "mock_generic_system", + RCLCPP_ERROR( + get_logger(), "Got multiple (%zu) starting interfaces for joint '%s' - this is not " "supported!", joint_found_in_x_requests_[i] - FOUND_ONCE_FLAG, info_.joints[i].name.c_str()); @@ -483,12 +453,12 @@ return_type GenericSystem::read(const rclcpp::Time & /*time*/, const rclcpp::Dur { if (command_propagation_disabled_) { - RCUTILS_LOG_WARN_NAMED( - "mock_generic_system", "Command propagation is disabled - no values will be returned!"); + RCLCPP_WARN(get_logger(), "Command propagation is disabled - no values will be returned!"); 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) { @@ -498,8 +468,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) @@ -585,20 +560,19 @@ 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 : mimic_joints_) + for (const auto & mimic_joint : info_.mimic_joints) { for (auto i = 0u; i < joint_states_.size(); ++i) { joint_states_[i][mimic_joint.joint_index] = + mimic_joint.offset + mimic_joint.multiplier * joint_states_[i][mimic_joint.mimicked_joint_index]; } } @@ -626,13 +600,13 @@ return_type GenericSystem::read(const rclcpp::Time & /*time*/, const rclcpp::Dur mirror_command_to_state(sensor_states_, sensor_mock_commands_); } - // do loopback on all gpio interfaces if (use_mock_gpio_command_interfaces_) { mirror_command_to_state(gpio_states_, gpio_mock_commands_); } else { + // do loopback on all gpio interfaces mirror_command_to_state(gpio_states_, gpio_commands_); } diff --git a/hardware_interface/src/resource_manager.cpp b/hardware_interface/src/resource_manager.cpp index 2e8ccc7b1f..a6eadd49fe 100644 --- a/hardware_interface/src/resource_manager.cpp +++ b/hardware_interface/src/resource_manager.cpp @@ -60,13 +60,33 @@ auto trigger_and_print_hardware_state_transition = } else { - RCUTILS_LOG_INFO_NAMED( + RCUTILS_LOG_ERROR_NAMED( "resource_manager", "Failed to '%s' hardware '%s'", transition_name.c_str(), hardware_name.c_str()); } 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"; @@ -78,71 +98,146 @@ class ResourceStorage public: // TODO(VX792): Change this when HW ifs get their own update rate, // because the ResourceStorage really shouldn't know about the cm's parameters - ResourceStorage( - unsigned int update_rate = 100, - rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface = nullptr) + explicit ResourceStorage( + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface, + rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr logger_interface) : actuator_loader_(pkg_name, actuator_interface_name), sensor_loader_(pkg_name, sensor_interface_name), system_loader_(pkg_name, system_interface_name), - cm_update_rate_(update_rate), - clock_interface_(clock_interface) + clock_interface_(clock_interface), + rm_logger_(rclcpp::get_logger("resource_manager")) { + if (!clock_interface_) + { + throw std::invalid_argument( + "Clock interface is nullptr. ResourceManager needs a valid clock interface."); + } + if (logger_interface) + { + rm_logger_ = logger_interface->get_logger().get_child("resource_manager"); + } } 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 + { + RCLCPP_INFO(get_logger(), "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) + { + RCLCPP_INFO( + get_logger(), "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 + { + RCLCPP_ERROR( + get_logger(), "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) + { + RCLCPP_ERROR(get_logger(), "Exception while loading hardware: %s", ex.what()); + } + catch (const std::exception & ex) + { + RCLCPP_ERROR( + get_logger(), "Exception occurred while loading hardware '%s': %s", + hardware_info.name.c_str(), ex.what()); + } + catch (...) + { + RCLCPP_ERROR( + get_logger(), "Unknown exception occurred while loading hardware '%s'", + hardware_info.name.c_str()); + } + return is_loaded; } template bool initialize_hardware(const HardwareInfo & hardware_info, HardwareT & hardware) { - RCUTILS_LOG_INFO_NAMED( - "resource_manager", "Initialize hardware '%s' ", hardware_info.name.c_str()); + RCLCPP_INFO(get_logger(), "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, rm_logger_, clock_interface_); + result = new_state.id() == lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED; - if (result) + if (result) + { + RCLCPP_INFO( + get_logger(), "Successful initialization of hardware '%s'", hardware_info.name.c_str()); + } + else + { + RCLCPP_ERROR( + get_logger(), "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()); + RCLCPP_ERROR( + get_logger(), "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()); + RCLCPP_ERROR( + get_logger(), "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) + { + RCLCPP_ERROR( + get_logger(), "Exception occurred while configuring hardware '%s': %s", + hardware.get_name().c_str(), ex.what()); + } + catch (...) + { + RCLCPP_ERROR( + get_logger(), "Unknown exception occurred while configuring hardware '%s'", + hardware.get_name().c_str()); + } if (result) { @@ -160,16 +255,16 @@ class ResourceStorage if (found_it == available_state_interfaces_.end()) { available_state_interfaces_.emplace_back(interface); - RCUTILS_LOG_DEBUG_NAMED( - "resource_manager", "(hardware '%s'): '%s' state interface added into available list", + RCLCPP_DEBUG( + get_logger(), "(hardware '%s'): '%s' state interface added into available list", hardware.get_name().c_str(), interface.c_str()); } else { // TODO(destogl): do here error management if interfaces are only partially added into // "available" list - this should never be the case! - RCUTILS_LOG_WARN_NAMED( - "resource_manager", + RCLCPP_WARN( + get_logger(), "(hardware '%s'): '%s' state interface already in available list." " This can happen due to multiple calls to 'configure'", hardware.get_name().c_str(), interface.c_str()); @@ -186,16 +281,16 @@ class ResourceStorage if (found_it == available_command_interfaces_.end()) { available_command_interfaces_.emplace_back(interface); - RCUTILS_LOG_DEBUG_NAMED( - "resource_manager", "(hardware '%s'): '%s' command interface added into available list", + RCLCPP_DEBUG( + get_logger(), "(hardware '%s'): '%s' command interface added into available list", hardware.get_name().c_str(), interface.c_str()); } else { // TODO(destogl): do here error management if interfaces are only partially added into // "available" list - this should never be the case! - RCUTILS_LOG_WARN_NAMED( - "resource_manager", + RCLCPP_WARN( + get_logger(), "(hardware '%s'): '%s' command interface already in available list." " This can happen due to multiple calls to 'configure'", hardware.get_name().c_str(), interface.c_str()); @@ -206,9 +301,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; } @@ -223,16 +324,16 @@ class ResourceStorage if (found_it != available_command_interfaces_.end()) { available_command_interfaces_.erase(found_it); - RCUTILS_LOG_DEBUG_NAMED( - "resource_manager", "(hardware '%s'): '%s' command interface removed from available list", + RCLCPP_DEBUG( + get_logger(), "(hardware '%s'): '%s' command interface removed from available list", hardware_name.c_str(), interface.c_str()); } else { // TODO(destogl): do here error management if interfaces are only partially added into // "available" list - this should never be the case! - RCUTILS_LOG_WARN_NAMED( - "resource_manager", + RCLCPP_WARN( + get_logger(), "(hardware '%s'): '%s' command interface not in available list. " "This should not happen (hint: multiple cleanup calls).", hardware_name.c_str(), interface.c_str()); @@ -247,16 +348,16 @@ class ResourceStorage if (found_it != available_state_interfaces_.end()) { available_state_interfaces_.erase(found_it); - RCUTILS_LOG_DEBUG_NAMED( - "resource_manager", "(hardware '%s'): '%s' state interface removed from available list", + RCLCPP_DEBUG( + get_logger(), "(hardware '%s'): '%s' state interface removed from available list", hardware_name.c_str(), interface.c_str()); } else { // TODO(destogl): do here error management if interfaces are only partially added into // "available" list - this should never be the case! - RCUTILS_LOG_WARN_NAMED( - "resource_manager", + RCLCPP_WARN( + get_logger(), "(hardware '%s'): '%s' state interface not in available list. " "This should not happen (hint: multiple cleanup calls).", hardware_name.c_str(), interface.c_str()); @@ -267,23 +368,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) + { + RCLCPP_ERROR( + get_logger(), "Exception occurred while cleaning up hardware '%s': %s", + hardware.get_name().c_str(), ex.what()); + } + catch (...) + { + RCLCPP_ERROR( + get_logger(), "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) + { + RCLCPP_ERROR( + get_logger(), "Exception occurred while shutting down hardware '%s': %s", + hardware.get_name().c_str(), ex.what()); + } + catch (...) + { + RCLCPP_ERROR( + get_logger(), "Unknown exception occurred while shutting down hardware '%s'", + hardware.get_name().c_str()); + } if (result) { @@ -293,6 +430,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 +441,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) + { + RCLCPP_ERROR( + get_logger(), "Exception occurred while activating hardware '%s': %s", + hardware.get_name().c_str(), ex.what()); + } + catch (...) + { + RCLCPP_ERROR( + get_logger(), "Unknown exception occurred while activating hardware '%s'", + hardware.get_name().c_str()); + } if (result) { @@ -319,9 +476,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) + { + RCLCPP_ERROR( + get_logger(), "Exception occurred while deactivating hardware '%s': %s", + hardware.get_name().c_str(), ex.what()); + } + catch (...) + { + RCLCPP_ERROR( + get_logger(), "Unknown exception occurred while deactivating hardware '%s'", + hardware.get_name().c_str()); + } if (result) { @@ -358,8 +531,8 @@ class ResourceStorage break; case State::PRIMARY_STATE_FINALIZED: result = false; - RCUTILS_LOG_WARN_NAMED( - "resource_manager", "hardware '%s' is in finalized state and can be only destroyed.", + RCLCPP_WARN( + get_logger(), "hardware '%s' is in finalized state and can be only destroyed.", hardware.get_name().c_str()); break; } @@ -378,8 +551,8 @@ class ResourceStorage break; case State::PRIMARY_STATE_FINALIZED: result = false; - RCUTILS_LOG_WARN_NAMED( - "resource_manager", "hardware '%s' is in finalized state and can be only destroyed.", + RCLCPP_WARN( + get_logger(), "hardware '%s' is in finalized state and can be only destroyed.", hardware.get_name().c_str()); break; } @@ -402,8 +575,8 @@ class ResourceStorage break; case State::PRIMARY_STATE_FINALIZED: result = false; - RCUTILS_LOG_WARN_NAMED( - "resource_manager", "hardware '%s' is in finalized state and can be only destroyed.", + RCLCPP_WARN( + get_logger(), "hardware '%s' is in finalized state and can be only destroyed.", hardware.get_name().c_str()); break; } @@ -433,7 +606,75 @@ class ResourceStorage template void import_state_interfaces(HardwareT & hardware) { - auto interfaces = hardware.export_state_interfaces(); + try + { + 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) + { + RCLCPP_ERROR( + get_logger(), + "Exception occurred while importing state interfaces for the hardware '%s' : %s", + hardware.get_name().c_str(), e.what()); + } + catch (...) + { + RCLCPP_ERROR( + get_logger(), + "Unknown exception occurred while importing state interfaces for the hardware '%s'", + hardware.get_name().c_str()); + } + } + + template + void import_command_interfaces(HardwareT & hardware) + { + try + { + auto interfaces = hardware.export_command_interfaces(); + hardware_info_map_[hardware.get_name()].command_interfaces = + add_command_interfaces(interfaces); + } + catch (const std::exception & ex) + { + RCLCPP_ERROR( + get_logger(), + "Exception occurred while importing command interfaces for the hardware '%s' : %s", + hardware.get_name().c_str(), ex.what()); + } + catch (...) + { + RCLCPP_ERROR( + get_logger(), + "Unknown exception occurred while importing command interfaces for the hardware '%s'", + hardware.get_name().c_str()); + } + } + + /// Adds exported state interfaces into internal storage. + /** + * Adds state interfaces to the internal storage. State interfaces exported from hardware or + * chainable controllers are moved to the map with name-interface pairs and available list's + * size is increased to reserve storage when interface change theirs status in real-time + * control loop. + * + * \param[interfaces] list of state interface to add into storage. + * \returns list of interface names that are added into internal storage. The output is used to + * avoid additional iterations to cache interface names, e.g., for initializing info structures. + */ + std::vector add_state_interfaces(std::vector & interfaces) + { std::vector interface_names; interface_names.reserve(interfaces.size()); for (auto & interface : interfaces) @@ -442,16 +683,24 @@ class ResourceStorage 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()); + + return interface_names; } - template - void import_command_interfaces(HardwareT & hardware) + /// Removes state interfaces from internal storage. + /** + * State interface are removed from the maps with theirs storage and their claimed status. + * + * \param[interface_names] list of state interface names to remove from storage. + */ + void remove_state_interfaces(const std::vector & interface_names) { - auto interfaces = hardware.export_command_interfaces(); - hardware_info_map_[hardware.get_name()].command_interfaces = add_command_interfaces(interfaces); + for (const auto & interface : interface_names) + { + state_interface_map_.erase(interface); + } } /// Adds exported command interfaces into internal storage. @@ -497,24 +746,15 @@ class ResourceStorage } } - void check_for_duplicates(const HardwareInfo & hardware_info) - { - // Check for identical names - if (hardware_info_map_.find(hardware_info.name) != hardware_info_map_.end()) - { - throw std::runtime_error( - "Hardware name " + hardware_info.name + - " is duplicated. Please provide a unique 'name' in the URDF."); - } - } - // TODO(destogl): Propagate "false" up, if happens in initialize_hardware - void load_and_initialize_actuator(const HardwareInfo & hardware_info) + bool load_and_initialize_actuator(const HardwareInfo & hardware_info) { 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 false; + } if (initialize_hardware(hardware_info, container.back())) { import_state_interfaces(container.back()); @@ -522,58 +762,64 @@ class ResourceStorage } else { - RCUTILS_LOG_WARN_NAMED( - "resource_manager", - "Actuator hardware component '%s' from plugin '%s' failed to initialize.", + RCLCPP_WARN( + get_logger(), "Actuator hardware component '%s' from plugin '%s' failed to initialize.", hardware_info.name.c_str(), hardware_info.hardware_plugin_name.c_str()); + return false; } + return true; }; if (hardware_info.is_async) { - load_and_init_actuators(async_actuators_); + return load_and_init_actuators(async_actuators_); } else { - load_and_init_actuators(actuators_); + return load_and_init_actuators(actuators_); } } - void load_and_initialize_sensor(const HardwareInfo & hardware_info) + bool load_and_initialize_sensor(const HardwareInfo & hardware_info) { 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 false; + } if (initialize_hardware(hardware_info, container.back())) { import_state_interfaces(container.back()); } else { - RCUTILS_LOG_WARN_NAMED( - "resource_manager", - "Sensor hardware component '%s' from plugin '%s' failed to initialize.", + RCLCPP_WARN( + get_logger(), "Sensor hardware component '%s' from plugin '%s' failed to initialize.", hardware_info.name.c_str(), hardware_info.hardware_plugin_name.c_str()); + return false; } + return true; }; if (hardware_info.is_async) { - load_and_init_sensors(async_sensors_); + return load_and_init_sensors(async_sensors_); } else { - load_and_init_sensors(sensors_); + return load_and_init_sensors(sensors_); } } - void load_and_initialize_system(const HardwareInfo & hardware_info) + bool load_and_initialize_system(const HardwareInfo & hardware_info) { 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 false; + } if (initialize_hardware(hardware_info, container.back())) { import_state_interfaces(container.back()); @@ -581,20 +827,21 @@ class ResourceStorage } else { - RCUTILS_LOG_WARN_NAMED( - "resource_manager", - "System hardware component '%s' from plugin '%s' failed to initialize.", + RCLCPP_WARN( + get_logger(), "System hardware component '%s' from plugin '%s' failed to initialize.", hardware_info.name.c_str(), hardware_info.hardware_plugin_name.c_str()); + return false; } + return true; }; if (hardware_info.is_async) { - load_and_init_systems(async_systems_); + return load_and_init_systems(async_systems_); } else { - load_and_init_systems(systems_); + return load_and_init_systems(systems_); } } @@ -611,9 +858,8 @@ class ResourceStorage } else { - RCUTILS_LOG_WARN_NAMED( - "resource_manager", - "Actuator hardware component '%s' from plugin '%s' failed to initialize.", + RCLCPP_WARN( + get_logger(), "Actuator hardware component '%s' from plugin '%s' failed to initialize.", hardware_info.name.c_str(), hardware_info.hardware_plugin_name.c_str()); } }; @@ -640,9 +886,8 @@ class ResourceStorage } else { - RCUTILS_LOG_WARN_NAMED( - "resource_manager", - "Sensor hardware component '%s' from plugin '%s' failed to initialize.", + RCLCPP_WARN( + get_logger(), "Sensor hardware component '%s' from plugin '%s' failed to initialize.", hardware_info.name.c_str(), hardware_info.hardware_plugin_name.c_str()); } }; @@ -670,9 +915,8 @@ class ResourceStorage } else { - RCUTILS_LOG_WARN_NAMED( - "resource_manager", - "System hardware component '%s' from plugin '%s' failed to initialize.", + RCLCPP_WARN( + get_logger(), "System hardware component '%s' from plugin '%s' failed to initialize.", hardware_info.name.c_str(), hardware_info.hardware_plugin_name.c_str()); } }; @@ -687,11 +931,69 @@ class ResourceStorage } } + void clear() + { + actuators_.clear(); + sensors_.clear(); + systems_.clear(); + + async_actuators_.clear(); + async_sensors_.clear(); + async_systems_.clear(); + + hardware_info_map_.clear(); + state_interface_map_.clear(); + command_interface_map_.clear(); + + available_state_interfaces_.clear(); + available_command_interfaces_.clear(); + + claimed_command_interface_map_.clear(); + } + + /** + * 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); + } + + /// Gets the logger for the resource storage + /** + * \return logger of the resource storage + */ + const rclcpp::Logger & get_logger() const { return rm_logger_; } + + /// Gets the clock for the resource storage + /** + * \return clock of the resource storage + */ + rclcpp::Clock::SharedPtr get_clock() const { return clock_interface_->get_clock(); } + // hardware plugins pluginlib::ClassLoader actuator_loader_; pluginlib::ClassLoader sensor_loader_; pluginlib::ClassLoader system_loader_; + // Logger and Clock interfaces + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface_; + rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr logger_interface_; + rclcpp::Logger rm_logger_; + std::vector actuators_; std::vector sensors_; std::vector systems_; @@ -701,11 +1003,14 @@ 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_; - /// Mapping between controllers and list of reference interfaces they are using + /// Mapping between controllers and list of interfaces they are using + std::unordered_map> + controllers_exported_state_interfaces_map_; std::unordered_map> controllers_reference_interfaces_map_; /// Storage of all available state interfaces @@ -725,24 +1030,25 @@ class ResourceStorage // Update rate of the controller manager, and the clock interface of its node // Used by async components. - unsigned int cm_update_rate_; - rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface_; + unsigned int cm_update_rate_ = 100; }; ResourceManager::ResourceManager( - unsigned int update_rate, rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface) -: resource_storage_(std::make_unique(update_rate, clock_interface)) + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface, + rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr logger_interface) +: resource_storage_(std::make_unique(clock_interface, logger_interface)) { } ResourceManager::~ResourceManager() = default; ResourceManager::ResourceManager( - const std::string & urdf, bool validate_interfaces, bool activate_all, unsigned int update_rate, - rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface) -: resource_storage_(std::make_unique(update_rate, clock_interface)) + const std::string & urdf, rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface, + rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr logger_interface, bool activate_all, + const unsigned int update_rate) +: resource_storage_(std::make_unique(clock_interface, logger_interface)) { - load_urdf(urdf, validate_interfaces); + load_and_initialize_components(urdf, update_rate); if (activate_all) { @@ -756,50 +1062,87 @@ ResourceManager::ResourceManager( } // CM API: Called in "callback/slow"-thread -void ResourceManager::load_urdf( - const std::string & urdf, bool validate_interfaces, bool load_and_initialize_components) +bool ResourceManager::load_and_initialize_components( + const std::string & urdf, const unsigned int update_rate) { - is_urdf_loaded__ = true; + components_are_loaded_and_initialized_ = true; + + resource_storage_->cm_update_rate_ = update_rate; + + const auto hardware_info = hardware_interface::parse_control_resources_from_urdf(urdf); + const std::string system_type = "system"; const std::string sensor_type = "sensor"; const std::string actuator_type = "actuator"; - const auto hardware_info = hardware_interface::parse_control_resources_from_urdf(urdf); - if (load_and_initialize_components) + std::lock_guard resource_guard(resources_lock_); + for (const auto & individual_hardware_info : hardware_info) { - for (const auto & individual_hardware_info : hardware_info) + // Check for identical names + if ( + resource_storage_->hardware_info_map_.find(individual_hardware_info.name) != + resource_storage_->hardware_info_map_.end()) + { + RCUTILS_LOG_ERROR_NAMED( + "resource_manager", + "Hardware name %s is duplicated. Please provide a unique 'name' " + "in the URDF.", + individual_hardware_info.name.c_str()); + components_are_loaded_and_initialized_ = false; + break; + } + + if (individual_hardware_info.type == actuator_type) { - if (individual_hardware_info.type == actuator_type) + std::scoped_lock guard(resource_interfaces_lock_, claimed_command_interfaces_lock_); + if (!resource_storage_->load_and_initialize_actuator(individual_hardware_info)) { - std::scoped_lock guard(resource_interfaces_lock_, claimed_command_interfaces_lock_); - resource_storage_->load_and_initialize_actuator(individual_hardware_info); + components_are_loaded_and_initialized_ = false; + break; } - if (individual_hardware_info.type == sensor_type) + } + if (individual_hardware_info.type == sensor_type) + { + std::lock_guard guard(resource_interfaces_lock_); + if (!resource_storage_->load_and_initialize_sensor(individual_hardware_info)) { - std::lock_guard guard(resource_interfaces_lock_); - resource_storage_->load_and_initialize_sensor(individual_hardware_info); + components_are_loaded_and_initialized_ = false; + break; } - if (individual_hardware_info.type == system_type) + } + if (individual_hardware_info.type == system_type) + { + std::scoped_lock guard(resource_interfaces_lock_, claimed_command_interfaces_lock_); + if (!resource_storage_->load_and_initialize_system(individual_hardware_info)) { - std::scoped_lock guard(resource_interfaces_lock_, claimed_command_interfaces_lock_); - resource_storage_->load_and_initialize_system(individual_hardware_info); + components_are_loaded_and_initialized_ = false; + break; } } } - // throw on missing state and command interfaces, not specified keys are being ignored - if (validate_interfaces) + if (components_are_loaded_and_initialized_ && validate_storage(hardware_info)) { - validate_storage(hardware_info); + std::lock_guard guard(resources_lock_); + read_write_status.failed_hardware_names.reserve( + resource_storage_->actuators_.size() + resource_storage_->sensors_.size() + + resource_storage_->systems_.size()); + } + else + { + std::scoped_lock guard(resource_interfaces_lock_, claimed_command_interfaces_lock_); + resource_storage_->clear(); + // cleanup everything + components_are_loaded_and_initialized_ = false; } - std::lock_guard guard(resources_lock_); - read_write_status.failed_hardware_names.reserve( - resource_storage_->actuators_.size() + resource_storage_->sensors_.size() + - resource_storage_->systems_.size()); + return components_are_loaded_and_initialized_; } -bool ResourceManager::is_urdf_already_loaded() const { return is_urdf_loaded__; } +bool ResourceManager::are_components_initialized() const +{ + return components_are_loaded_and_initialized_; +} // CM API: Called in "update"-thread LoanedStateInterface ResourceManager::claim_state_interface(const std::string & key) @@ -842,6 +1185,68 @@ bool ResourceManager::state_interface_is_available(const std::string & name) con name) != resource_storage_->available_state_interfaces_.end(); } +// CM API: Called in "callback/slow"-thread +void ResourceManager::import_controller_exported_state_interfaces( + const std::string & controller_name, std::vector & interfaces) +{ + std::lock_guard guard(resource_interfaces_lock_); + auto interface_names = resource_storage_->add_state_interfaces(interfaces); + resource_storage_->controllers_exported_state_interfaces_map_[controller_name] = interface_names; +} + +// CM API: Called in "callback/slow"-thread +std::vector ResourceManager::get_controller_exported_state_interface_names( + const std::string & controller_name) +{ + return resource_storage_->controllers_exported_state_interfaces_map_.at(controller_name); +} + +// CM API: Called in "update"-thread +void ResourceManager::make_controller_exported_state_interfaces_available( + const std::string & controller_name) +{ + auto interface_names = + resource_storage_->controllers_exported_state_interfaces_map_.at(controller_name); + std::lock_guard guard(resource_interfaces_lock_); + resource_storage_->available_state_interfaces_.insert( + resource_storage_->available_state_interfaces_.end(), interface_names.begin(), + interface_names.end()); +} + +// CM API: Called in "update"-thread +void ResourceManager::make_controller_exported_state_interfaces_unavailable( + const std::string & controller_name) +{ + auto interface_names = + resource_storage_->controllers_exported_state_interfaces_map_.at(controller_name); + + std::lock_guard guard(resource_interfaces_lock_); + for (const auto & interface : interface_names) + { + auto found_it = std::find( + resource_storage_->available_state_interfaces_.begin(), + resource_storage_->available_state_interfaces_.end(), interface); + if (found_it != resource_storage_->available_state_interfaces_.end()) + { + resource_storage_->available_state_interfaces_.erase(found_it); + RCUTILS_LOG_DEBUG_NAMED( + "resource_manager", "'%s' state interface removed from available list", interface.c_str()); + } + } +} + +// CM API: Called in "callback/slow"-thread +void ResourceManager::remove_controller_exported_state_interfaces( + const std::string & controller_name) +{ + auto interface_names = + resource_storage_->controllers_exported_state_interfaces_map_.at(controller_name); + resource_storage_->controllers_exported_state_interfaces_map_.erase(controller_name); + + std::lock_guard guard(resource_interfaces_lock_); + resource_storage_->remove_state_interfaces(interface_names); +} + // CM API: Called in "callback/slow"-thread void ResourceManager::import_controller_reference_interfaces( const std::string & controller_name, std::vector & interfaces) @@ -886,9 +1291,8 @@ void ResourceManager::make_controller_reference_interfaces_unavailable( if (found_it != resource_storage_->available_command_interfaces_.end()) { resource_storage_->available_command_interfaces_.erase(found_it); - RCUTILS_LOG_DEBUG_NAMED( - "resource_manager", "'%s' command interface removed from available list", - interface.c_str()); + RCLCPP_DEBUG( + get_logger(), "'%s' command interface removed from available list", interface.c_str()); } } } @@ -1083,24 +1487,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; @@ -1120,9 +1506,10 @@ bool ResourceManager::prepare_command_mode_switch( if (!(check_exist(start_interfaces) && check_exist(stop_interfaces))) { 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()); + RCLCPP_ERROR( + get_logger(), "Not acceptable command interfaces combination: \n%s%s", + interfaces_to_string(start_interfaces, stop_interfaces).c_str(), + ss_not_existing.str().c_str()); return false; } @@ -1145,14 +1532,15 @@ bool ResourceManager::prepare_command_mode_switch( if (!(check_available(start_interfaces) && check_available(stop_interfaces))) { 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()); + RCLCPP_ERROR( + get_logger(), "Not acceptable command interfaces combination: \n%s%s", + 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) + [&start_interfaces, &stop_interfaces, logger = get_logger()](auto & components) { bool ret = true; for (auto & component : components) @@ -1161,14 +1549,37 @@ 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)) + { + RCLCPP_ERROR( + logger, "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()); + RCLCPP_ERROR( + logger, + "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 (...) + { + RCLCPP_ERROR( + logger, + "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; } } @@ -1193,7 +1604,8 @@ bool ResourceManager::perform_command_mode_switch( return true; } - auto call_perform_mode_switch = [&start_interfaces, &stop_interfaces](auto & components) + auto call_perform_mode_switch = + [&start_interfaces, &stop_interfaces, logger = get_logger()](auto & components) { bool ret = true; for (auto & component : components) @@ -1202,13 +1614,36 @@ 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)) + { + RCLCPP_ERROR( + logger, "Component '%s' could not perform switch", component.get_name().c_str()); + ret = false; + } + } + catch (const std::exception & e) + { + RCLCPP_ERROR( + logger, + "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()); + RCLCPP_ERROR( + logger, + "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; } } @@ -1234,9 +1669,8 @@ return_type ResourceManager::set_component_state( if (found_it == resource_storage_->hardware_info_map_.end()) { - RCUTILS_LOG_INFO_NAMED( - "resource_manager", "Hardware Component with name '%s' does not exists", - component_name.c_str()); + RCLCPP_INFO( + get_logger(), "Hardware Component with name '%s' does not exists", component_name.c_str()); return return_type::ERROR; } @@ -1287,6 +1721,7 @@ return_type ResourceManager::set_component_state( return false; }; + std::lock_guard guard(resources_lock_); bool found = find_set_component_state( std::bind(&ResourceStorage::set_component_state, resource_storage_.get(), _1, _2), resource_storage_->actuators_); @@ -1324,6 +1759,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 +1778,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) + { + RCLCPP_ERROR( + get_logger(), "Exception thrown durind read of the component '%s': %s", + component.get_name().c_str(), e.what()); + ret_val = return_type::ERROR; + } + catch (...) + { + RCLCPP_ERROR( + get_logger(), "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 +1839,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) + { + RCLCPP_ERROR( + get_logger(), "Exception thrown during write of the component '%s': %s", + component.get_name().c_str(), e.what()); + ret_val = return_type::ERROR; + } + catch (...) + { + RCLCPP_ERROR( + get_logger(), "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; @@ -1428,9 +1912,16 @@ bool ResourceManager::state_interface_exists(const std::string & key) const // END: "used only in tests and locally" +rclcpp::Logger ResourceManager::get_logger() const { return resource_storage_->get_logger(); } + +rclcpp::Clock::SharedPtr ResourceManager::get_clock() const +{ + return resource_storage_->get_clock(); +} + // BEGIN: private methods -void ResourceManager::validate_storage( +bool ResourceManager::validate_storage( const std::vector & hardware_info) const { std::vector missing_state_keys = {}; @@ -1486,20 +1977,28 @@ void ResourceManager::validate_storage( if (!missing_state_keys.empty() || !missing_command_keys.empty()) { - std::string err_msg = "Wrong state or command interface configuration.\n"; - err_msg += "missing state interfaces:\n"; + std::string message = "Wrong state or command interface configuration.\n"; + message += "missing state interfaces:\n"; for (const auto & missing_key : missing_state_keys) { - err_msg += "' " + missing_key + " '" + "\t"; + message += " " + missing_key + "\t"; } - err_msg += "\nmissing command interfaces:\n"; + message += "\nmissing command interfaces:\n"; for (const auto & missing_key : missing_command_keys) { - err_msg += "' " + missing_key + " '" + "\t"; + message += " " + missing_key + "\t"; } - throw std::runtime_error(err_msg); + RCUTILS_LOG_ERROR_NAMED( + "resource_manager", + "Discrepancy between robot description file (urdf) and actually exported HW interfaces.\n" + "Details: %s", + message.c_str()); + + return false; } + + return true; } // END: private methods diff --git a/hardware_interface/src/sensor.cpp b/hardware_interface/src/sensor.cpp index 2e53e447b9..9b7f1f69d6 100644 --- a/hardware_interface/src/sensor.cpp +++ b/hardware_interface/src/sensor.cpp @@ -34,11 +34,13 @@ using CallbackReturn = rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface Sensor::Sensor(std::unique_ptr impl) : impl_(std::move(impl)) {} -const rclcpp_lifecycle::State & Sensor::initialize(const HardwareInfo & sensor_info) +const rclcpp_lifecycle::State & Sensor::initialize( + const HardwareInfo & sensor_info, rclcpp::Logger logger, + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface) { if (impl_->get_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN) { - switch (impl_->on_init(sensor_info)) + switch (impl_->init(sensor_info, logger, clock_interface)) { case CallbackReturn::SUCCESS: impl_->set_state(rclcpp_lifecycle::State( @@ -191,6 +193,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..ba327d8ab2 100644 --- a/hardware_interface/src/system.cpp +++ b/hardware_interface/src/system.cpp @@ -34,11 +34,13 @@ using CallbackReturn = rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface System::System(std::unique_ptr impl) : impl_(std::move(impl)) {} -const rclcpp_lifecycle::State & System::initialize(const HardwareInfo & system_info) +const rclcpp_lifecycle::State & System::initialize( + const HardwareInfo & system_info, rclcpp::Logger logger, + rclcpp::node_interfaces::NodeClockInterface::SharedPtr clock_interface) { if (impl_->get_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN) { - switch (impl_->on_init(system_info)) + switch (impl_->init(system_info, logger, clock_interface)) { case CallbackReturn::SUCCESS: impl_->set_state(rclcpp_lifecycle::State( @@ -210,6 +212,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 0641cfda57..c90bc85055 100644 --- a/hardware_interface/test/mock_components/test_generic_system.cpp +++ b/hardware_interface/test/mock_components/test_generic_system.cpp @@ -26,6 +26,7 @@ #include "hardware_interface/resource_manager.hpp" #include "hardware_interface/types/lifecycle_state_names.hpp" #include "lifecycle_msgs/msg/state.hpp" +#include "rclcpp/node.hpp" #include "rclcpp_lifecycle/state.hpp" #include "ros2_control_test_assets/components_urdfs.hpp" #include "ros2_control_test_assets/descriptions.hpp" @@ -96,6 +97,7 @@ class TestGenericSystem : public ::testing::Test mock_components/GenericSystem + Hardware Group @@ -121,6 +123,7 @@ class TestGenericSystem : public ::testing::Test mock_components/GenericSystem + Hardware Group @@ -142,12 +145,12 @@ class TestGenericSystem : public ::testing::Test 0.2 - + 0.5 - + )"; @@ -251,11 +254,7 @@ class TestGenericSystem : public ::testing::Test - - joint1 - -2 - - + @@ -293,6 +292,7 @@ class TestGenericSystem : public ::testing::Test mock_components/GenericSystem + Hardware Group -3 actual_position @@ -355,6 +355,7 @@ class TestGenericSystem : public ::testing::Test mock_components/GenericSystem + Hardware Group 2 2 @@ -583,6 +584,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_; @@ -604,6 +669,9 @@ 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_; + rclcpp::Node node_ = rclcpp::Node("TestGenericSystem"); }; // Forward declaration @@ -628,11 +696,16 @@ class TestableResourceManager : public hardware_interface::ResourceManager FRIEND_TEST(TestGenericSystem, valid_urdf_ros2_control_system_robot_with_gpio_mock_command); FRIEND_TEST(TestGenericSystem, valid_urdf_ros2_control_system_robot_with_gpio_mock_command_True); - TestableResourceManager() : hardware_interface::ResourceManager() {} + explicit TestableResourceManager(rclcpp::Node & node) + : hardware_interface::ResourceManager( + node.get_node_clock_interface(), node.get_node_logging_interface()) + { + } - TestableResourceManager( - const std::string & urdf, bool validate_interfaces = true, bool activate_all = false) - : hardware_interface::ResourceManager(urdf, validate_interfaces, activate_all) + explicit TestableResourceManager( + rclcpp::Node & node, const std::string & urdf, bool activate_all = false) + : hardware_interface::ResourceManager( + urdf, node.get_node_clock_interface(), node.get_node_logging_interface(), activate_all, 100) { } }; @@ -679,7 +752,7 @@ TEST_F(TestGenericSystem, load_generic_system_2dof) { auto urdf = ros2_control_test_assets::urdf_head + hardware_system_2dof_ + ros2_control_test_assets::urdf_tail; - ASSERT_NO_THROW(TestableResourceManager rm(urdf)); + ASSERT_NO_THROW(TestableResourceManager rm(node_, urdf)); } // Test inspired by hardware_interface/test_resource_manager.cpp @@ -687,7 +760,7 @@ TEST_F(TestGenericSystem, generic_system_2dof_symetric_interfaces) { auto urdf = ros2_control_test_assets::urdf_head + hardware_system_2dof_ + ros2_control_test_assets::urdf_tail; - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); // Activate components to get all interfaces available activate_components(rm, {"MockHardwareSystem"}); @@ -718,7 +791,7 @@ TEST_F(TestGenericSystem, generic_system_2dof_asymetric_interfaces) { auto urdf = ros2_control_test_assets::urdf_head + hardware_system_2dof_asymetric_ + ros2_control_test_assets::urdf_tail; - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); // Activate components to get all interfaces available activate_components(rm, {"MockHardwareSystem"}); @@ -766,7 +839,8 @@ void generic_system_functional_test( const std::string & urdf, const std::string component_name = "GenericSystem2dof", const double offset = 0) { - TestableResourceManager rm(urdf); + rclcpp::Node node("test_generic_system"); + TestableResourceManager rm(node, urdf); // check is hardware is configured auto status_map = rm.get_components_status(); EXPECT_EQ( @@ -818,7 +892,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()); @@ -829,7 +903,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()); @@ -861,6 +935,159 @@ 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) +{ + rclcpp::Node node("test_generic_system"); + TestableResourceManager rm(node, 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_ + @@ -869,11 +1096,29 @@ 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_ + ros2_control_test_assets::urdf_tail; - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); // Activate components to get all interfaces available activate_components(rm, {"MockHardwareSystem"}); @@ -956,7 +1201,7 @@ TEST_F(TestGenericSystem, generic_system_2dof_sensor) { auto urdf = ros2_control_test_assets::urdf_head + hardware_system_2dof_with_sensor_ + ros2_control_test_assets::urdf_tail; - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); // Activate components to get all interfaces available activate_components(rm, {"MockHardwareSystem"}); @@ -1055,7 +1300,7 @@ TEST_F(TestGenericSystem, generic_system_2dof_sensor) void TestGenericSystem::test_generic_system_with_mock_sensor_commands( std::string & urdf, const std::string & component_name) { - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); // Activate components to get all interfaces available activate_components(rm, {component_name}); @@ -1195,7 +1440,7 @@ TEST_F(TestGenericSystem, generic_system_2dof_sensor_mock_command_True) void TestGenericSystem::test_generic_system_with_mimic_joint( std::string & urdf, const std::string & component_name) { - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); // Activate components to get all interfaces available activate_components(rm, {component_name}); @@ -1207,11 +1452,9 @@ void TestGenericSystem::test_generic_system_with_mimic_joint( EXPECT_TRUE(rm.state_interface_exists("joint2/position")); EXPECT_TRUE(rm.state_interface_exists("joint2/velocity")); - ASSERT_EQ(4u, rm.command_interface_keys().size()); + ASSERT_EQ(2u, rm.command_interface_keys().size()); EXPECT_TRUE(rm.command_interface_exists("joint1/position")); EXPECT_TRUE(rm.command_interface_exists("joint1/velocity")); - EXPECT_TRUE(rm.command_interface_exists("joint2/position")); - EXPECT_TRUE(rm.command_interface_exists("joint2/velocity")); // Check initial values hardware_interface::LoanedStateInterface j1p_s = rm.claim_state_interface("joint1/position"); @@ -1261,7 +1504,7 @@ void TestGenericSystem::test_generic_system_with_mimic_joint( TEST_F(TestGenericSystem, hardware_system_2dof_with_mimic_joint) { - auto urdf = ros2_control_test_assets::urdf_head + hardware_system_2dof_with_mimic_joint_ + + auto urdf = ros2_control_test_assets::urdf_head_mimic + hardware_system_2dof_with_mimic_joint_ + ros2_control_test_assets::urdf_tail; test_generic_system_with_mimic_joint(urdf, "MockHardwareSystem"); @@ -1294,7 +1537,7 @@ TEST_F(TestGenericSystem, generic_system_2dof_functionality_with_offset_custom_i const double offset = -3; - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); const std::string hardware_name = "MockHardwareSystem"; @@ -1407,7 +1650,7 @@ TEST_F(TestGenericSystem, valid_urdf_ros2_control_system_robot_with_gpio) { auto urdf = ros2_control_test_assets::urdf_head + valid_urdf_ros2_control_system_robot_with_gpio_ + ros2_control_test_assets::urdf_tail; - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); const std::string hardware_name = "MockHardwareSystem"; @@ -1504,7 +1747,7 @@ TEST_F(TestGenericSystem, valid_urdf_ros2_control_system_robot_with_gpio) void TestGenericSystem::test_generic_system_with_mock_gpio_commands( std::string & urdf, const std::string & component_name) { - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); // check is hardware is started auto status_map = rm.get_components_status(); @@ -1630,7 +1873,7 @@ TEST_F(TestGenericSystem, sensor_with_initial_value) { auto urdf = ros2_control_test_assets::urdf_head + sensor_with_initial_value_ + ros2_control_test_assets::urdf_tail; - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); // Activate components to get all interfaces available activate_components(rm, {"MockHardwareSystem"}); @@ -1658,7 +1901,7 @@ TEST_F(TestGenericSystem, gpio_with_initial_value) { auto urdf = ros2_control_test_assets::urdf_head + gpio_with_initial_value_ + ros2_control_test_assets::urdf_tail; - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); // Activate components to get all interfaces available activate_components(rm, {"MockHardwareSystem"}); @@ -1679,7 +1922,7 @@ TEST_F(TestGenericSystem, simple_dynamics_pos_vel_acc_control_modes_interfaces) hardware_system_2dof_standard_interfaces_with_different_control_modes_ + ros2_control_test_assets::urdf_tail; - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); // Activate components to get all interfaces available activate_components(rm, {"MockHardwareSystem"}); @@ -1873,7 +2116,7 @@ TEST_F(TestGenericSystem, disabled_commands_flag_is_active) { auto urdf = ros2_control_test_assets::urdf_head + disabled_commands_ + ros2_control_test_assets::urdf_tail; - TestableResourceManager rm(urdf); + TestableResourceManager rm(node_, urdf); // Activate components to get all interfaces available activate_components(rm, {"MockHardwareSystem"}); @@ -1918,10 +2161,11 @@ TEST_F(TestGenericSystem, disabled_commands_flag_is_active) TEST_F(TestGenericSystem, prepare_command_mode_switch_works_with_all_example_tags) { - auto check_prepare_command_mode_switch = [&](const std::string & urdf) + auto check_prepare_command_mode_switch = + [&]( + const std::string & urdf, const std::string & urdf_head = ros2_control_test_assets::urdf_head) { - TestableResourceManager rm( - ros2_control_test_assets::urdf_head + urdf + ros2_control_test_assets::urdf_tail); + TestableResourceManager rm(node_, urdf_head + urdf + ros2_control_test_assets::urdf_tail); rclcpp_lifecycle::State state(lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, "active"); rm.set_component_state("MockHardwareSystem", state); auto start_interfaces = rm.command_interface_keys(); @@ -1937,7 +2181,8 @@ TEST_F(TestGenericSystem, prepare_command_mode_switch_works_with_all_example_tag ASSERT_TRUE(check_prepare_command_mode_switch(hardware_system_2dof_with_sensor_mock_command_)); ASSERT_TRUE( check_prepare_command_mode_switch(hardware_system_2dof_with_sensor_mock_command_True_)); - ASSERT_TRUE(check_prepare_command_mode_switch(hardware_system_2dof_with_mimic_joint_)); + ASSERT_TRUE(check_prepare_command_mode_switch( + hardware_system_2dof_with_mimic_joint_, ros2_control_test_assets::urdf_head_mimic)); ASSERT_TRUE( check_prepare_command_mode_switch(hardware_system_2dof_standard_interfaces_with_offset_)); ASSERT_TRUE(check_prepare_command_mode_switch( @@ -1957,3 +2202,10 @@ TEST_F(TestGenericSystem, prepare_command_mode_switch_works_with_all_example_tag valid_hardware_system_2dof_standard_interfaces_with_different_control_modes_)); ASSERT_TRUE(check_prepare_command_mode_switch(disabled_commands_)); } + +int main(int argc, char ** argv) +{ + rclcpp::init(argc, argv); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/hardware_interface/test/test_component_interfaces.cpp b/hardware_interface/test/test_component_interfaces.cpp index d90756c324..42ccdae8fa 100644 --- a/hardware_interface/test/test_component_interfaces.cpp +++ b/hardware_interface/test/test_component_interfaces.cpp @@ -341,12 +341,12 @@ class DummySystem : public hardware_interface::SystemInterface private: std::array position_state_ = { - std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN(), - std::numeric_limits::quiet_NaN()}; + {std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN()}}; std::array velocity_state_ = { - std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN(), - std::numeric_limits::quiet_NaN()}; - std::array velocity_command_ = {0.0, 0.0, 0.0}; + {std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN()}}; + std::array velocity_command_ = {{0.0, 0.0, 0.0}}; // Helper variables to initiate error on read unsigned int read_calls_ = 0; @@ -425,7 +425,8 @@ TEST(TestComponentInterfaces, dummy_actuator) hardware_interface::Actuator actuator_hw(std::make_unique()); hardware_interface::HardwareInfo mock_hw_info{}; - auto state = actuator_hw.initialize(mock_hw_info); + rclcpp::Logger logger = rclcpp::get_logger("test_actuator_components"); + auto state = actuator_hw.initialize(mock_hw_info, logger, nullptr); EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, state.id()); EXPECT_EQ(hardware_interface::lifecycle_state_names::UNCONFIGURED, state.label()); @@ -515,7 +516,8 @@ TEST(TestComponentInterfaces, dummy_sensor) hardware_interface::Sensor sensor_hw(std::make_unique()); hardware_interface::HardwareInfo mock_hw_info{}; - auto state = sensor_hw.initialize(mock_hw_info); + rclcpp::Logger logger = rclcpp::get_logger("test_sensor_components"); + auto state = sensor_hw.initialize(mock_hw_info, logger, nullptr); EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, state.id()); EXPECT_EQ(hardware_interface::lifecycle_state_names::UNCONFIGURED, state.label()); @@ -546,7 +548,8 @@ TEST(TestComponentInterfaces, dummy_system) hardware_interface::System system_hw(std::make_unique()); hardware_interface::HardwareInfo mock_hw_info{}; - auto state = system_hw.initialize(mock_hw_info); + rclcpp::Logger logger = rclcpp::get_logger("test_system_components"); + auto state = system_hw.initialize(mock_hw_info, logger, nullptr); EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, state.id()); EXPECT_EQ(hardware_interface::lifecycle_state_names::UNCONFIGURED, state.label()); @@ -670,7 +673,8 @@ TEST(TestComponentInterfaces, dummy_command_mode_system) hardware_interface::System system_hw( std::make_unique()); hardware_interface::HardwareInfo mock_hw_info{}; - auto state = system_hw.initialize(mock_hw_info); + rclcpp::Logger logger = rclcpp::get_logger("test_system_components"); + auto state = system_hw.initialize(mock_hw_info, logger, nullptr); EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, state.id()); EXPECT_EQ(hardware_interface::lifecycle_state_names::UNCONFIGURED, state.label()); @@ -701,7 +705,8 @@ TEST(TestComponentInterfaces, dummy_actuator_read_error_behavior) hardware_interface::Actuator actuator_hw(std::make_unique()); hardware_interface::HardwareInfo mock_hw_info{}; - auto state = actuator_hw.initialize(mock_hw_info); + rclcpp::Logger logger = rclcpp::get_logger("test_actuator_components"); + auto state = actuator_hw.initialize(mock_hw_info, logger, nullptr); EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, state.id()); EXPECT_EQ(hardware_interface::lifecycle_state_names::UNCONFIGURED, state.label()); @@ -760,7 +765,8 @@ TEST(TestComponentInterfaces, dummy_actuator_write_error_behavior) hardware_interface::Actuator actuator_hw(std::make_unique()); hardware_interface::HardwareInfo mock_hw_info{}; - auto state = actuator_hw.initialize(mock_hw_info); + rclcpp::Logger logger = rclcpp::get_logger("test_actuator_components"); + auto state = actuator_hw.initialize(mock_hw_info, logger, nullptr); EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, state.id()); EXPECT_EQ(hardware_interface::lifecycle_state_names::UNCONFIGURED, state.label()); @@ -819,7 +825,8 @@ TEST(TestComponentInterfaces, dummy_sensor_read_error_behavior) hardware_interface::Sensor sensor_hw(std::make_unique()); hardware_interface::HardwareInfo mock_hw_info{}; - auto state = sensor_hw.initialize(mock_hw_info); + rclcpp::Logger logger = rclcpp::get_logger("test_sensor_components"); + auto state = sensor_hw.initialize(mock_hw_info, logger, nullptr); auto state_interfaces = sensor_hw.export_state_interfaces(); // Updated because is is INACTIVE @@ -883,7 +890,8 @@ TEST(TestComponentInterfaces, dummy_system_read_error_behavior) hardware_interface::System system_hw(std::make_unique()); hardware_interface::HardwareInfo mock_hw_info{}; - auto state = system_hw.initialize(mock_hw_info); + rclcpp::Logger logger = rclcpp::get_logger("test_system_components"); + auto state = system_hw.initialize(mock_hw_info, logger, nullptr); EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, state.id()); EXPECT_EQ(hardware_interface::lifecycle_state_names::UNCONFIGURED, state.label()); @@ -947,7 +955,8 @@ TEST(TestComponentInterfaces, dummy_system_write_error_behavior) hardware_interface::System system_hw(std::make_unique()); hardware_interface::HardwareInfo mock_hw_info{}; - auto state = system_hw.initialize(mock_hw_info); + rclcpp::Logger logger = rclcpp::get_logger("test_system_components"); + auto state = system_hw.initialize(mock_hw_info, logger, nullptr); EXPECT_EQ(lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED, state.id()); EXPECT_EQ(hardware_interface::lifecycle_state_names::UNCONFIGURED, state.label()); diff --git a/hardware_interface/test/test_component_parser.cpp b/hardware_interface/test/test_component_parser.cpp index b0c7c5a16d..be891787f3 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) @@ -564,9 +800,33 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_system_robot_with_gpio EXPECT_THAT(hardware_info.gpios[1].state_interfaces, SizeIs(1)); EXPECT_THAT(hardware_info.gpios[1].command_interfaces, SizeIs(1)); EXPECT_EQ(hardware_info.gpios[1].state_interfaces[0].name, "vacuum"); + EXPECT_EQ(hardware_info.gpios[1].state_interfaces[0].initial_value, "1.0"); 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) @@ -615,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 = @@ -639,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) @@ -674,3 +1308,99 @@ TEST_F(TestComponentParser, transmission_given_too_many_joints_throws_error) ros2_control_test_assets::urdf_tail; ASSERT_THROW(parse_control_resources_from_urdf(urdf_to_test), std::runtime_error); } + +TEST_F(TestComponentParser, gripper_mimic_true_valid_config) +{ + const auto urdf_to_test = + std::string(ros2_control_test_assets::gripper_urdf_head) + + std::string(ros2_control_test_assets::gripper_hardware_resources_mimic_true_no_command_if) + + std::string(ros2_control_test_assets::urdf_tail); + std::vector hw_info; + ASSERT_NO_THROW(hw_info = parse_control_resources_from_urdf(urdf_to_test)); + ASSERT_THAT(hw_info, SizeIs(1)); + ASSERT_THAT(hw_info[0].mimic_joints, SizeIs(1)); + EXPECT_DOUBLE_EQ(hw_info[0].mimic_joints[0].multiplier, 2.0); + EXPECT_DOUBLE_EQ(hw_info[0].mimic_joints[0].offset, 1.0); + EXPECT_EQ(hw_info[0].mimic_joints[0].mimicked_joint_index, 0); + EXPECT_EQ(hw_info[0].mimic_joints[0].joint_index, 1); +} + +TEST_F(TestComponentParser, gripper_no_mimic_valid_config) +{ + const auto urdf_to_test = + std::string(ros2_control_test_assets::gripper_urdf_head) + + std::string(ros2_control_test_assets::gripper_hardware_resources_no_command_if) + + std::string(ros2_control_test_assets::urdf_tail); + std::vector hw_info; + ASSERT_NO_THROW(hw_info = parse_control_resources_from_urdf(urdf_to_test)); + ASSERT_THAT(hw_info, SizeIs(1)); + ASSERT_THAT(hw_info[0].mimic_joints, SizeIs(1)); + EXPECT_DOUBLE_EQ(hw_info[0].mimic_joints[0].multiplier, 2.0); + EXPECT_DOUBLE_EQ(hw_info[0].mimic_joints[0].offset, 1.0); + EXPECT_EQ(hw_info[0].mimic_joints[0].mimicked_joint_index, 0); + EXPECT_EQ(hw_info[0].mimic_joints[0].joint_index, 1); +} + +TEST_F(TestComponentParser, gripper_mimic_with_unknown_joint_throws_error) +{ + const auto urdf_to_test = + std::string(ros2_control_test_assets::gripper_urdf_head_unknown_joint) + + std::string(ros2_control_test_assets::gripper_hardware_resources_mimic_true_no_command_if) + + std::string(ros2_control_test_assets::urdf_tail); + ASSERT_THROW(parse_control_resources_from_urdf(urdf_to_test), std::runtime_error); +} + +TEST_F(TestComponentParser, gripper_mimic_true_without_mimic_info_throws_error) +{ + const auto urdf_to_test = + std::string(ros2_control_test_assets::gripper_urdf_head_no_mimic) + + std::string(ros2_control_test_assets::gripper_hardware_resources_mimic_true_no_command_if) + + std::string(ros2_control_test_assets::urdf_tail); + ASSERT_THROW(parse_control_resources_from_urdf(urdf_to_test), std::runtime_error); +} + +TEST_F(TestComponentParser, gripper_mimic_true_invalid_config_throws_error) +{ + const auto urdf_to_test = + std::string(ros2_control_test_assets::gripper_urdf_head) + + std::string(ros2_control_test_assets::gripper_hardware_resources_mimic_true_command_if) + + std::string(ros2_control_test_assets::urdf_tail); + ASSERT_THROW(parse_control_resources_from_urdf(urdf_to_test), std::runtime_error); +} + +TEST_F(TestComponentParser, gripper_mimic_false_valid_config) +{ + const auto urdf_to_test = + std::string(ros2_control_test_assets::gripper_urdf_head) + + std::string(ros2_control_test_assets::gripper_hardware_resources_mimic_false_command_if) + + std::string(ros2_control_test_assets::urdf_tail); + std::vector hw_info; + ASSERT_NO_THROW(hw_info = parse_control_resources_from_urdf(urdf_to_test)); + ASSERT_THAT(hw_info, SizeIs(1)); + ASSERT_THAT(hw_info[0].mimic_joints, SizeIs(0)); +} + +/** + * @brief Test that the parser throws an error if the URDF contains a link with no parent. + */ +TEST_F(TestComponentParser, urdf_two_root_links_throws_error) +{ + const auto urdf_to_test = + std::string(ros2_control_test_assets::gripper_urdf_head_invalid_two_root_links) + + std::string(ros2_control_test_assets::gripper_hardware_resources_mimic_true_no_command_if) + + std::string(ros2_control_test_assets::urdf_tail); + ASSERT_THROW(parse_control_resources_from_urdf(urdf_to_test), std::runtime_error); +} + +/** + * @brief Test that the parser throws an error if a joint defined in the ros2_control tag is missing + * in the URDF + */ +TEST_F(TestComponentParser, urdf_incomplete_throws_error) +{ + const auto urdf_to_test = + std::string(ros2_control_test_assets::gripper_urdf_head_incomplete) + + std::string(ros2_control_test_assets::gripper_hardware_resources_mimic_true_no_command_if) + + std::string(ros2_control_test_assets::urdf_tail); + ASSERT_THROW(parse_control_resources_from_urdf(urdf_to_test), std::runtime_error); +} diff --git a/hardware_interface/test/test_handle.cpp b/hardware_interface/test/test_handle.cpp index 16ca710e9d..da8258c643 100644 --- a/hardware_interface/test/test_handle.cpp +++ b/hardware_interface/test/test_handle.cpp @@ -27,9 +27,7 @@ constexpr auto FOO_INTERFACE = "FooInterface"; TEST(TestHandle, command_interface) { double value = 1.337; - CommandInterface interface { - JOINT_NAME, FOO_INTERFACE, &value - }; + CommandInterface interface{JOINT_NAME, FOO_INTERFACE, &value}; EXPECT_DOUBLE_EQ(interface.get_value(), value); EXPECT_NO_THROW(interface.set_value(0.0)); EXPECT_DOUBLE_EQ(interface.get_value(), 0.0); @@ -38,9 +36,7 @@ TEST(TestHandle, command_interface) TEST(TestHandle, state_interface) { double value = 1.337; - StateInterface interface { - JOINT_NAME, FOO_INTERFACE, &value - }; + StateInterface interface{JOINT_NAME, FOO_INTERFACE, &value}; EXPECT_DOUBLE_EQ(interface.get_value(), value); // interface.set_value(5); compiler error, no set_value function } diff --git a/hardware_interface/test/test_hardware_components/test_force_torque_sensor.cpp b/hardware_interface/test/test_hardware_components/test_force_torque_sensor.cpp index d226e08fd9..47b19f9769 100644 --- a/hardware_interface/test/test_hardware_components/test_force_torque_sensor.cpp +++ b/hardware_interface/test/test_hardware_components/test_force_torque_sensor.cpp @@ -43,8 +43,7 @@ class TestForceTorqueSensor : public SensorInterface { if ( std::find_if( - state_interfaces.begin(), state_interfaces.end(), - [&ft_key](const auto & interface_info) + state_interfaces.begin(), state_interfaces.end(), [&ft_key](const auto & interface_info) { return interface_info.name == ft_key; }) == state_interfaces.end()) { return CallbackReturn::ERROR; diff --git a/hardware_interface/test/test_hardware_components/test_hardware_components.xml b/hardware_interface/test/test_hardware_components/test_hardware_components.xml index b297bde668..5d99017287 100644 --- a/hardware_interface/test/test_hardware_components/test_hardware_components.xml +++ b/hardware_interface/test/test_hardware_components/test_hardware_components.xml @@ -12,6 +12,12 @@ + + + Test sensor component emulating an IMU sensor + + + Test system component for a two degree of freedom position joint robot diff --git a/hardware_interface/test/test_hardware_components/test_imu_sensor.cpp b/hardware_interface/test/test_hardware_components/test_imu_sensor.cpp new file mode 100644 index 0000000000..9997466106 --- /dev/null +++ b/hardware_interface/test/test_hardware_components/test_imu_sensor.cpp @@ -0,0 +1,142 @@ +// Copyright 2023 PAL Robotics SL. +// +// 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. + +/* + * Author: Sai Kishor Kothakota + */ + +#include +#include +#include +#include +#include + +#include "hardware_interface/sensor_interface.hpp" + +using hardware_interface::return_type; +using hardware_interface::SensorInterface; +using hardware_interface::StateInterface; + +namespace test_hardware_components +{ +class TestIMUSensor : public SensorInterface +{ + CallbackReturn on_init(const hardware_interface::HardwareInfo & sensor_info) override + { + if (SensorInterface::on_init(sensor_info) != CallbackReturn::SUCCESS) + { + return CallbackReturn::ERROR; + } + + const auto & state_interfaces = info_.sensors[0].state_interfaces; + if (state_interfaces.size() != 10) + { + return CallbackReturn::ERROR; + } + for (const auto & imu_key : + {"orientation.x", "orientation.y", "orientation.z", "orientation.w", "angular_velocity.x", + "angular_velocity.y", "angular_velocity.z", "linear_acceleration.x", + "linear_acceleration.y", "linear_acceleration.z"}) + { + if ( + std::find_if( + state_interfaces.begin(), state_interfaces.end(), [&imu_key](const auto & interface_info) + { return interface_info.name == imu_key; }) == state_interfaces.end()) + { + return CallbackReturn::ERROR; + } + } + + fprintf(stderr, "TestIMUSensor configured successfully.\n"); + return CallbackReturn::SUCCESS; + } + + std::vector export_state_interfaces() override + { + std::vector state_interfaces; + + const std::string & sensor_name = info_.sensors[0].name; + state_interfaces.emplace_back( + hardware_interface::StateInterface(sensor_name, "orientation.x", &orientation_.x)); + state_interfaces.emplace_back( + hardware_interface::StateInterface(sensor_name, "orientation.y", &orientation_.y)); + state_interfaces.emplace_back( + hardware_interface::StateInterface(sensor_name, "orientation.z", &orientation_.z)); + state_interfaces.emplace_back( + hardware_interface::StateInterface(sensor_name, "orientation.w", &orientation_.w)); + state_interfaces.emplace_back( + hardware_interface::StateInterface(sensor_name, "angular_velocity.x", &angular_velocity_.x)); + state_interfaces.emplace_back( + hardware_interface::StateInterface(sensor_name, "angular_velocity.y", &angular_velocity_.y)); + state_interfaces.emplace_back( + hardware_interface::StateInterface(sensor_name, "angular_velocity.z", &angular_velocity_.z)); + state_interfaces.emplace_back(hardware_interface::StateInterface( + sensor_name, "linear_acceleration.x", &linear_acceleration_.x)); + state_interfaces.emplace_back(hardware_interface::StateInterface( + sensor_name, "linear_acceleration.y", &linear_acceleration_.y)); + state_interfaces.emplace_back(hardware_interface::StateInterface( + sensor_name, "linear_acceleration.z", &linear_acceleration_.z)); + + return state_interfaces; + } + + return_type read(const rclcpp::Time & /*time*/, const rclcpp::Duration & /*period*/) override + { + // generate a random distribution of the quaternion + std::uniform_real_distribution distribution_1(0.0, 1.0); + const double u1 = distribution_1(generator_); + const double u2 = distribution_1(generator_); + const double u3 = distribution_1(generator_); + orientation_.w = std::sqrt(1. - u1) * std::sin(2 * M_PI * u2); + orientation_.x = std::sqrt(1. - u1) * std::cos(2 * M_PI * u2); + orientation_.y = std::sqrt(u1) * std::sin(2 * M_PI * u3); + orientation_.z = std::sqrt(u1) * std::cos(2 * M_PI * u3); + + // generate random angular velocities and linear accelerations + std::uniform_real_distribution distribution_2(0.0, 0.1); + angular_velocity_.x = distribution_2(generator_); + angular_velocity_.y = distribution_2(generator_); + angular_velocity_.z = distribution_2(generator_); + + linear_acceleration_.x = distribution_2(generator_); + linear_acceleration_.y = distribution_2(generator_); + linear_acceleration_.z = distribution_2(generator_); + return return_type::OK; + } + +private: + struct QuaternionValues + { + double x = 0.0; + double y = 0.0; + double z = 0.0; + double w = 1.0; + }; + struct AxisValues + { + double x = 0.0; + double y = 0.0; + double z = 0.0; + }; + + std::default_random_engine generator_; + QuaternionValues orientation_; + AxisValues angular_velocity_; + AxisValues linear_acceleration_; +}; + +} // namespace test_hardware_components + +#include "pluginlib/class_list_macros.hpp" // NOLINT +PLUGINLIB_EXPORT_CLASS(test_hardware_components::TestIMUSensor, hardware_interface::SensorInterface) diff --git a/hardware_interface/test/test_hardware_components/test_system_with_command_modes.cpp b/hardware_interface/test/test_hardware_components/test_system_with_command_modes.cpp index a594d3b70a..aba2f86fe5 100644 --- a/hardware_interface/test/test_hardware_components/test_system_with_command_modes.cpp +++ b/hardware_interface/test/test_hardware_components/test_system_with_command_modes.cpp @@ -184,11 +184,11 @@ class TestSystemCommandModes : public hardware_interface::SystemInterface std::vector start_modes_ = {"position", "position"}; std::vector stop_modes_ = {false, false}; - std::array position_command_ = {0.0, 0.0}; - std::array velocity_command_ = {0.0, 0.0}; - std::array position_state_ = {0.0, 0.0}; - std::array velocity_state_ = {0.0, 0.0}; - std::array acceleration_state_ = {0.0, 0.0}; + std::array position_command_ = {{0.0, 0.0}}; + std::array velocity_command_ = {{0.0, 0.0}}; + std::array position_state_ = {{0.0, 0.0}}; + std::array velocity_state_ = {{0.0, 0.0}}; + std::array acceleration_state_ = {{0.0, 0.0}}; }; } // namespace test_hardware_components diff --git a/hardware_interface/test/test_hardware_components/test_two_joint_system.cpp b/hardware_interface/test/test_hardware_components/test_two_joint_system.cpp index b597804104..fe06a64223 100644 --- a/hardware_interface/test/test_hardware_components/test_two_joint_system.cpp +++ b/hardware_interface/test/test_hardware_components/test_two_joint_system.cpp @@ -103,8 +103,8 @@ class TestTwoJointSystem : public SystemInterface } private: - std::array position_command_ = {0.0, 0.0}; - std::array position_state_ = {0.0, 0.0}; + std::array position_command_ = {{0.0, 0.0}}; + std::array position_state_ = {{0.0, 0.0}}; }; } // namespace test_hardware_components diff --git a/hardware_interface_testing/CHANGELOG.rst b/hardware_interface_testing/CHANGELOG.rst index 1ad077aa5c..520f28540d 100644 --- a/hardware_interface_testing/CHANGELOG.rst +++ b/hardware_interface_testing/CHANGELOG.rst @@ -2,6 +2,41 @@ Changelog for package hardware_interface_testing ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.13.0 (2024-07-08) +------------------- +* [ResourceManager] Propagate access to logger and clock interfaces to HardwareComponent (`#1585 `_) +* Contributors: Sai Kishor Kothakota + +4.12.0 (2024-07-01) +------------------- +* [RM] Rename `load_urdf` method to `load_and_initialize_components` and add error handling there to avoid stack crashing when error happens. (`#1354 `_) +* Contributors: Dr. Denis + +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) +------------------ + +4.7.0 (2024-03-22) +------------------ + +4.6.0 (2024-03-02) +------------------ +* Add -Werror=missing-braces to compile options (`#1423 `_) +* Contributors: Sai Kishor Kothakota + +4.5.0 (2024-02-12) +------------------ + 4.4.0 (2024-01-31) ------------------ * Fix version diff --git a/hardware_interface_testing/CMakeLists.txt b/hardware_interface_testing/CMakeLists.txt index 8e88a677bf..cd8e30028a 100644 --- a/hardware_interface_testing/CMakeLists.txt +++ b/hardware_interface_testing/CMakeLists.txt @@ -2,7 +2,8 @@ cmake_minimum_required(VERSION 3.16) project(hardware_interface_testing LANGUAGES CXX) if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") - add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow) + add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow + -Werror=missing-braces) endif() set(THIS_PACKAGE_INCLUDE_DEPENDS diff --git a/hardware_interface_testing/package.xml b/hardware_interface_testing/package.xml index de7d75b2f5..f65de8598d 100644 --- a/hardware_interface_testing/package.xml +++ b/hardware_interface_testing/package.xml @@ -1,7 +1,7 @@ hardware_interface_testing - 4.4.0 + 4.13.0 ros2_control hardware interface testing Bence Magyar Denis Štogl diff --git a/hardware_interface_testing/test/test_components/test_actuator.cpp b/hardware_interface_testing/test/test_components/test_actuator.cpp index 41f27e201b..1277fecfb6 100644 --- a/hardware_interface_testing/test/test_components/test_actuator.cpp +++ b/hardware_interface_testing/test/test_components/test_actuator.cpp @@ -32,15 +32,22 @@ class TestActuator : public ActuatorInterface return CallbackReturn::ERROR; } + if (info_.joints[0].state_interfaces[1].name == "does_not_exist") + { + return CallbackReturn::ERROR; + } + /* * a hardware can optional prove for incorrect info here. * * // can only control one joint * if (info_.joints.size() != 1) {return CallbackReturn::ERROR;} * // can only control in position - * if (info_.joints[0].command_interfaces.size() != 1) {return CallbackReturn::ERROR;} + * if (info_.joints[0].command_interfaces.size() != 1) {return + * CallbackReturn::ERROR;} * // can only give feedback state for position and velocity - * if (info_.joints[0].state_interfaces.size() != 2) {return CallbackReturn::ERROR;} + * if (info_.joints[0].state_interfaces.size() != 2) {return + * CallbackReturn::ERROR;} */ return CallbackReturn::SUCCESS; @@ -95,7 +102,8 @@ class TestActuator : public ActuatorInterface // simulate error on read if (velocity_command_ == test_constants::READ_FAIL_VALUE) { - // reset value to get out from error on the next call - simplifies CM tests + // reset value to get out from error on the next call - simplifies CM + // tests velocity_command_ = 0.0; return return_type::ERROR; } @@ -104,10 +112,11 @@ class TestActuator : public ActuatorInterface { return return_type::DEACTIVATE; } - // The next line is for the testing purposes. We need value to be changed to be sure that - // the feedback from hardware to controllers in the chain is working as it should. - // This makes value checks clearer and confirms there is no "state = command" line or some - // other mixture of interfaces somewhere in the test stack. + // The next line is for the testing purposes. We need value to be changed to + // be sure that the feedback from hardware to controllers in the chain is + // working as it should. This makes value checks clearer and confirms there + // is no "state = command" line or some other mixture of interfaces + // somewhere in the test stack. velocity_state_ = velocity_command_ / 2; return return_type::OK; } @@ -117,7 +126,8 @@ class TestActuator : public ActuatorInterface // simulate error on write if (velocity_command_ == test_constants::WRITE_FAIL_VALUE) { - // reset value to get out from error on the next call - simplifies CM tests + // reset value to get out from error on the next call - simplifies CM + // tests velocity_command_ = 0.0; return return_type::ERROR; } @@ -136,7 +146,7 @@ class TestActuator : public ActuatorInterface double max_velocity_command_ = 0.0; }; -class TestUnitilizableActuator : public TestActuator +class TestUninitializableActuator : public TestActuator { CallbackReturn on_init(const hardware_interface::HardwareInfo & info) override { @@ -147,4 +157,4 @@ class TestUnitilizableActuator : public TestActuator #include "pluginlib/class_list_macros.hpp" // NOLINT PLUGINLIB_EXPORT_CLASS(TestActuator, hardware_interface::ActuatorInterface) -PLUGINLIB_EXPORT_CLASS(TestUnitilizableActuator, hardware_interface::ActuatorInterface) +PLUGINLIB_EXPORT_CLASS(TestUninitializableActuator, hardware_interface::ActuatorInterface) diff --git a/hardware_interface_testing/test/test_components/test_components.xml b/hardware_interface_testing/test/test_components/test_components.xml index f029d3dd37..e24ee28317 100644 --- a/hardware_interface_testing/test/test_components/test_components.xml +++ b/hardware_interface_testing/test/test_components/test_components.xml @@ -18,21 +18,21 @@ - + - Test Unitilizable Actuator + Test Uninitializable Actuator - + - Test Unitilizable Sensor + Test Uninitializable Sensor - + - Test Unitilizable System + Test Uninitializable System diff --git a/hardware_interface_testing/test/test_components/test_sensor.cpp b/hardware_interface_testing/test/test_components/test_sensor.cpp index 4811244138..3fc2ef2445 100644 --- a/hardware_interface_testing/test/test_components/test_sensor.cpp +++ b/hardware_interface_testing/test/test_components/test_sensor.cpp @@ -30,7 +30,7 @@ class TestSensor : public SensorInterface return CallbackReturn::ERROR; } // can only give feedback state for velocity - if (info_.sensors[0].state_interfaces.size() != 1) + if (info_.sensors[0].state_interfaces.size() == 2) { return CallbackReturn::ERROR; } @@ -55,7 +55,7 @@ class TestSensor : public SensorInterface double velocity_state_ = 0.0; }; -class TestUnitilizableSensor : public TestSensor +class TestUninitializableSensor : public TestSensor { CallbackReturn on_init(const hardware_interface::HardwareInfo & info) override { @@ -66,4 +66,4 @@ class TestUnitilizableSensor : public TestSensor #include "pluginlib/class_list_macros.hpp" // NOLINT PLUGINLIB_EXPORT_CLASS(TestSensor, hardware_interface::SensorInterface) -PLUGINLIB_EXPORT_CLASS(TestUnitilizableSensor, hardware_interface::SensorInterface) +PLUGINLIB_EXPORT_CLASS(TestUninitializableSensor, hardware_interface::SensorInterface) diff --git a/hardware_interface_testing/test/test_components/test_system.cpp b/hardware_interface_testing/test/test_components/test_system.cpp index bc7a75df6f..502f5b4c21 100644 --- a/hardware_interface_testing/test/test_components/test_system.cpp +++ b/hardware_interface_testing/test/test_components/test_system.cpp @@ -27,27 +27,40 @@ using hardware_interface::SystemInterface; class TestSystem : public SystemInterface { + CallbackReturn on_init(const hardware_interface::HardwareInfo & info) override + { + if (SystemInterface::on_init(info) != CallbackReturn::SUCCESS) + { + return CallbackReturn::ERROR; + } + + // Simulating initialization error + if (info_.joints[0].state_interfaces[1].name == "does_not_exist") + { + return CallbackReturn::ERROR; + } + + return CallbackReturn::SUCCESS; + } + std::vector export_state_interfaces() override { std::vector state_interfaces; for (auto i = 0u; i < info_.joints.size(); ++i) { - if (info_.joints[i].name != "configuration") - { - state_interfaces.emplace_back(hardware_interface::StateInterface( - info_.joints[i].name, hardware_interface::HW_IF_POSITION, &position_state_[i])); - state_interfaces.emplace_back(hardware_interface::StateInterface( - info_.joints[i].name, hardware_interface::HW_IF_VELOCITY, &velocity_state_[i])); - state_interfaces.emplace_back(hardware_interface::StateInterface( - info_.joints[i].name, hardware_interface::HW_IF_ACCELERATION, &acceleration_state_[i])); - } + state_interfaces.emplace_back(hardware_interface::StateInterface( + info_.joints[i].name, hardware_interface::HW_IF_POSITION, &position_state_[i])); + state_interfaces.emplace_back(hardware_interface::StateInterface( + info_.joints[i].name, hardware_interface::HW_IF_VELOCITY, &velocity_state_[i])); + state_interfaces.emplace_back(hardware_interface::StateInterface( + info_.joints[i].name, hardware_interface::HW_IF_ACCELERATION, &acceleration_state_[i])); } - if (info_.joints.size() > 2) + if (info_.gpios.size() > 0) { // Add configuration/max_tcp_jerk interface state_interfaces.emplace_back(hardware_interface::StateInterface( - info_.joints[2].name, info_.joints[2].state_interfaces[0].name, &configuration_state_)); + info_.gpios[0].name, info_.gpios[0].state_interfaces[0].name, &configuration_state_)); } return state_interfaces; @@ -58,22 +71,19 @@ class TestSystem : public SystemInterface std::vector command_interfaces; for (auto i = 0u; i < info_.joints.size(); ++i) { - if (info_.joints[i].name != "configuration") - { - command_interfaces.emplace_back(hardware_interface::CommandInterface( - info_.joints[i].name, hardware_interface::HW_IF_VELOCITY, &velocity_command_[i])); - } + command_interfaces.emplace_back(hardware_interface::CommandInterface( + info_.joints[i].name, hardware_interface::HW_IF_VELOCITY, &velocity_command_[i])); } // Add max_acceleration command interface command_interfaces.emplace_back(hardware_interface::CommandInterface( info_.joints[0].name, info_.joints[0].command_interfaces[1].name, &max_acceleration_command_)); - if (info_.joints.size() > 2) + if (info_.gpios.size() > 0) { // Add configuration/max_tcp_jerk interface command_interfaces.emplace_back(hardware_interface::CommandInterface( - info_.joints[2].name, info_.joints[2].command_interfaces[0].name, &configuration_command_)); + info_.gpios[0].name, info_.gpios[0].command_interfaces[0].name, &configuration_command_)); } return command_interfaces; @@ -84,7 +94,8 @@ class TestSystem : public SystemInterface // simulate error on read if (velocity_command_[0] == test_constants::READ_FAIL_VALUE) { - // reset value to get out from error on the next call - simplifies CM tests + // reset value to get out from error on the next call - simplifies CM + // tests velocity_command_[0] = 0.0; return return_type::ERROR; } @@ -101,7 +112,8 @@ class TestSystem : public SystemInterface // simulate error on write if (velocity_command_[0] == test_constants::WRITE_FAIL_VALUE) { - // reset value to get out from error on the next call - simplifies CM tests + // reset value to get out from error on the next call - simplifies CM + // tests velocity_command_[0] = 0.0; return return_type::ERROR; } @@ -114,16 +126,16 @@ class TestSystem : public SystemInterface } private: - std::array velocity_command_ = {0.0, 0.0}; - std::array position_state_ = {0.0, 0.0}; - std::array velocity_state_ = {0.0, 0.0}; - std::array acceleration_state_ = {0.0, 0.0}; + std::array velocity_command_ = {{0.0, 0.0}}; + std::array position_state_ = {{0.0, 0.0}}; + std::array velocity_state_ = {{0.0, 0.0}}; + std::array acceleration_state_ = {{0.0, 0.0}}; double max_acceleration_command_ = 0.0; double configuration_state_ = 0.0; double configuration_command_ = 0.0; }; -class TestUnitilizableSystem : public TestSystem +class TestUninitializableSystem : public TestSystem { CallbackReturn on_init(const hardware_interface::HardwareInfo & info) override { @@ -134,4 +146,4 @@ class TestUnitilizableSystem : public TestSystem #include "pluginlib/class_list_macros.hpp" // NOLINT PLUGINLIB_EXPORT_CLASS(TestSystem, hardware_interface::SystemInterface) -PLUGINLIB_EXPORT_CLASS(TestUnitilizableSystem, hardware_interface::SystemInterface) +PLUGINLIB_EXPORT_CLASS(TestUninitializableSystem, hardware_interface::SystemInterface) diff --git a/hardware_interface_testing/test/test_resource_manager.cpp b/hardware_interface_testing/test/test_resource_manager.cpp index 4bb9e0c5fe..5fb155fa3a 100644 --- a/hardware_interface_testing/test/test_resource_manager.cpp +++ b/hardware_interface_testing/test/test_resource_manager.cpp @@ -44,6 +44,7 @@ using ros2_control_test_assets::TEST_SYSTEM_HARDWARE_NAME; using ros2_control_test_assets::TEST_SYSTEM_HARDWARE_PLUGIN_NAME; using ros2_control_test_assets::TEST_SYSTEM_HARDWARE_STATE_INTERFACES; using ros2_control_test_assets::TEST_SYSTEM_HARDWARE_TYPE; +using testing::SizeIs; auto configure_components = [](TestableResourceManager & rm, const std::vector & components = {}) @@ -87,36 +88,32 @@ auto shutdown_components = TEST_F(ResourceManagerTest, initialization_empty) { - ASSERT_ANY_THROW(TestableResourceManager rm("")); + ASSERT_ANY_THROW(TestableResourceManager rm(node_, "");); } TEST_F(ResourceManagerTest, initialization_with_urdf) { - ASSERT_NO_THROW(TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf)); + ASSERT_NO_THROW(TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf);); } TEST_F(ResourceManagerTest, post_initialization_with_urdf) { - TestableResourceManager rm; - ASSERT_NO_THROW(rm.load_urdf(ros2_control_test_assets::minimal_robot_urdf)); + TestableResourceManager rm(node_); + ASSERT_NO_THROW(rm.load_and_initialize_components(ros2_control_test_assets::minimal_robot_urdf)); } -TEST_F(ResourceManagerTest, test_unitilizable_hardware_validation) +void test_load_and_initialized_components_failure(const std::string & urdf) { - // If the the hardware can not be initialized and load_urdf tried to validate the interfaces a - // runtime exception is thrown - TestableResourceManager rm; - ASSERT_THROW( - rm.load_urdf(ros2_control_test_assets::minimal_unitilizable_robot_urdf, true), - std::runtime_error); -} + rclcpp::Node node = rclcpp::Node("TestableResourceManager"); + TestableResourceManager rm(node); + ASSERT_FALSE(rm.load_and_initialize_components(urdf)); -TEST_F(ResourceManagerTest, test_unitilizable_hardware_no_validation) -{ - // If the the hardware can not be initialized and load_urdf didn't try to validate the interfaces, - // the interface should not show up - TestableResourceManager rm; - EXPECT_NO_THROW(rm.load_urdf(ros2_control_test_assets::minimal_unitilizable_robot_urdf, false)); + ASSERT_FALSE(rm.are_components_initialized()); + + // resource manager should also not have any components + EXPECT_EQ(rm.actuator_components_size(), 0); + EXPECT_EQ(rm.sensor_components_size(), 0); + EXPECT_EQ(rm.system_components_size(), 0); // test actuator EXPECT_FALSE(rm.state_interface_exists("joint1/position")); @@ -140,17 +137,26 @@ TEST_F(ResourceManagerTest, test_unitilizable_hardware_no_validation) EXPECT_FALSE(rm.command_interface_exists("joint3/max_acceleration")); } -TEST_F(ResourceManagerTest, initialization_with_urdf_manual_validation) +TEST_F(ResourceManagerTest, test_unitilizable_hardware) +{ + SCOPED_TRACE("test_unitilizable_hardware_no_validation"); + // If the the hardware can not be initialized and load_and_initialize_components didn't try to + // validate the interfaces, the interface should not show up + test_load_and_initialized_components_failure( + ros2_control_test_assets::minimal_uninitializable_robot_urdf); +} + +TEST_F(ResourceManagerTest, initialization_with_urdf_and_manual_validation) { // we validate the results manually - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf, false); + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf, false); EXPECT_EQ(1u, rm.actuator_components_size()); EXPECT_EQ(1u, rm.sensor_components_size()); EXPECT_EQ(1u, rm.system_components_size()); auto state_interface_keys = rm.state_interface_keys(); - ASSERT_EQ(11u, state_interface_keys.size()); + ASSERT_THAT(state_interface_keys, SizeIs(11)); EXPECT_TRUE(rm.state_interface_exists("joint1/position")); EXPECT_TRUE(rm.state_interface_exists("joint1/velocity")); EXPECT_TRUE(rm.state_interface_exists("sensor1/velocity")); @@ -158,32 +164,29 @@ TEST_F(ResourceManagerTest, initialization_with_urdf_manual_validation) EXPECT_TRUE(rm.state_interface_exists("joint3/position")); auto command_interface_keys = rm.command_interface_keys(); - ASSERT_EQ(6u, command_interface_keys.size()); + ASSERT_THAT(command_interface_keys, SizeIs(6)); EXPECT_TRUE(rm.command_interface_exists("joint1/position")); EXPECT_TRUE(rm.command_interface_exists("joint2/velocity")); EXPECT_TRUE(rm.command_interface_exists("joint3/velocity")); } -TEST_F(ResourceManagerTest, initialization_with_wrong_urdf) +TEST_F(ResourceManagerTest, expect_validation_failure_if_not_all_interfaces_are_exported) { + SCOPED_TRACE("missing state keys"); // missing state keys - { - EXPECT_THROW( - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_missing_state_keys_urdf), - std::exception); - } + test_load_and_initialized_components_failure( + ros2_control_test_assets::minimal_robot_missing_state_keys_urdf); + + SCOPED_TRACE("missing command keys"); // missing command keys - { - EXPECT_THROW( - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_missing_command_keys_urdf), - std::exception); - } + test_load_and_initialized_components_failure( + ros2_control_test_assets::minimal_robot_missing_command_keys_urdf); } TEST_F(ResourceManagerTest, initialization_with_urdf_unclaimed) { // we validate the results manually - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf); + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf); auto command_interface_keys = rm.command_interface_keys(); for (const auto & key : command_interface_keys) @@ -199,37 +202,80 @@ TEST_F(ResourceManagerTest, initialization_with_urdf_unclaimed) } } -TEST_F(ResourceManagerTest, no_load_urdf_function_called) +TEST_F(ResourceManagerTest, no_load_and_initialize_components_function_called) { - TestableResourceManager rm; - ASSERT_FALSE(rm.is_urdf_already_loaded()); + TestableResourceManager rm(node_); + ASSERT_FALSE(rm.are_components_initialized()); } -TEST_F(ResourceManagerTest, load_urdf_called_if_urdf_is_invalid) +TEST_F( + ResourceManagerTest, expect_load_and_initialize_to_fail_when_a_hw_component_plugin_does_not_exist) { - TestableResourceManager rm; - EXPECT_THROW( - rm.load_urdf(ros2_control_test_assets::minimal_robot_missing_state_keys_urdf), std::exception); - ASSERT_TRUE(rm.is_urdf_already_loaded()); + SCOPED_TRACE("Actuator plugin does not exist"); + // Actuator + test_load_and_initialized_components_failure( + ros2_control_test_assets::minimal_robot_not_existing_actuator_plugin); + + SCOPED_TRACE("Sensor plugin does not exist"); + // Sensor + test_load_and_initialized_components_failure( + ros2_control_test_assets::minimal_robot_not_existing_sensors_plugin); + + SCOPED_TRACE("System plugin does not exist"); + // System + test_load_and_initialized_components_failure( + ros2_control_test_assets::minimal_robot_not_existing_system_plugin); } -TEST_F(ResourceManagerTest, load_urdf_called_if_urdf_is_valid) +TEST_F(ResourceManagerTest, expect_load_and_initialize_to_fail_when_there_are_dupplicate_of_hw_comp) { - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf); - ASSERT_TRUE(rm.is_urdf_already_loaded()); + SCOPED_TRACE("Duplicated components"); + test_load_and_initialized_components_failure( + ros2_control_test_assets::minimal_robot_duplicated_component); } -TEST_F(ResourceManagerTest, can_load_urdf_later) +TEST_F( + ResourceManagerTest, expect_load_and_initialize_to_fail_when_a_hw_component_initialization_fails) { - TestableResourceManager rm; - ASSERT_FALSE(rm.is_urdf_already_loaded()); - rm.load_urdf(ros2_control_test_assets::minimal_robot_urdf); - ASSERT_TRUE(rm.is_urdf_already_loaded()); + SCOPED_TRACE("Actuator initialization fails"); + // Actuator + test_load_and_initialized_components_failure( + ros2_control_test_assets::minimal_robot_actuator_initialization_error); + + SCOPED_TRACE("Sensor initialization fails"); + // Sensor + test_load_and_initialized_components_failure( + ros2_control_test_assets::minimal_robot_sensor_initialization_error); + + SCOPED_TRACE("System initialization fails"); + // System + test_load_and_initialized_components_failure( + ros2_control_test_assets::minimal_robot_system_initialization_error); +} + +TEST_F(ResourceManagerTest, load_and_initialize_components_called_if_urdf_is_valid) +{ + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf); + ASSERT_TRUE(rm.are_components_initialized()); +} + +TEST_F(ResourceManagerTest, load_and_initialize_components_called_if_async_urdf_is_valid) +{ + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_async_robot_urdf); + ASSERT_TRUE(rm.are_components_initialized()); +} + +TEST_F(ResourceManagerTest, can_load_and_initialize_components_later) +{ + TestableResourceManager rm(node_); + ASSERT_FALSE(rm.are_components_initialized()); + rm.load_and_initialize_components(ros2_control_test_assets::minimal_robot_urdf); + ASSERT_TRUE(rm.are_components_initialized()); } TEST_F(ResourceManagerTest, resource_claiming) { - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf); + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf); // Activate components to get all interfaces available activate_components(rm); @@ -343,7 +389,7 @@ class ExternalComponent : public hardware_interface::ActuatorInterface TEST_F(ResourceManagerTest, post_initialization_add_components) { // we validate the results manually - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf, false); + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf); // Activate components to get all interfaces available activate_components(rm); @@ -351,8 +397,8 @@ TEST_F(ResourceManagerTest, post_initialization_add_components) EXPECT_EQ(1u, rm.sensor_components_size()); EXPECT_EQ(1u, rm.system_components_size()); - ASSERT_EQ(11u, rm.state_interface_keys().size()); - ASSERT_EQ(6u, rm.command_interface_keys().size()); + ASSERT_THAT(rm.state_interface_keys(), SizeIs(11)); + ASSERT_THAT(rm.command_interface_keys(), SizeIs(6)); hardware_interface::HardwareInfo external_component_hw_info; external_component_hw_info.name = "ExternalComponent"; @@ -361,9 +407,9 @@ TEST_F(ResourceManagerTest, post_initialization_add_components) rm.import_component(std::make_unique(), external_component_hw_info); EXPECT_EQ(2u, rm.actuator_components_size()); - ASSERT_EQ(12u, rm.state_interface_keys().size()); + ASSERT_THAT(rm.state_interface_keys(), SizeIs(12)); EXPECT_TRUE(rm.state_interface_exists("external_joint/external_state_interface")); - ASSERT_EQ(7u, rm.command_interface_keys().size()); + ASSERT_THAT(rm.command_interface_keys(), SizeIs(7)); EXPECT_TRUE(rm.command_interface_exists("external_joint/external_command_interface")); auto status_map = rm.get_components_status(); @@ -387,7 +433,7 @@ TEST_F(ResourceManagerTest, post_initialization_add_components) TEST_F(ResourceManagerTest, default_prepare_perform_switch) { - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf); + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf); // Activate components to get all interfaces available activate_components(rm); @@ -398,7 +444,7 @@ TEST_F(ResourceManagerTest, default_prepare_perform_switch) TEST_F(ResourceManagerTest, resource_status) { - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf); + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf); auto status_map = rm.get_components_status(); @@ -471,7 +517,7 @@ TEST_F(ResourceManagerTest, resource_status) TEST_F(ResourceManagerTest, lifecycle_all_resources) { - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf); + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf); // All resources start as UNCONFIGURED { @@ -614,7 +660,7 @@ TEST_F(ResourceManagerTest, lifecycle_all_resources) TEST_F(ResourceManagerTest, lifecycle_individual_resources) { - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf); + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf); // All resources start as UNCONFIGURED { @@ -827,7 +873,7 @@ TEST_F(ResourceManagerTest, lifecycle_individual_resources) TEST_F(ResourceManagerTest, resource_availability_and_claiming_in_lifecycle) { using std::placeholders::_1; - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf); + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf); auto check_interfaces = [](const std::vector & interface_names, auto check_method, bool expected_result) @@ -874,7 +920,8 @@ TEST_F(ResourceManagerTest, resource_availability_and_claiming_in_lifecycle) std::bind(&TestableResourceManager::command_interface_is_claimed, &rm, _1), expected_result); }; - // All resources start as UNCONFIGURED - All interfaces are imported but not available + // All resources start as UNCONFIGURED - All interfaces are imported but not + // available { check_interfaces( TEST_ACTUATOR_HARDWARE_COMMAND_INTERFACES, @@ -954,7 +1001,8 @@ TEST_F(ResourceManagerTest, resource_availability_and_claiming_in_lifecycle) TEST_SYSTEM_HARDWARE_STATE_INTERFACES, TEST_SYSTEM_HARDWARE_COMMAND_INTERFACES, false); } - // When actuator is activated all state- and command- interfaces become available + // When actuator is activated all state- and command- interfaces become + // available activate_components(rm, {TEST_ACTUATOR_HARDWARE_NAME}); { check_interfaces( @@ -1173,7 +1221,7 @@ TEST_F(ResourceManagerTest, resource_availability_and_claiming_in_lifecycle) TEST_F(ResourceManagerTest, managing_controllers_reference_interfaces) { - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf); + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf); std::string CONTROLLER_NAME = "test_controller"; std::vector REFERENCE_INTERFACE_NAMES = {"input1", "input2", "input3"}; @@ -1293,7 +1341,7 @@ class ResourceManagerTestReadWriteError : public ResourceManagerTest void setup_resource_manager_and_do_initial_checks() { rm = std::make_shared( - ros2_control_test_assets::minimal_robot_urdf, false); + node_, ros2_control_test_assets::minimal_robot_urdf, false); activate_components(*rm); auto status_map = rm->get_components_status(); @@ -1430,7 +1478,8 @@ class ResourceManagerTestReadWriteError : public ResourceManagerTest check_if_interface_available(true, true); } - // read failure for both, TEST_ACTUATOR_HARDWARE_NAME and TEST_SYSTEM_HARDWARE_NAME + // read failure for both, TEST_ACTUATOR_HARDWARE_NAME and + // TEST_SYSTEM_HARDWARE_NAME claimed_itfs[0].set_value(fail_value); claimed_itfs[1].set_value(fail_value); { @@ -1547,7 +1596,8 @@ class ResourceManagerTestReadWriteError : public ResourceManagerTest check_if_interface_available(true, true); } - // deactivate both, TEST_ACTUATOR_HARDWARE_NAME and TEST_SYSTEM_HARDWARE_NAME + // deactivate both, TEST_ACTUATOR_HARDWARE_NAME and + // TEST_SYSTEM_HARDWARE_NAME claimed_itfs[0].set_value(deactivate_value); claimed_itfs[1].set_value(deactivate_value); { @@ -1640,7 +1690,7 @@ TEST_F(ResourceManagerTestReadWriteError, handle_deactivate_on_hardware_write) TEST_F(ResourceManagerTest, test_caching_of_controllers_to_hardware) { - TestableResourceManager rm(ros2_control_test_assets::minimal_robot_urdf, false); + TestableResourceManager rm(node_, ros2_control_test_assets::minimal_robot_urdf, false); activate_components(rm); static const std::string TEST_CONTROLLER_ACTUATOR_NAME = "test_controller_actuator"; @@ -1682,3 +1732,10 @@ TEST_F(ResourceManagerTest, test_caching_of_controllers_to_hardware) {TEST_BROADCASTER_SENSOR_NAME, TEST_BROADCASTER_ALL_NAME}))); } } + +int main(int argc, char ** argv) +{ + rclcpp::init(argc, argv); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/hardware_interface_testing/test/test_resource_manager.hpp b/hardware_interface_testing/test/test_resource_manager.hpp index 46a487f2ef..18e51342f6 100644 --- a/hardware_interface_testing/test/test_resource_manager.hpp +++ b/hardware_interface_testing/test/test_resource_manager.hpp @@ -19,6 +19,7 @@ #include +#include #include #include @@ -31,6 +32,8 @@ class ResourceManagerTest : public ::testing::Test static void SetUpTestCase() {} void SetUp() {} + + rclcpp::Node node_{"ResourceManagerTest"}; }; // Forward declaration @@ -44,17 +47,22 @@ class TestableResourceManager : public hardware_interface::ResourceManager public: friend ResourceManagerTest; - FRIEND_TEST(ResourceManagerTest, initialization_with_urdf_manual_validation); + FRIEND_TEST(ResourceManagerTest, initialization_with_urdf_and_manual_validation); FRIEND_TEST(ResourceManagerTest, post_initialization_add_components); FRIEND_TEST(ResourceManagerTest, managing_controllers_reference_interfaces); FRIEND_TEST(ResourceManagerTest, resource_availability_and_claiming_in_lifecycle); - FRIEND_TEST(ResourceManagerTest, test_unitilizable_hardware_no_validation); + FRIEND_TEST(ResourceManagerTest, test_uninitializable_hardware_no_validation); - TestableResourceManager() : hardware_interface::ResourceManager() {} + explicit TestableResourceManager(rclcpp::Node & node) + : hardware_interface::ResourceManager( + node.get_node_clock_interface(), node.get_node_logging_interface()) + { + } - TestableResourceManager( - const std::string & urdf, bool validate_interfaces = true, bool activate_all = false) - : hardware_interface::ResourceManager(urdf, validate_interfaces, activate_all) + explicit TestableResourceManager( + rclcpp::Node & node, const std::string & urdf, bool activate_all = false) + : hardware_interface::ResourceManager( + urdf, node.get_node_clock_interface(), node.get_node_logging_interface(), activate_all, 100) { } }; diff --git a/hardware_interface_testing/test/test_resource_manager_prepare_perform_switch.cpp b/hardware_interface_testing/test/test_resource_manager_prepare_perform_switch.cpp index 8f6ba8f99a..d730029d90 100644 --- a/hardware_interface_testing/test/test_resource_manager_prepare_perform_switch.cpp +++ b/hardware_interface_testing/test/test_resource_manager_prepare_perform_switch.cpp @@ -64,7 +64,7 @@ class ResourceManagerPreparePerformTest : public ResourceManagerTest { ResourceManagerTest::SetUp(); - rm_ = std::make_unique(command_mode_urdf); + rm_ = std::make_unique(node_, command_mode_urdf); ASSERT_EQ(1u, rm_->actuator_components_size()); ASSERT_EQ(1u, rm_->system_components_size()); @@ -104,6 +104,7 @@ class ResourceManagerPreparePerformTest : public ResourceManagerTest std::unique_ptr claimed_actuator_position_state_; // Scenarios defined by example criteria + rclcpp::Node node_{"ResourceManagerPreparePerformTest"}; std::vector empty_keys = {}; std::vector non_existing_keys = {"elbow_joint/position", "should_joint/position"}; std::vector legal_keys_system = {"joint1/position", "joint2/position"}; @@ -387,3 +388,10 @@ TEST_F( EXPECT_EQ(claimed_system_acceleration_state_->get_value(), 0.0); EXPECT_EQ(claimed_actuator_position_state_->get_value(), 0.0); }; + +int main(int argc, char ** argv) +{ + rclcpp::init(argc, argv); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/joint_limits/CHANGELOG.rst b/joint_limits/CHANGELOG.rst index aa9d8db111..5fea915b6b 100644 --- a/joint_limits/CHANGELOG.rst +++ b/joint_limits/CHANGELOG.rst @@ -2,6 +2,47 @@ Changelog for package joint_limits ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.13.0 (2024-07-08) +------------------- +* [JointLimits] Add Saturation Joint Limiter that uses clamping method (`#971 `_) +* Contributors: Dr. Denis + +4.12.0 (2024-07-01) +------------------- +* Reactivate generate_version_header (`#1544 `_) +* Bump version of pre-commit hooks (`#1556 `_) +* Contributors: Christoph Fröhlich, github-actions[bot] + +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 `_) +* Contributors: Sai Kishor Kothakota + +4.7.0 (2024-03-22) +------------------ +* Codeformat from new pre-commit config (`#1433 `_) +* Contributors: Christoph Fröhlich + +4.6.0 (2024-03-02) +------------------ +* Add -Werror=missing-braces to compile options (`#1423 `_) +* Contributors: Sai Kishor Kothakota + +4.5.0 (2024-02-12) +------------------ + 4.4.0 (2024-01-31) ------------------ * [Format] Correct formatting of JointLimits URDF file. (`#1339 `_) diff --git a/joint_limits/CMakeLists.txt b/joint_limits/CMakeLists.txt index 1ed74c7603..8be804fa23 100644 --- a/joint_limits/CMakeLists.txt +++ b/joint_limits/CMakeLists.txt @@ -2,16 +2,22 @@ cmake_minimum_required(VERSION 3.16) project(joint_limits LANGUAGES CXX) if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") - add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow) + add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow + -Werror=missing-braces) endif() set(THIS_PACKAGE_INCLUDE_DEPENDS + pluginlib + realtime_tools rclcpp rclcpp_lifecycle + trajectory_msgs urdf ) find_package(ament_cmake REQUIRED) +find_package(backward_ros REQUIRED) +find_package(ament_cmake_gen_version_h REQUIRED) foreach(Dependency IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS}) find_package(${Dependency} REQUIRED) endforeach() @@ -24,10 +30,42 @@ target_include_directories(joint_limits INTERFACE ) ament_target_dependencies(joint_limits INTERFACE ${THIS_PACKAGE_INCLUDE_DEPENDS}) +add_library(joint_limiter_interface SHARED + src/joint_limiter_interface.cpp +) +target_compile_features(joint_limiter_interface PUBLIC cxx_std_17) +target_include_directories(joint_limiter_interface PUBLIC + $ + $ +) +ament_target_dependencies(joint_limiter_interface PUBLIC ${THIS_PACKAGE_INCLUDE_DEPENDS}) +# Causes the visibility macros to use dllexport rather than dllimport, +# which is appropriate when building the dll but not consuming it. +target_compile_definitions(joint_limiter_interface PRIVATE "JOINT_LIMITS_BUILDING_DLL") + + +add_library(joint_saturation_limiter SHARED + src/joint_saturation_limiter.cpp +) +target_compile_features(joint_saturation_limiter PUBLIC cxx_std_17) +target_include_directories(joint_saturation_limiter PUBLIC + $ + $ +) +ament_target_dependencies(joint_saturation_limiter PUBLIC ${THIS_PACKAGE_INCLUDE_DEPENDS}) +# Causes the visibility macros to use dllexport rather than dllimport, +# which is appropriate when building the dll but not consuming it. +target_compile_definitions(joint_saturation_limiter PRIVATE "JOINT_LIMITS_BUILDING_DLL") + +pluginlib_export_plugin_description_file(joint_limits joint_limiters.xml) if(BUILD_TESTING) find_package(ament_cmake_gtest REQUIRED) + find_package(ament_cmake_gmock REQUIRED) + find_package(generate_parameter_library REQUIRED) find_package(launch_testing_ament_cmake REQUIRED) + find_package(pluginlib REQUIRED) + find_package(rclcpp REQUIRED) ament_add_gtest_executable(joint_limits_rosparam_test test/joint_limits_rosparam_test.cpp) target_link_libraries(joint_limits_rosparam_test joint_limits) @@ -41,16 +79,31 @@ if(BUILD_TESTING) DESTINATION lib/joint_limits ) install( - FILES test/joint_limits_rosparam.yaml + FILES test/joint_limits_rosparam.yaml test/joint_saturation_limiter_param.yaml DESTINATION share/joint_limits/test ) + + add_rostest_with_parameters_gmock(test_joint_saturation_limiter test/test_joint_saturation_limiter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test/joint_saturation_limiter_param.yaml + ) + target_include_directories(test_joint_saturation_limiter PRIVATE include) + target_link_libraries(test_joint_saturation_limiter joint_limiter_interface) + ament_target_dependencies( + test_joint_saturation_limiter + pluginlib + rclcpp + ) + endif() install( DIRECTORY include/ DESTINATION include/joint_limits ) -install(TARGETS joint_limits +install(TARGETS + joint_limits + joint_limiter_interface + joint_saturation_limiter EXPORT export_joint_limits ARCHIVE DESTINATION lib LIBRARY DESTINATION lib @@ -60,3 +113,4 @@ 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}) diff --git a/joint_limits/include/joint_limits/joint_limiter_interface.hpp b/joint_limits/include/joint_limits/joint_limiter_interface.hpp new file mode 100644 index 0000000000..ed2f269812 --- /dev/null +++ b/joint_limits/include/joint_limits/joint_limiter_interface.hpp @@ -0,0 +1,250 @@ +// Copyright (c) 2024, Stogl Robotics Consulting UG (haftungsbeschränkt) +// +// 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. + +/// \author Denis Stogl + +#ifndef JOINT_LIMITS__JOINT_LIMITER_INTERFACE_HPP_ +#define JOINT_LIMITS__JOINT_LIMITER_INTERFACE_HPP_ + +#include +#include + +#include "joint_limits/joint_limits.hpp" +#include "joint_limits/joint_limits_rosparam.hpp" +#include "joint_limits/visibility_control.h" +#include "rclcpp/node.hpp" +#include "rclcpp_lifecycle/lifecycle_node.hpp" +#include "realtime_tools/realtime_buffer.h" +#include "trajectory_msgs/msg/joint_trajectory_point.hpp" + +namespace joint_limits +{ +using JointLimitsStateDataType = trajectory_msgs::msg::JointTrajectoryPoint; + +template +class JointLimiterInterface +{ +public: + JOINT_LIMITS_PUBLIC JointLimiterInterface() = default; + + JOINT_LIMITS_PUBLIC virtual ~JointLimiterInterface() = default; + + /// Initialization of every JointLimiter. + /** + * Initialization of JointLimiter for defined joints with their names. + * Robot description topic provides a topic name where URDF of the robot can be found. + * This is needed to use joint limits from URDF (not implemented yet!). + * Override this method only if initialization and reading joint limits should be adapted. + * Otherwise, initialize your custom limiter in `on_limit` method. + * + * \param[in] joint_names names of joints where limits should be applied. + * \param[in] param_itf node parameters interface object to access parameters. + * \param[in] logging_itf node logging interface to log if error happens. + * \param[in] robot_description_topic string of a topic where robot description is accessible. + */ + JOINT_LIMITS_PUBLIC virtual bool init( + const std::vector & joint_names, + const rclcpp::node_interfaces::NodeParametersInterface::SharedPtr & param_itf, + const rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr & logging_itf, + const std::string & robot_description_topic = "/robot_description") + { + number_of_joints_ = joint_names.size(); + joint_names_ = joint_names; + joint_limits_.resize(number_of_joints_); + node_param_itf_ = param_itf; + node_logging_itf_ = logging_itf; + + bool result = true; + + // TODO(destogl): get limits from URDF + + // Initialize and get joint limits from parameter server + for (size_t i = 0; i < number_of_joints_; ++i) + { + if (!declare_parameters(joint_names[i], node_param_itf_, node_logging_itf_)) + { + RCLCPP_ERROR( + node_logging_itf_->get_logger(), + "JointLimiter: Joint '%s': parameter declaration has failed", joint_names[i].c_str()); + result = false; + break; + } + if (!get_joint_limits(joint_names[i], node_param_itf_, node_logging_itf_, joint_limits_[i])) + { + RCLCPP_ERROR( + node_logging_itf_->get_logger(), + "JointLimiter: Joint '%s': getting parameters has failed", joint_names[i].c_str()); + result = false; + break; + } + RCLCPP_INFO( + node_logging_itf_->get_logger(), "Limits for joint %zu (%s) are:\n%s", i, + joint_names[i].c_str(), joint_limits_[i].to_string().c_str()); + } + updated_limits_.writeFromNonRT(joint_limits_); + + auto on_parameter_event_callback = [this](const std::vector & parameters) + { + rcl_interfaces::msg::SetParametersResult set_parameters_result; + set_parameters_result.successful = true; + + std::vector updated_joint_limits = joint_limits_; + bool changed = false; + + for (size_t i = 0; i < number_of_joints_; ++i) + { + changed |= joint_limits::check_for_limits_update( + joint_names_[i], parameters, node_logging_itf_, updated_joint_limits[i]); + } + + if (changed) + { + updated_limits_.writeFromNonRT(updated_joint_limits); + RCLCPP_INFO(node_logging_itf_->get_logger(), "Limits are dynamically updated!"); + } + + return set_parameters_result; + }; + + parameter_callback_ = + node_param_itf_->add_on_set_parameters_callback(on_parameter_event_callback); + + if (result) + { + result = on_init(); + } + + (void)robot_description_topic; // avoid linters output + + return result; + } + + /** + * Wrapper init method that accepts pointer to the Node. + * For details see other init method. + */ + JOINT_LIMITS_PUBLIC virtual bool init( + const std::vector & joint_names, const rclcpp::Node::SharedPtr & node, + const std::string & robot_description_topic = "/robot_description") + { + return init( + joint_names, node->get_node_parameters_interface(), node->get_node_logging_interface(), + robot_description_topic); + } + + /** + * Wrapper init method that accepts pointer to the LifecycleNode. + * For details see other init method. + */ + JOINT_LIMITS_PUBLIC virtual bool init( + const std::vector & joint_names, + const rclcpp_lifecycle::LifecycleNode::SharedPtr & lifecycle_node, + const std::string & robot_description_topic = "/robot_description") + { + return init( + joint_names, lifecycle_node->get_node_parameters_interface(), + lifecycle_node->get_node_logging_interface(), robot_description_topic); + } + + JOINT_LIMITS_PUBLIC virtual bool configure(const JointLimitsStateDataType & current_joint_states) + { + return on_configure(current_joint_states); + } + + /** \brief Enforce joint limits to desired joint state for multiple physical quantities. + * + * Generic enforce method that calls implementation-specific `on_enforce` method. + * + * \param[in] current_joint_states current joint states a robot is in. + * \param[in,out] desired_joint_states joint state that should be adjusted to obey the limits. + * \param[in] dt time delta to calculate missing integrals and derivation in joint limits. + * \returns true if limits are enforced, otherwise false. + */ + JOINT_LIMITS_PUBLIC virtual bool enforce( + JointLimitsStateDataType & current_joint_states, + JointLimitsStateDataType & desired_joint_states, const rclcpp::Duration & dt) + { + joint_limits_ = *(updated_limits_.readFromRT()); + return on_enforce(current_joint_states, desired_joint_states, dt); + } + + /** \brief Enforce joint limits to desired joint state for single physical quantity. + * + * Generic enforce method that calls implementation-specific `on_enforce` method. + * + * \param[in,out] desired_joint_states joint state that should be adjusted to obey the limits. + * \returns true if limits are enforced, otherwise false. + */ + JOINT_LIMITS_PUBLIC virtual bool enforce(std::vector & desired_joint_states) + { + joint_limits_ = *(updated_limits_.readFromRT()); + return on_enforce(desired_joint_states); + } + +protected: + /** \brief Method is realized by an implementation. + * + * Implementation-specific initialization of limiter's internal states and libraries. + * \returns true if initialization was successful, otherwise false. + */ + JOINT_LIMITS_PUBLIC virtual bool on_init() = 0; + + /** \brief Method is realized by an implementation. + * + * Implementation-specific configuration of limiter's internal states and libraries. + * \returns true if initialization was successful, otherwise false. + */ + JOINT_LIMITS_PUBLIC virtual bool on_configure( + const JointLimitsStateDataType & current_joint_states) = 0; + + /** \brief Method is realized by an implementation. + * + * Filter-specific implementation of the joint limits enforce algorithm for multiple dependent + * physical quantities. + * + * \param[in] current_joint_states current joint states a robot is in. + * \param[in,out] desired_joint_states joint state that should be adjusted to obey the limits. + * \param[in] dt time delta to calculate missing integrals and derivation in joint limits. + * \returns true if limits are enforced, otherwise false. + */ + JOINT_LIMITS_PUBLIC virtual bool on_enforce( + JointLimitsStateDataType & current_joint_states, + JointLimitsStateDataType & desired_joint_states, const rclcpp::Duration & dt) = 0; + + /** \brief Method is realized by an implementation. + * + * Filter-specific implementation of the joint limits enforce algorithm for single physical + * quantity. + * This method might use "effort" limits since they are often used as wild-card. + * Check the documentation of the exact implementation for more details. + * + * \param[in,out] desired_joint_states joint state that should be adjusted to obey the limits. + * \returns true if limits are enforced, otherwise false. + */ + JOINT_LIMITS_PUBLIC virtual bool on_enforce(std::vector & desired_joint_states) = 0; + + size_t number_of_joints_; + std::vector joint_names_; + std::vector joint_limits_; + rclcpp::node_interfaces::NodeParametersInterface::SharedPtr node_param_itf_; + rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr node_logging_itf_; + +private: + rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr parameter_callback_; + realtime_tools::RealtimeBuffer> updated_limits_; +}; + +} // namespace joint_limits + +#endif // JOINT_LIMITS__JOINT_LIMITER_INTERFACE_HPP_ diff --git a/joint_limits/include/joint_limits/joint_limits_rosparam.hpp b/joint_limits/include/joint_limits/joint_limits_rosparam.hpp index 2f32d49271..b23607f53d 100644 --- a/joint_limits/include/joint_limits/joint_limits_rosparam.hpp +++ b/joint_limits/include/joint_limits/joint_limits_rosparam.hpp @@ -19,6 +19,7 @@ #include #include +#include #include "joint_limits/joint_limits.hpp" #include "rclcpp/rclcpp.hpp" @@ -96,8 +97,6 @@ inline bool declare_parameters( auto_declare( param_itf, param_base_name + ".max_position", std::numeric_limits::quiet_NaN()); auto_declare(param_itf, param_base_name + ".has_velocity_limits", false); - auto_declare( - param_itf, param_base_name + ".min_velocity", std::numeric_limits::quiet_NaN()); auto_declare( param_itf, param_base_name + ".max_velocity", std::numeric_limits::quiet_NaN()); auto_declare(param_itf, param_base_name + ".has_acceleration_limits", false); @@ -237,7 +236,6 @@ inline bool get_joint_limits( !param_itf->has_parameter(param_base_name + ".min_position") && !param_itf->has_parameter(param_base_name + ".max_position") && !param_itf->has_parameter(param_base_name + ".has_velocity_limits") && - !param_itf->has_parameter(param_base_name + ".min_velocity") && !param_itf->has_parameter(param_base_name + ".max_velocity") && !param_itf->has_parameter(param_base_name + ".has_acceleration_limits") && !param_itf->has_parameter(param_base_name + ".max_acceleration") && @@ -247,12 +245,7 @@ inline bool get_joint_limits( !param_itf->has_parameter(param_base_name + ".max_jerk") && !param_itf->has_parameter(param_base_name + ".has_effort_limits") && !param_itf->has_parameter(param_base_name + ".max_effort") && - !param_itf->has_parameter(param_base_name + ".angle_wraparound") && - !param_itf->has_parameter(param_base_name + ".has_soft_limits") && - !param_itf->has_parameter(param_base_name + ".k_position") && - !param_itf->has_parameter(param_base_name + ".k_velocity") && - !param_itf->has_parameter(param_base_name + ".soft_lower_limit") && - !param_itf->has_parameter(param_base_name + ".soft_upper_limit")) + !param_itf->has_parameter(param_base_name + ".angle_wraparound")) { RCLCPP_ERROR( logging_itf->get_logger(), @@ -421,6 +414,218 @@ inline bool get_joint_limits( lifecycle_node->get_node_logging_interface(), limits); } +/** + * Check if any of updated parameters are related to JointLimits. + * + * This method is intended to be used in the parameters update callback. + * It is recommended that it's result is temporarily stored and synchronized with the JointLimits + * structure in the main loop. + * + * \param[in] joint_name Name of the joint whose limits should be checked. + * \param[in] parameters List of the parameters that should be checked if they belong to this + * structure and that are used for updating the internal values. + * \param[in] logging_itf node logging interface to provide log errors. + * \param[out] updated_limits structure with updated JointLimits. It is recommended that the + * currently used limits are stored into this structure before calling this method. + */ +inline bool check_for_limits_update( + const std::string & joint_name, const std::vector & parameters, + const rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr & logging_itf, + JointLimits & updated_limits) +{ + const std::string param_base_name = "joint_limits." + joint_name; + bool changed = false; + + // update first numerical values to make later checks for "has" limits members + for (auto & parameter : parameters) + { + const std::string param_name = parameter.get_name(); + try + { + if (param_name == param_base_name + ".min_position") + { + changed = updated_limits.min_position != parameter.get_value(); + updated_limits.min_position = parameter.get_value(); + } + else if (param_name == param_base_name + ".max_position") + { + changed = updated_limits.max_position != parameter.get_value(); + updated_limits.max_position = parameter.get_value(); + } + else if (param_name == param_base_name + ".max_velocity") + { + changed = updated_limits.max_velocity != parameter.get_value(); + updated_limits.max_velocity = parameter.get_value(); + } + else if (param_name == param_base_name + ".max_acceleration") + { + changed = updated_limits.max_acceleration != parameter.get_value(); + updated_limits.max_acceleration = parameter.get_value(); + } + else if (param_name == param_base_name + ".max_deceleration") + { + changed = updated_limits.max_deceleration != parameter.get_value(); + updated_limits.max_deceleration = parameter.get_value(); + } + else if (param_name == param_base_name + ".max_jerk") + { + changed = updated_limits.max_jerk != parameter.get_value(); + updated_limits.max_jerk = parameter.get_value(); + } + else if (param_name == param_base_name + ".max_effort") + { + changed = updated_limits.max_effort != parameter.get_value(); + updated_limits.max_effort = parameter.get_value(); + } + } + catch (const rclcpp::exceptions::InvalidParameterTypeException & e) + { + RCLCPP_WARN(logging_itf->get_logger(), "Please use the right type: %s", e.what()); + } + } + + for (auto & parameter : parameters) + { + const std::string param_name = parameter.get_name(); + try + { + if (param_name == param_base_name + ".has_position_limits") + { + updated_limits.has_position_limits = parameter.get_value(); + if (updated_limits.has_position_limits) + { + if (std::isnan(updated_limits.min_position) || std::isnan(updated_limits.max_position)) + { + RCLCPP_WARN( + logging_itf->get_logger(), + "PARAMETER NOT UPDATED: Position limits can not be used, i.e., " + "'has_position_limits' flag can not be set, if 'min_position' " + "and 'max_position' are not set or not have valid double values."); + updated_limits.has_position_limits = false; + } + else if (updated_limits.min_position >= updated_limits.max_position) + { + RCLCPP_WARN( + logging_itf->get_logger(), + "PARAMETER NOT UPDATED: Position limits can not be used, i.e., " + "'has_position_limits' flag can not be set, if not " + "'min_position' < 'max_position'"); + updated_limits.has_position_limits = false; + } + else + { + changed = true; + } + } + } + else if (param_name == param_base_name + ".has_velocity_limits") + { + updated_limits.has_velocity_limits = parameter.get_value(); + if (updated_limits.has_velocity_limits && std::isnan(updated_limits.max_velocity)) + { + RCLCPP_WARN( + logging_itf->get_logger(), + "PARAMETER NOT UPDATED: 'has_velocity_limits' flag can not be set if 'min_velocity' " + "and 'max_velocity' are not set or not have valid double values."); + updated_limits.has_velocity_limits = false; + } + else + { + changed = true; + } + } + else if (param_name == param_base_name + ".has_acceleration_limits") + { + updated_limits.has_acceleration_limits = parameter.get_value(); + if (updated_limits.has_acceleration_limits && std::isnan(updated_limits.max_acceleration)) + { + RCLCPP_WARN( + logging_itf->get_logger(), + "PARAMETER NOT UPDATED: 'has_acceleration_limits' flag can not be set if " + "'max_acceleration' is not set or not have valid double values."); + updated_limits.has_acceleration_limits = false; + } + else + { + changed = true; + } + } + else if (param_name == param_base_name + ".has_deceleration_limits") + { + updated_limits.has_deceleration_limits = parameter.get_value(); + if (updated_limits.has_deceleration_limits && std::isnan(updated_limits.max_deceleration)) + { + RCLCPP_WARN( + logging_itf->get_logger(), + "PARAMETER NOT UPDATED: 'has_deceleration_limits' flag can not be set if " + "'max_deceleration' is not set or not have valid double values."); + updated_limits.has_deceleration_limits = false; + } + else + { + changed = true; + } + } + else if (param_name == param_base_name + ".has_jerk_limits") + { + updated_limits.has_jerk_limits = parameter.get_value(); + if (updated_limits.has_jerk_limits && std::isnan(updated_limits.max_jerk)) + { + RCLCPP_WARN( + logging_itf->get_logger(), + "PARAMETER NOT UPDATED: 'has_jerk_limits' flag can not be set if 'max_jerk' is not set " + "or not have valid double values."); + updated_limits.has_jerk_limits = false; + } + else + { + changed = true; + } + } + else if (param_name == param_base_name + ".has_effort_limits") + { + updated_limits.has_effort_limits = parameter.get_value(); + if (updated_limits.has_effort_limits && std::isnan(updated_limits.max_effort)) + { + RCLCPP_WARN( + logging_itf->get_logger(), + "PARAMETER NOT UPDATED: 'has_effort_limits' flag can not be set if 'max_effort' is not " + "set or not have valid double values."); + updated_limits.has_effort_limits = false; + } + else + { + changed = true; + } + } + else if (param_name == param_base_name + ".angle_wraparound") + { + updated_limits.angle_wraparound = parameter.get_value(); + if (updated_limits.angle_wraparound && updated_limits.has_position_limits) + { + RCLCPP_WARN( + logging_itf->get_logger(), + "PARAMETER NOT UPDATED: 'angle_wraparound' flag can not be set if " + "'has_position_limits' flag is set."); + updated_limits.angle_wraparound = false; + } + else + { + changed = true; + } + } + } + catch (const rclcpp::exceptions::InvalidParameterTypeException & e) + { + RCLCPP_WARN( + logging_itf->get_logger(), "PARAMETER NOT UPDATED: Please use the right type: %s", + e.what()); + } + } + + return changed; +} + /// Populate a SoftJointLimits instance from the ROS parameter server. /** * It is assumed that the parameter structure is the following: @@ -550,6 +755,85 @@ inline bool get_joint_limits( lifecycle_node->get_node_logging_interface(), soft_limits); } +/** + * Check if any of updated parameters are related to SoftJointLimits. + * + * This method is intended to be used in the parameters update callback. + * It is recommended that it's result is temporarily stored and synchronized with the + * SoftJointLimits structure in the main loop. + * + * \param[in] joint_name Name of the joint whose limits should be checked. + * \param[in] parameters List of the parameters that should be checked if they belong to this + * structure and that are used for updating the internal values. + * \param[in] logging_itf node logging interface to provide log errors. + * \param[out] updated_limits structure with updated SoftJointLimits. It is recommended that the + * currently used limits are stored into this structure before calling this method. + */ +inline bool check_for_limits_update( + const std::string & joint_name, const std::vector & parameters, + const rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr & logging_itf, + SoftJointLimits & updated_limits) +{ + const std::string param_base_name = "joint_limits." + joint_name; + bool changed = false; + + for (auto & parameter : parameters) + { + const std::string param_name = parameter.get_name(); + try + { + if (param_name == param_base_name + ".has_soft_limits") + { + if (!parameter.get_value()) + { + RCLCPP_WARN( + logging_itf->get_logger(), + "Parameter 'has_soft_limits' is not set, therefore the limits will not be updated!"); + return false; + } + } + } + catch (const rclcpp::exceptions::InvalidParameterTypeException & e) + { + RCLCPP_INFO(logging_itf->get_logger(), "Please use the right type: %s", e.what()); + } + } + + for (auto & parameter : parameters) + { + const std::string param_name = parameter.get_name(); + try + { + if (param_name == param_base_name + ".k_position") + { + changed = updated_limits.k_position != parameter.get_value(); + updated_limits.k_position = parameter.get_value(); + } + else if (param_name == param_base_name + ".k_velocity") + { + changed = updated_limits.k_velocity != parameter.get_value(); + updated_limits.k_velocity = parameter.get_value(); + } + else if (param_name == param_base_name + ".soft_lower_limit") + { + changed = updated_limits.min_position != parameter.get_value(); + updated_limits.min_position = parameter.get_value(); + } + else if (param_name == param_base_name + ".soft_upper_limit") + { + changed = updated_limits.max_position != parameter.get_value(); + updated_limits.max_position = parameter.get_value(); + } + } + catch (const rclcpp::exceptions::InvalidParameterTypeException & e) + { + RCLCPP_INFO(logging_itf->get_logger(), "Please use the right type: %s", e.what()); + } + } + + return changed; +} + } // namespace joint_limits #endif // JOINT_LIMITS__JOINT_LIMITS_ROSPARAM_HPP_ diff --git a/joint_limits/include/joint_limits/joint_saturation_limiter.hpp b/joint_limits/include/joint_limits/joint_saturation_limiter.hpp new file mode 100644 index 0000000000..3641fc5c75 --- /dev/null +++ b/joint_limits/include/joint_limits/joint_saturation_limiter.hpp @@ -0,0 +1,105 @@ +// Copyright (c) 2024, PickNik Inc. +// +// 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. + +/// \author Dr. Denis Stogl + +#ifndef JOINT_LIMITS__JOINT_SATURATION_LIMITER_HPP_ +#define JOINT_LIMITS__JOINT_SATURATION_LIMITER_HPP_ + +#include +#include +#include +#include + +#include "joint_limits/joint_limiter_interface.hpp" +#include "joint_limits/joint_limits.hpp" +#include "rclcpp/clock.hpp" +#include "rclcpp/duration.hpp" + +namespace joint_limits +{ +/** + * Joint Saturation Limiter limits joints' position, velocity and acceleration by clamping values + * to its minimal and maximal allowed values. Since the position, velocity and accelerations are + * variables in physical relation, it might be that some values are limited lower then specified + * limit. For example, if a joint is close to its position limit, velocity and acceleration will be + * reduced accordingly. + */ +template +class JointSaturationLimiter : public JointLimiterInterface +{ +public: + /** \brief Constructor */ + JOINT_LIMITS_PUBLIC JointSaturationLimiter(); + + /** \brief Destructor */ + JOINT_LIMITS_PUBLIC ~JointSaturationLimiter(); + + JOINT_LIMITS_PUBLIC bool on_init() override { return true; } + + JOINT_LIMITS_PUBLIC bool on_configure( + const trajectory_msgs::msg::JointTrajectoryPoint & /*current_joint_states*/) override + { + return true; + } + + /** \brief Enforce joint limits to desired position, velocity and acceleration using clamping. + * Class implements this method accepting following data types: + * - trajectory_msgs::msg::JointTrajectoryPoint: limiting position, velocity and acceleration; + * + * Implementation of saturation approach for joints with position, velocity or acceleration limits + * and values. First, position limits are checked to adjust desired velocity accordingly, then + * velocity and finally acceleration. + * The method support partial existence of limits, e.g., missing position limits for continuous + * joins. + * + * \param[in] current_joint_states current joint states a robot is in. + * \param[in,out] desired_joint_states joint state that should be adjusted to obey the limits. + * \param[in] dt time delta to calculate missing integrals and derivation in joint limits. + * \returns true if limits are enforced, otherwise false. + */ + JOINT_LIMITS_PUBLIC bool on_enforce( + trajectory_msgs::msg::JointTrajectoryPoint & current_joint_states, + trajectory_msgs::msg::JointTrajectoryPoint & desired_joint_states, + const rclcpp::Duration & dt) override; + + /** \brief Enforce joint limits to desired arbitrary physical quantity. + * Additional, commonly used method for enforcing limits by clamping desired input value. + * The limit is defined in effort limits of the `joint::limits/JointLimit` structure. + * + * If `has_effort_limits` is set to false, the limits will be not enforced to a joint. + * + * \param[in,out] desired_joint_states physical quantity that should be adjusted to obey the + * limits. \returns true if limits are enforced, otherwise false. + */ + JOINT_LIMITS_PUBLIC bool on_enforce(std::vector & desired_joint_states) override; + +private: + rclcpp::Clock::SharedPtr clock_; +}; + +template +JointSaturationLimiter::JointSaturationLimiter() : JointLimiterInterface() +{ + clock_ = std::make_shared(rclcpp::Clock(RCL_ROS_TIME)); +} + +template +JointSaturationLimiter::~JointSaturationLimiter() +{ +} + +} // namespace joint_limits + +#endif // JOINT_LIMITS__JOINT_SATURATION_LIMITER_HPP_ diff --git a/joint_limits/include/joint_limits/visibility_control.h b/joint_limits/include/joint_limits/visibility_control.h new file mode 100644 index 0000000000..54edbc15f3 --- /dev/null +++ b/joint_limits/include/joint_limits/visibility_control.h @@ -0,0 +1,49 @@ +// Copyright (c) 2024, Stogl Robotics Consulting UG (haftungsbeschränkt) +// +// 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 JOINT_LIMITS__VISIBILITY_CONTROL_H_ +#define JOINT_LIMITS__VISIBILITY_CONTROL_H_ + +// This logic was borrowed (then namespaced) from the examples on the gcc wiki: +// https://gcc.gnu.org/wiki/Visibility + +#if defined _WIN32 || defined __CYGWIN__ +#ifdef __GNUC__ +#define JOINT_LIMITS_EXPORT __attribute__((dllexport)) +#define JOINT_LIMITS_IMPORT __attribute__((dllimport)) +#else +#define JOINT_LIMITS_EXPORT __declspec(dllexport) +#define JOINT_LIMITS_IMPORT __declspec(dllimport) +#endif +#ifdef JOINT_LIMITS_BUILDING_DLL +#define JOINT_LIMITS_PUBLIC JOINT_LIMITS_EXPORT +#else +#define JOINT_LIMITS_PUBLIC JOINT_LIMITS_IMPORT +#endif +#define JOINT_LIMITS_PUBLIC_TYPE JOINT_LIMITS_PUBLIC +#define JOINT_LIMITS_LOCAL +#else +#define JOINT_LIMITS_EXPORT __attribute__((visibility("default"))) +#define JOINT_LIMITS_IMPORT +#if __GNUC__ >= 4 +#define JOINT_LIMITS_PUBLIC __attribute__((visibility("default"))) +#define JOINT_LIMITS_LOCAL __attribute__((visibility("hidden"))) +#else +#define JOINT_LIMITS_PUBLIC +#define JOINT_LIMITS_LOCAL +#endif +#define JOINT_LIMITS_PUBLIC_TYPE +#endif + +#endif // JOINT_LIMITS__VISIBILITY_CONTROL_H_ diff --git a/joint_limits/joint_limiters.xml b/joint_limits/joint_limiters.xml new file mode 100644 index 0000000000..a49d88fbc9 --- /dev/null +++ b/joint_limits/joint_limiters.xml @@ -0,0 +1,11 @@ + + + + + Simple joint limiter using clamping approach. Warning: clamping can cause discontinuities. + + + + diff --git a/joint_limits/package.xml b/joint_limits/package.xml index 9deb33af7d..6e8f35255d 100644 --- a/joint_limits/package.xml +++ b/joint_limits/package.xml @@ -1,10 +1,10 @@ joint_limits - 4.4.0 - Interfaces for handling of joint limits for controllers or hardware. + 4.13.0 + Package with interfaces for handling of joint limits in controllers or in hardware. The package also implements Saturation Joint Limiter for position-velocity-acceleration set and other individual interfaces. Bence Magyar - Denis Štogl + Denis Štogl Apache License 2.0 @@ -13,13 +13,21 @@ https://github.com/ros-controls/ros2_control ament_cmake + ament_cmake_gen_version_h + backward_ros + pluginlib + realtime_tools rclcpp rclcpp_lifecycle + trajectory_msgs urdf - launch_testing_ament_cmake + ament_cmake_gmock ament_cmake_gtest + generate_parameter_library + launch_ros + launch_testing_ament_cmake ament_cmake diff --git a/joint_limits/src/joint_limiter_interface.cpp b/joint_limits/src/joint_limiter_interface.cpp new file mode 100644 index 0000000000..aea0e6704d --- /dev/null +++ b/joint_limits/src/joint_limiter_interface.cpp @@ -0,0 +1,21 @@ +// Copyright (c) 2024, Stogl Robotics Consulting UG (haftungsbeschränkt) +// +// 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. + +/// \author Dr. Denis Stogl + +#include "joint_limits/joint_limiter_interface.hpp" + +namespace joint_limits +{ +} // namespace joint_limits diff --git a/joint_limits/src/joint_saturation_limiter.cpp b/joint_limits/src/joint_saturation_limiter.cpp new file mode 100644 index 0000000000..2f8ac9636f --- /dev/null +++ b/joint_limits/src/joint_saturation_limiter.cpp @@ -0,0 +1,478 @@ +// Copyright (c) 2024, PickNik Inc. +// +// 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. + +/// \authors Nathan Brooks, Dr. Denis Stogl, Guillaume Walck + +#include "joint_limits/joint_saturation_limiter.hpp" + +#include + +#include "rclcpp/duration.hpp" +#include "rcutils/logging_macros.h" + +constexpr size_t ROS_LOG_THROTTLE_PERIOD = 1 * 1000; // Milliseconds to throttle logs inside loops +constexpr double VALUE_CONSIDERED_ZERO = 1e-10; + +namespace joint_limits +{ +template <> +bool JointSaturationLimiter::on_enforce( + trajectory_msgs::msg::JointTrajectoryPoint & current_joint_states, + trajectory_msgs::msg::JointTrajectoryPoint & desired_joint_states, const rclcpp::Duration & dt) +{ + bool limits_enforced = false; + + const auto dt_seconds = dt.seconds(); + // negative or null is not allowed + if (dt_seconds <= 0.0) + { + return false; + } + + // TODO(gwalck) compute if the max are not implicitly violated with the given dt + // e.g. for max vel 2.0 and max acc 5.0, with dt >0.4 + // velocity max is implicitly already violated due to max_acc * dt > 2.0 + + // check for required inputs combination + bool has_desired_position = (desired_joint_states.positions.size() == number_of_joints_); + bool has_desired_velocity = (desired_joint_states.velocities.size() == number_of_joints_); + bool has_desired_acceleration = (desired_joint_states.accelerations.size() == number_of_joints_); + bool has_current_position = (current_joint_states.positions.size() == number_of_joints_); + bool has_current_velocity = (current_joint_states.velocities.size() == number_of_joints_); + + // pos state and vel or pos cmd is required, vel state is optional + if (!(has_current_position && (has_desired_position || has_desired_velocity))) + { + return false; + } + + if (!has_current_velocity) + { + // First update() after activating does not have velocity available, assume 0 + current_joint_states.velocities.resize(number_of_joints_, 0.0); + } + + // TODO(destogl): please check if we get too much malloc from this initialization, + // if so then we should use members instead local variables and initialize them in other method + std::vector desired_pos(number_of_joints_); + std::vector desired_vel(number_of_joints_); + std::vector desired_acc(number_of_joints_); + std::vector expected_vel(number_of_joints_); + std::vector expected_pos(number_of_joints_); + + // limits triggered + std::vector limited_jnts_pos, limited_jnts_vel, limited_jnts_acc, limited_jnts_dec; + + bool braking_near_position_limit_triggered = false; + + for (size_t index = 0; index < number_of_joints_; ++index) + { + if (has_desired_position) + { + desired_pos[index] = desired_joint_states.positions[index]; + } + if (has_desired_velocity) + { + desired_vel[index] = desired_joint_states.velocities[index]; + } + if (has_desired_acceleration) + { + desired_acc[index] = desired_joint_states.accelerations[index]; + } + + if (has_desired_position) + { + // limit position + if (joint_limits_[index].has_position_limits) + { + // clamp input pos_cmd + auto pos = std::clamp( + desired_pos[index], joint_limits_[index].min_position, joint_limits_[index].max_position); + if (pos != desired_pos[index]) + { + desired_pos[index] = pos; + limited_jnts_pos.emplace_back(joint_names_[index]); + limits_enforced = true; + } + } + // priority to pos_cmd derivative over cmd_vel when not defined. If done always then we might + // get jumps in the velocity based on the system's dynamics. Position limit clamping is done + // below once again. + const double position_difference = desired_pos[index] - current_joint_states.positions[index]; + if ( + std::fabs(position_difference) > VALUE_CONSIDERED_ZERO && + std::fabs(desired_vel[index]) <= VALUE_CONSIDERED_ZERO) + { + desired_vel[index] = position_difference / dt_seconds; + } + } + + // limit velocity + if (joint_limits_[index].has_velocity_limits) + { + // if desired velocity is not defined calculate it from positions + if (std::fabs(desired_vel[index]) <= VALUE_CONSIDERED_ZERO || std::isnan(desired_vel[index])) + { + desired_vel[index] = + (desired_pos[index] - current_joint_states.positions[index]) / dt_seconds; + } + // clamp input vel_cmd + if (std::fabs(desired_vel[index]) > joint_limits_[index].max_velocity) + { + desired_vel[index] = std::copysign(joint_limits_[index].max_velocity, desired_vel[index]); + limited_jnts_vel.emplace_back(joint_names_[index]); + limits_enforced = true; + + // recompute pos_cmd if needed + if (has_desired_position) + { + desired_pos[index] = + current_joint_states.positions[index] + desired_vel[index] * dt_seconds; + } + + desired_acc[index] = + (desired_vel[index] - current_joint_states.velocities[index]) / dt_seconds; + } + } + + // limit acceleration + if ( + joint_limits_[index].has_acceleration_limits || joint_limits_[index].has_deceleration_limits) + { + // if(has_current_velocity) + if (1) // for now use a zero velocity if not provided + { + // limiting acc or dec function + auto apply_acc_or_dec_limit = [&]( + const double max_acc_or_dec, std::vector & acc, + std::vector & limited_jnts) -> bool + { + if (std::fabs(acc[index]) > max_acc_or_dec) + { + acc[index] = std::copysign(max_acc_or_dec, acc[index]); + limited_jnts.emplace_back(joint_names_[index]); + limits_enforced = true; + return true; + } + else + { + return false; + } + }; + + // if desired acceleration if not provided compute it from desired_vel and vel_state + if ( + std::fabs(desired_acc[index]) <= VALUE_CONSIDERED_ZERO || std::isnan(desired_acc[index])) + { + desired_acc[index] = + (desired_vel[index] - current_joint_states.velocities[index]) / dt_seconds; + } + + // check if decelerating - if velocity is changing toward 0 + bool deceleration_limit_applied = false; + bool limit_applied = false; + if ( + (desired_acc[index] < 0 && current_joint_states.velocities[index] > 0) || + (desired_acc[index] > 0 && current_joint_states.velocities[index] < 0)) + { + // limit deceleration + if (joint_limits_[index].has_deceleration_limits) + { + limit_applied = apply_acc_or_dec_limit( + joint_limits_[index].max_deceleration, desired_acc, limited_jnts_dec); + deceleration_limit_applied = true; + } + } + + // limit acceleration (fallback to acceleration if no deceleration limits) + if (joint_limits_[index].has_acceleration_limits && !deceleration_limit_applied) + { + limit_applied = apply_acc_or_dec_limit( + joint_limits_[index].max_acceleration, desired_acc, limited_jnts_acc); + } + + if (limit_applied) + { + // vel_cmd from integration of desired_acc, needed even if no vel output + desired_vel[index] = + current_joint_states.velocities[index] + desired_acc[index] * dt_seconds; + if (has_desired_position) + { + // pos_cmd from from double integration of desired_acc + desired_pos[index] = current_joint_states.positions[index] + + current_joint_states.velocities[index] * dt_seconds + + 0.5 * desired_acc[index] * dt_seconds * dt_seconds; + } + } + } + // else we cannot compute acc, so not limiting it + } + + // plan ahead for position limits + if (joint_limits_[index].has_position_limits) + { + if (has_desired_velocity && !has_desired_position) + { + // Check immediate next step when using vel_cmd only, other cases already handled + // integrate pos + expected_pos[index] = + current_joint_states.positions[index] + desired_vel[index] * dt_seconds; + // if expected_pos over limit + auto pos = std::clamp( + expected_pos[index], joint_limits_[index].min_position, + joint_limits_[index].max_position); + if (pos != expected_pos[index]) + { + // TODO(gwalck) compute vel_cmd that would permit to slow down in time at full + // deceleration in any case limit pos to max + expected_pos[index] = pos; + // and recompute vel_cmd that would lead to pos_max (not ideal as velocity would not be + // zero) + desired_vel[index] = + (expected_pos[index] - current_joint_states.positions[index]) / dt_seconds; + limited_jnts_pos.emplace_back(joint_names_[index]); + limits_enforced = true; + } + } + + // Check that stopping distance is within joint limits + // Slow down all joints at maximum decel if any don't have stopping distance within joint + // limits + + // delta_x = (v2*v2 - v1*v1) / (2*a) + // stopping_distance = (- v1*v1) / (2*max_acceleration) + // Here we assume we will not trigger velocity limits while maximally decelerating. + // This is a valid assumption if we are not currently at a velocity limit since we are just + // coming to a rest. + double stopping_deccel = std::fabs(desired_vel[index] / dt_seconds); + if (joint_limits_[index].has_deceleration_limits) + { + stopping_deccel = joint_limits_[index].max_deceleration; + } + else if (joint_limits_[index].has_acceleration_limits) + { + stopping_deccel = joint_limits_[index].max_acceleration; + } + + double stopping_distance = + std::fabs((-desired_vel[index] * desired_vel[index]) / (2 * stopping_deccel)); + // compute stopping duration at stopping_deccel + double stopping_duration = std::fabs((desired_vel[index]) / (stopping_deccel)); + + // Check that joint limits are beyond stopping_distance and desired_velocity is towards + // that limit + if ( + (desired_vel[index] < 0 && + (current_joint_states.positions[index] - joint_limits_[index].min_position < + stopping_distance)) || + (desired_vel[index] > 0 && + (joint_limits_[index].max_position - current_joint_states.positions[index] < + stopping_distance))) + { + limited_jnts_pos.emplace_back(joint_names_[index]); + braking_near_position_limit_triggered = true; + limits_enforced = true; + } + else + { + // compute the travel_distance at new desired velocity, with best case duration + // stopping_duration + double motion_after_stopping_duration = desired_vel[index] * stopping_duration; + // re-check what happens if we don't slow down + if ( + (desired_vel[index] < 0 && + (current_joint_states.positions[index] - joint_limits_[index].min_position < + motion_after_stopping_duration)) || + (desired_vel[index] > 0 && + (joint_limits_[index].max_position - current_joint_states.positions[index] < + motion_after_stopping_duration))) + { + limited_jnts_pos.emplace_back(joint_names_[index]); + braking_near_position_limit_triggered = true; + limits_enforced = true; + } + // else no need to slow down. in worse case we won't hit the limit at current velocity + } + } + } + + // update variables according to triggers + if (braking_near_position_limit_triggered) + { + // this limit applies to all joints even if a single one is triggered + for (size_t index = 0; index < number_of_joints_; ++index) + { + // Compute accel to stop + // Here we aren't explicitly maximally decelerating, but for joints near their limits this + // should still result in max decel being used + desired_acc[index] = -current_joint_states.velocities[index] / dt_seconds; + if (joint_limits_[index].has_deceleration_limits) + { + desired_acc[index] = std::copysign( + std::min(std::fabs(desired_acc[index]), joint_limits_[index].max_deceleration), + desired_acc[index]); + } + else if (joint_limits_[index].has_acceleration_limits) + { + desired_acc[index] = std::copysign( + std::min(std::fabs(desired_acc[index]), joint_limits_[index].max_acceleration), + desired_acc[index]); + } + + // Recompute velocity and position + if (has_desired_velocity) + { + desired_vel[index] = + current_joint_states.velocities[index] + desired_acc[index] * dt_seconds; + } + if (has_desired_position) + { + desired_pos[index] = current_joint_states.positions[index] + + current_joint_states.velocities[index] * dt_seconds + + 0.5 * desired_acc[index] * dt_seconds * dt_seconds; + } + } + std::ostringstream ostr; + for (auto jnt : limited_jnts_pos) + { + ostr << jnt << " "; + } + ostr << "\b \b"; // erase last character + RCLCPP_WARN_STREAM_THROTTLE( + node_logging_itf_->get_logger(), *clock_, ROS_LOG_THROTTLE_PERIOD, + "Joint(s) [" << ostr.str().c_str() + << "] would exceed position limits" + " if continuing at current state, limiting all joints"); + } + + // display limitations + + // if position limiting + if (limited_jnts_pos.size() > 0) + { + std::ostringstream ostr; + for (auto jnt : limited_jnts_pos) + { + ostr << jnt << " "; + } + ostr << "\b \b"; // erase last character + RCLCPP_WARN_STREAM_THROTTLE( + node_logging_itf_->get_logger(), *clock_, ROS_LOG_THROTTLE_PERIOD, + "Joint(s) [" << ostr.str().c_str() << "] would exceed position limits, limiting"); + } + + if (limited_jnts_vel.size() > 0) + { + std::ostringstream ostr; + for (auto jnt : limited_jnts_vel) + { + ostr << jnt << " "; + } + ostr << "\b \b"; // erase last character + RCLCPP_WARN_STREAM_THROTTLE( + node_logging_itf_->get_logger(), *clock_, ROS_LOG_THROTTLE_PERIOD, + "Joint(s) [" << ostr.str().c_str() << "] would exceed velocity limits, limiting"); + } + + if (limited_jnts_acc.size() > 0) + { + std::ostringstream ostr; + for (auto jnt : limited_jnts_acc) + { + ostr << jnt << " "; + } + ostr << "\b \b"; // erase last character + RCLCPP_WARN_STREAM_THROTTLE( + node_logging_itf_->get_logger(), *clock_, ROS_LOG_THROTTLE_PERIOD, + "Joint(s) [" << ostr.str().c_str() << "] would exceed acceleration limits, limiting"); + } + + if (limited_jnts_dec.size() > 0) + { + std::ostringstream ostr; + for (auto jnt : limited_jnts_dec) + { + ostr << jnt << " "; + } + ostr << "\b \b"; // erase last character + RCLCPP_WARN_STREAM_THROTTLE( + node_logging_itf_->get_logger(), *clock_, ROS_LOG_THROTTLE_PERIOD, + "Joint(s) [" << ostr.str().c_str() << "] would exceed deceleration limits, limiting"); + } + + if (has_desired_position) + { + desired_joint_states.positions = desired_pos; + } + if (has_desired_velocity) + { + desired_joint_states.velocities = desired_vel; + } + if (has_desired_acceleration) + { + desired_joint_states.accelerations = desired_acc; + } + + return limits_enforced; +} + +template <> +bool JointSaturationLimiter::on_enforce(std::vector & desired_joint_states) +{ + std::vector limited_joints_effort; + bool limits_enforced = false; + + bool has_cmd = (desired_joint_states.size() == number_of_joints_); + if (!has_cmd) + { + return false; + } + + for (size_t index = 0; index < number_of_joints_; ++index) + { + if (joint_limits_[index].has_effort_limits) + { + double max_effort = joint_limits_[index].max_effort; + if (std::fabs(desired_joint_states[index]) > max_effort) + { + desired_joint_states[index] = std::copysign(max_effort, desired_joint_states[index]); + limited_joints_effort.emplace_back(joint_names_[index]); + limits_enforced = true; + } + } + } + + if (limited_joints_effort.size() > 0) + { + std::ostringstream ostr; + for (auto jnt : limited_joints_effort) + { + ostr << jnt << " "; + } + ostr << "\b \b"; // erase last character + RCLCPP_WARN_STREAM_THROTTLE( + node_logging_itf_->get_logger(), *clock_, ROS_LOG_THROTTLE_PERIOD, + "Joint(s) [" << ostr.str().c_str() << "] would exceed effort limits, limiting"); + } + + return limits_enforced; +} + +} // namespace joint_limits + +#include "pluginlib/class_list_macros.hpp" + +PLUGINLIB_EXPORT_CLASS( + joint_limits::JointSaturationLimiter, + joint_limits::JointLimiterInterface) diff --git a/joint_limits/test/joint_saturation_limiter_param.yaml b/joint_limits/test/joint_saturation_limiter_param.yaml new file mode 100644 index 0000000000..f025421519 --- /dev/null +++ b/joint_limits/test/joint_saturation_limiter_param.yaml @@ -0,0 +1,57 @@ +joint_saturation_limiter: + ros__parameters: + joint_limits: + # Get full specification from parameter server + foo_joint: + has_position_limits: true + min_position: -5.0 + max_position: 5.0 + has_velocity_limits: true + max_velocity: 2.0 + has_acceleration_limits: true + max_acceleration: 5.0 + has_deceleration_limits: true + max_deceleration: 7.5 + has_jerk_limits: true + max_jerk: 100.0 + has_effort_limits: true + max_effort: 20.0 + angle_wraparound: true # should be ignored, has position limits + has_soft_limits: true + k_position: 10.0 + k_velocity: 20.0 + soft_lower_limit: 0.1 + soft_upper_limit: 0.9 + + foo_joint_no_effort: + has_position_limits: true + min_position: -5.0 + max_position: 5.0 + has_velocity_limits: true + max_velocity: 2.0 + has_acceleration_limits: true + max_acceleration: 5.0 + has_deceleration_limits: true + max_deceleration: 7.5 + has_jerk_limits: true + max_jerk: 100.0 + has_effort_limits: false + max_effort: 20.0 + angle_wraparound: true # should be ignored, has position limits + has_soft_limits: true + k_position: 10.0 + k_velocity: 20.0 + soft_lower_limit: 0.1 + soft_upper_limit: 0.9 + +joint_saturation_limiter_nodeclimit: + ros__parameters: + joint_limits: + foo_joint: + has_position_limits: true + min_position: -5.0 + max_position: 5.0 + has_velocity_limits: true + max_velocity: 2.0 + has_acceleration_limits: true + max_acceleration: 5.0 diff --git a/joint_limits/test/test_joint_saturation_limiter.cpp b/joint_limits/test/test_joint_saturation_limiter.cpp new file mode 100644 index 0000000000..6ecbd46d71 --- /dev/null +++ b/joint_limits/test/test_joint_saturation_limiter.cpp @@ -0,0 +1,569 @@ +// Copyright (c) 2024, Stogl Robotics Consulting UG (haftungsbeschränkt) +// +// 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. + +/// \author Dr. Denis Stogl, Guillaume Walck + +#include "test_joint_saturation_limiter.hpp" + +TEST_F(JointSaturationLimiterTest, when_loading_limiter_plugin_expect_loaded) +{ + // Test JointSaturationLimiter loading + ASSERT_NO_THROW( + joint_limiter_ = std::unique_ptr( + joint_limiter_loader_.createUnmanagedInstance(joint_limiter_type_))); + ASSERT_NE(joint_limiter_, nullptr); +} + +// NOTE: We accept also if there is no limit defined for a joint. +TEST_F(JointSaturationLimiterTest, when_joint_not_found_expect_init_fail) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + // initialize the limiter + std::vector joint_names = {"bar_joint"}; + ASSERT_TRUE(joint_limiter_->init(joint_names, node_)); + } +} + +TEST_F(JointSaturationLimiterTest, when_invalid_dt_expect_enforce_fail) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + rclcpp::Duration period(0, 0); // 0 second + ASSERT_FALSE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + } +} + +TEST_F(JointSaturationLimiterTest, when_neigher_poscmd_nor_velcmd_expect_enforce_fail) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + // no size check occurs (yet) so expect true + ASSERT_TRUE(joint_limiter_->configure(last_commanded_state_)); + + rclcpp::Duration period(1, 0); // 1 second + // test no desired interface + desired_joint_states_.positions.clear(); + desired_joint_states_.velocities.clear(); + // currently not handled desired_joint_states_.accelerations.clear(); + ASSERT_FALSE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + } +} + +TEST_F(JointSaturationLimiterTest, when_no_posstate_expect_enforce_false) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + joint_limiter_->configure(last_commanded_state_); + + rclcpp::Duration period(1, 0); // 1 second + // test no position interface + current_joint_states_.positions.clear(); + ASSERT_FALSE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // also fail if out of limits + desired_joint_states_.positions[0] = 20.0; + ASSERT_FALSE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + } +} + +TEST_F(JointSaturationLimiterTest, when_no_velstate_expect_limiting) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + joint_limiter_->configure(last_commanded_state_); + + rclcpp::Duration period(1, 0); // 1 second + // test no vel interface + current_joint_states_.velocities.clear(); + ASSERT_FALSE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + // also fail if out of limits + desired_joint_states_.positions[0] = 20.0; + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + } +} + +TEST_F(JointSaturationLimiterTest, when_within_limits_expect_no_limits_applied) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + rclcpp::Duration period(1.0, 0.0); // 1 second + // pos, vel, acc, dec = 1.0, 2.0, 5.0, 7.5 + + // within limits + desired_joint_states_.positions[0] = 1.0; + desired_joint_states_.velocities[0] = 1.0; // valid pos derivatite as well + ASSERT_FALSE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if no limits applied + CHECK_STATE_SINGLE_JOINT( + desired_joint_states_, 0, + 1.0, // pos unchanged + 1.0, // vel unchanged + 1.0 // acc = vel / 1.0 + ); + } +} + +TEST_F(JointSaturationLimiterTest, when_within_limits_expect_no_limits_applied_with_acc) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + rclcpp::Duration period(1.0, 0.0); // 1 second + // pos, vel, acc, dec = 1.0, 2.0, 5.0, 7.5 + + // within limits + desired_joint_states_.positions[0] = 1.0; + desired_joint_states_.velocities[0] = 1.5; // valid pos derivative as well + desired_joint_states_.accelerations[0] = 2.9; // valid pos derivative as well + ASSERT_FALSE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if no limits applied + CHECK_STATE_SINGLE_JOINT( + desired_joint_states_, 0, + 1.0, // pos unchanged + 1.5, // vel unchanged + 2.9 // acc = vel / 1.0 + ); + } +} + +TEST_F(JointSaturationLimiterTest, when_posvel_leads_to_vel_exceeded_expect_limits_enforced) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + rclcpp::Duration period(1.0, 0.0); // 1 second + + // pos/vel cmd ifs + current_joint_states_.positions[0] = -2.0; + desired_joint_states_.positions[0] = 1.0; + // desired velocity exceeds although correctly computed from pos derivative + desired_joint_states_.velocities[0] = 3.0; + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if limits applied + CHECK_STATE_SINGLE_JOINT( + desired_joint_states_, 0, + 0.0, // pos = pos + max_vel * dt + 2.0, // vel limited to max_vel + 2.0 / 1.0 // acc set to vel change/DT + ); + + // check opposite velocity direction (sign copy) + current_joint_states_.positions[0] = 2.0; + desired_joint_states_.positions[0] = -1.0; + // desired velocity exceeds although correctly computed from pos derivative + desired_joint_states_.velocities[0] = -3.0; + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if vel and acc limits applied + CHECK_STATE_SINGLE_JOINT( + desired_joint_states_, 0, + 0.0, // pos = pos - max_vel * dt + -2.0, // vel limited to -max_vel + -2.0 / 1.0 // acc set to vel change/DT + ); + } +} + +TEST_F(JointSaturationLimiterTest, when_posonly_leads_to_vel_exceeded_expect_pos_acc_enforced) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + rclcpp::Duration period(1.0, 0.0); // 1 second + + // set current velocity close to limits to not be blocked by max acc to reach max + current_joint_states_.velocities[0] = 1.9; + // desired pos leads to vel exceeded (4.0 / sec instead of 2.0) + desired_joint_states_.positions[0] = 4.0; + // no vel cmd (will lead to internal computation of velocity) + desired_joint_states_.velocities.clear(); + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if pos and acc limits applied + ASSERT_EQ(desired_joint_states_.positions[0], 2.0); // pos limited to max_vel * DT + // no vel cmd ifs + ASSERT_EQ( + desired_joint_states_.accelerations[0], (2.0 - 1.9) / 1.0); // acc set to vel change/DT + + // check opposite position and direction + current_joint_states_.positions[0] = 0.0; + current_joint_states_.velocities[0] = -1.9; + desired_joint_states_.positions[0] = -4.0; + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if pos and acc limits applied + ASSERT_EQ(desired_joint_states_.positions[0], -2.0); // pos limited to -max_vel * DT + // no vel cmd ifs + ASSERT_EQ( + desired_joint_states_.accelerations[0], (-2.0 + 1.9) / 1.0); // acc set to vel change/DT + } +} + +TEST_F(JointSaturationLimiterTest, when_velonly_leads_to_vel_exceeded_expect_vel_acc_enforced) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + rclcpp::Duration period(1.0, 0.0); // 1 second + + // vel cmd ifs + current_joint_states_.positions[0] = -2.0; + // set current velocity close to limits to not be blocked by max acc to reach max + current_joint_states_.velocities[0] = 1.9; + // no pos cmd + desired_joint_states_.positions.clear(); + // desired velocity exceeds + desired_joint_states_.velocities[0] = 3.0; + + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if vel and acc limits applied + ASSERT_EQ(desired_joint_states_.velocities[0], 2.0); // vel limited to max_vel + // no vel cmd ifs + ASSERT_EQ( + desired_joint_states_.accelerations[0], (2.0 - 1.9) / 1.0); // acc set to vel change/DT + + // check opposite velocity direction + current_joint_states_.positions[0] = 2.0; + // set current velocity close to limits to not be blocked by max acc to reach max + current_joint_states_.velocities[0] = -1.9; + // desired velocity exceeds + desired_joint_states_.velocities[0] = -3.0; + + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if vel and acc limits applied + ASSERT_EQ(desired_joint_states_.velocities[0], -2.0); // vel limited to -max_vel + // no vel cmd ifs + ASSERT_EQ( + desired_joint_states_.accelerations[0], (-2.0 + 1.9) / 1.0); // acc set to vel change/DT + } +} + +TEST_F(JointSaturationLimiterTest, when_posonly_exceeded_expect_pos_enforced) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + rclcpp::Duration period(1.0, 0.0); // 1 second + + // desired pos exceeds + current_joint_states_.positions[0] = 5.0; + desired_joint_states_.positions[0] = 20.0; + // no velocity interface + desired_joint_states_.velocities.clear(); + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if pos limits applied + ASSERT_EQ( + desired_joint_states_.positions[0], 5.0); // pos limited clamped (was already at limit) + // no vel cmd ifs + ASSERT_EQ(desired_joint_states_.accelerations[0], 0.0); // acc set to vel change/DT + } +} + +TEST_F(JointSaturationLimiterTest, when_position_close_to_pos_limit_expect_deceleration_enforced) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + // using 0.05 because 1.0 sec invalidates the "small dt integration" + rclcpp::Duration period(0, 50000000); // 0.05 second + + // close to pos limit should reduce velocity and stop + current_joint_states_.positions[0] = 4.851; + current_joint_states_.velocities[0] = 1.5; + desired_joint_states_.positions[0] = 4.926; + desired_joint_states_.velocities[0] = 1.5; + + // this setup requires 0.15 distance to stop, and 0.2 seconds (so 4 cycles at 0.05) + std::vector expected_ret = {true, true, true, false}; + for (auto i = 0u; i < 4; ++i) + { + auto previous_vel_request = desired_joint_states_.velocities[0]; + // expect limits applied until the end stop + ASSERT_EQ( + joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period), + expected_ret[i]); + + ASSERT_LE( + desired_joint_states_.velocities[0], + previous_vel_request); // vel adapted to reach end-stop should be decreasing + // NOTE: after the first cycle, vel is reduced and does not trigger stopping position limit + // hence no max deceleration anymore... + ASSERT_LT( + desired_joint_states_.positions[0], + 5.0 + COMMON_THRESHOLD); // should decelerate at each cycle and stay below limits + ASSERT_LE(desired_joint_states_.accelerations[0], 0.0); // should decelerate + + Integrate(period.seconds()); + + ASSERT_LT(current_joint_states_.positions[0], 5.0); // below joint limit + } + } +} + +TEST_F(JointSaturationLimiterTest, when_posvel_leads_to_acc_exceeded_expect_limits_enforced) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + rclcpp::Duration period(0, 50000000); + + // desired acceleration exceeds + current_joint_states_.positions[0] = 0.1; + current_joint_states_.velocities[0] = 0.1; + desired_joint_states_.positions[0] = 0.125; // valid pos cmd for the desired velocity + desired_joint_states_.velocities[0] = 0.5; // leads to acc > max acc + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if limits applied + CHECK_STATE_SINGLE_JOINT( + desired_joint_states_, 0, + 0.11125, // pos = double integration from max acc with current state + 0.35, // vel limited to vel + max acc * dt + 5.0 // acc max acc + ); + } +} + +TEST_F(JointSaturationLimiterTest, when_posonly_leads_to_acc_exceeded_expect_limits_enforced) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + rclcpp::Duration period(0, 50000000); + + // desired acceleration exceeds + current_joint_states_.positions[0] = 0.1; + current_joint_states_.velocities[0] = 0.1; + desired_joint_states_.positions[0] = + 0.125; // pos cmd leads to vel 0.5 that leads to acc > max acc + desired_joint_states_.velocities.clear(); + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if pos and acc limits applied + ASSERT_NEAR( + desired_joint_states_.positions[0], 0.111250, + COMMON_THRESHOLD); // pos = double integration from max acc with current state + // no vel cmd ifs + ASSERT_EQ(desired_joint_states_.accelerations[0], 5.0); // acc max acc + } +} + +TEST_F(JointSaturationLimiterTest, when_velonly_leads_to_acc_exceeded_expect_limits_enforced) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + rclcpp::Duration period(0, 50000000); + + // desired acceleration exceeds + current_joint_states_.positions[0] = 0.1; + current_joint_states_.velocities[0] = 0.1; + desired_joint_states_.positions.clear(); // = 0.125; + desired_joint_states_.velocities[0] = 0.5; // leads to acc > max acc + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if pos and acc limits applied + // no pos cmd ifs + ASSERT_EQ(desired_joint_states_.velocities[0], 0.35); // vel limited to vel + max acc * dt + ASSERT_EQ(desired_joint_states_.accelerations[0], 5.0); // acc max acc + } +} + +TEST_F(JointSaturationLimiterTest, when_deceleration_exceeded_expect_dec_enforced) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + rclcpp::Duration period(0, 50000000); + + // desired deceleration exceeds + current_joint_states_.positions[0] = 0.3; + current_joint_states_.velocities[0] = 0.5; + desired_joint_states_.positions[0] = 0.305; // valid pos cmd for the desired velocity + desired_joint_states_.velocities[0] = 0.1; // leads to acc < -max dec + + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if vel and acc limits applied + CHECK_STATE_SINGLE_JOINT( + desired_joint_states_, 0, + 0.315625, // pos = double integration from max dec with current state + 0.125, // vel limited by vel - max dec * dt + -7.5 // acc limited by -max dec + ); + } +} + +TEST_F(JointSaturationLimiterTest, when_deceleration_exceeded_with_no_maxdec_expect_acc_enforced) +{ + SetupNode("joint_saturation_limiter_nodeclimit"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + rclcpp::Duration period(0, 50000000); + + // desired deceleration exceeds + current_joint_states_.positions[0] = 1.0; + current_joint_states_.velocities[0] = 1.0; + desired_joint_states_.positions[0] = 1.025; // valid pos cmd for the desired decreased velocity + desired_joint_states_.velocities[0] = 0.5; // leads to acc > -max acc + ASSERT_TRUE(joint_limiter_->enforce(current_joint_states_, desired_joint_states_, period)); + + // check if vel and acc limits applied + CHECK_STATE_SINGLE_JOINT( + desired_joint_states_, 0, + 1.04375, // pos = double integration from max acc with current state + 0.75, // vel limited by vel-max acc * dt + -5.0 // acc limited to -max acc + ); + } +} + +TEST_F(JointSaturationLimiterTest, when_there_are_effort_limits_expect_them_to_be_applyed) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init(); + Configure(); + + // value not over limit + desired_joint_states_.effort[0] = 15.0; + ASSERT_FALSE(joint_limiter_->enforce(desired_joint_states_.effort)); + + // value over limit + desired_joint_states_.effort[0] = 21.0; + ASSERT_TRUE(joint_limiter_->enforce(desired_joint_states_.effort)); + ASSERT_EQ(desired_joint_states_.effort[0], 20.0); + } +} + +TEST_F(JointSaturationLimiterTest, when_there_are_no_effort_limits_expect_them_not_applyed) +{ + SetupNode("joint_saturation_limiter"); + Load(); + + if (joint_limiter_) + { + Init("foo_joint_no_effort"); + Configure(); + + // value not over limit + desired_joint_states_.effort[0] = 15.0; + ASSERT_FALSE(joint_limiter_->enforce(desired_joint_states_.effort)); + + // value over limit + desired_joint_states_.effort[0] = 21.0; + ASSERT_FALSE(joint_limiter_->enforce(desired_joint_states_.effort)); + ASSERT_EQ(desired_joint_states_.effort[0], 21.0); + } +} + +int main(int argc, char ** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + rclcpp::init(argc, argv); + int result = RUN_ALL_TESTS(); + rclcpp::shutdown(); + return result; +} diff --git a/joint_limits/test/test_joint_saturation_limiter.hpp b/joint_limits/test/test_joint_saturation_limiter.hpp new file mode 100644 index 0000000000..3da0706177 --- /dev/null +++ b/joint_limits/test/test_joint_saturation_limiter.hpp @@ -0,0 +1,107 @@ +// Copyright (c) 2024, Stogl Robotics Consulting UG (haftungsbeschränkt) +// +// 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 TEST_JOINT_SATURATION_LIMITER_HPP_ +#define TEST_JOINT_SATURATION_LIMITER_HPP_ + +#include + +#include +#include +#include +#include "joint_limits/joint_limiter_interface.hpp" +#include "joint_limits/joint_limits.hpp" +#include "pluginlib/class_loader.hpp" +#include "rclcpp/duration.hpp" +#include "rclcpp/node.hpp" +#include "trajectory_msgs/msg/joint_trajectory_point.hpp" + +const double COMMON_THRESHOLD = 0.0011; + +#define CHECK_STATE_SINGLE_JOINT(tested_traj_point, idx, expected_pos, expected_vel, expected_acc) \ + EXPECT_NEAR(tested_traj_point.positions[idx], expected_pos, COMMON_THRESHOLD); \ + EXPECT_NEAR(tested_traj_point.velocities[idx], expected_vel, COMMON_THRESHOLD); \ + EXPECT_NEAR(tested_traj_point.accelerations[idx], expected_acc, COMMON_THRESHOLD); + +using JointLimiter = joint_limits::JointLimiterInterface; + +class JointSaturationLimiterTest : public ::testing::Test +{ +public: + void SetUp() override + { + node_name_ = ::testing::UnitTest::GetInstance()->current_test_info()->name(); + } + + void SetupNode(const std::string node_name = "") + { + if (!node_name.empty()) + { + node_name_ = node_name; + } + node_ = std::make_shared(node_name_); + } + + void Load() + { + joint_limiter_ = std::unique_ptr( + joint_limiter_loader_.createUnmanagedInstance(joint_limiter_type_)); + } + + void Init(const std::string & joint_name = "foo_joint") + { + joint_names_ = {joint_name}; + joint_limiter_->init(joint_names_, node_); + num_joints_ = joint_names_.size(); + last_commanded_state_.positions.resize(num_joints_, 0.0); + last_commanded_state_.velocities.resize(num_joints_, 0.0); + last_commanded_state_.accelerations.resize(num_joints_, 0.0); + last_commanded_state_.effort.resize(num_joints_, 0.0); + desired_joint_states_ = last_commanded_state_; + current_joint_states_ = last_commanded_state_; + } + + void Configure() { joint_limiter_->configure(last_commanded_state_); } + + void Integrate(double dt) + { + current_joint_states_.positions[0] += desired_joint_states_.velocities[0] * dt + + 0.5 * desired_joint_states_.accelerations[0] * dt * dt; + current_joint_states_.velocities[0] += desired_joint_states_.accelerations[0] * dt; + } + + JointSaturationLimiterTest() + : joint_limiter_type_("joint_limits/JointSaturationLimiter"), + joint_limiter_loader_( + "joint_limits", "joint_limits::JointLimiterInterface") + { + } + + void TearDown() override { node_.reset(); } + +protected: + std::string node_name_; + rclcpp::Node::SharedPtr node_; + std::vector joint_names_; + size_t num_joints_; + std::unique_ptr joint_limiter_; + std::string joint_limiter_type_; + pluginlib::ClassLoader joint_limiter_loader_; + + trajectory_msgs::msg::JointTrajectoryPoint last_commanded_state_; + trajectory_msgs::msg::JointTrajectoryPoint desired_joint_states_; + trajectory_msgs::msg::JointTrajectoryPoint current_joint_states_; +}; + +#endif // TEST_JOINT_SATURATION_LIMITER_HPP_ 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 3a54f3249c..0861761188 100644 --- a/ros2_control/CHANGELOG.rst +++ b/ros2_control/CHANGELOG.rst @@ -2,6 +2,35 @@ Changelog for package ros2_control ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.13.0 (2024-07-08) +------------------- + +4.12.0 (2024-07-01) +------------------- +* Add custom rosdoc2 config for ros2_control metapackage (`#1484 `_) +* Contributors: Christoph Fröhlich + +4.11.0 (2024-05-14) +------------------- + +4.10.0 (2024-05-08) +------------------- + +4.9.0 (2024-04-30) +------------------ + +4.8.0 (2024-03-27) +------------------ + +4.7.0 (2024-03-22) +------------------ + +4.6.0 (2024-03-02) +------------------ + +4.5.0 (2024-02-12) +------------------ + 4.4.0 (2024-01-31) ------------------ diff --git a/ros2_control/doc/conf.py b/ros2_control/doc/conf.py new file mode 100644 index 0000000000..b6134e9abd --- /dev/null +++ b/ros2_control/doc/conf.py @@ -0,0 +1,5 @@ +# Configuration file for the Sphinx documentation builder. +# settings will be overridden by rosdoc2, so we add here only custom settings + +copyright = "2024, ros2_control development team" +html_logo = "https://control.ros.org/master/_static/logo_ros-controls.png" diff --git a/ros2_control/doc/index.rst b/ros2_control/doc/index.rst new file mode 100644 index 0000000000..c91246eec7 --- /dev/null +++ b/ros2_control/doc/index.rst @@ -0,0 +1,32 @@ +Welcome to the documentation for ros2_control +================================================ + +For more information of the ros2_control framework see `control.ros.org `__. + +.. list-table:: + :header-rows: 1 + + * - Package Name + - API + - ROS Index + * - controller_interface + - `API `__ + - `ROS Index `__ + * - controller_manager + - `API `__ + - `ROS Index `__ + * - controller_manager_msgs + - `API `__ + - `ROS Index `__ + * - hardware_interface + - `API `__ + - `ROS Index `__ + * - joint_limits + - `API `__ + - `ROS Index `__ + * - ros2_control_test_assets + - `API `__ + - `ROS Index `__ + * - transmission_interface + - `API `__ + - `ROS Index `__ diff --git a/ros2_control/package.xml b/ros2_control/package.xml index 9e7c446b66..c7687e3cd6 100644 --- a/ros2_control/package.xml +++ b/ros2_control/package.xml @@ -1,11 +1,13 @@ ros2_control - 4.4.0 + 4.13.0 Metapackage for ROS2 control related packages Bence Magyar Denis Štogl + https://control.ros.org + Apache License 2.0 ament_cmake @@ -21,5 +23,6 @@ ament_cmake + rosdoc2.yaml diff --git a/ros2_control/rosdoc2.yaml b/ros2_control/rosdoc2.yaml new file mode 100644 index 0000000000..53048e1a73 --- /dev/null +++ b/ros2_control/rosdoc2.yaml @@ -0,0 +1,20 @@ +type: 'rosdoc2 config' +version: 1 + +--- + +settings: + # Not generating any documentation of code + generate_package_index: false + always_run_doxygen: false + enable_breathe: false + enable_exhale: false + always_run_sphinx_apidoc: false + +builders: + # Configure Sphinx with the location of the docs: + - sphinx: { + name: 'ros2_control', + sphinx_sourcedir: 'doc', + output_dir: '' + } diff --git a/ros2_control_test_assets/CHANGELOG.rst b/ros2_control_test_assets/CHANGELOG.rst index 9e2f966d0d..7dac855794 100644 --- a/ros2_control_test_assets/CHANGELOG.rst +++ b/ros2_control_test_assets/CHANGELOG.rst @@ -2,6 +2,45 @@ Changelog for package ros2_control_test_assets ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.13.0 (2024-07-08) +------------------- +* [ControllerChaining] Export state interfaces from chainable controllers (`#1021 `_) +* Remove mimic parameter from ros2_control tag (`#1553 `_) +* Contributors: Christoph Fröhlich, Sai Kishor Kothakota + +4.12.0 (2024-07-01) +------------------- +* [RM] Rename `load_urdf` method to `load_and_initialize_components` and add error handling there to avoid stack crashing when error happens. (`#1354 `_) +* [Feature] Hardware Components Grouping (`#1458 `_) +* Contributors: Dr. Denis, Sai Kishor Kothakota + +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) +------------------ + +4.7.0 (2024-03-22) +------------------ + +4.6.0 (2024-03-02) +------------------ + +4.5.0 (2024-02-12) +------------------ + 4.4.0 (2024-01-31) ------------------ 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 a42d39a241..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 @@ -372,7 +378,9 @@ const auto valid_urdf_ros2_control_system_robot_with_gpio = - + + 1.0 + )"; @@ -398,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"( @@ -565,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 a2f6195c67..cc2b1798d4 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 = + @@ -113,6 +110,34 @@ const auto urdf_head = + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -122,12 +147,850 @@ const auto urdf_head = )"; -const auto urdf_tail = +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"( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + +const auto urdf_tail = + R"( + +)"; + +const auto hardware_resources = + R"( + + + test_actuator + + + + + + + + + + + test_sensor + 2 + 2 + + + + + + + + test_system + 2 + 2 + + + + + + + + + + + + + + + + + + + +)"; + +const auto async_hardware_resources = + R"( + + + test_actuator + + + + + + + + + + + test_sensor + 2 + 2 + + + + + + + + test_system + 2 + 2 + + + + + + + + + + + + + + + + + + + +)"; + +const auto uninitializable_hardware_resources = + R"( + + + test_uninitializable_actuator + + + + + + + + + + + test_uninitializable_sensor + 2 + 2 + + + + + + + + test_uninitializable_system + 2 + 2 + + + + + + + + + + + + + + + +)"; + +const auto hardware_resources_not_existing_actuator_plugin = + R"( + + + test_actuator23 + + + + + + + + + + + test_sensor + 2 + 2 + + + + + + + + test_system + 2 + 2 + + + + + + + + + + + + + + + + + + + +)"; + +const auto hardware_resources_not_existing_sensor_plugin = + R"( + + + test_actuator + + + + + + + + + + + test_sensor23 + 2 + 2 + + + + + + + + test_system + 2 + 2 + + + + + + + + + + + + + + + + + + + +)"; +const auto hardware_resources_not_existing_system_plugin = + R"( + + + test_actuator + + + + + + + + + + + test_sensor + 2 + 2 + + + + + + + + test_system23 + 2 + 2 + + + + + + + + + + + + + + + + + + + +)"; + +const auto hardware_resources_duplicated_component = + R"( + + + test_actuator + + + + + + + + + + + test_sensor + 2 + 2 + + + + + + + + test_system + 2 + 2 + + + + + + + + + + + + + + + + + + + +)"; + +const auto hardware_resources_actuator_initializaion_error = + R"( + + + test_actuator + + + + + + + + + + test_sensor + 2 + 2 + + + + + + + + test_system + 2 + 2 + + + + + + + + + + + + )"; -const auto hardware_resources = +const auto hardware_resources_sensor_initializaion_error = R"( @@ -137,7 +1000,6 @@ const auto hardware_resources = - @@ -148,6 +1010,7 @@ const auto hardware_resources = + @@ -160,38 +1023,30 @@ const auto hardware_resources = - - - - - - - )"; -const auto unitilizable_hardware_resources = +const auto hardware_resources_system_initializaion_error = R"( - + - test_unitilizable_actuator + test_actuator - - + - test_unitilizable_sensor + test_sensor 2 2 @@ -199,28 +1054,21 @@ const auto unitilizable_hardware_resources = - + - test_unitilizable_system + test_system 2 2 - - - + - - - - - - + )"; @@ -235,6 +1083,7 @@ const auto hardware_resources_missing_state_keys = + @@ -246,6 +1095,7 @@ const auto hardware_resources_missing_state_keys = + @@ -256,12 +1106,14 @@ const auto hardware_resources_missing_state_keys = + - + + @@ -298,13 +1150,15 @@ const auto hardware_resources_missing_command_keys = - + + + )"; @@ -372,16 +1226,17 @@ const auto diffbot_urdf = - + + - - - + + + 1 1 @@ -407,16 +1262,17 @@ const auto diffbot_urdf = - + + - - - + + + 1 1 @@ -450,13 +1306,445 @@ const auto diffbot_urdf = + + + test_hardware_components/TestIMUSensor + + + + + + + + + + + + + + )"; +const auto gripper_urdf_head = + R"( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + +const auto gripper_urdf_head_no_mimic = + R"( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + +const auto gripper_urdf_head_unknown_joint = + R"( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + +const auto gripper_urdf_head_incomplete = + R"( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + +const auto gripper_urdf_head_invalid_two_root_links = + R"( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + +const auto gripper_hardware_resources_no_command_if = + R"( + + + + + + + + + + + + + )"; + +const auto gripper_hardware_resources_mimic_true_no_command_if = + R"( + + + + + + + + + + + + + )"; + +const auto gripper_hardware_resources_mimic_true_command_if = + R"( + + + + + + + + + + + + + + )"; + +const auto gripper_hardware_resources_mimic_false_command_if = + R"( + + + + + + + + + + + + + + )"; + const auto minimal_robot_urdf = std::string(urdf_head) + std::string(hardware_resources) + std::string(urdf_tail); -const auto minimal_unitilizable_robot_urdf = - std::string(urdf_head) + std::string(unitilizable_hardware_resources) + std::string(urdf_tail); +const auto minimal_async_robot_urdf = + std::string(urdf_head) + std::string(async_hardware_resources) + std::string(urdf_tail); +const auto minimal_uninitializable_robot_urdf = + std::string(urdf_head) + std::string(uninitializable_hardware_resources) + std::string(urdf_tail); + +const auto minimal_robot_not_existing_actuator_plugin = + std::string(urdf_head) + std::string(hardware_resources_not_existing_actuator_plugin) + + std::string(urdf_tail); +const auto minimal_robot_not_existing_sensors_plugin = + std::string(urdf_head) + std::string(hardware_resources_not_existing_sensor_plugin) + + std::string(urdf_tail); +const auto minimal_robot_not_existing_system_plugin = + std::string(urdf_head) + std::string(hardware_resources_not_existing_system_plugin) + + std::string(urdf_tail); + +const auto minimal_robot_duplicated_component = + std::string(urdf_head) + std::string(hardware_resources_duplicated_component) + + std::string(urdf_tail); + +const auto minimal_robot_actuator_initialization_error = + std::string(urdf_head) + std::string(hardware_resources_not_existing_actuator_plugin) + + std::string(urdf_tail); +const auto minimal_robot_sensor_initialization_error = + std::string(urdf_head) + std::string(hardware_resources_sensor_initializaion_error) + + std::string(urdf_tail); +const auto minimal_robot_system_initialization_error = + std::string(urdf_head) + std::string(hardware_resources_system_initializaion_error) + + std::string(urdf_tail); const auto minimal_robot_missing_state_keys_urdf = std::string(urdf_head) + std::string(hardware_resources_missing_state_keys) + diff --git a/ros2_control_test_assets/package.xml b/ros2_control_test_assets/package.xml index c403246015..1912391b5a 100644 --- a/ros2_control_test_assets/package.xml +++ b/ros2_control_test_assets/package.xml @@ -2,7 +2,7 @@ ros2_control_test_assets - 4.4.0 + 4.13.0 The package provides shared test resources for ros2_control stack Bence Magyar diff --git a/ros2controlcli/CHANGELOG.rst b/ros2controlcli/CHANGELOG.rst index b659a5bf97..7ed24372b0 100644 --- a/ros2controlcli/CHANGELOG.rst +++ b/ros2controlcli/CHANGELOG.rst @@ -2,6 +2,40 @@ Changelog for package ros2controlcli ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.13.0 (2024-07-08) +------------------- +* Remove ament linters (`#1601 `_) +* Contributors: Bence Magyar + +4.12.0 (2024-07-01) +------------------- + +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) +------------------ + +4.7.0 (2024-03-22) +------------------ + +4.6.0 (2024-03-02) +------------------ +* Added spawner colours to `list_controllers` depending upon active or inactive (`#1409 `_) +* Contributors: Soham Patil + +4.5.0 (2024-02-12) +------------------ + 4.4.0 (2024-01-31) ------------------ diff --git a/ros2controlcli/doc/userdoc.rst b/ros2controlcli/doc/userdoc.rst index 6dadd31226..5dd88cd6d9 100644 --- a/ros2controlcli/doc/userdoc.rst +++ b/ros2controlcli/doc/userdoc.rst @@ -16,6 +16,7 @@ Currently supported commands are - ros2 control load_controller - ros2 control reload_controller_libraries - ros2 control set_controller_state + - ros2 control set_hardware_component_state - ros2 control switch_controllers - ros2 control unload_controller - ros2 control view_controller_chains @@ -210,6 +211,33 @@ set_controller_state --include-hidden-nodes Consider hidden nodes as well +set_hardware_component_state +---------------------------- + +.. code-block:: console + + $ ros2 control set_hardware_component_state -h + usage: ros2 control set_hardware_component_state [-h] [--spin-time SPIN_TIME] [-s] [-c CONTROLLER_MANAGER] [--include-hidden-nodes] + hardware_component_name {unconfigured,inactive,active} + + Adjust the state of the hardware component + + positional arguments: + hardware_component_name + Name of the hardware_component to be changed + {unconfigured,inactive,active} + State in which the hardware component should be changed to + + options: + -h, --help show this help message and exit + --spin-time SPIN_TIME + Spin time in seconds to wait for discovery (only applies when not using an already running daemon) + -s, --use-sim-time Enable ROS simulation time + -c CONTROLLER_MANAGER, --controller-manager CONTROLLER_MANAGER + Name of the controller manager ROS node + --include-hidden-nodes + Consider hidden nodes as well + switch_controllers ------------------ diff --git a/ros2controlcli/package.xml b/ros2controlcli/package.xml index 33f265a607..d1693cdf2a 100644 --- a/ros2controlcli/package.xml +++ b/ros2controlcli/package.xml @@ -2,7 +2,7 @@ ros2controlcli - 4.4.0 + 4.13.0 The ROS 2 command line tools for ROS2 Control. @@ -21,11 +21,6 @@ rosidl_runtime_py python3-pygraphviz - ament_copyright - ament_flake8 - ament_pep257 - ament_xmllint - ament_python diff --git a/ros2controlcli/ros2controlcli/api/__init__.py b/ros2controlcli/ros2controlcli/api/__init__.py index df522670ec..2b4b805980 100644 --- a/ros2controlcli/ros2controlcli/api/__init__.py +++ b/ros2controlcli/ros2controlcli/api/__init__.py @@ -13,7 +13,7 @@ # limitations under the License. -from controller_manager import list_controllers +from controller_manager import list_controllers, list_hardware_components import rclpy @@ -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() @@ -75,6 +75,20 @@ def __call__(self, prefix, parsed_args, **kwargs): return [c.name for c in controllers if c.state in self.valid_states] +class LoadedHardwareComponentNameCompleter: + """Callable returning a list of loaded hardware components.""" + + def __init__(self, valid_states=["active", "inactive", "configured", "unconfigured"]): + self.valid_states = valid_states + + def __call__(self, prefix, parsed_args, **kwargs): + with DirectNode(parsed_args) as node: + hardware_components = list_hardware_components( + node, parsed_args.controller_manager + ).component + return [c.name for c in hardware_components if c.state.label in self.valid_states] + + def add_controller_mgr_parsers(parser): """Parser arguments to get controller manager node name, defaults to /controller_manager.""" arg = parser.add_argument( diff --git a/ros2controlcli/ros2controlcli/verb/list_controllers.py b/ros2controlcli/ros2controlcli/verb/list_controllers.py index ca73e7afef..31ce814865 100644 --- a/ros2controlcli/ros2controlcli/verb/list_controllers.py +++ b/ros2controlcli/ros2controlcli/verb/list_controllers.py @@ -13,6 +13,7 @@ # limitations under the License. from controller_manager import list_controllers +from controller_manager.spawner import bcolors from ros2cli.node.direct import add_arguments from ros2cli.node.strategy import NodeStrategy @@ -22,7 +23,15 @@ def print_controller_state(c, args): - print(f"{c.name:20s}[{c.type:20s}] {c.state:10s}") + state_color = "" + if c.state == "active": + state_color = bcolors.OKGREEN + elif c.state == "inactive": + state_color = bcolors.OKCYAN + elif c.state == "unconfigured": + state_color = bcolors.WARNING + + print(f"{c.name:20s}[{c.type:20s}] {state_color}{c.state:10s}{bcolors.ENDC}") if args.claimed_interfaces or args.verbose: print("\tclaimed interfaces:") for claimed_interface in c.claimed_interfaces: 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/ros2controlcli/verb/set_controller_state.py b/ros2controlcli/ros2controlcli/verb/set_controller_state.py index d584abe987..7cc44775a1 100644 --- a/ros2controlcli/ros2controlcli/verb/set_controller_state.py +++ b/ros2controlcli/ros2controlcli/verb/set_controller_state.py @@ -81,7 +81,7 @@ def main(self, *, args): else: return ( - f'cannot put {matched_controller.name} in "inactive" state' + f"cannot put {matched_controller.name} in 'inactive' state " f"from its current state {matched_controller.state}" ) diff --git a/ros2controlcli/ros2controlcli/verb/set_hardware_component_state.py b/ros2controlcli/ros2controlcli/verb/set_hardware_component_state.py new file mode 100644 index 0000000000..4b1093f4f7 --- /dev/null +++ b/ros2controlcli/ros2controlcli/verb/set_hardware_component_state.py @@ -0,0 +1,80 @@ +# Copyright 2023 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. + +from controller_manager import set_hardware_component_state + +from ros2cli.node.direct import add_arguments +from ros2cli.node.strategy import NodeStrategy +from ros2cli.verb import VerbExtension +from lifecycle_msgs.msg import State + +from ros2controlcli.api import add_controller_mgr_parsers, LoadedHardwareComponentNameCompleter + + +class SetHardwareComponentStateVerb(VerbExtension): + """Adjust the state of the hardware component.""" + + def add_arguments(self, parser, cli_name): + add_arguments(parser) + arg = parser.add_argument( + "hardware_component_name", help="Name of the hardware_component to be changed" + ) + arg.completer = LoadedHardwareComponentNameCompleter() + arg = parser.add_argument( + "state", + choices=["unconfigured", "inactive", "active"], + help="State in which the hardware component should be changed to", + ) + add_controller_mgr_parsers(parser) + + def main(self, *, args): + with NodeStrategy(args) as node: + + if args.state == "unconfigured": + + unconfigured_state = State() + unconfigured_state.id = State.PRIMARY_STATE_UNCONFIGURED + unconfigured_state.label = "unconfigured" + + response = set_hardware_component_state( + node, args.controller_manager, args.hardware_component_name, unconfigured_state + ) + if not response.ok: + return "Error cleaning up hardware component, check controller_manager logs" + + if args.state == "inactive": + inactive_state = State() + inactive_state.id = State.PRIMARY_STATE_INACTIVE + inactive_state.label = "inactive" + + response = set_hardware_component_state( + node, args.controller_manager, args.hardware_component_name, inactive_state + ) + if not response.ok: + return "Error stopping hardware component, check controller_manager logs" + + if args.state == "active": + + active_state = State() + active_state.id = State.PRIMARY_STATE_ACTIVE + active_state.label = "active" + + response = set_hardware_component_state( + node, args.controller_manager, args.hardware_component_name, active_state + ) + if not response.ok: + return "Error activating hardware component, check controller_manager logs" + + print(f"Successfully set {args.hardware_component_name} to state {response.state}") + return 0 diff --git a/ros2controlcli/setup.py b/ros2controlcli/setup.py index 02c9f108d3..5205c0f024 100644 --- a/ros2controlcli/setup.py +++ b/ros2controlcli/setup.py @@ -19,7 +19,7 @@ setup( name=package_name, - version="4.4.0", + version="4.13.0", packages=find_packages(exclude=["test"]), data_files=[ ("share/" + package_name, ["package.xml"]), @@ -62,6 +62,8 @@ ros2controlcli.verb.reload_controller_libraries:ReloadControllerLibrariesVerb", "set_controller_state = \ ros2controlcli.verb.set_controller_state:SetControllerStateVerb", + "set_hardware_component_state = \ + ros2controlcli.verb.set_hardware_component_state:SetHardwareComponentStateVerb", "switch_controllers = ros2controlcli.verb.switch_controllers:SwitchControllersVerb", "unload_controller = ros2controlcli.verb.unload_controller:UnloadControllerVerb", ], diff --git a/rqt_controller_manager/CHANGELOG.rst b/rqt_controller_manager/CHANGELOG.rst index 07724c98a3..065861ca07 100644 --- a/rqt_controller_manager/CHANGELOG.rst +++ b/rqt_controller_manager/CHANGELOG.rst @@ -2,6 +2,43 @@ Changelog for package rqt_controller_manager ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.13.0 (2024-07-08) +------------------- + +4.12.0 (2024-07-01) +------------------- +* [rqt_controller_manager] Add hardware components (`#1455 `_) +* Contributors: Christoph Fröhlich + +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 `_) +* Add cm as dependency to rqt_cm (`#1447 `_) +* Contributors: Christoph Fröhlich + +4.7.0 (2024-03-22) +------------------ +* Codeformat from new pre-commit config (`#1433 `_) +* rqt_controller_manager compatibility for humble (`#1429 `_) +* Contributors: Christoph Fröhlich + +4.6.0 (2024-03-02) +------------------ +* [CI] Code coverage + pre-commit (`#1413 `_) +* Contributors: Christoph Fröhlich + +4.5.0 (2024-02-12) +------------------ + 4.4.0 (2024-01-31) ------------------ diff --git a/rqt_controller_manager/package.xml b/rqt_controller_manager/package.xml index c280751db7..e4356b90fe 100644 --- a/rqt_controller_manager/package.xml +++ b/rqt_controller_manager/package.xml @@ -2,7 +2,7 @@ rqt_controller_manager - 4.4.0 + 4.13.0 Graphical frontend for interacting with the controller manager. Bence Magyar Denis Štogl @@ -12,12 +12,14 @@ https://github.com/ros-controls/ros2_control/issues https://github.com/ros-controls/ros2_control + Adolfo Rodríguez Tsouroukdissian Bence Magyar + Christoph Froehlich Enrique Fernandez - Mathias Lüdtke Kelsey Hawkins - Adolfo Rodríguez Tsouroukdissian + Mathias Lüdtke + controller_manager controller_manager_msgs rclpy rqt_gui diff --git a/rqt_controller_manager/resource/controller_manager.ui b/rqt_controller_manager/resource/controller_manager.ui index 2ede975248..17031d9044 100644 --- a/rqt_controller_manager/resource/controller_manager.ui +++ b/rqt_controller_manager/resource/controller_manager.ui @@ -37,7 +37,7 @@ - + 0 @@ -70,6 +70,40 @@ + + + + + 0 + 0 + + + + true + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + false + + + true + + + false + + + diff --git a/rqt_controller_manager/resource/controller_info.ui b/rqt_controller_manager/resource/popup_info.ui similarity index 96% rename from rqt_controller_manager/resource/controller_info.ui rename to rqt_controller_manager/resource/popup_info.ui index b8c03799a9..b910397a8d 100644 --- a/rqt_controller_manager/resource/controller_info.ui +++ b/rqt_controller_manager/resource/popup_info.ui @@ -16,9 +16,6 @@ 0 - - Controller Information - diff --git a/rqt_controller_manager/rqt_controller_manager/controller_manager.py b/rqt_controller_manager/rqt_controller_manager/controller_manager.py index 7ca1f6d8c3..4cb6c01e09 100644 --- a/rqt_controller_manager/rqt_controller_manager/controller_manager.py +++ b/rqt_controller_manager/rqt_controller_manager/controller_manager.py @@ -20,12 +20,16 @@ from controller_manager.controller_manager_services import ( configure_controller, list_controllers, + list_hardware_components, + set_hardware_component_state, load_controller, switch_controllers, unload_controller, ) + from controller_manager_msgs.msg import ControllerState from controller_manager_msgs.srv import SwitchController +from lifecycle_msgs.msg import State from python_qt_binding import loadUi from python_qt_binding.QtCore import QAbstractTableModel, Qt, QTimer from python_qt_binding.QtGui import QCursor, QFont, QIcon, QStandardItem, QStandardItemModel @@ -60,7 +64,7 @@ def __init__(self, context): # Pop-up that displays controller information self._popup_widget = QWidget() ui_file = os.path.join( - get_package_share_directory("rqt_controller_manager"), "resource", "controller_info.ui" + get_package_share_directory("rqt_controller_manager"), "resource", "popup_info.ui" ) loadUi(ui_file, self._popup_widget) self._popup_widget.setObjectName("ControllerInfoUi") @@ -78,7 +82,9 @@ def __init__(self, context): # Initialize members self._cm_name = "" # Name of the selected controller manager's node self._controllers = [] # State of each controller - self._table_model = None + self._hw_components = [] # State of each hw component + self._ctrl_table_model = None + self._hw_table_model = None # Store reference to node self._node = context.node @@ -93,16 +99,26 @@ def __init__(self, context): } # Controllers display - table_view = self._widget.table_view - table_view.setContextMenuPolicy(Qt.CustomContextMenu) - table_view.customContextMenuRequested.connect(self._on_ctrl_menu) - - table_view.doubleClicked.connect(self._on_ctrl_info) - - header = table_view.horizontalHeader() - header.setSectionResizeMode(QHeaderView.ResizeToContents) - header.setContextMenuPolicy(Qt.CustomContextMenu) - header.customContextMenuRequested.connect(self._on_header_menu) + ctrl_table_view = self._widget.ctrl_table_view + ctrl_table_view.setContextMenuPolicy(Qt.CustomContextMenu) + ctrl_table_view.customContextMenuRequested.connect(self._on_ctrl_menu) + ctrl_table_view.doubleClicked.connect(self._on_ctrl_info) + + ctrl_header = ctrl_table_view.horizontalHeader() + ctrl_header.setSectionResizeMode(QHeaderView.ResizeToContents) + ctrl_header.setContextMenuPolicy(Qt.CustomContextMenu) + ctrl_header.customContextMenuRequested.connect(self._on_ctrl_header_menu) + + # Hardware components display + hw_table_view = self._widget.hw_table_view + hw_table_view.setContextMenuPolicy(Qt.CustomContextMenu) + hw_table_view.customContextMenuRequested.connect(self._on_hw_menu) + hw_table_view.doubleClicked.connect(self._on_hw_info) + + hw_header = hw_table_view.horizontalHeader() + hw_header.setSectionResizeMode(QHeaderView.ResizeToContents) + hw_header.setContextMenuPolicy(Qt.CustomContextMenu) + hw_header.customContextMenuRequested.connect(self._on_hw_header_menu) # Timer for controller manager updates self._update_cm_list_timer = QTimer(self) @@ -116,6 +132,12 @@ def __init__(self, context): self._update_ctrl_list_timer.timeout.connect(self._update_controllers) self._update_ctrl_list_timer.start() + # Timer for running hw components updates + self._update_hw_components_list_timer = QTimer(self) + self._update_hw_components_list_timer.setInterval(int(1000.0 / self._cm_update_freq)) + self._update_hw_components_list_timer.timeout.connect(self._update_hw_components) + self._update_hw_components_list_timer.start() + # Signal connections w = self._widget w.cm_combo.currentIndexChanged[str].connect(self._on_cm_change) @@ -148,6 +170,7 @@ def _on_cm_change(self, cm_name): if cm_name: self._update_controllers() + self._update_hw_components() def _update_controllers(self): if not self._cm_name: @@ -190,20 +213,20 @@ def _list_controllers(self): return [] def _show_controllers(self): - table_view = self._widget.table_view - self._table_model = ControllerTable(self._controllers, self._icons) - table_view.setModel(self._table_model) + ctrl_table_view = self._widget.ctrl_table_view + self._ctrl_table_model = ControllerTable(self._controllers, self._icons) + ctrl_table_view.setModel(self._ctrl_table_model) def _on_ctrl_menu(self, pos): # Get data of selected controller - row = self._widget.table_view.rowAt(pos.y()) + row = self._widget.ctrl_table_view.rowAt(pos.y()) if row < 0: return # Cursor is not under a valid item ctrl = self._controllers[row] # Show context menu - menu = QMenu(self._widget.table_view) + menu = QMenu(self._widget.ctrl_table_view) if ctrl.state == "active": action_deactivate = menu.addAction(self._icons["inactive"], "Deactivate") action_kill = menu.addAction(self._icons["finalized"], "Deactivate and Unload") @@ -219,7 +242,7 @@ def _on_ctrl_menu(self, pos): action_configure = menu.addAction(self._icons["inactive"], "Load and Configure") action_activate = menu.addAction(self._icons["active"], "Load, Configure and Activate") - action = menu.exec_(self._widget.table_view.mapToGlobal(pos)) + action = menu.exec_(self._widget.ctrl_table_view.mapToGlobal(pos)) # Evaluate user action if ctrl.state == "active": @@ -253,6 +276,7 @@ def _on_ctrl_menu(self, pos): def _on_ctrl_info(self, index): popup = self._popup_widget + popup.setWindowTitle("Controller Information") ctrl = self._controllers[index.row()] popup.ctrl_name.setText(ctrl.name) @@ -271,42 +295,186 @@ def _on_ctrl_info(self, index): popup.move(QCursor.pos()) popup.show() - def _on_header_menu(self, pos): - header = self._widget.table_view.horizontalHeader() + def _on_ctrl_header_menu(self, pos): + ctrl_header = self._widget.ctrl_table_view.horizontalHeader() + + # Show context menu + menu = QMenu(self._widget.ctrl_table_view) + action_toggle_auto_resize = menu.addAction("Toggle Auto-Resize") + action = menu.exec_(ctrl_header.mapToGlobal(pos)) + + # Evaluate user action + if action is action_toggle_auto_resize: + if ctrl_header.resizeMode(0) == QHeaderView.ResizeToContents: + ctrl_header.setSectionResizeMode(QHeaderView.Interactive) + else: + ctrl_header.setSectionResizeMode(QHeaderView.ResizeToContents) + + def _update_hw_components(self): + if not self._cm_name: + return + + # Find hw_components associated to the selected controller manager + hw_components = self._list_hw_components() + + # Update controller display, if necessary + if self._hw_components != hw_components: + self._hw_components = hw_components + self._show_hw_components() # NOTE: Model is recomputed from scratch + + def _list_hw_components(self): + """ + List the hw_components associated to a controller manager node. + + @return List of hw_components associated to a controller manager + node. Contains both stopped/running hw_components, as returned by + the C{list_hardware_components} service + @rtype [str] + """ + # Add loaded hw_components first + try: + hw_components = list_hardware_components( + self._node, self._cm_name, 2.0 / self._cm_update_freq + ).component + return hw_components + except RuntimeError as e: + print(e) + return [] + + def _show_hw_components(self): + hw_table_view = self._widget.hw_table_view + self._hw_table_model = HwComponentTable(self._hw_components, self._icons) + hw_table_view.setModel(self._hw_table_model) + + def _on_hw_menu(self, pos): + # Get data of selected controller + row = self._widget.hw_table_view.rowAt(pos.y()) + if row < 0: + return # Cursor is not under a valid item + + hw_component = self._hw_components[row] + + # Show context menu + menu = QMenu(self._widget.hw_table_view) + if hw_component.state.label == "active": + action_deactivate = menu.addAction(self._icons["inactive"], "Deactivate") + action_cleanup = menu.addAction(self._icons["finalized"], "Deactivate and Cleanup") + elif hw_component.state.label == "inactive": + action_activate = menu.addAction(self._icons["active"], "Activate") + action_cleanup = menu.addAction(self._icons["unconfigured"], "Cleanup") + elif hw_component.state.label == "unconfigured": + action_configure = menu.addAction(self._icons["inactive"], "Configure") + action_spawn = menu.addAction(self._icons["active"], "Configure and Activate") + + action = menu.exec_(self._widget.hw_table_view.mapToGlobal(pos)) + + # Evaluate user action + if hw_component.state.label == "active": + if action is action_deactivate: + self._set_inactive_hw_component(hw_component.name) + elif action is action_cleanup: + self._set_unconfigured_hw_component(hw_component.name) + elif hw_component.state.label == "inactive": + if action is action_activate: + self._set_active_hw_component(hw_component.name) + elif action is action_cleanup: + self._set_unconfigured_hw_component(hw_component.name) + elif hw_component.state.label == "unconfigured": + if action is action_configure: + self._set_inactive_hw_component(hw_component.name) + elif action is action_spawn: + self._set_active_hw_component(hw_component.name) + + def _on_hw_info(self, index): + popup = self._popup_widget + popup.setWindowTitle("Hardware Component Info") + + hw_component = self._hw_components[index.row()] + popup.ctrl_name.setText(hw_component.name) + popup.ctrl_type.setText(hw_component.type) + + res_model = QStandardItemModel() + model_root = QStandardItem("Command Interfaces") + res_model.appendRow(model_root) + for command_interface in hw_component.command_interfaces: + hw_iface_item = QStandardItem(command_interface.name) + model_root.appendRow(hw_iface_item) + + model_root = QStandardItem("State Interfaces") + res_model.appendRow(model_root) + for state_interface in hw_component.state_interfaces: + hw_iface_item = QStandardItem(state_interface.name) + model_root.appendRow(hw_iface_item) + + popup.resource_tree.setModel(res_model) + popup.resource_tree.setItemDelegate(FontDelegate(popup.resource_tree)) + popup.resource_tree.expandAll() + popup.move(QCursor.pos()) + popup.show() + + def _on_hw_header_menu(self, pos): + hw_header = self._widget.hw_table_view.horizontalHeader() # Show context menu - menu = QMenu(self._widget.table_view) + menu = QMenu(self._widget.hw_table_view) action_toggle_auto_resize = menu.addAction("Toggle Auto-Resize") - action = menu.exec_(header.mapToGlobal(pos)) + action = menu.exec_(hw_header.mapToGlobal(pos)) # Evaluate user action if action is action_toggle_auto_resize: - if header.resizeMode(0) == QHeaderView.ResizeToContents: - header.setSectionResizeMode(QHeaderView.Interactive) + if hw_header.resizeMode(0) == QHeaderView.ResizeToContents: + hw_header.setSectionResizeMode(QHeaderView.Interactive) else: - header.setSectionResizeMode(QHeaderView.ResizeToContents) + hw_header.setSectionResizeMode(QHeaderView.ResizeToContents) def _activate_controller(self, name): - switch_controllers( - node=self._node, - controller_manager_name=self._cm_name, - deactivate_controllers=[], - activate_controllers=[name], - strict=SwitchController.Request.STRICT, - activate_asap=False, - timeout=0.3, - ) + self._switch_controllers([name], []) def _deactivate_controller(self, name): - switch_controllers( - node=self._node, - controller_manager_name=self._cm_name, - deactivate_controllers=[name], - activate_controllers=[], - strict=SwitchController.Request.STRICT, - activate_asap=False, - timeout=0.3, - ) + self._switch_controllers([], [name]) + + def _switch_controllers(self, activate, deactivate): + try: + switch_controllers( + node=self._node, + controller_manager_name=self._cm_name, + activate_controllers=activate, + deactivate_controllers=deactivate, + strict=SwitchController.Request.STRICT, + activate_asap=False, + timeout=0.3, + ) + except Exception as e: + print(e) + + def _set_active_hw_component(self, name): + active_state = State() + active_state.id = State.PRIMARY_STATE_ACTIVE + active_state.label = "active" + self._set_state_hw_component(name, active_state) + + def _set_inactive_hw_component(self, name): + inactive_state = State() + inactive_state.id = State.PRIMARY_STATE_INACTIVE + inactive_state.label = "inactive" + self._set_state_hw_component(name, inactive_state) + + def _set_unconfigured_hw_component(self, name): + unconfigure_state = State() + unconfigure_state.id = State.PRIMARY_STATE_UNCONFIGURED + unconfigure_state.label = "unconfigured" + self._set_state_hw_component(name, unconfigure_state) + + def _set_state_hw_component(self, name, state): + try: + set_hardware_component_state( + node=self._node, + controller_manager_name=self._cm_name, + component_name=name, + lifecyle_state=state, + ) + except Exception as e: + print(e) class ControllerTable(QAbstractTableModel): @@ -360,6 +528,57 @@ def data(self, index, role): return Qt.AlignCenter +class HwComponentTable(QAbstractTableModel): + """ + Model containing hardware component information for tabular display. + + The model allows display of basic read-only information like component + name and state. + """ + + def __init__(self, hw_component_info, icons, parent=None): + QAbstractTableModel.__init__(self, parent) + self._data = hw_component_info + self._icons = icons + + def rowCount(self, parent): + return len(self._data) + + def columnCount(self, parent): + return 2 + + def headerData(self, col, orientation, role): + if orientation != Qt.Horizontal or role != Qt.DisplayRole: + return None + if col == 0: + return "component" + elif col == 1: + return "state" + + def data(self, index, role): + if not index.isValid(): + return None + + hw_component = self._data[index.row()] + + if role == Qt.DisplayRole: + if index.column() == 0: + return hw_component.name + elif index.column() == 1: + return hw_component.state.label or "not loaded" + + if role == Qt.DecorationRole and index.column() == 0: + return self._icons.get(hw_component.state.label) + + if role == Qt.FontRole and index.column() == 0: + bf = QFont() + bf.setBold(True) + return bf + + if role == Qt.TextAlignmentRole and index.column() == 1: + return Qt.AlignCenter + + class FontDelegate(QStyledItemDelegate): """ Simple delegate for customizing font weight and italization. @@ -414,4 +633,11 @@ def _get_parameter_controller_names(node, node_name): """Get list of ROS parameter names that potentially represent a controller configuration.""" parameter_names = call_list_parameters(node=node, node_name=node_name) suffix = ".type" - return [n[: -len(suffix)] for n in parameter_names.result().result.names if n.endswith(suffix)] + # @note: The versions conditioning is added here to support the source-compatibility with Humble + if os.environ.get("ROS_DISTRO") == "humble": + # for humble, ros2param < 0.20.0 + return [n[: -len(suffix)] for n in parameter_names if n.endswith(suffix)] + else: + return [ + n[: -len(suffix)] for n in parameter_names.result().result.names if n.endswith(suffix) + ] diff --git a/rqt_controller_manager/setup.py b/rqt_controller_manager/setup.py index f93c231cd9..69d38c7273 100644 --- a/rqt_controller_manager/setup.py +++ b/rqt_controller_manager/setup.py @@ -1,3 +1,17 @@ +# Copyright 2024 Apache License, Version 2.0 +# +# 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. + from glob import glob from setuptools import setup @@ -6,7 +20,7 @@ setup( name=package_name, - version="4.4.0", + version="4.13.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 c63c29c3ab..63bd6aeb89 100644 --- a/transmission_interface/CHANGELOG.rst +++ b/transmission_interface/CHANGELOG.rst @@ -2,6 +2,42 @@ Changelog for package transmission_interface ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.13.0 (2024-07-08) +------------------- + +4.12.0 (2024-07-01) +------------------- + +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 `_) +* Contributors: Sai Kishor Kothakota + +4.7.0 (2024-03-22) +------------------ + +4.6.0 (2024-03-02) +------------------ +* Add -Werror=missing-braces to compile options (`#1423 `_) +* Contributors: Sai Kishor Kothakota + +4.5.0 (2024-02-12) +------------------ + 4.4.0 (2024-01-31) ------------------ diff --git a/transmission_interface/CMakeLists.txt b/transmission_interface/CMakeLists.txt index efd8db1652..185fb32d3a 100644 --- a/transmission_interface/CMakeLists.txt +++ b/transmission_interface/CMakeLists.txt @@ -2,7 +2,8 @@ cmake_minimum_required(VERSION 3.16) project(transmission_interface LANGUAGES CXX) if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") - add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow) + add_compile_options(-Wall -Wextra -Werror=conversion -Werror=unused-but-set-variable -Werror=return-type -Werror=shadow + -Werror=missing-braces) endif() set(THIS_PACKAGE_INCLUDE_DEPENDS @@ -12,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() @@ -30,6 +32,7 @@ pluginlib_export_plugin_description_file(transmission_interface ros2_control_plu if(BUILD_TESTING) find_package(ament_cmake_gmock REQUIRED) + find_package(ros2_control_test_assets REQUIRED) ament_add_gmock(test_simple_transmission test/simple_transmission_test.cpp @@ -50,16 +53,19 @@ if(BUILD_TESTING) test/simple_transmission_loader_test.cpp ) target_link_libraries(test_simple_transmission_loader transmission_interface) + ament_target_dependencies(test_simple_transmission_loader ros2_control_test_assets) ament_add_gmock(test_four_bar_linkage_transmission_loader test/four_bar_linkage_transmission_loader_test.cpp ) target_link_libraries(test_four_bar_linkage_transmission_loader transmission_interface) + ament_target_dependencies(test_four_bar_linkage_transmission_loader ros2_control_test_assets) ament_add_gmock(test_differential_transmission_loader test/differential_transmission_loader_test.cpp ) target_link_libraries(test_differential_transmission_loader transmission_interface) + ament_target_dependencies(test_differential_transmission_loader ros2_control_test_assets) ament_add_gmock( test_utils @@ -83,3 +89,4 @@ install(TARGETS transmission_interface ament_export_targets(export_transmission_interface HAS_LIBRARY_TARGET) ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS}) ament_package() +ament_generate_version_header(${PROJECT_NAME}) diff --git a/doc/Doxyfile b/transmission_interface/Doxyfile similarity index 93% rename from doc/Doxyfile rename to transmission_interface/Doxyfile index d05c75a0b6..db5b9c7e6f 100644 --- a/doc/Doxyfile +++ b/transmission_interface/Doxyfile @@ -1,4 +1,4 @@ -# Doxyfile 1.8.17 +# Doxyfile 1.9.1 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. @@ -32,7 +32,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = "ROS2 Control" +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 @@ -227,6 +227,14 @@ QT_AUTOBRIEF = 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. @@ -263,12 +271,6 @@ TAB_SIZE = 4 ALIASES = -# This tag can be used to specify a number of word-keyword mappings (TCL only). -# A mapping has the form "name=value". For example adding "class=itcl::class" -# will allow you to use the command class in the itcl::class meaning. - -TCL_SUBST = - # 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 @@ -310,18 +312,21 @@ OPTIMIZE_OUTPUT_SLICE = NO # 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, +# 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), VHDL, tcl. 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. +# 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. +# 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 = @@ -455,6 +460,19 @@ TYPEDEF_HIDES_STRUCT = NO 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 #--------------------------------------------------------------------------- @@ -518,6 +536,13 @@ EXTRACT_LOCAL_METHODS = 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 @@ -555,11 +580,18 @@ HIDE_IN_BODY_DOCS = NO INTERNAL_DOCS = NO -# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file -# names in lower-case letters. If set to YES, upper-case letters are also -# allowed. This is useful if you have classes or files whose names only differ -# in case and if your file system supports case sensitive file names. Windows -# (including Cygwin) ands Mac users are advised to set this option to 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 @@ -798,7 +830,10 @@ WARN_IF_DOC_ERROR = YES WARN_NO_PARAMDOC = NO # If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when -# a warning is encountered. +# 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 @@ -829,13 +864,14 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = README.md . +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. +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. # The default value is: UTF-8. INPUT_ENCODING = UTF-8 @@ -848,13 +884,15 @@ INPUT_ENCODING = UTF-8 # 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), -# *.doc (to be provided as doxygen C comment), *.txt (to be provided as doxygen -# C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f, *.for, *.tcl, *.vhd, -# *.vhdl, *.ucf, *.qsf and *.ice. +# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl, +# *.ucf, *.qsf and *.ice. FILE_PATTERNS = *.c \ *.cc \ @@ -933,7 +971,7 @@ EXCLUDE_SYMLINKS = NO # 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 = +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 @@ -970,7 +1008,7 @@ EXAMPLE_RECURSIVE = NO # that contain images that are to be included in the documentation (see the # \image command). -IMAGE_PATH = +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 @@ -1115,16 +1153,22 @@ USE_HTAGS = NO 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. +# 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 @@ -1134,10 +1178,13 @@ CLANG_ASSISTED_PARSING = NO CLANG_OPTIONS = # If clang assisted parsing is enabled you can provide the clang parser with the -# path to the compilation database (see: -# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) used when the 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. +# 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. @@ -1154,13 +1201,6 @@ CLANG_DATABASE_PATH = ALPHABETICAL_INDEX = YES -# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in -# which the alphabetical index list will be split. -# Minimum value: 1, maximum value: 20, default value: 5. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -COLS_IN_ALPHA_INDEX = 5 - # 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 @@ -1331,10 +1371,11 @@ 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 +# 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. @@ -1376,8 +1417,8 @@ 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. +# (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 @@ -1407,7 +1448,7 @@ CHM_FILE = HHC_LOCATION = # The GENERATE_CHI flag controls if a separate .chi index file is generated -# (YES) or that it should be included in the master .chm file (NO). +# (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. @@ -1452,7 +1493,8 @@ 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). +# (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. @@ -1460,8 +1502,8 @@ 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). +# 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. @@ -1469,16 +1511,16 @@ 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). +# 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). +# 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 = @@ -1490,9 +1532,9 @@ QHP_CUST_FILTER_ATTRS = QHP_SECT_FILTER_ATTRS = -# The QHG_LOCATION tag can be used to specify the location of Qt's -# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the -# generated .qhp file. +# 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 = @@ -1569,6 +1611,17 @@ TREEVIEW_WIDTH = 250 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 @@ -1608,7 +1661,7 @@ 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/latest/output.html) for more details. +# 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. @@ -1624,7 +1677,7 @@ MATHJAX_FORMAT = HTML-CSS # 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://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/. +# 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/ @@ -1638,7 +1691,8 @@ 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/latest/output.html) for more details. For an +# (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. @@ -1685,7 +1739,8 @@ SERVER_BASED_SEARCH = NO # # Doxygen ships with an example indexer (doxyindexer) and search engine # (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: https://xapian.org/). +# Xapian (see: +# https://xapian.org/). # # See the section "External Indexing and Searching" for details. # The default value is: NO. @@ -1698,8 +1753,9 @@ EXTERNAL_SEARCH = NO # # Doxygen ships with an example indexer (doxyindexer) and search engine # (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: https://xapian.org/). See the section "External Indexing and -# Searching" for details. +# Xapian (see: +# https://xapian.org/). See the section "External Indexing and Searching" for +# details. # This tag requires that the tag SEARCHENGINE is set to YES. SEARCHENGINE_URL = @@ -1863,9 +1919,11 @@ LATEX_EXTRA_FILES = PDF_HYPERLINKS = YES -# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate -# the PDF file directly from the LaTeX files. Set this option to YES, to get a -# higher quality PDF documentation. +# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as +# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX +# files. Set this option to YES, to get a higher quality PDF documentation. +# +# See also section LATEX_CMD_NAME for selecting the engine. # The default value is: YES. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -2376,10 +2434,32 @@ UML_LOOK = NO # but if the number exceeds 15, the total amount of fields shown is limited to # 10. # Minimum value: 0, maximum value: 100, default value: 10. -# This tag requires that the tag HAVE_DOT is set to YES. +# This tag requires that the tag UML_LOOK is set to YES. UML_LIMIT_NUM_FIELDS = 10 +# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and +# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS +# tag is set to YES, doxygen will add type and arguments for attributes and +# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen +# will not generate fields with class member information in the UML graphs. The +# class diagrams will look similar to the default class diagrams but using UML +# notation for the relationships. +# Possible values are: NO, YES and NONE. +# The default value is: NO. +# This tag requires that the tag UML_LOOK is set to YES. + +DOT_UML_DETAILS = NO + +# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters +# to display on a single line. If the actual line length exceeds this threshold +# significantly it will wrapped across multiple lines. Some heuristics are apply +# to avoid ugly line breaks. +# Minimum value: 0, maximum value: 1000, default value: 17. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_WRAP_THRESHOLD = 17 + # If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and # collaboration graphs will show the relations between templates and their # instances. @@ -2571,9 +2651,11 @@ DOT_MULTI_TARGETS = NO GENERATE_LEGEND = YES -# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot +# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate # files that are used to generate the various graphs. +# +# Note: This setting is not only used for dot files but also for msc and +# plantuml temporary files. # The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. DOT_CLEANUP = YES diff --git a/transmission_interface/doc/conf.py b/transmission_interface/doc/conf.py new file mode 100644 index 0000000000..b6134e9abd --- /dev/null +++ b/transmission_interface/doc/conf.py @@ -0,0 +1,5 @@ +# Configuration file for the Sphinx documentation builder. +# settings will be overridden by rosdoc2, so we add here only custom settings + +copyright = "2024, ros2_control development team" +html_logo = "https://control.ros.org/master/_static/logo_ros-controls.png" diff --git a/transmission_interface/doc/index.rst b/transmission_interface/doc/index.rst new file mode 100644 index 0000000000..6c4ce42c2d --- /dev/null +++ b/transmission_interface/doc/index.rst @@ -0,0 +1,21 @@ +Welcome to the documentation for transmission_interface +======================================================= + +``transmission_interface`` contains data structures for representing mechanical transmissions, methods for propagating values between actuator and joint spaces and tooling to support this. + +For more information of the ros2_control framework see `control.ros.org `__. + +API documentation +------------------ + +.. toctree:: + :maxdepth: 2 + + C++ API + + +Indices and Search +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/transmission_interface/include/transmission_interface/differential_transmission.hpp b/transmission_interface/include/transmission_interface/differential_transmission.hpp index 2002f6f9d7..85da01116d 100644 --- a/transmission_interface/include/transmission_interface/differential_transmission.hpp +++ b/transmission_interface/include/transmission_interface/differential_transmission.hpp @@ -31,7 +31,9 @@ namespace transmission_interface /** * * This transmission relates two actuators and two joints through a differential - * mechanism, as illustrated below. \image html differential_transmission.png + * mechanism, as illustrated below. + * + * \image html differential_transmission.png * *
* diff --git a/transmission_interface/package.xml b/transmission_interface/package.xml index 894bbd9b45..726ddc70cc 100644 --- a/transmission_interface/package.xml +++ b/transmission_interface/package.xml @@ -2,18 +2,20 @@ transmission_interface - 4.4.0 + 4.13.0 transmission_interface contains data structures for representing mechanical transmissions, methods for propagating values between actuator and joint spaces and tooling to support this. Bence Magyar Denis Štogl Apache License 2.0 ament_cmake + ament_cmake_gen_version_h hardware_interface pluginlib ament_cmake_gmock + ros2_control_test_assets ament_cmake diff --git a/transmission_interface/test/differential_transmission_loader_test.cpp b/transmission_interface/test/differential_transmission_loader_test.cpp index 70d0adf2cd..946c4703b5 100644 --- a/transmission_interface/test/differential_transmission_loader_test.cpp +++ b/transmission_interface/test/differential_transmission_loader_test.cpp @@ -22,6 +22,7 @@ #include "hardware_interface/hardware_info.hpp" #include "hardware_interface/types/hardware_interface_type_values.hpp" #include "pluginlib/class_loader.hpp" +#include "ros2_control_test_assets/descriptions.hpp" #include "transmission_interface/differential_transmission.hpp" #include "transmission_interface/differential_transmission_loader.hpp" #include "transmission_interface/transmission_loader.hpp" @@ -57,9 +58,7 @@ class TransmissionPluginLoader TEST(DifferentialTransmissionLoaderTest, FullSpec) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -132,9 +131,7 @@ TEST(DifferentialTransmissionLoaderTest, FullSpec) TEST(DifferentialTransmissionLoaderTest, only_mech_red_specified) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -203,9 +200,7 @@ TEST(DifferentialTransmissionLoaderTest, only_mech_red_specified) TEST(SimpleTransmissionLoaderTest, offset_and_mech_red_not_specified) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -266,9 +261,7 @@ TEST(SimpleTransmissionLoaderTest, offset_and_mech_red_not_specified) TEST(DifferentialTransmissionLoaderTest, mechanical_reduction_not_a_number) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -338,9 +331,7 @@ TEST(DifferentialTransmissionLoaderTest, mechanical_reduction_not_a_number) TEST(DifferentialTransmissionLoaderTest, offset_ill_defined) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -413,9 +404,7 @@ TEST(DifferentialTransmissionLoaderTest, offset_ill_defined) TEST(DifferentialTransmissionLoaderTest, mech_red_invalid_value) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( diff --git a/transmission_interface/test/four_bar_linkage_transmission_loader_test.cpp b/transmission_interface/test/four_bar_linkage_transmission_loader_test.cpp index 53b584b7b5..720cad68b4 100644 --- a/transmission_interface/test/four_bar_linkage_transmission_loader_test.cpp +++ b/transmission_interface/test/four_bar_linkage_transmission_loader_test.cpp @@ -22,6 +22,7 @@ #include "hardware_interface/hardware_info.hpp" #include "hardware_interface/types/hardware_interface_type_values.hpp" #include "pluginlib/class_loader.hpp" +#include "ros2_control_test_assets/descriptions.hpp" #include "transmission_interface/four_bar_linkage_transmission.hpp" #include "transmission_interface/four_bar_linkage_transmission_loader.hpp" #include "transmission_interface/transmission_loader.hpp" @@ -57,9 +58,7 @@ class TransmissionPluginLoader TEST(FourBarLinkageTransmissionLoaderTest, FullSpec) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -133,9 +132,7 @@ TEST(FourBarLinkageTransmissionLoaderTest, FullSpec) TEST(FourBarLinkageTransmissionLoaderTest, only_mech_red_specified) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -205,9 +202,7 @@ TEST(FourBarLinkageTransmissionLoaderTest, only_mech_red_specified) TEST(DifferentialTransmissionLoaderTest, offset_and_mech_red_not_specified) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -269,9 +264,7 @@ TEST(DifferentialTransmissionLoaderTest, offset_and_mech_red_not_specified) TEST(FourBarLinkageTransmissionLoaderTest, mechanical_reduction_not_a_number) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -342,9 +335,7 @@ TEST(FourBarLinkageTransmissionLoaderTest, mechanical_reduction_not_a_number) TEST(FourBarLinkageTransmissionLoaderTest, offset_ill_defined) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -418,9 +409,7 @@ TEST(FourBarLinkageTransmissionLoaderTest, offset_ill_defined) TEST(FourBarLinkageTransmissionLoaderTest, mech_red_invalid_value) { // Parse transmission info - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( diff --git a/transmission_interface/test/simple_transmission_loader_test.cpp b/transmission_interface/test/simple_transmission_loader_test.cpp index 4623d8c409..968f64c6e8 100644 --- a/transmission_interface/test/simple_transmission_loader_test.cpp +++ b/transmission_interface/test/simple_transmission_loader_test.cpp @@ -22,6 +22,7 @@ #include "hardware_interface/hardware_info.hpp" #include "hardware_interface/types/hardware_interface_type_values.hpp" #include "pluginlib/class_loader.hpp" +#include "ros2_control_test_assets/descriptions.hpp" #include "transmission_interface/simple_transmission.hpp" #include "transmission_interface/simple_transmission_loader.hpp" #include "transmission_interface/transmission_loader.hpp" @@ -58,106 +59,8 @@ TEST(SimpleTransmissionLoaderTest, FullSpec) { // Parse transmission info - std::string urdf_to_test = - R"( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + + R"( ros2_control_demo_hardware/VelocityActuatorHardware @@ -239,9 +142,7 @@ TEST(SimpleTransmissionLoaderTest, FullSpec) TEST(SimpleTransmissionLoaderTest, only_mech_red_specified) { - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -285,9 +186,7 @@ TEST(SimpleTransmissionLoaderTest, only_mech_red_specified) TEST(SimpleTransmissionLoaderTest, offset_and_mech_red_not_specified) { - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -327,9 +226,7 @@ TEST(SimpleTransmissionLoaderTest, offset_and_mech_red_not_specified) TEST(SimpleTransmissionLoaderTest, mechanical_reduction_not_a_number) { - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -369,9 +266,7 @@ TEST(SimpleTransmissionLoaderTest, mechanical_reduction_not_a_number) TEST(SimpleTransmissionLoaderTest, offset_ill_defined) { - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( @@ -413,11 +308,9 @@ TEST(SimpleTransmissionLoaderTest, offset_ill_defined) TEST(SimpleTransmissionLoaderTest, mech_red_invalid_value) { - std::string urdf_to_test = R"( - - + std::string urdf_to_test = std::string(ros2_control_test_assets::urdf_head) + R"( - + -1 1 @@ -426,7 +319,7 @@ TEST(SimpleTransmissionLoaderTest, mech_red_invalid_value) transmission_interface/SimpleTransmission - + 0