diff --git a/.clang-format b/.clang-format index 1f99de9..c0e3e0b 100644 --- a/.clang-format +++ b/.clang-format @@ -8,7 +8,6 @@ AllowAllParametersOfDeclarationOnNextLine: false AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AllowShortFunctionsOnASingleLine: None -AllowShortLoopsOnASingleLine: false AlwaysBreakTemplateDeclarations: true AlwaysBreakBeforeMultilineStrings: false BreakBeforeBinaryOperators: false @@ -51,7 +50,7 @@ SpaceAfterCStyleCast: false BreakBeforeBraces: Custom # Control of individual brace wrapping cases -BraceWrapping: { +BraceWrapping: AfterClass: 'true' AfterControlStatement: 'true' AfterEnum : 'true' @@ -62,5 +61,4 @@ BraceWrapping: { BeforeCatch : 'true' BeforeElse : 'true' IndentBraces : 'false' -} ... diff --git a/.flake8 b/.flake8 index 03dbaad..acb10fb 100644 --- a/.flake8 +++ b/.flake8 @@ -1,12 +1,5 @@ [flake8] -# E501: line-length checking, handled by black # E203: space after :, not PEP8 compliant # W503: no operator after line break, not PEP8 compliant -ignore = E501, E203, W503 -exclude = - # ignore templates - src/robot_ui/src/pathpilot/robot/program/templates/* - # ignore example programs - src/robot_command/examples/programs/* - # ignore playground files - src/robot_command/playground/* +ignore = E203, W503 +max-line-length = 80 diff --git a/.github/docker/script_pre.sh b/.github/docker/script_pre.sh index 45ae5fa..c810853 100755 --- a/.github/docker/script_pre.sh +++ b/.github/docker/script_pre.sh @@ -5,22 +5,22 @@ ROS_DISTRO=noetic # Add package repos needed to build linuxcnc-ethercat # - IgH EtherLab Master curl -1sLf \ - 'https://dl.cloudsmith.io/public/zultron/etherlabmaster/cfg/setup/bash.deb.sh' \ - | bash + 'https://dl.cloudsmith.io/public/zultron/etherlabmaster/cfg/setup/bash.deb.sh' | + bash # - redis_store curl -1sLf \ - 'https://dl.cloudsmith.io/public/zultron/hal_ros_control/cfg/setup/bash.deb.sh' \ - | bash + 'https://dl.cloudsmith.io/public/zultron/hal_ros_control/cfg/setup/bash.deb.sh' | + bash # - Machinekit curl -1sLf \ - 'https://dl.cloudsmith.io/public/machinekit/machinekit-hal/cfg/setup/bash.deb.sh' \ - | bash + 'https://dl.cloudsmith.io/public/machinekit/machinekit-hal/cfg/setup/bash.deb.sh' | + bash # Bootstrap ROS installation # http://wiki.ros.org/noetic/Installation/Source echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" \ - > /etc/apt/sources.list.d/ros-latest.list + >/etc/apt/sources.list.d/ros-latest.list apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' \ --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654 apt-get update @@ -28,14 +28,14 @@ apt-get update pip3 install -U \ vcstool \ rosdep \ - rosinstall-generator \ + rosinstall-generator apt-get install -y \ build-essential # Custom rosdep keys mkdir -p /etc/ros/rosdep/sources.list.d -cat > /etc/ros/rosdep/machinekit-rosdep.yaml </etc/ros/rosdep/machinekit-rosdep.yaml < /etc/ros/rosdep/sources.list.d/10-local.list </etc/ros/rosdep/sources.list.d/10-local.list < - Package ${{ matrix.vendor }} ${{ matrix.codename }}, ${{ matrix.architecture }} - runs-on: ubuntu-latest - needs: prepareState + default: strategy: - matrix: ${{ fromJson(needs.prepareState.outputs.MainMatrix) }} fail-fast: false - - steps: - - name: Clone git repository - uses: actions/checkout@v2 - - - name: Prepare specific Python version for build scripts - uses: actions/setup-python@v2 - with: - python-version: '3.8' - - - name: Install script dependencies - uses: zultron/machinekit_ci/actions/initDeps@v1 - - - name: Pull or build Docker image - id: docker_image - uses: zultron/machinekit_ci/actions/dockerImage@v1 - with: - codename: ${{ matrix.codename }} - architecture: ${{ matrix.architecture }} - dockerRegistryURL: ${{ needs.prepareState.outputs.GithubRegistryURL }} - dockerRegistryRepo: ${{ github.event.repository.name }} - dockerRegistryUser: ${{ github.actor }} - dockerRegistryPassword: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and sign packages - id: build_packages - uses: zultron/machinekit_ci/actions/buildPackages@v1 - with: - codename: ${{ matrix.codename }} - architecture: ${{ matrix.architecture }} - dockerRegistryURL: ${{ needs.prepareState.outputs.GithubRegistryURL }} - dockerRegistryRepo: ${{ github.event.repository.name }} - dockerRegistryUser: ${{ github.actor }} - packageSigningKey: ${{ secrets.PACKAGE_SIGNING_KEY }} - uploadDirectory: ${{ github.event.repository.name }}-${{ matrix.vendorLower }} - - - name: > - Upload package artifacts ${{ matrix.vendor }} - ${{ matrix.codename}}, ${{ matrix.architecture }} - uses: actions/upload-artifact@v2 - env: - NAME_BASE: ${{ matrix.artifactNameBase }} - GITHUB_SHA: ${{ github.sha }} - GITHUB_REPO: ${{ github.event.repository.name }} - TIMESTAMP: ${{ needs.prepareState.outputs.Timestamp }} - with: - name: ${{ env.NAME_BASE }}-${{ env.GITHUB_SHA }}-${{ env.TIMESTAMP }} - path: ${{ github.event.repository.name }}-${{ matrix.vendorLower }} - if-no-files-found: error - - ###################################################################################### - uploadDebianPackagesToCloudsmith: - name: Push packages to Cloudsmith + matrix: + env: + - IMAGE: noetic-ci + BUILDER: .github/workflows/catkin_tools_devel.sh + - IMAGE: noetic-ci-shadow-fixed + CATKIN_LINT: true + CLANG_TIDY: pedantic + env: + DOCKER_IMAGE: moveit/moveit:${{ matrix.env.IMAGE }} + UPSTREAM_WORKSPACE: .github/workflows/upstream.rosinstall + BEFORE_SETUP_UPSTREAM_WORKSPACE: .github/workflows/upstream_install.sh + + TARGET_CMAKE_ARGS: > + -DCMAKE_BUILD_TYPE=Release' + -DCMAKE_CXX_FLAGS="-Werror $CXXFLAGS" + CCACHE_DIR: ${{ github.workspace }}/.ccache + BASEDIR: ${{ github.workspace }}/.work + CLANG_TIDY_BASE_REF: ${{ github.base_ref || github.ref }} + BEFORE_CLANG_TIDY_CHECKS: (cd $TARGET_REPO_PATH; clang-tidy --list-checks) + BUILDER: ${{ matrix.env.BUILDER || 'catkin_tools' }} + CC: ${{ matrix.env.CLANG_TIDY && 'clang' }} + CXX: ${{ matrix.env.CLANG_TIDY && 'clang++' }} + + name: "${{ matrix.env.IMAGE }}${{ matrix.env.CATKIN_LINT && ' + catkin_lint' || ''}}${{ matrix.env.CLANG_TIDY && ' + clang-tidy' || '' }}" runs-on: ubuntu-latest - if: > - needs.prepareState.outputs.HasCloudsmithAPIKey == 'true' && - github.event_name == 'push' - needs: [prepareState, buildPackages] - steps: - - name: Clone git repository - uses: actions/checkout@v2 - - - name: Prepare specific Python version for Cloudsmith CLI - uses: actions/setup-python@v2 + - uses: actions/checkout@v2 + - name: cache upstream workspace + uses: pat-s/always-upload-cache@v2.1.5 with: - python-version: '3.8' - - - name: Install script dependencies - uses: zultron/machinekit_ci/actions/initDeps@v1 - - - name: Download all built artifacts from GitHub storage - uses: actions/download-artifact@v2 + path: ${{ env.BASEDIR }}/upstream_ws + key: ${{ env.CACHE_PREFIX }}-${{ github.run_id }} + restore-keys: ${{ env.CACHE_PREFIX }} + env: + CACHE_PREFIX: upstream_ws-${{ matrix.env.IMAGE }}-${{ hashFiles('.github/workflows/upstream.rosinstall', '.github/workflows/ci.yaml') }} + # The target directory cache doesn't include the source directory because + # that comes from the checkout. See "prepare target_ws for cache" task below + - name: cache target workspace + uses: pat-s/always-upload-cache@v2.1.5 with: - path: ./artifacts - - - name: Upload packages to Cloudsmith - uses: zultron/machinekit_ci/actions/pushCloudsmith@v1 + path: ${{ env.BASEDIR }}/target_ws + key: ${{ env.CACHE_PREFIX }}-${{ github.run_id }} + restore-keys: ${{ env.CACHE_PREFIX }} + env: + CACHE_PREFIX: target_ws-${{ matrix.env.IMAGE }}-${{ hashFiles('**/CMakeLists.txt', '**/package.xml', '.github/workflows/ci.yaml') }} + - name: cache ccache + uses: pat-s/always-upload-cache@v2.1.5 + with: + path: ${{ env.CCACHE_DIR }} + key: ${{ env.CACHE_PREFIX }}-${{ github.sha }}-${{ github.run_id }} + restore-keys: | + ${{ env.CACHE_PREFIX }}-${{ github.sha }} + ${{ env.CACHE_PREFIX }} + env: + CACHE_PREFIX: ccache-${{ matrix.env.IMAGE }} + + - id: industrial_ci + uses: ros-industrial/industrial_ci@master + env: ${{ matrix.env }} + + - name: upload test artifacts (on failure) + uses: actions/upload-artifact@v2 + if: steps.industrial_ci.outcome != 'success' with: - cloudsmithAPIKey: ${{ secrets.CLOUDSMITH_API_KEY }} - artifactDirectory: ./artifacts + name: test-results-${{ matrix.env.IMAGE }} + path: ${{ env.BASEDIR }}/target_ws/**/test_results/**/*.xml + - name: prepare target_ws for cache + if: ${{ always() }} + 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/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000..d261e0b --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,174 @@ +name: docker + +on: + schedule: + # 6 AM UTC every Sunday + - cron: "0 6 * * 6" + workflow_dispatch: + push: + branches: + - master + +jobs: + release: + strategy: + fail-fast: false + matrix: + ROS_DISTRO: [noetic] + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + env: + IMAGE: moveit/moveit:${{ matrix.ROS_DISTRO }}-${{ github.job }} + + steps: + - uses: addnab/docker-run-action@v3 + name: Check for apt updates + id: apt + with: + image: ${{ env.IMAGE }} + run: | + apt-get update + have_updates=$(apt-get --simulate upgrade | grep -q "^0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.$" && echo false || echo true) + echo "::set-output name=no_cache::$have_updates" + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + if: ${{ github.event_name == 'workflow_dispatch' || github.event_name != 'schedule' || steps.apt.outputs.no_cache }} + - name: Login to Container Registry + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and Push + uses: docker/build-push-action@v2 + if: ${{ github.event_name == 'workflow_dispatch' || github.event_name != 'schedule' || steps.apt.outputs.no_cache }} + with: + file: .docker/${{ github.job }}/Dockerfile + build-args: ROS_DISTRO=${{ matrix.ROS_DISTRO }} + push: true + no-cache: ${{ steps.apt.outputs.no_cache || github.event_name == 'workflow_dispatch' }} + cache-from: type=registry,ref=${{ env.IMAGE }} + cache-to: type=inline + tags: ${{ env.IMAGE }} + + ci: + strategy: + fail-fast: false + matrix: + IMAGE: [noetic, master] + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + env: + IMAGE: moveit/moveit:${{ matrix.IMAGE }}-${{ github.job }} + ROS_DISTRO: ${{ matrix.IMAGE == 'master' && 'melodic' || 'noetic' }} + + steps: + - uses: addnab/docker-run-action@v3 + name: Check for apt updates + id: apt + with: + image: ${{ env.IMAGE }} + run: | + apt-get update + have_updates=$(apt-get --simulate upgrade | grep -q "^0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.$" && echo false || echo true) + echo "::set-output name=no_cache::$have_updates" + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + if: ${{ github.event_name == 'workflow_dispatch' || github.event_name != 'schedule' || steps.apt.outputs.no_cache }} + - name: Login to Container Registry + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and Push + uses: docker/build-push-action@v2 + if: ${{ github.event_name == 'workflow_dispatch' || github.event_name != 'schedule' || steps.apt.outputs.no_cache }} + with: + file: .docker/${{ github.job }}/Dockerfile + build-args: ROS_DISTRO=${{ env.ROS_DISTRO }} + push: true + no-cache: ${{ steps.apt.outputs.no_cache || github.event_name == 'workflow_dispatch' }} + cache-from: type=registry,ref=${{ env.IMAGE }} + cache-to: type=inline + tags: ${{ env.IMAGE }} + + ci-testing: + needs: ci + strategy: + fail-fast: false + matrix: + IMAGE: [noetic, master] + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + env: + IMAGE: moveit/moveit:${{ matrix.IMAGE }}-${{ github.job }} + + steps: + - uses: addnab/docker-run-action@v3 + name: Check for apt updates + id: apt + with: + image: ${{ env.IMAGE }} + run: | + apt-get update + have_updates=$(apt-get --simulate upgrade | grep -q "^0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.$" && echo false || echo true) + echo "::set-output name=no_cache::$have_updates" + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + if: ${{ github.event_name == 'workflow_dispatch' || github.event_name != 'schedule' || steps.apt.outputs.no_cache }} + - name: Login to Container Registry + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and Push + uses: docker/build-push-action@v2 + if: ${{ github.event_name == 'workflow_dispatch' || github.event_name != 'schedule' || steps.apt.outputs.no_cache }} + with: + file: .docker/${{ github.job }}/Dockerfile + build-args: IMAGE=${{ matrix.IMAGE }} + push: true + no-cache: ${{ steps.apt.outputs.no_cache || github.event_name == 'workflow_dispatch' }} + cache-from: type=registry,ref=${{ env.IMAGE }} + cache-to: type=inline + tags: | + ${{ env.IMAGE }} + moveit/moveit:${{ matrix.IMAGE }}-ci-shadow-fixed + + source: + needs: ci-testing + strategy: + fail-fast: false + matrix: + IMAGE: [noetic, master] + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + env: + IMAGE: moveit/moveit:${{ matrix.IMAGE }}-${{ github.job }} + + steps: + - uses: actions/checkout@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to Container Registry + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and Push + uses: docker/build-push-action@v2 + with: + context: . + file: .docker/${{ github.job }}/Dockerfile + build-args: IMAGE=${{ matrix.IMAGE }} + push: true + cache-from: type=registry,ref=${{ env.IMAGE }} + cache-to: type=inline + tags: ${{ env.IMAGE }} diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml new file mode 100644 index 0000000..7c117c2 --- /dev/null +++ b/.github/workflows/format.yaml @@ -0,0 +1,40 @@ +# 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: + push: + +jobs: + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Install clang-format-10 + run: sudo apt-get install clang-format-10 + - name: Install catkin-lint + run: | + lsb_release -sc + sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' + sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654 + sudo apt-get -q update + sudo apt-get -q install python3-rosdep + sudo rosdep init + rosdep update + sudo apt-get -q install catkin-lint + export ROS_DISTRO=noetic + - name: Install black + run: sudo -H pip3 install black + - name: Install shfmt + run: | + wget -O /tmp/go.tgz https://golang.org/dl/go1.17.1.linux-amd64.tar.gz + sudo tar -C /usr/local -xzf /tmp/go.tgz + sudo ln -s ../go/bin/go /usr/local/bin/go + sudo GOPATH=/usr/local/go go install mvdan.cc/sh/v3/cmd/shfmt@latest + sudo ln -s ../go/bin/shfmt /usr/local/bin/shfmt + - uses: pre-commit/action@v2.0.0 diff --git a/.github/workflows/prerelease.yaml b/.github/workflows/prerelease.yaml new file mode 100644 index 0000000..5c8b6b8 --- /dev/null +++ b/.github/workflows/prerelease.yaml @@ -0,0 +1,38 @@ +# This config uses industrial_ci (https://github.com/ros-industrial/industrial_ci.git). +# For troubleshooting, see readme (https://github.com/ros-industrial/industrial_ci/blob/master/README.rst) + +name: pre-release + +on: + workflow_dispatch: + push: + +jobs: + default: + strategy: + matrix: + distro: [noetic] + + env: + # https://github.com/ros-industrial/industrial_ci/issues/666 + BUILDER: catkin_make_isolated + ROS_DISTRO: ${{ matrix.distro }} + PRERELEASE: true + BASEDIR: ${{ github.workspace }}/.work + + if: github.event_name == 'workflow_dispatch' # only allow manual triggering + name: "${{ matrix.distro }}" + runs-on: ubuntu-latest + steps: + - name: "Free up disk space" + run: | + sudo apt-get -qq purge build-essential "ghc*" + sudo apt-get clean + # cleanup docker images not used by us + docker system prune -af + # free up a lot of stuff from /usr/local + sudo rm -rf /usr/local + df -h + - uses: actions/checkout@v2 + - name: industrial_ci + uses: ros-industrial/industrial_ci@master diff --git a/.github/workflows/upstream.rosinstall b/.github/workflows/upstream.rosinstall new file mode 100644 index 0000000..ba3c1ee --- /dev/null +++ b/.github/workflows/upstream.rosinstall @@ -0,0 +1,8 @@ +- git: + local-name: redis_store + uri: https://github.com/zultron/redis_store.git + version: noetic-devel +- git: + local-name: redis_store_msgs + uri: https://github.com/zultron/redis_store_msgs.git + version: noetic-devel diff --git a/.github/workflows/upstream_install.sh b/.github/workflows/upstream_install.sh new file mode 100755 index 0000000..b292382 --- /dev/null +++ b/.github/workflows/upstream_install.sh @@ -0,0 +1,47 @@ +#!/bin/bash -xe + +# Install tools +apt-get update +apt-get install -y \ + apt-transport-https \ + curl + +# Install Machinekit APT repos +install_cloudsmith_repo() { + BASE=https://dl.cloudsmith.io/public + ORG=$1 + REPO=$2 + KEY_ID=$3 + CLOUDSMITH_ARGS="distro=${ID}&codename=${VERSION_CODENAME}" + curl -1sLf ${BASE}/${ORG}/${REPO}/cfg/gpg/gpg.${KEY_ID}.key | + apt-key add - + curl -1sLf "${BASE}/${ORG}/${REPO}/cfg/setup/config.deb.txt?${CLOUDSMITH_ARGS}" \ + >/etc/apt/sources.list.d/${ORG}-${REPO}.list +} + +source /etc/os-release +install_cloudsmith_repo machinekit machinekit-hal D35981AB4276AC36 +install_cloudsmith_repo machinekit machinekit A9B6D8B4BD8321F3 +apt-get update + +# Add Machinekit rosdep keys +rm -f /etc/ros/rosdep/sources.list.d/20-default.list +rosdep init +UPSTREAM_ROSDEP_YML=/etc/ros/rosdep/upstream-rosdep.yaml +cat >$UPSTREAM_ROSDEP_YML <<-EOF + machinekit: + debian: [machinekit-hal] + ubuntu: [machinekit-hal] + machinekit-dev: + debian: [machinekit-hal-dev] + ubuntu: [machinekit-hal-dev] + python3-redis: + debian: [python3-redis] + ubuntu: [python3-redis] + python3-attrs: + debian: [python3-attr] + ubuntu: [python3-attr] + EOF +echo "yaml file://$UPSTREAM_ROSDEP_YML" > \ + /etc/ros/rosdep/sources.list.d/10-local.list +rosdep update diff --git a/.gitignore b/.gitignore index 674a035..2bad04d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ autosave.halscope +__pycache__ # bloom-generate rosdebian /hal_hw_interface/debian/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8006ba8..4f11d2f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,10 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.1.0 + rev: v4.0.1 hooks: - id: check-ast - - id: check-byte-order-marker + - id: fix-byte-order-marker - id: trailing-whitespace - exclude: '.*\.patch' - id: check-docstring-first - id: check-executables-have-shebangs - id: check-json @@ -14,7 +13,6 @@ repos: - id: check-xml - id: check-yaml - id: end-of-file-fixer - - id: fix-encoding-pragma - repo: local hooks: @@ -23,7 +21,7 @@ repos: description: This hook formats Python code. entry: env LC_ALL=C.UTF-8 black -q language: system - args: [-S, -l, "80"] + args: [-l, "80"] types: [python] - id: clang-format @@ -35,21 +33,6 @@ repos: types: [file, c++] # note: formatting style in .clang-format - - id: qmllint - name: Run qmllint - description: QML linter - entry: qmllint - language: system - files: .*\.qml - - - id: qmlfmt - name: Run qmlfmt - description: qmlfmt QML code formatter. - entry: qmlfmt -w -e - args: [-i 2, -t 2] - language: system - files: .*\.qml - - id: shfmt name: Run shfmt description: shfmt Shell code formatter. diff --git a/.travis.build-docs.sh b/.travis.build-docs.sh index 899947b..ddd79f6 100755 --- a/.travis.build-docs.sh +++ b/.travis.build-docs.sh @@ -47,23 +47,22 @@ __AUTHOR__="Jeroen de Bruijn" # Re-run in Docker if $DOCKER_IMAGE is set if test -n "$DOCKER_IMAGE"; then exec docker run \ - --rm -t \ - -v $HOME:$HOME -w $(pwd) \ - -e UID=`id -u` \ - -e GID=`id -g` \ - -e HOME \ - -e USER=${USER} \ - -e TRAVIS_REPO_SLUG \ - -e TRAVIS_BUILD_NUMBER \ - -e TRAVIS_COMMIT \ - -e DOC_SUBDIRS \ - -e GH_REPO_TOKEN \ - -e NO_CLEAN \ - ${DOCKER_IMAGE} \ - $0 "$@" + --rm -t \ + -v $HOME:$HOME -w $(pwd) \ + -e UID=$(id -u) \ + -e GID=$(id -g) \ + -e HOME \ + -e USER=${USER} \ + -e TRAVIS_REPO_SLUG \ + -e TRAVIS_BUILD_NUMBER \ + -e TRAVIS_COMMIT \ + -e DOC_SUBDIRS \ + -e GH_REPO_TOKEN \ + -e NO_CLEAN \ + ${DOCKER_IMAGE} \ + $0 "$@" fi - ################################################################################ ##### Setup this script and get the current gh-pages branch. ##### echo 'Setting up the script...' @@ -75,8 +74,8 @@ cd "$(dirname $0)" # Computed parameters # # - GitHub repo variables from $TRAVIS_REPO_SLUG -GH_REPO_ID=${GH_REPO_ID:-${TRAVIS_REPO_SLUG#/*}} # my_ghid/my_repo -> my_ghid -GH_REPO_NAME=${GH_REPO_NAME:-${TRAVIS_REPO_SLUG#*/}} # my_ghid/my_repo -> my_repo +GH_REPO_ID=${GH_REPO_ID:-${TRAVIS_REPO_SLUG#/*}} # my_ghid/my_repo -> my_ghid +GH_REPO_NAME=${GH_REPO_NAME:-${TRAVIS_REPO_SLUG#*/}} # my_ghid/my_repo -> my_repo GH_REPO_URI=${GH_REPO_URI:-github.com/$TRAVIS_REPO_SLUG} GH_REPO_PULL_URL=${GH_REPO_PULL_URL:-https://${GH_REPO_URI}.git} GH_REPO_PUSH_URL=${GH_REPO_PUSH_URL:-https://${GH_REPO_TOKEN}@${GH_REPO_URI}} @@ -165,7 +164,7 @@ for subdir in ${DOC_SUBDIRS}; do echo "Running rosdoc_lite for $subdir" >&2 rosdoc_lite . 2>&1 | tee doc/rosdoc.log cd - - cp -a src/${GH_REPO_NAME}/$subdir/doc/html/* doc/ + cp -a src/${GH_REPO_NAME}/$subdir/doc/html/* doc/ done ################################################################################ @@ -194,7 +193,7 @@ if [ -f "index.html" ]; then # Force push to the remote gh-pages branch. # The ouput is redirected to /dev/null to hide any sensitive credential data # that might otherwise be exposed. - git push --force "${GH_REPO_PUSH_URL}" > /dev/null 2>&1 + git push --force "${GH_REPO_PUSH_URL}" >/dev/null 2>&1 else echo 'Not pushing docs with no $GH_REPO_TOKEN set' >&2 fi diff --git a/.travis.setup-docker-image.sh b/.travis.setup-docker-image.sh index cc2a4c2..1ecd4fe 100755 --- a/.travis.setup-docker-image.sh +++ b/.travis.setup-docker-image.sh @@ -8,7 +8,7 @@ fi if test -n "${DOCKER_REPO_AUTH}"; then echo "Setting up docker repo authentication" >&2 mkdir -p ~/.docker - cat > ~/.docker/config.json <~/.docker/config.json < +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# ============================================================================= + +find_package(HAL) + +# hal_comp_add_module(src/my_mod) +function(hal_add_instcomp icomp_modpath) + get_filename_component(icomp_name ${icomp_modpath} NAME) + get_filename_component(icomp_dir ${icomp_modpath} DIRECTORY) + set(icomp_src "${icomp_name}.icomp") + set(icomp_c "${icomp_name}.c") + set(icomp_src_path ${CMAKE_CURRENT_SOURCE_DIR}/${icomp_dir}/${icomp_src}) + + # Generate C source with `instcomp` + add_custom_command( + OUTPUT ${icomp_c} + # Copy .icomp file: instcomp generates .c in same directory + COMMAND cp ${icomp_src_path} ${icomp_src} + COMMAND ${HAL_INSTCOMP} -p ${icomp_src} + DEPENDS ${icomp_src_path} + COMMENT "Preprocessing instcomp ${icomp_modpath}") + + # Add the generated .c target + add_custom_target(${icomp_c} DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${icomp_src}) + + # Add the HAL comp .so target + add_library(${icomp_name} MODULE ${icomp_c}) + + # Add CFLAGS + target_compile_definitions(${icomp_name} PRIVATE RTAPI=1) + + # Omit the `lib` prefix + set_target_properties(${icomp_name} PROPERTIES PREFIX "") + + # Install HAL component + install(TARGETS ${icomp_name} + LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}) +endfunction() diff --git a/hal_hw_interface/cmake/hal_hw_interface-extras.cmake.develspace.in b/hal_hw_interface/cmake/hal_hw_interface-extras.cmake.develspace.in new file mode 100644 index 0000000..8d8014a --- /dev/null +++ b/hal_hw_interface/cmake/hal_hw_interface-extras.cmake.develspace.in @@ -0,0 +1,2 @@ +# Append cmake modules from source directory to the cmake module path +list(APPEND CMAKE_MODULE_PATH @CMAKE_CURRENT_SOURCE_DIR@/cmake) diff --git a/hal_hw_interface/cmake/hal_hw_interface-extras.cmake.installspace.in b/hal_hw_interface/cmake/hal_hw_interface-extras.cmake.installspace.in new file mode 100644 index 0000000..d4241ef --- /dev/null +++ b/hal_hw_interface/cmake/hal_hw_interface-extras.cmake.installspace.in @@ -0,0 +1,2 @@ +# Append cmake modules from source directory to the cmake module path +list(APPEND CMAKE_MODULE_PATH "${hal_hw_interface_DIR}/../../../@CATKIN_PACKAGE_SHARE_DESTINATION@/cmake") diff --git a/hal_hw_interface/doc/conf.py b/hal_hw_interface/doc/conf.py index d128cf0..0b99d8f 100644 --- a/hal_hw_interface/doc/conf.py +++ b/hal_hw_interface/doc/conf.py @@ -39,36 +39,36 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', - 'sphinx.ext.inheritance_diagram', + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx.ext.inheritance_diagram", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['.templates'] +templates_path = [".templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'hal_hw_interface' -copyright = u'2019, John Morris' -author = u'John Morris' +project = u"hal_hw_interface" +copyright = u"2019, John Morris" +author = u"John Morris" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -101,7 +101,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['.build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = [".build", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -123,7 +123,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -142,7 +142,7 @@ # # Added according to http://wiki.ros.org/Sphinx # html_theme = 'alabaster' -html_theme = 'agogo' +html_theme = "agogo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. @@ -175,7 +175,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['.static'] +# html_static_path = ['.static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied @@ -255,7 +255,7 @@ # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'hal_hw_interfacedoc' +htmlhelp_basename = "hal_hw_interfacedoc" # -- Options for LaTeX output --------------------------------------------- @@ -280,10 +280,10 @@ latex_documents = [ ( master_doc, - 'hal_hw_interface.tex', - u'hal\\_hw\\_interface Documentation', - u'John Morris', - 'manual', + "hal_hw_interface.tex", + u"hal\\_hw\\_interface Documentation", + u"John Morris", + "manual", ) ] @@ -327,8 +327,8 @@ man_pages = [ ( master_doc, - 'hal_hw_interface', - u'hal_hw_interface Documentation', + "hal_hw_interface", + u"hal_hw_interface Documentation", [author], 1, ) @@ -347,12 +347,12 @@ texinfo_documents = [ ( master_doc, - 'hal_hw_interface', - u'hal_hw_interface Documentation', + "hal_hw_interface", + u"hal_hw_interface Documentation", author, - 'hal_hw_interface', - 'One line description of project.', - 'Miscellaneous', + "hal_hw_interface", + "One line description of project.", + "Miscellaneous", ) ] @@ -374,9 +374,9 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} +intersphinx_mapping = {"https://docs.python.org/": None} # -- Options for inheritance graphs --------------------------------------- -inheritance_graph_attrs = dict(rankdir='TB') +inheritance_graph_attrs = dict(rankdir="TB") diff --git a/hal_hw_interface/include/hal_hw_interface/hal_control_loop.h b/hal_hw_interface/include/hal_hw_interface/hal_control_loop.h index d9eb4a9..9977035 100644 --- a/hal_hw_interface/include/hal_hw_interface/hal_control_loop.h +++ b/hal_hw_interface/include/hal_hw_interface/hal_control_loop.h @@ -40,21 +40,21 @@ namespace hal_hw_interface { /** -* \brief A `ros_control_boilerplate::GenericHWControlLoop`-like class for -* Machinekit HAL -* -* Implements the ROS node and ros_control `read()`/`update()`/`write()` loop -* running in a Machinekit HAL component -* -* This class does the messy work of managing a C++ ROS node linked into a C HAL -* component. It sets up the control loop object in the component's -* `rtapi_app_main()` init function, runs the ros_control `read(); update(); -* write()` in the update function, and finally shuts down the node and -* component. -* -* Most of the real time work is done in the `hal_hw_interface::HalHWInterface` -* class. -*/ + * \brief A `ros_control_boilerplate::GenericHWControlLoop`-like class for + * Machinekit HAL + * + * Implements the ROS node and ros_control `read()`/`update()`/`write()` loop + * running in a Machinekit HAL component + * + * This class does the messy work of managing a C++ ROS node linked into a C HAL + * component. It sets up the control loop object in the component's + * `rtapi_app_main()` init function, runs the ros_control `read(); update(); + * write()` in the update function, and finally shuts down the node and + * component. + * + * Most of the real time work is done in the `hal_hw_interface::HalHWInterface` + * class. + */ class HalRosControlLoop { @@ -62,11 +62,6 @@ class HalRosControlLoop /** * \brief Constructor * - * The constructor: - * * Sets up the ROS node - * * Runs the ROS spinner thread - * * Initializes the `hal_hw_interface::HalHWInterface` object - * * Initializes the `controller_manager::ControllerManager` object */ HalRosControlLoop(); @@ -77,6 +72,16 @@ class HalRosControlLoop */ ~HalRosControlLoop(); + /** + * \brief Initialize control loop object + * + * * Set up the ROS node + * * Run the ROS spinner thread + * * Initialize the `hal_hw_interface::HalHWInterface` object + * * Initialize the `controller_manager::ControllerManager` object + */ + int init(); + /** * \brief Run one ros_control `read()/update()/write()` cycle * @@ -128,6 +133,6 @@ class HalRosControlLoop }; // class -} // namespace +} // namespace hal_hw_interface #endif // HAL_HW_INTERFACE_HAL_CONTROL_LOOP_H diff --git a/hal_hw_interface/include/hal_hw_interface/hal_hw_interface.h b/hal_hw_interface/include/hal_hw_interface/hal_hw_interface.h index 47867f7..e9b8b46 100644 --- a/hal_hw_interface/include/hal_hw_interface/hal_hw_interface.h +++ b/hal_hw_interface/include/hal_hw_interface/hal_hw_interface.h @@ -36,7 +36,7 @@ #include // HAL -#include +#include // ROS #include @@ -47,37 +47,37 @@ namespace hal_hw_interface { /** -* \brief A `ros_control_boilerplate::GenericHWInterface` subclass for Machinekit -* HAL -* -* The `hal_hw_interface::HalHWInterface` class implements the Machinekit HAL -* realtime component: -* 1. Initializes the component -* 2. Implements the ros_control `read()` and `write()` functions -* 3. Shuts down the component -* -* The HAL component name is `hw_hw_interface`, and has one `reset` pin and six -* pins for each joint. -* -* The `reset` pin resets the ROS controllers whenever it is high. -* -* Joint names are read from configuration in [`ros_control_boilerplate`][1]. -* Six HAL pins are created for each joint: -* -* * Output pins connecting joint command from ROS into HAL -* * `.pos-cmd`, `.vel-cmd` and `.eff-cmd` -* * Input pins connecting joint feedback from HAL back to ROS -* * `.pos-fb`, `.vel-fb` and `.eff-fb` -* -* The `read()` function reads joint feedback values from the `.*-fb` HAL -* pins into the `hardware_interface::JointHandle`, and the `write()` function -* writes joint command values back out to the `.*-cmd` HAL pins. -* -* This is plumbed into a ROS node in the `hal_hw_interface::HalRosControlLoop` -* class. -* -* [1]: https://github.com/PickNikRobotics/ros_control_boilerplate -*/ + * \brief A `ros_control_boilerplate::GenericHWInterface` subclass for + * Machinekit HAL + * + * The `hal_hw_interface::HalHWInterface` class implements the Machinekit HAL + * realtime component: + * 1. Initializes the component + * 2. Implements the ros_control `read()` and `write()` functions + * 3. Shuts down the component + * + * The HAL component name is `hw_hw_interface`, and has one `reset` pin and six + * pins for each joint. + * + * The `reset` pin resets the ROS controllers whenever it is high. + * + * Joint names are read from configuration in [`ros_control_boilerplate`][1]. + * Six HAL pins are created for each joint: + * + * * Output pins connecting joint command from ROS into HAL + * * `.pos-cmd`, `.vel-cmd` and `.eff-cmd` + * * Input pins connecting joint feedback from HAL back to ROS + * * `.pos-fb`, `.vel-fb` and `.eff-fb` + * + * The `read()` function reads joint feedback values from the `.*-fb` HAL + * pins into the `hardware_interface::JointHandle`, and the `write()` function + * writes joint command values back out to the `.*-cmd` HAL pins. + * + * This is plumbed into a ROS node in the `hal_hw_interface::HalRosControlLoop` + * class. + * + * [1]: https://github.com/PickNikRobotics/ros_control_boilerplate + */ class HalHWInterface : public ros_control_boilerplate::GenericHWInterface { @@ -95,9 +95,8 @@ class HalHWInterface : public ros_control_boilerplate::GenericHWInterface * * Initializes the HAL component and sets up HAL pins for each joint. */ - //* \todo Give this an int return value for reporting failure //* \todo Make the `reset` pin an IO pin - void init(void (*funct)(void*, long)); + int init_hal(void (*funct)(void*, long)); /** * \brief Create float-type HAL pins for each joint @@ -179,6 +178,6 @@ class HalHWInterface : public ros_control_boilerplate::GenericHWInterface }; // HalHWInterface -} // hardware_interface +} // namespace hal_hw_interface #endif // HAL_HW_INTERFACE_HAL_HW_INTERFACE_H diff --git a/hal_hw_interface/include/hal_hw_interface/hal_ros_logging.h b/hal_hw_interface/include/hal_hw_interface/hal_ros_logging.h index 94dd6e6..47b7586 100644 --- a/hal_hw_interface/include/hal_hw_interface/hal_ros_logging.h +++ b/hal_hw_interface/include/hal_hw_interface/hal_ros_logging.h @@ -32,7 +32,7 @@ #ifndef HAL_HW_INTERFACE_HAL_ROS_LOGGING_H #define HAL_HW_INTERFACE_HAL_ROS_LOGGING_H -#include +#include #include #define HAL_ROS_LOG(hal_lev, ros_lev, name, ...) \ diff --git a/hal_hw_interface/package.xml b/hal_hw_interface/package.xml index f6dddfb..b5bfb7f 100644 --- a/hal_hw_interface/package.xml +++ b/hal_hw_interface/package.xml @@ -1,5 +1,5 @@ - + hal_hw_interface 0.0.0 @@ -24,22 +24,31 @@ message_generation python3-catkin-pkg redis_store - roslaunch machinekit-dev - ros_control_boilerplate - controller_manager - roscpp - rospy - python3-attrs - std_msgs - std_srvs - message_runtime - redis_store - rosbash - machinekit - + ros_control_boilerplate + controller_manager + roscpp + rospy + redis_store + machinekit-dev + + ros_control_boilerplate + controller_manager + roscpp + rospy + python3-attrs + std_msgs + std_srvs + message_runtime + redis_store + rosbash + machinekit + + rostest ros_pytest + python3-mock + roslaunch diff --git a/hal_hw_interface/scripts/hal_io b/hal_hw_interface/scripts/hal_io index 7c026ba..d68b01c 100755 --- a/hal_hw_interface/scripts/hal_io +++ b/hal_hw_interface/scripts/hal_io @@ -34,5 +34,5 @@ from hal_hw_interface.hal_io_comp import HalIO -if __name__ == '__main__': +if __name__ == "__main__": HalIO().main() diff --git a/hal_hw_interface/scripts/hal_mgr b/hal_hw_interface/scripts/hal_mgr index 59665eb..970c70c 100755 --- a/hal_hw_interface/scripts/hal_mgr +++ b/hal_hw_interface/scripts/hal_mgr @@ -36,12 +36,12 @@ import os from hal_hw_interface import hal_mgr -MAIN_HAL = 'main.py' -NAME = 'hal_mgr' -HAL_IO = 'hal_io' +MAIN_HAL = "main.py" +NAME = "hal_mgr" +HAL_IO = "hal_io" SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) -if __name__ == '__main__': +if __name__ == "__main__": os.chdir(SCRIPT_DIR) hal_mgr.main() diff --git a/hal_hw_interface/scripts/hal_offset_mgr b/hal_hw_interface/scripts/hal_offset_mgr index 59eda3b..3580e2f 100755 --- a/hal_hw_interface/scripts/hal_offset_mgr +++ b/hal_hw_interface/scripts/hal_offset_mgr @@ -3,5 +3,5 @@ from hal_hw_interface.hal_offset_mgr import HalOffsetMgr -if __name__ == '__main__': +if __name__ == "__main__": HalOffsetMgr().main() diff --git a/hal_hw_interface/setup.py b/hal_hw_interface/setup.py index ad564eb..8df979e 100644 --- a/hal_hw_interface/setup.py +++ b/hal_hw_interface/setup.py @@ -5,7 +5,7 @@ from catkin_pkg.python_setup import generate_distutils_setup d = generate_distutils_setup( - packages=['hal_hw_interface'], package_dir={'': 'src'} + packages=["hal_hw_interface"], package_dir={"": "src"} ) setup(**d) diff --git a/hal_hw_interface/src/hal_control_loop.cpp b/hal_hw_interface/src/hal_control_loop.cpp index 55791ea..a6ef826 100644 --- a/hal_hw_interface/src/hal_control_loop.cpp +++ b/hal_hw_interface/src/hal_control_loop.cpp @@ -40,6 +40,10 @@ extern "C" void funct(void* arg, long period); namespace hal_hw_interface { HalRosControlLoop::HalRosControlLoop() : node_is_shutdown(0) +{ +} + +int HalRosControlLoop::init() { // ROS node handle nh_.reset(new ros::NodeHandle("")); @@ -74,13 +78,16 @@ HalRosControlLoop::HalRosControlLoop() : node_is_shutdown(0) CNAME); // Init HAL hardware interface - hardware_interface_->init(&funct); + if (hardware_interface_->init_hal(&funct) != 0) + return 1; // Failure HAL_ROS_LOG_INFO(CNAME, "%s: Done initializing HAL hardware interface", CNAME); HAL_ROS_LOG_INFO(CNAME, "HAL control loop ready."); -} // constructor + + return 0; // Success +} // init // Non-RT thread CB function void HalRosControlLoop::serviceNonRtRosQueue() @@ -139,7 +146,7 @@ void HalRosControlLoop::update(long period) hardware_interface_->write(ros_period); } -} // namespace +} // namespace hal_hw_interface // The HAL hardware interface control loop object boost::shared_ptr control_loop_; @@ -157,8 +164,7 @@ int rtapi_app_main(void) // Create HAL controller and hardware interface control_loop_.reset(new hal_hw_interface::HalRosControlLoop()); - - return 0; + return control_loop_->init(); } void funct(void* arg, long period) diff --git a/hal_hw_interface/src/hal_hw_interface.cpp b/hal_hw_interface/src/hal_hw_interface.cpp index b121934..6c6a566 100644 --- a/hal_hw_interface/src/hal_hw_interface.cpp +++ b/hal_hw_interface/src/hal_hw_interface.cpp @@ -39,7 +39,7 @@ HalHWInterface::HalHWInterface(ros::NodeHandle& nh, urdf::Model* urdf_model) { } -void HalHWInterface::init(void (*funct)(void*, long)) +int HalHWInterface::init_hal(void (*funct)(void*, long)) { HAL_ROS_LOG_INFO(CNAME, "%s: Initializing HAL hardware interface", CNAME); @@ -53,8 +53,7 @@ void HalHWInterface::init(void (*funct)(void*, long)) if (comp_id_ < 0) { HAL_ROS_LOG_ERR(CNAME, "%s: ERROR: Component creation ABORTED", CNAME); - // return false; // FIXME - return; + return false; } HAL_ROS_LOG_INFO(CNAME, "%s: Initialized HAL component", CNAME); @@ -66,29 +65,30 @@ void HalHWInterface::init(void (*funct)(void*, long)) HAL_ROS_LOG_INFO(CNAME, "%s: Init joint #%zu %s", CNAME, ix, joint_names_[ix].c_str()); - if (!create_joint_float_pins(ix, &joint_pos_cmd_ptrs_, HAL_OUT, "pos-" - "cmd") || - !create_joint_float_pins(ix, &joint_vel_cmd_ptrs_, HAL_OUT, "vel-" - "cmd") || - !create_joint_float_pins(ix, &joint_eff_cmd_ptrs_, HAL_OUT, "eff-" - "cmd") || + if (!create_joint_float_pins(ix, &joint_pos_cmd_ptrs_, HAL_OUT, + "pos-" + "cmd") || + !create_joint_float_pins(ix, &joint_vel_cmd_ptrs_, HAL_OUT, + "vel-" + "cmd") || + !create_joint_float_pins(ix, &joint_eff_cmd_ptrs_, HAL_OUT, + "eff-" + "cmd") || !create_joint_float_pins(ix, &joint_pos_fb_ptrs_, HAL_IN, "pos-fb") || !create_joint_float_pins(ix, &joint_vel_fb_ptrs_, HAL_IN, "vel-fb") || !create_joint_float_pins(ix, &joint_eff_fb_ptrs_, HAL_IN, "eff-fb")) { HAL_ROS_LOG_ERR(CNAME, "%s: Failed to initialize joint %zu %s.%s", CNAME, ix, CNAME, joint_names_[ix].c_str()); - // return false; // FIXME - return; + return false; } } - // Initialize started pin + // Initialize reset pin if (!create_bit_pin(&reset_ptr_, HAL_IN, "reset")) { HAL_ROS_LOG_ERR(CNAME, "%s: Failed to initialize reset pin", CNAME); - // return false; // FIXME - return; + return false; } HAL_ROS_LOG_INFO(CNAME, "%s: Initialized HAL pins", CNAME); @@ -98,8 +98,7 @@ void HalHWInterface::init(void (*funct)(void*, long)) { HAL_ROS_LOG_INFO(CNAME, "%s: ERROR: hal_export_functf failed", CNAME); hal_exit(comp_id_); - // return false; // FIXME - return; + return false; } HAL_ROS_LOG_INFO(CNAME, "%s: Exported HAL function", CNAME); @@ -108,7 +107,7 @@ void HalHWInterface::init(void (*funct)(void*, long)) HAL_ROS_LOG_INFO(CNAME, "%s: HAL component ready!", CNAME); - // return true; // FIXME + return true; } // init() bool HalHWInterface::create_joint_float_pins(const std::size_t ix, @@ -212,4 +211,4 @@ void HalHWInterface::shutdown() } } -} // namespace +} // namespace hal_hw_interface diff --git a/hal_hw_interface/src/hal_hw_interface/__init__.py b/hal_hw_interface/src/hal_hw_interface/__init__.py index ca0c2c8..70c7462 100644 --- a/hal_hw_interface/src/hal_hw_interface/__init__.py +++ b/hal_hw_interface/src/hal_hw_interface/__init__.py @@ -4,3 +4,7 @@ .. moduleauthor:: John Morris """ + +__all__ = ("loadrt_local", "hal", "rtapi") + +from .loadrt_local import loadrt_local, hal, rtapi diff --git a/hal_hw_interface/src/hal_hw_interface/hal_io_comp.py b/hal_hw_interface/src/hal_hw_interface/hal_io_comp.py index 9afc7ae..e99de9a 100644 --- a/hal_hw_interface/src/hal_hw_interface/hal_io_comp.py +++ b/hal_hw_interface/src/hal_hw_interface/hal_io_comp.py @@ -5,6 +5,7 @@ RosHalPinPublisher, RosHalPinService, ) +from hal_hw_interface.redis_store_hal_pin import RedisStoreHalPin class HalIO(RosHalComponent): @@ -26,25 +27,34 @@ class HalIO(RosHalComponent): enable: hal_type: BIT hal_dir: OUT + publish_pins: + digital_out_1: + hal_type: BIT + hal_dir: IN service_pins: encoder_scale: hal_type: FLOAT hal_dir: OUT + redis_pins: + current_tool: + hal_type: U32 + hal_dir: IO The periodic :py:func:`update` function calls the pins' :py:func:`update` functions, if any. """ - compname = 'hal_io' + compname = "hal_io" def setup_component(self): - """Load pin configuration from ROS param server and create pin objects - """ + """Load pin configuration from ROS param server and create pin + objects""" self.pins = [] pin_class_map = dict( subscribe_pins=RosHalPinSubscriber, publish_pins=RosHalPinPublisher, service_pins=RosHalPinService, + redis_pins=RedisStoreHalPin, ) for config_key, pin_class in pin_class_map.items(): pins = self.get_ros_param(config_key, dict()) @@ -53,7 +63,6 @@ def setup_component(self): self.pins.append(p) def update(self): - """Run pin `update()` functions - """ + """Run pin `update()` functions""" for p in self.pins: p.update() diff --git a/hal_hw_interface/src/hal_hw_interface/hal_mgr.py b/hal_hw_interface/src/hal_hw_interface/hal_mgr.py index ebc74c6..91f62f7 100644 --- a/hal_hw_interface/src/hal_hw_interface/hal_mgr.py +++ b/hal_hw_interface/src/hal_hw_interface/hal_mgr.py @@ -4,7 +4,8 @@ import subprocess import signal -from machinekit import launcher, config, rtapi +from machinekit.hal.launcher import launcher +from machinekit.hal.cyruntime import rtapi # ROS import rospy @@ -12,7 +13,7 @@ class HalMgr(object): - NAME = 'hal_mgr' + NAME = "hal_mgr" READY_TOPIC = "hal_mgr/ready" shutdown_begun = False @@ -21,7 +22,7 @@ def __init__(self): rospy.init_node(self.NAME) # Call end_session() on ROS shutdown; don't have the launcher # register its own exit handler - rospy.on_shutdown(lambda: self.shutdown('Graceful shutdown via ROS')) + rospy.on_shutdown(lambda: self.shutdown("Graceful shutdown via ROS")) self._rate = rospy.Rate(1) # 1hz rospy.loginfo("hal_mgr: Initialized node") @@ -30,13 +31,16 @@ def __init__(self): ) def start(self): - # Find the hal_hw_interface comp's directory in LD_LIBRARY_PATH and put it - # into $COMP_DIR - comp_dir = "" - for path in os.environ.get('LD_LIBRARY_PATH', '').split(':'): - if os.path.exists(os.path.join(path, 'hal_hw_interface.so')): + # Find the hal_hw_interface comp's directory in + # LD_LIBRARY_PATH and put it into $COMP_DIR + for path in os.environ.get("LD_LIBRARY_PATH", "").split(":"): + rospy.loginfo(f"Checking for hal_hw_interface.so in {path}") + if os.path.exists(os.path.join(path, "hal_hw_interface.so")): comp_dir = path - os.environ['COMP_DIR'] = comp_dir + break + else: + comp_dir = "" + os.environ["COMP_DIR"] = comp_dir rospy.loginfo("hal_mgr: COMP_DIR set to '%s'" % comp_dir) # Get parameters @@ -48,9 +52,9 @@ def start(self): self.shutdown("No keys defined at %s" % self.NAME, 1) return - if 'hal_files' not in hal_mgr_config: + if "hal_files" not in hal_mgr_config: self.shutdown("%s has no 'hal_files' key" % self.NAME, 1) - if 'hal_file_dir' not in hal_mgr_config: + if "hal_file_dir" not in hal_mgr_config: self.shutdown("%s has no 'hal_file_dir' key" % self.NAME, 1) # Set up HAL @@ -59,22 +63,23 @@ def start(self): rospy.loginfo("hal_mgr: Started realtime") # Load rtapi module and set up signal handlers - if not getattr(rtapi, '__rtapicmd'): + if not getattr(rtapi, "__rtapicmd"): rtapi.init_RTAPI() def shutdown_graceful(signum, frame): - self.shutdown('Gracefully shutting down after interrupt signal') + self.shutdown("Gracefully shutting down after interrupt signal") signal.signal(signal.SIGINT, shutdown_graceful) signal.signal(signal.SIGTERM, shutdown_graceful) # Load HAL configuration - for fname in hal_mgr_config['hal_files']: - fpath = os.path.join(hal_mgr_config['hal_file_dir'], fname) + for fname in hal_mgr_config["hal_files"]: + fpath = os.path.join(hal_mgr_config["hal_file_dir"], fname) if not os.path.exists(fpath): self.shutdown( "No file '%s' in directory '%s'" - % (fname, hal_mgr_config['hal_file_dir']) + % (fname, hal_mgr_config["hal_file_dir"]), + res=1, ) rospy.loginfo("hal_mgr: Loading hal file '%s'" % fname) launcher.load_hal_file(fpath) @@ -106,12 +111,12 @@ def shutdown(self, msg="Shutting down for unknown reason", res=0): def main(): - debug = int(os.environ.get('DEBUG', 0)) + debug = int(os.environ.get("DEBUG", 0)) launcher.set_debug_level(debug) - if 'MACHINEKIT_INI' not in os.environ: # export for package installs - mkconfig = config.Config() - os.environ['MACHINEKIT_INI'] = mkconfig.MACHINEKIT_INI + if "MACHINEKIT_INI" not in os.environ: # export for package installs + #mkconfig = config.Config() + os.environ["MACHINEKIT_INI"] = "/etc/machinekit/hal/machinekit.ini" #mkconfig.MACHINEKIT_INI hal_mgr = HalMgr() try: diff --git a/hal_hw_interface/src/hal_hw_interface/hal_obj_base.py b/hal_hw_interface/src/hal_hw_interface/hal_obj_base.py index f365351..8979a9a 100644 --- a/hal_hw_interface/src/hal_hw_interface/hal_obj_base.py +++ b/hal_hw_interface/src/hal_hw_interface/hal_obj_base.py @@ -7,7 +7,7 @@ .. moduleauthor:: John Morris """ -import hal +import machinekit.hal.pyhal as hal import rospy @@ -31,11 +31,11 @@ def init_hal_comp(self): :py:class:`hal_hw_interface.ros_hal_pin.RosHalComponent` object setup to initialize the new HAL component """ - if not hasattr(self, 'compname'): + if not hasattr(self, "compname"): raise RuntimeError('No "compname" attribute configured') - if 'hal_comp' in self._cached_objs: - raise RuntimeError('HAL component already initialized') - self._cached_objs['hal_comp'] = hal.component(self.compname) + if "hal_comp" in self._cached_objs: + raise RuntimeError("HAL component already initialized") + self._cached_objs["hal_comp"] = hal.component(self.compname) @property def hal_comp(self): @@ -47,12 +47,12 @@ def hal_comp(self): :returns: HAL component object :rtype: :py:class:`hal.component` """ - if 'hal_comp' not in self._cached_objs: - raise RuntimeError('No HAL component initialized') - return self._cached_objs['hal_comp'] + if "hal_comp" not in self._cached_objs: + raise RuntimeError("No HAL component initialized") + return self._cached_objs["hal_comp"] def get_ros_param(self, suffix, default=None): - '''Retrieve a parameter from the ROS param server, key name prefixed + """Retrieve a parameter from the ROS param server, key name prefixed with HAL component name This is shorthand for retrieving a ROS param server key @@ -64,8 +64,18 @@ def get_ros_param(self, suffix, default=None): :type default: any :returns: parameter value :rtype: XmlRpcLegalValue - ''' + """ return self._cached_objs.setdefault( suffix, - rospy.get_param('{}/{}'.format(self.compname, suffix), default), + rospy.get_param("{}/{}".format(self.compname, suffix), default), ) + + def add_shutdown_callback(self, cb): + """Add a shutdown callback + + Add a callback performing some extra shutdown action (such as + disconnect from a service) to a list that will be run from + :py:func:`hal_hw_interface.ros_hal_component.RosHalComponent.shutdown_component` + at component shut down time. + """ + self._cached_objs.setdefault("shutdown_cbs", []).append(cb) diff --git a/hal_hw_interface/src/hal_hw_interface/hal_offset_mgr.py b/hal_hw_interface/src/hal_hw_interface/hal_offset_mgr.py deleted file mode 100644 index 8bade7b..0000000 --- a/hal_hw_interface/src/hal_hw_interface/hal_offset_mgr.py +++ /dev/null @@ -1,165 +0,0 @@ -# -*- coding: utf-8 -*- -import rospy -import attr -from hal_hw_interface.redis_store_hal_pin import RedisStoreHalPin -from hal_hw_interface.ros_hal_pin import RosHalPin -from hal_hw_interface.ros_hal_component import ( - RosHalComponent, - HalHWInterfaceException, -) - - -@attr.s -class HalOffsetMgrPin(RedisStoreHalPin): - '''Represents the offset of one joint. - - By default, the pin name is :code:`_offset`, where - :code:`` might simply be a joint name. This pin connects to - an :code:`offset` comp's :code:`offset` pin. Its value is - persisted in a redis store key - :code:`hal_offset_mgr/_offset`. - - A second, complementary :code:`_fb-in` input pin connects to - the :code:`offset` comp :code:`fb-in` pin. The - :code:`zero_joint()` function copies this value to the offset pin - and updates the redis store. - - :param name: The HAL pin name prefix - :type name: str - :param hal_type: HAL pin data type, one of :code:`['BIT', 'U32', - 'S32', 'FLOAT']` - :type hal_type: :py:class:`hal_hw_interface.hal_pin_attrs.HalPinType` - :param hal_dir: HAL pin direction, one of :code:`['IN', 'OUT', 'IO']` - :type hal_dir: :py:class:`hal_hw_interface.hal_pin_attrs.HalPinDir` - ''' - - _default_hal_type = 'FLOAT' - _default_hal_dir = 'OUT' - - offset_pin_name = attr.ib() - fb_in_pin_name = attr.ib() - - # Attribute default factories - @offset_pin_name.default - def _offset_pin_name_default(self): - return self.name + '_offset' - - @fb_in_pin_name.default - def _fb_in_pin_name_default(self): - return self.name + '_fb-in' - - @property - def pin_name(self): - return self.offset_pin_name - - def _hal_init(self): - # Set up the offset pin - super(HalOffsetMgrPin, self)._hal_init() - - # Set up fb-in pin - self.hal_fb_in_pin = RosHalPin(self.fb_in_pin_name, self.hal_type, 'IN') - - rospy.loginfo( - 'Created {} pin "{}" type "{}"'.format( - self.hal_dir, self.fb_in_pin_name, self.hal_type - ) - ) - - def zero_joint(self): - '''Zero the offset by copying the :code:`fb-in` pin to the - :code:`offset` pin and to redis - ''' - new_offset = self.hal_fb_in_pin.get_pin() - self.set_pin(new_offset) - self.set_redis_from_pin() - return new_offset - - def load_offset(self): - '''Load the offset value from redis and copy to the :code:`offset` pin - ''' - self.set_pin_from_redis(default=0) - value = self.get_pin() # Re-read pin for feedback - rospy.loginfo("Restored %s offset to %.4f", self.pin_name, value) - return value - - -class HalOffsetMgr(RosHalComponent): - '''HAL user component managing zero offsets that persist across - machine restarts - - It reads joint names from the ROS parameter server, and creates - two HAL pins for each: :code:`hal_offset_mgr._offset` and - :code:`hal_offset_mgr._fb-in`, meant to be attached to - corresponding pins of the joint's :code:`offset` component. - - The machine start-up procedure should pull the - :code:`hal_offset_mgr.load_params` command pin high. The - component will then copy saved offset values from the redis - :code:`hal_offset_mgr/_offset` keys to the corresponding - :code:`hal_offset_mgr._offset` pins for each joint, and pull - :code:`hal_offset_mgr.load_params` low again. - - The user interface should present the user with a 'zero offsets' - switch that requests all offsets zeroed at the current position by - pulling the :code:`hal_offset_mgr.zero_all_joints` pin high. The - :code:`hal_offset_mgr.enable` pin must be low, or the component - will refuse to do anything and issue an error message to that - effect. Otherwise, the component will zero offsets by copying the - :code:`hal_offset_mgr._fb-in` pins' values to the - :code:`hal_offset_mgr._offset` pins and the redis - :code:`hal_offset_mgr/_offset` keys for each joint. - ''' - - compname = 'hal_offset_mgr' - - def setup_component(self): - '''Read joints from ROS parameter server and create component pins - ''' - - # get joint info from hardware_interface config - joint_names = rospy.get_param('hardware_interface/joints', None) - if joint_names is None: - raise HalHWInterfaceException( - "Error reading ROS param hardware_interface/joints" - ) - - # create joint offset pins - self.offset_pins = [] - for name in joint_names: - o = HalOffsetMgrPin(name) - self.offset_pins.append(o) - - # create enable and zero + load command pins - self.zero_all_joints = RosHalPin('zero_all_joints', 'BIT', 'IO') - self.load_params = RosHalPin('load_params', 'BIT', 'IO') - self.enable = RosHalPin('enable', 'BIT', 'IN') - - def update(self): - '''Periodic update function; watches for and executes - :code:`load_params` or :code:`zero_all_joints` requests - ''' - - if self.load_params.get_pin(): - # Command: Load initial offsets from stored parameters - for p in self.offset_pins: - p.load_offset() - self.load_params.set_pin(False) - return - - if self.zero_all_joints.get_pin(): - # Command: Zero all joints - if self.enable.get_pin(): - rospy.logerr("Not zeroing joints while enabled") - self.zero_all_joints.set_pin(False) - return - - for p in self.offset_pins: - p.zero_joint() - - self.zero_all_joints.set_pin(False) - rospy.loginfo("All joints zeroed") - - def shutdown_component(self): - '''Close redis client connection at shutdown - ''' - RedisStoreHalPin.shutdown() diff --git a/hal_hw_interface/src/hal_hw_interface/hal_pin_attrs.py b/hal_hw_interface/src/hal_hw_interface/hal_pin_attrs.py index 23c9486..50933ce 100644 --- a/hal_hw_interface/src/hal_hw_interface/hal_pin_attrs.py +++ b/hal_hw_interface/src/hal_hw_interface/hal_pin_attrs.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -import hal +import machinekit.hal.pyhal as hal class HalPinAttrBase(int): - '''Subclass int to make a simple interface for accessing + """Subclass int to make a simple interface for accessing hal.HAL_ enums by integer or string - ''' + """ _suffixes = [] # hal.HAL_%s; define in subclasses _updated = False @@ -14,7 +14,7 @@ class HalPinAttrBase(int): _fwd_map = dict() # 16 -> 'IO' _bwd_map = dict() # 'HAL_IN' -> 16; 'IN' -> 16 for attr in dir(hal): - if not attr.startswith('HAL_'): + if not attr.startswith("HAL_"): continue value = getattr(hal, attr) attr_short = attr[4:] @@ -22,14 +22,14 @@ class HalPinAttrBase(int): _bwd_map[attr_short] = value def __new__(cls, value): - '''Create new object, translating strings to ints and validating value - ''' + """Create new object, translating strings to ints and validating + value""" if isinstance(value, int): if cls._fwd_map.get(value, None) not in cls._suffixes: raise ValueError("Illegal value '{}'".format(value)) return int.__new__(cls, value) elif isinstance(value, str): - if value.startswith('HAL_'): + if value.startswith("HAL_"): value = value[4:] if value not in cls._suffixes: raise ValueError("Illegal value '{}'".format(value)) @@ -38,7 +38,7 @@ def __new__(cls, value): raise ValueError("Illegal value '{}'".format(value)) def __repr__(self): - return 'HAL_' + self._fwd_map[self] + return "HAL_" + self._fwd_map[self] def __str__(self): return self.__repr__() @@ -55,7 +55,7 @@ class HalPinDir(HalPinAttrBase): .. inheritance-diagram:: hal_hw_interface.hal_pin_attrs.HalPinDir """ - _suffixes = set(['IN', 'OUT', 'IO']) + _suffixes = set(["IN", "OUT", "IO"]) class HalPinType(HalPinAttrBase): @@ -70,4 +70,4 @@ class HalPinType(HalPinAttrBase): .. inheritance-diagram:: hal_hw_interface.hal_pin_attrs.HalPinType """ - _suffixes = set(['BIT', 'U32', 'S32', 'FLOAT']) + _suffixes = set(["BIT", "U32", "S32", "FLOAT"]) diff --git a/hal_hw_interface/src/hal_hw_interface/loadrt_local.py b/hal_hw_interface/src/hal_hw_interface/loadrt_local.py new file mode 100644 index 0000000..2f2fafe --- /dev/null +++ b/hal_hw_interface/src/hal_hw_interface/loadrt_local.py @@ -0,0 +1,20 @@ +import rospy +import os +from machinekit import hal +from machinekit.hal.cyruntime import rtapi + + +def loadrt_local(modname): + """Load a locally-built HAL component not installed in the standard + module directory + """ + if modname in hal.components: + return + for path in os.environ.get("LD_LIBRARY_PATH", "").split(":"): + rospy.logdebug(f"Checking for {modname}.so in {path}") + modpath = os.path.join(path, f"{modname}") + if os.path.exists(f"{modpath}.so"): + break + else: + raise RuntimeError(f"Unable to locate {modname} module") + rtapi.loadrt(modpath) diff --git a/hal_hw_interface/src/hal_hw_interface/redis_store_hal_pin.py b/hal_hw_interface/src/hal_hw_interface/redis_store_hal_pin.py index 2159c73..bc82f69 100644 --- a/hal_hw_interface/src/hal_hw_interface/redis_store_hal_pin.py +++ b/hal_hw_interface/src/hal_hw_interface/redis_store_hal_pin.py @@ -11,84 +11,89 @@ hal_hw_interface.redis_store_hal_pin.RedisStoreHalPin """ +import attr import rospy # Mock patching breaks with `from redis_store import ConfigClient`; why? import redis_store.config as redis_config -from hal_hw_interface.ros_hal_pin import RosHalPin +from hal_hw_interface.ros_hal_pin import RosHalPin, HalPinDir +@attr.s class RedisStoreHalPin(RosHalPin): - '''HAL pin attached to :code:`redis_store` ROS package's parameter + """HAL pin attached to :code:`redis_store` ROS package's parameter server This HAL pin's value may be read from and written to a - :code:`redis_store` parameter server. The default key name is - :code:`/`. + :code:`redis_store` parameter server. The default redis key name + is :code:`/`. :param name: The HAL pin name :type name: str + :param key: Set the redis key + :type key: str :param hal_type: HAL pin data type, one of :code:`['BIT', 'U32', 'S32', 'FLOAT']` :type hal_type: :py:class:`hal_hw_interface.hal_pin_attrs.HalPinType` :param hal_dir: HAL pin direction, one of :code:`['IN', 'OUT', 'IO']` :type hal_dir: :py:class:`hal_hw_interface.hal_pin_attrs.HalPinDir` - ''' + """ - # Usually (?) read output pin default value from redis - _default_hal_dir = 'OUT' + key = attr.ib() - @property - def _redis_config(self): - if 'redis_config_client' not in self._cached_objs: - # Autovivify - c = redis_config.ConfigClient() - self._cached_objs['redis_config_client'] = c - return self._cached_objs['redis_config_client'] - - @property - def _redis_param_key(self): + # Attribute default factories + @key.default + def _key_default(self): return "{}/{}".format(self.compname, self.pin_name) - def set_pin_from_redis(self, default=None): - '''Read and return value from redis_store - ''' - redis_value = self._redis_config.get_param(self._redis_param_key) - if redis_value is None and default is None: - rospy.logwarn( - "%s: Unable to initialize offset: " - "no saved value and no default given" % self.compname - ) - return - new_value = default if redis_value is None else redis_value - self.set_pin(new_value) - return new_value - - def set_redis_from_pin(self): - '''Read HAL pin value and write to redis_store - ''' - old_val = self._redis_config.get_param(self._redis_param_key) + redis_service_timeout_default = 20.0 # seconds to wait for service + + @property + def _redis_config(self): + if "redis_config_client" in self._cached_objs: + return self._cached_objs["redis_config_client"] + + # Autovivify + timeout = self.get_ros_param( + "redis_service_timeout", self.redis_service_timeout_default + ) + rospy.loginfo(f"Connecting to redis database, timeout {timeout}s") + client = redis_config.ConfigClient(subscribe=True) + client.wait_for_service(timeout=timeout) + self._cached_objs["redis_config_client"] = client + # Disconnect from redis at shutdown + self.add_shutdown_callback(client.stop) + + return client + + def _ros_init(self): + if self.hal_dir == HalPinDir("IN"): + # Input pins write value out to redis + self._prev_pin_val = self.get_pin() + self._prev_redis_val = None + rospy.loginfo(f'Updating to redis key "{self.key}"') + else: + # Output pins read value from redis; IO pins go both ways + # but (arbitrarily) take starting value from redis. + val = self._redis_config.get_param(self.key) + self._prev_pin_val = self._prev_redis_val = val + if val is not None: # Key exists in redis + self.set_pin(val) + # Updates from redis are effected through a callback + self._redis_config.on_update_received.append(self._update_fm_redis) + rospy.loginfo(f'Updating from redis key "{self.key}"') + + def _update_fm_redis(self, key, value): + if key != self.key: + return # Not applicable + self.set_pin(value) + self._prev_pin_val = self._prev_redis_val = value + + def update(self): + """Write changed pin value to redis for input and IO pins""" new_val = self.get_pin() - self._redis_config.set_param(self._redis_param_key, new_val) - if old_val is not None and not self._isclose( - new_val, old_val, 1e-5, 1e-5 - ): - rospy.loginfo( - "%s: Updated %s from %.4f to %.4f", - self.compname, - self._redis_param_key, - old_val, - self._redis_config.get_param(self._redis_param_key), - ) - return new_val - - @classmethod - def shutdown(cls): - '''Closes the shared redis client connection. Call this from - :py:func:`hal_hw_interface.ros_hal_component.RosHalComponent.shutdown_component` - to close the connection at the HAL component's exit. - ''' - config = cls._cached_objs.get('redis_config_client', None) - if config is not None: - config.stop() + if self.hal_dir == HalPinDir("OUT") or self._prev_pin_val == new_val: + return # Not applicable + self._redis_config.set_param(self.key, new_val) + self._prev_pin_val = self._prev_redis_val = new_val diff --git a/hal_hw_interface/src/hal_hw_interface/ros_hal_component.py b/hal_hw_interface/src/hal_hw_interface/ros_hal_component.py index 1904d32..0c9996b 100644 --- a/hal_hw_interface/src/hal_hw_interface/ros_hal_component.py +++ b/hal_hw_interface/src/hal_hw_interface/ros_hal_component.py @@ -67,7 +67,7 @@ def __init__(self): rospy.loginfo("Initializing '%s' component" % self.compname) # Publisher update rate in Hz - self.update_rate = rospy.get_param('%s/update_rate' % self.compname, 10) + self.update_rate = self.get_ros_param("update_rate", 10) self.rate = rospy.Rate(self.update_rate) rospy.logdebug("Publish update rate = %.1f" % self.update_rate) @@ -81,17 +81,6 @@ def __init__(self): self.hal_comp.ready() rospy.loginfo("User component '%s' ready" % self.compname) - def get_param(self, key_suffix, default=None): - """Shorthand for `rospy.get_param(self.compname + key_suffix)` - - :param key_suffix: A suffix to append to :py:attr:`compname` - to form the full ROS parameter name - :type key_suffix: str - :param default: A default value, same as :py:func:`rospy.get_param` - """ - key = '{}/{}'.format(self.compname, key_suffix) - return rospy.get_param(key, default) - def setup_component(self): """Set up the ROS node and HAL component @@ -132,10 +121,11 @@ def update(self): def shutdown_component(self): """Perform extra shutdown actions - This may optionally be defined in subclasses to take care of - extra shutdown actions, such as disconnecting from services. + Executes the list of callbacks defined by calls to + :py:func:`hal_hw_interface.hal_obj_base.HalObjBase.add_shutdown_callback`. """ - pass + for cb in self._cached_objs.setdefault("shutdown_cbs", []): + cb() def main(self): """The ROS node and HAL component `main()` function diff --git a/hal_hw_interface/src/hal_hw_interface/ros_hal_pin.py b/hal_hw_interface/src/hal_hw_interface/ros_hal_pin.py index 17ca007..861f9db 100644 --- a/hal_hw_interface/src/hal_hw_interface/ros_hal_pin.py +++ b/hal_hw_interface/src/hal_hw_interface/ros_hal_pin.py @@ -13,7 +13,6 @@ hal_hw_interface.ros_hal_pin.RosHalPinService """ -import sys import attr import rospy from hal_hw_interface.hal_obj_base import HalObjBase @@ -27,7 +26,7 @@ @attr.s class RosHalPin(HalObjBase): - '''Basic HAL pin for use in + """Basic HAL pin for use in :py:class:`hal_hw_interface.ros_hal_component.RosHalComponent` user components @@ -42,10 +41,10 @@ class RosHalPin(HalObjBase): :type hal_type: :py:class:`hal_hw_interface.hal_pin_attrs.HalPinType` :param hal_dir: HAL pin direction, one of :code:`['IN', 'OUT', 'IO']` :type hal_dir: :py:class:`hal_hw_interface.hal_pin_attrs.HalPinDir` - ''' + """ _default_hal_type = None - _default_hal_dir = 'IN' # Subclasses may override for hal_dir attribute + _default_hal_dir = "IN" # Subclasses may override for hal_dir attribute name = attr.ib() hal_type = attr.ib(converter=HalPinType) @@ -59,18 +58,18 @@ def _hal_dir_default(self): @hal_type.default def _hal_type_default(self): if self._default_hal_type is None: - raise TypeError('%s requires hal_type= argument' % self.__class__) + raise TypeError("%s requires hal_type= argument" % self.__class__) return HalPinType(self._default_hal_type) @property def pin_name(self): - '''Return the pin_name; read-only property + """Return the pin_name; read-only property In some subclasses, this may not be the same as the :code:`name` parameter supplied to the constructor. :returns: :py:class:`str` pin name - ''' + """ return self.name def __attrs_post_init__(self): @@ -93,8 +92,7 @@ def _ros_init(self): pass def update(self): - '''An update function; used in some subclasses - ''' + """An update function; used in some subclasses""" # May be implemented in subclasses raise NotImplementedError() @@ -116,10 +114,10 @@ def get_pin(self): @property def compname(self): - '''The HAL component name; read-only property + """The HAL component name; read-only property :returns: :py:class:`str` of component name - ''' + """ return self.hal_comp.getprefix() @classmethod @@ -129,7 +127,7 @@ def _isclose(cls, a, b, rel_tol=1e-9, abs_tol=1e-9): @attr.s class RosHalPinPublisher(RosHalPin): - '''HAL pin with attached ROS publisher + """HAL pin with attached ROS publisher This HAL pin is set with publishes its value on a ROS topic, :code:`/` by default. Its :py:func:`update` function @@ -161,29 +159,31 @@ class RosHalPinPublisher(RosHalPin): directions make sense for all subclasses .. todo:: Link documentation to ROS ``srv`` messages - ''' + """ - _default_hal_dir = 'IN' + _default_hal_dir = "IN" pub_topic = attr.ib() msg_type = attr.ib() - last_value = attr.ib(default=None) # Attribute default factories @pub_topic.default def _pub_topic_default(self): - return '{}/{}'.format(self.compname, self.pin_name) + return "{}/{}".format(self.compname, self.pin_name) + + _pin_to_msg_type_map = { + HalPinType("BIT"): Bool, + HalPinType("U32"): UInt32, + HalPinType("S32"): Int32, + HalPinType("FLOAT"): Float64, + } @msg_type.default def _msg_type_default(self): - return { - HalPinType('BIT'): Bool, - HalPinType('U32'): UInt32, - HalPinType('S32'): Int32, - HalPinType('FLOAT'): Float64, - }[self.hal_type] + return self._pin_to_msg_type_map[self.hal_type] def _ros_init(self): self._ros_publisher_init() + self._msg = self._pin_to_msg_type_map[self.hal_type](self.get_pin()) def _ros_publisher_init(self): rospy.loginfo('Creating publisher on topic "{}"'.format(self.pub_topic)) @@ -192,32 +192,36 @@ def _ros_publisher_init(self): ) def _value_changed(self, value): - if self.hal_type == HalPinType('FLOAT'): - changed = self.last_value is None or not self._isclose( - self.last_value, + if self.hal_type == HalPinType("FLOAT"): + changed = not self._isclose( + self._msg.data, value, - rel_tol=self.get_ros_param('relative_tolerance', 1e-9), - abs_tol=self.get_ros_param('absolute_tolerance', 1e-9), + rel_tol=self.get_ros_param("relative_tolerance", 1e-9), + abs_tol=self.get_ros_param("absolute_tolerance", 1e-9), ) else: - changed = self.last_value != value + changed = self._msg.data != value return changed def update(self): - """If pin value has changed, publish to ROS topic - """ - # rospy.logdebug( - # "publish_pins: Publishing pin '%s' value '%s'" % - # (self.pin_name, self.get_pin())) + """If pin value has changed, publish to ROS topic""" value = self.get_pin() if self._value_changed(value): - self.last_value = value - self.pub.publish(value) + rospy.logdebug( + "publish_pins: Publishing pin '%s' value '%s'" + % (self.pin_name, self.get_pin()) + ) + rospy.loginfo( + f"Pin {self.pin_name} changed:" + f" old={self._msg.data}; new={value}" + ) + self._msg.data = value + self.pub.publish(self._msg) @attr.s class RosHalPinSubscriber(RosHalPinPublisher): - '''HAL pin with attached ROS publisher and subscriber + """HAL pin with attached ROS publisher and subscriber This HAL pin isn't set via :py:func:`set_pin`, but subscribes to a ROS topic for its value. As a subclass of @@ -238,18 +242,18 @@ class RosHalPinSubscriber(RosHalPinPublisher): :type pub_topic: str :param sub_topic: ROS subscriber topic :type sub_topic: str - ''' + """ - _default_hal_dir = 'OUT' + _default_hal_dir = "OUT" sub_topic = attr.ib() # Attribute default factories @sub_topic.default def _sub_topic_default(self): - return '{}/{}'.format(self.compname, self.pin_name) + return "{}/{}".format(self.compname, self.pin_name) def _ros_init(self): - self._ros_publisher_init() + super()._ros_init() self._ros_subscriber_init() def _ros_subscriber_init(self): @@ -269,12 +273,14 @@ def _subscriber_cb(self, msg): % (type(msg), self.pin_name, self.msg_type) ) - self.set_pin(msg.data) + if self._value_changed(msg.data): + self.set_pin(msg.data) + self.update() @attr.s class RosHalPinService(RosHalPinPublisher): - '''HAL pin with attached ROS service and publisher + """HAL pin with attached ROS service and publisher This HAL pin may be set via a ROS service, in addition to :py:func:`set_pin`. As a subclass of @@ -297,9 +303,9 @@ class RosHalPinService(RosHalPinPublisher): :type pub_topic: str :param service_name: ROS service name :type service_name: str - ''' + """ - _default_hal_dir = 'OUT' + _default_hal_dir = "OUT" service_name = attr.ib() service_msg_type = attr.ib() @@ -307,21 +313,22 @@ class RosHalPinService(RosHalPinPublisher): # Attribute default factories @service_name.default def _service_name_default(self): - return '{}/{}'.format(self.compname, self.pin_name) + return "{}/{}".format(self.compname, self.pin_name) + + _pin_to_service_msg_type_map = { + HalPinType("BIT"): SetBool, + HalPinType("U32"): SetUInt32, + HalPinType("S32"): SetInt32, + HalPinType("FLOAT"): SetFloat64, + } @service_msg_type.default def _service_msg_type_default(self): - return { - HalPinType('BIT'): SetBool, - HalPinType('U32'): SetUInt32, - HalPinType('S32'): SetInt32, - HalPinType('FLOAT'): SetFloat64, - }[self.hal_type] + return self._pin_to_service_msg_type_map[self.hal_type] def _ros_init(self): + super()._ros_init() self._ros_service_init() - # Publish the value on a topic, too - self._ros_publisher_init() def _ros_service_init(self): self.service = rospy.Service( @@ -331,4 +338,4 @@ def _ros_service_init(self): def _svc_cb(self, req): self.set_pin(req.data) - return True, 'OK' + return True, "OK" diff --git a/hal_hw_interface/src/hal_hw_interface/tests/conftest.py b/hal_hw_interface/src/hal_hw_interface/tests/conftest.py index ea13020..5beffb9 100644 --- a/hal_hw_interface/src/hal_hw_interface/tests/conftest.py +++ b/hal_hw_interface/src/hal_hw_interface/tests/conftest.py @@ -12,46 +12,46 @@ def mock_objs(): @pytest.fixture() -def mock_comp_obj(): +def mock_comp_obj(request): # Mock hal.component and returned object # - Settable and readable pins - pin_value_map = dict(__default=0xDEADBEEF) + request.instance.pin_values = pin_values = dict(__default=0xDEADBEEF) def get_pin(key): - if key in pin_value_map: - value = pin_value_map[key] + if key in pin_values: + value = pin_values[key] print("Returning pin %s value=%s" % (key, value)) else: - value = pin_value_map['__default'] + value = pin_values["__default"] print("Returning pin %s DEFAULT value=0x%x" % (key, value)) return value def set_pin(key, value): print("Setting pin %s value=%s" % (key, value)) - pin_value_map[key] = value + pin_values[key] = value - mock_objs_dict['comp_name'] = 'test_comp' - comp_getprefix = MagicMock(side_effect=lambda: mock_objs_dict['comp_name']) + mock_objs_dict["comp_name"] = "test_comp" + comp_getprefix = MagicMock(side_effect=lambda: mock_objs_dict["comp_name"]) def set_comp_name(n): - mock_objs_dict['comp_name'] = n + mock_objs_dict["comp_name"] = n comp_setprefix = MagicMock(side_effect=set_comp_name) - mock_comp_obj = MagicMock(name='mock_hal_comp_obj') + mock_comp_obj = MagicMock(name="mock_hal_comp_obj") mock_comp_obj.configure_mock( - name='mock_hal_comp_obj', + name="mock_hal_comp_obj", getprefix=comp_getprefix, setprefix=comp_setprefix, set_pin=set_pin, **{ - '__getitem__.side_effect': get_pin, - '__setitem__.side_effect': set_pin, + "__getitem__.side_effect": get_pin, + "__setitem__.side_effect": set_pin, } ) - patcher = patch('hal.component', return_value=mock_comp_obj) + patcher = patch("hal.component", return_value=mock_comp_obj) mock_hal = patcher.start() - mock_objs_dict['hal_comp'] = mock_hal # Pass hal.component fixture + mock_objs_dict["hal_comp"] = mock_hal # Pass hal.component fixture yield mock_comp_obj patcher.stop() @@ -63,24 +63,24 @@ def mock_rospy(): def set_key(key, value): get_param_keys[key] = value - mock_get_param = MagicMock(name='mock_rospy_get_param') + mock_get_param = MagicMock(name="mock_rospy_get_param") get_param_keys = dict() mock_get_param.side_effect = get_param_keys.get mock_get_param.set_key = set_key # - rospy.Rate with mock rospy.Rate() - mock_Rate_obj = MagicMock(name='mock_rospy_Rate_obj') + mock_Rate_obj = MagicMock(name="mock_rospy_Rate_obj") mock_Rate = MagicMock(return_value=mock_Rate_obj) # - rospy.is_shutdown() that shuts down after a few loops mock_is_shutdown = MagicMock(side_effect=[False] * 3 + [True]) # - rospy.{Subscriber,Publisher,Service}() methods & returned objects - mock_Subscriber_obj = MagicMock(name='mock_rospy_Subscriber_obj') + mock_Subscriber_obj = MagicMock(name="mock_rospy_Subscriber_obj") mock_Subscriber = MagicMock(return_value=mock_Subscriber_obj) - mock_Publisher_obj = MagicMock(name='mock_rospy_Publisher_obj') + mock_Publisher_obj = MagicMock(name="mock_rospy_Publisher_obj") mock_Publisher = MagicMock(return_value=mock_Publisher_obj) - mock_Service_obj = MagicMock(name='mock_rospy_Service_obj') + mock_Service_obj = MagicMock(name="mock_rospy_Service_obj") mock_Service = MagicMock(return_value=mock_Service_obj) # The patch.multiple() patcher doesn't pass non-DEFAULT @@ -109,13 +109,13 @@ def log_side_effect(msg_fmt, *args): return log_side_effect mock_loginfo = MagicMock( - name='rospy_loginfo', side_effect=log_side_effect_closure('loginfo') + name="rospy_loginfo", side_effect=log_side_effect_closure("loginfo") ) mock_logdebug = MagicMock( - name='rospy_logdebug', side_effect=log_side_effect_closure('logdebug') + name="rospy_logdebug", side_effect=log_side_effect_closure("logdebug") ) mock_logfatal = MagicMock( - name='rospy_logfatal', side_effect=log_side_effect_closure('logfatal') + name="rospy_logfatal", side_effect=log_side_effect_closure("logfatal") ) # patch ropsy @@ -131,7 +131,7 @@ def log_side_effect(msg_fmt, *args): Service=mock_Service, is_shutdown=mock_is_shutdown, ) - patcher = patch.multiple('rospy', **rpc) + patcher = patch.multiple("rospy", **rpc) mock_rospy = patcher.start() yield mock_rospy @@ -140,13 +140,13 @@ def log_side_effect(msg_fmt, *args): @pytest.fixture() -def mock_redis_client_obj(): +def mock_redis_client_obj(request): # Mock redis_store.ConfigClient method and returned object # - Settable and readable pins - key_value_map = dict(__default=0) + request.instance.key_value_map = key_value_map = dict(__default=0) def get_key(key): - value = key_value_map.get(key, key_value_map['__default']) + value = key_value_map.get(key, key_value_map["__default"]) print("Returning redis key %s value=%s" % (key, value)) return value @@ -154,18 +154,19 @@ def set_key(key, value): print("Setting redis key %s value=%s" % (key, value)) key_value_map[key] = value - mock_client_obj = MagicMock(name='ConfigClient_obj') + mock_client_obj = MagicMock(name="ConfigClient_obj") mock_client_obj.configure_mock( - name='mock_redis_client_obj', + name="mock_redis_client_obj", set_key=set_key, # Won't increment mock_calls - **{'get_param.side_effect': get_key, 'set_param.side_effect': set_key} + on_update_received=list(), + **{"get_param.side_effect": get_key, "set_param.side_effect": set_key} ) patcher = patch( - 'redis_store.config.ConfigClient', return_value=mock_client_obj + "redis_store.config.ConfigClient", return_value=mock_client_obj ) redis_store = patcher.start() - mock_objs_dict['redis_store'] = redis_store + mock_objs_dict["redis_store"] = redis_store yield mock_client_obj patcher.stop() diff --git a/hal_hw_interface/src/hal_hw_interface/tests/test_fixtures.py b/hal_hw_interface/src/hal_hw_interface/tests/test_fixtures.py index 63aaf23..21b08ad 100644 --- a/hal_hw_interface/src/hal_hw_interface/tests/test_fixtures.py +++ b/hal_hw_interface/src/hal_hw_interface/tests/test_fixtures.py @@ -7,93 +7,92 @@ keys2 = dict(pin1=False, pin2=1.88e42, pin4=0) -def test_mock_comp_obj_fixture(mock_comp_obj, mock_objs): - # Test hal.component returns mock_comp_obj - assert mock_objs['hal_comp']() is mock_comp_obj - - # Set each pin (with out-of-band method) and check - for name, value in keys1.items(): - mock_comp_obj.set_pin(name, value) - assert mock_comp_obj[name] == value - - # Recheck - for name, value in keys1.items(): - assert mock_comp_obj[name] == value - - # Set each pin and check - for name, value in keys2.items(): - mock_comp_obj[name] = value - assert mock_comp_obj[name] == value - - # Recheck everything - pins = keys1.copy() - pins.update(keys2) - for name, value in pins.items(): - assert mock_comp_obj[name] == value - - # Default case - assert mock_comp_obj['bogus'] == 0xDEADBEEF - - -def test_mock_rospy_fixture(mock_rospy, mock_objs): - # Test mock rospy.get_param() - gp = mock_objs['rospy_get_param'] - gp.set_key('foo', 1) - gp.set_key('bar', 2) - assert gp('foo') == 1 - assert gp('bar') == 2 - gp.set_key('baz', 3) - assert gp('foo') == 1 - assert gp('bar') == 2 - assert gp('baz') == 3 - - # Test rospy.Rate() returns expected object - assert mock_objs['rospy_Rate']() is mock_objs['rospy_Rate_obj'] - - # Test rospy.is_shutdown() returns True values, then False - found_false = False - for i in range(10): - val = mock_objs['rospy_is_shutdown']() - print("iter {} val {}".format(i, val)) - if val is False: - found_false = True - if val is True: - break - else: - raise Exception("is_shutdown never returned True") - if not found_false: - raise Exception("is_shutdown never returned False") - - # Test returned objects - for name in ('Subscriber', 'Publisher', 'Service'): - method = mock_objs['rospy_{}'.format(name)] - obj = mock_objs['rospy_{}_obj'.format(name)] - assert method() == obj - - -def test_mock_redis_client_obj(mock_redis_client_obj, mock_objs): - # Test redis_store.ConfigClient() returns object - assert mock_objs['redis_store']() is mock_redis_client_obj - - # Set each param (with out-of-band method) and check - for name, value in keys1.items(): - mock_redis_client_obj.set_key(name, value) - assert mock_redis_client_obj.get_param(name) == value - - # Recheck - for name, value in keys1.items(): - assert mock_redis_client_obj.get_param(name) == value - - # Set each param and check - for name, value in keys2.items(): - mock_redis_client_obj.set_param(name, value) - assert mock_redis_client_obj.get_param(name) == value - - # Recheck everything - params = keys1.copy() - params.update(keys2) - for name, value in params.items(): - assert mock_redis_client_obj.get_param(name) == value - - # Default case - assert mock_redis_client_obj.get_param('bogus') == 0 +class TestFixtures: + def test_mock_comp_obj_fixture(self, mock_comp_obj, mock_objs): + # Test hal.component returns mock_comp_obj + assert mock_objs["hal_comp"]() is mock_comp_obj + + # Set each pin (with out-of-band method) and check + for name, value in keys1.items(): + mock_comp_obj.set_pin(name, value) + assert mock_comp_obj[name] == value + + # Recheck + for name, value in keys1.items(): + assert mock_comp_obj[name] == value + + # Set each pin and check + for name, value in keys2.items(): + mock_comp_obj[name] = value + assert mock_comp_obj[name] == value + + # Recheck everything + pins = keys1.copy() + pins.update(keys2) + for name, value in pins.items(): + assert mock_comp_obj[name] == value + + # Default case + assert mock_comp_obj["bogus"] == 0xDEADBEEF + + def test_mock_rospy_fixture(self, mock_rospy, mock_objs): + # Test mock rospy.get_param() + gp = mock_objs["rospy_get_param"] + gp.set_key("foo", 1) + gp.set_key("bar", 2) + assert gp("foo") == 1 + assert gp("bar") == 2 + gp.set_key("baz", 3) + assert gp("foo") == 1 + assert gp("bar") == 2 + assert gp("baz") == 3 + + # Test rospy.Rate() returns expected object + assert mock_objs["rospy_Rate"]() is mock_objs["rospy_Rate_obj"] + + # Test rospy.is_shutdown() returns True values, then False + found_false = False + for i in range(10): + val = mock_objs["rospy_is_shutdown"]() + print("iter {} val {}".format(i, val)) + if val is False: + found_false = True + if val is True: + break + else: + raise Exception("is_shutdown never returned True") + if not found_false: + raise Exception("is_shutdown never returned False") + + # Test returned objects + for name in ("Subscriber", "Publisher", "Service"): + method = mock_objs["rospy_{}".format(name)] + obj = mock_objs["rospy_{}_obj".format(name)] + assert method() == obj + + def test_mock_redis_client_obj(self, mock_redis_client_obj, mock_objs): + # Test redis_store.ConfigClient() returns object + assert mock_objs["redis_store"]() is mock_redis_client_obj + + # Set each param (with out-of-band method) and check + for name, value in keys1.items(): + mock_redis_client_obj.set_key(name, value) + assert mock_redis_client_obj.get_param(name) == value + + # Recheck + for name, value in keys1.items(): + assert mock_redis_client_obj.get_param(name) == value + + # Set each param and check + for name, value in keys2.items(): + mock_redis_client_obj.set_param(name, value) + assert mock_redis_client_obj.get_param(name) == value + + # Recheck everything + params = keys1.copy() + params.update(keys2) + for name, value in params.items(): + assert mock_redis_client_obj.get_param(name) == value + + # Default case + assert mock_redis_client_obj.get_param("bogus") == 0 diff --git a/hal_hw_interface/src/hal_hw_interface/tests/test_hal_io_comp.py b/hal_hw_interface/src/hal_hw_interface/tests/test_hal_io_comp.py index eca5a93..f69df60 100644 --- a/hal_hw_interface/src/hal_hw_interface/tests/test_hal_io_comp.py +++ b/hal_hw_interface/src/hal_hw_interface/tests/test_hal_io_comp.py @@ -8,30 +8,30 @@ class TestHalIO(object): @pytest.fixture def pin_params(self, mock_rospy, mock_objs): - gp = mock_objs['rospy_get_param'] + gp = mock_objs["rospy_get_param"] test_pins = dict( publish_pins=dict( - bit_pub=dict(hal_type='HAL_BIT'), - u32_pub=dict(hal_type='HAL_U32'), - s32_pub=dict(hal_type='HAL_S32', hal_dir='HAL_IO'), - float_pub=dict(hal_type='HAL_FLOAT', pub_topic='/float/topic'), + bit_pub=dict(hal_type="HAL_BIT"), + u32_pub=dict(hal_type="HAL_U32"), + s32_pub=dict(hal_type="HAL_S32", hal_dir="HAL_IO"), + float_pub=dict(hal_type="HAL_FLOAT", pub_topic="/float/topic"), ), subscribe_pins=dict( - bit_sub=dict(hal_type='HAL_BIT', sub_topic='/bit/topic'), - u32_sub=dict(hal_type='HAL_U32', hal_dir='HAL_IO'), - s32_sub=dict(hal_type='HAL_S32'), - float_sub=dict(hal_type='HAL_FLOAT'), + bit_sub=dict(hal_type="HAL_BIT", sub_topic="/bit/topic"), + u32_sub=dict(hal_type="HAL_U32", hal_dir="HAL_IO"), + s32_sub=dict(hal_type="HAL_S32"), + float_sub=dict(hal_type="HAL_FLOAT"), ), service_pins=dict( - bit_svc=dict(hal_type='HAL_BIT'), - u32_svc=dict(hal_type='HAL_U32', hal_dir='HAL_IO'), - s32_svc=dict(hal_type='HAL_S32', service_name='/s32/svc'), - float_svc=dict(hal_type='HAL_FLOAT'), + bit_svc=dict(hal_type="HAL_BIT"), + u32_svc=dict(hal_type="HAL_U32", hal_dir="HAL_IO"), + s32_svc=dict(hal_type="HAL_S32", service_name="/s32/svc"), + float_svc=dict(hal_type="HAL_FLOAT"), ), ) for key, val in test_pins.items(): - gp.set_key('hal_io/%s' % key, val) + gp.set_key("hal_io/%s" % key, val) return gp, test_pins @pytest.fixture @@ -39,14 +39,14 @@ def obj(self, mock_comp_obj, mock_rospy, mock_objs, pin_params): for key in list(self.test_class._cached_objs.keys()): self.test_class._cached_objs.pop(key) - mock_comp_obj.setprefix('hal_io') + mock_comp_obj.setprefix("hal_io") return self.test_class() def test_hal_io_comp_fixture(self, pin_params): get_param, test_pins = pin_params - print(get_param('hal_io/publish_pins')) - assert isinstance(get_param('hal_io/publish_pins'), dict) - assert 'bit_sub' in get_param('hal_io/subscribe_pins') + print(get_param("hal_io/publish_pins")) + assert isinstance(get_param("hal_io/publish_pins"), dict) + assert "bit_sub" in get_param("hal_io/subscribe_pins") def test_hal_io_comp_setup_component(self, obj, pin_params): get_param, test_pins = pin_params @@ -60,7 +60,7 @@ def test_hal_io_comp_setup_component(self, obj, pin_params): for config_key, pins in test_pins.items(): # Check param server was queried print("Pin class: %s" % config_key) - get_param.assert_any_call('hal_io/%s' % config_key, {}) + get_param.assert_any_call("hal_io/%s" % config_key, {}) for pin_name, pin_data in pins.items(): # Check pin was created assert pin_name in obj_pins @@ -76,15 +76,13 @@ def test_hal_io_comp_update(self, obj, pin_params, mock_comp_obj): get_param, test_pins = pin_params obj.setup_component() - # Put pins in dict and set values + # Set pin values for pin in obj.pins: mock_comp_obj.set_pin(pin.name, 1) - pin.last_value = 0 # Run update() and check that pins changed print("----------- Running obj.update()") obj.update() for pin in obj.pins: - print("- pin %s" % pin) - assert pin.last_value == 1 - pin.pub.publish.assert_called_with(1) + print(f"- pin {pin}") + pin.pub.publish.assert_called() diff --git a/hal_hw_interface/src/hal_hw_interface/tests/test_hal_obj_base.py b/hal_hw_interface/src/hal_hw_interface/tests/test_hal_obj_base.py index f88d75a..0dc952a 100644 --- a/hal_hw_interface/src/hal_hw_interface/tests/test_hal_obj_base.py +++ b/hal_hw_interface/src/hal_hw_interface/tests/test_hal_obj_base.py @@ -5,7 +5,7 @@ class TestHalObjBase(object): - compname = 'test_comp' + compname = "test_comp" test_class = HalObjBase @pytest.fixture @@ -16,14 +16,14 @@ def obj(self, mock_rospy): class BogusComp(HalObjBase): compname = self.compname - BogusComp._cached_objs.pop('hal_comp', None) # Clean fixture + BogusComp._cached_objs.pop("hal_comp", None) # Clean fixture return BogusComp() def test_hal_obj_base_init_hal_comp(self, obj, mock_comp_obj): # Test init_hal_comp() creates cached component object obj.init_hal_comp() - assert 'hal_comp' in obj._cached_objs - assert obj._cached_objs['hal_comp'] is mock_comp_obj + assert "hal_comp" in obj._cached_objs + assert obj._cached_objs["hal_comp"] is mock_comp_obj def test_hal_obj_base_init_hal_comp_no_compname(self): # Test init_hal_comp() on class with no 'compname' attribute @@ -52,12 +52,12 @@ def test_hal_obj_base_hal_comp_not_initialized(self, obj, mock_comp_obj): obj.hal_comp def test_hal_obj_base_get_ros_param(self, obj, mock_objs): - test_params = dict(key1=42, key2='val2') - gp = mock_objs['rospy_get_param'] + test_params = dict(key1=42, key2="val2") + gp = mock_objs["rospy_get_param"] for key_short, set_val in test_params.items(): # Mock return value - key_long = '{}/{}'.format(self.compname, key_short) + key_long = "{}/{}".format(self.compname, key_short) gp.set_key(key_long, set_val) # Call get_ros_param() and check @@ -66,8 +66,8 @@ def test_hal_obj_base_get_ros_param(self, obj, mock_objs): assert get_val == set_val # Check default plumbing - get_val = obj.get_ros_param('bogus_key', default='default_val') + get_val = obj.get_ros_param("bogus_key", default="default_val") gp.assert_called_with( - '{}/bogus_key'.format(self.compname), 'default_val' + "{}/bogus_key".format(self.compname), "default_val" ) - assert get_val == 'default_val' + assert get_val == "default_val" diff --git a/hal_hw_interface/src/hal_hw_interface/tests/test_hal_offset_mgr.py b/hal_hw_interface/src/hal_hw_interface/tests/test_hal_offset_mgr.py deleted file mode 100644 index c1b797e..0000000 --- a/hal_hw_interface/src/hal_hw_interface/tests/test_hal_offset_mgr.py +++ /dev/null @@ -1,259 +0,0 @@ -# -*- coding: utf-8 -*- -import pytest - -from hal_hw_interface.ros_hal_pin import RosHalPin -from hal_hw_interface.hal_offset_mgr import HalOffsetMgrPin, HalOffsetMgr - -# Borrow tests from redis_store_hal_pin -from test_ros_hal_pin import HalPinDir, HalPinType -from test_redis_store_hal_pin import TestRedisStoreHalPin - - -class TestHalOffsetMgrPin(TestRedisStoreHalPin): - default_hal_type = HalPinType('FLOAT') - default_hal_dir = HalPinDir('OUT') - test_class = HalOffsetMgrPin - - newpin_calls = 2 - - def obj_test_name(self, obj): # Override base test - if isinstance(obj, self.test_class): - return self.get_obj_test_param(obj, 'name') + '_offset' - else: - return super(TestHalOffsetMgrPin, self).obj_test_name(obj) - - def test_ros_hal_pin_attrs(self, obj, mock_comp_obj): # Override base test - '''Test that attributes are set as expected, including defaults - ''' - # Call original test against offset pin - super(TestHalOffsetMgrPin, self).test_ros_hal_pin_attrs( - obj, mock_comp_obj - ) - - # Test complementary '_fb_in' newpin() call - pos_pin = obj.hal_fb_in_pin - assert pos_pin.name == obj.name + '_fb-in' - assert pos_pin.hal_comp is mock_comp_obj - assert type(pos_pin.hal_type) is HalPinType - assert pos_pin.hal_type == HalPinType(self.hal_type(obj)) - assert type(pos_pin.hal_dir) is HalPinDir - assert pos_pin.hal_dir == HalPinDir('IN') - - def test_ros_hal_pin_newpin(self, obj, mock_comp_obj): # Override base test - '''Test that hal.component.newpin() is called - ''' - # Call original test against offset pin - super(TestHalOffsetMgrPin, self).test_ros_hal_pin_newpin( - obj, mock_comp_obj - ) - - # Test complementary '_fb_in' newpin() call - mock_comp_obj.newpin.assert_any_call( - obj.name + '_fb-in', HalPinType(self.hal_type(obj)), HalPinDir('IN') - ) - - def test_hal_offset_mgr_pin_zero_joint( - self, obj, data, mock_comp_obj, mock_redis_client_obj - ): - '''Test zero_joint() sets offset pin and redis from position input pin - ''' - # Add obj test case data to position input pin object - pos_pin_case = obj._p.copy() - pos_pin_case['name'] = obj.name + '_fb-in' - obj.hal_fb_in_pin._p = pos_pin_case - - # Fake offset and position input values and run zero_joint() - new_val = self.set_pin(obj.hal_fb_in_pin, self.other_value(obj, data)) - old_val = self.set_pin(obj, data) - assert mock_comp_obj[obj.hal_fb_in_pin.name] == new_val # Sanity check - assert mock_comp_obj[obj.pin_name] == old_val # Sanity check - print("Calling zero_joint()") - updated_val = obj.zero_joint() - - # Check the pos pin was read correctly - assert updated_val == new_val - # Check the HAL pin value was written correctly - assert mock_comp_obj[self.obj_test_name(obj)] == new_val - # Check redis was written to - mock_redis_client_obj.set_param.assert_called_once_with( - '{}/{}'.format(self.compname, self.obj_test_name(obj)), new_val - ) - - def test_hal_offset_mgr_load_offset(self, obj, data): - # Add obj test case data to position input pin object - pos_pin_case = obj._p.copy() - pos_pin_case['name'] = obj.name + '_fb-in' - obj.hal_fb_in_pin._p = pos_pin_case - - # Fake redis and pin values and run load_offset() - redis_val = self.set_mock_redis_param(obj, data) - pin_val = self.set_pin(obj, data) - if data['changed']: # Sanity check - assert pin_val != redis_val - ret_val = obj.load_offset() - - # Check the HAL pin and return values are correct - assert obj.get_pin() == redis_val - assert ret_val == redis_val - - -class TestHalOffsetMgr(object): - test_class = HalOffsetMgr - test_pin_class = HalOffsetMgrPin - compname = 'hal_offset_mgr' - - joint_names = ['joint_1', 'joint_2'] - num_pins = len(joint_names) - offset_pins = ['{}_offset'.format(j) for j in joint_names] - fb_pins = ['{}_fb-in'.format(j) for j in joint_names] - redis_keys = ['%s/%s' % (compname, p) for p in offset_pins] - test_data = zip(offset_pins, fb_pins, redis_keys, (42, 13)) - - @pytest.fixture - def obj(self, mock_comp_obj, mock_rospy, mock_redis_client_obj, mock_objs): - for key in list(self.test_class._cached_objs.keys()): - self.test_class._cached_objs.pop(key) - - gp = mock_objs['rospy_get_param'] - gp.set_key('hardware_interface/joints', self.joint_names) - - mock_comp_obj.set_prefix(self.compname) - return self.test_class() - - def test_hal_offset_mgr_setup_component(self, obj): - # Check offset_pins list - assert len(obj.offset_pins) == len(self.joint_names) - for op in obj.offset_pins: - assert isinstance(op, self.test_pin_class) - assert op.hal_type == HalPinType('FLOAT') - assert op.hal_dir == HalPinDir('OUT') - - # Check other pins - for data in ( - ('zero_all_joints', 'IO'), - ('load_params', 'IO'), - ('enable', 'IN'), - ): - name, hal_dir = data - assert hasattr(obj, name) - pin = getattr(obj, name) - assert isinstance(pin, RosHalPin) - assert pin.hal_type == HalPinType('BIT') - assert pin.hal_dir == HalPinDir(hal_dir) - - def test_hal_offset_mgr_update_no_cmd(self, obj): - # Test no command - - obj.hal_comp.set_pin('load_params', 0) - obj.hal_comp.set_pin('zero_all_joints', 0) - obj.hal_comp.set_pin('enable', 0) - obj.hal_comp.reset_mock() - obj.update() - print(obj.hal_comp.mock_calls) - - # Cmd pins read, but nothing set - assert obj.hal_comp.__getitem__.call_count == 2 - obj.hal_comp.__getitem__.assert_any_call('load_params') - obj.hal_comp.__getitem__.assert_any_call('zero_all_joints') - obj.hal_comp.__setitem__.assert_not_called() - - def test_hal_offset_mgr_update_load_params( - self, obj, mock_redis_client_obj, mock_comp_obj - ): - # Test load_params command - - mock_comp_obj.setprefix(self.compname) - obj.hal_comp.set_pin('load_params', 1) - obj.hal_comp.set_pin('zero_all_joints', 0) - obj.hal_comp.set_pin('enable', 0) - for pin_o, pin_fb, redis_key, val in self.test_data: - mock_redis_client_obj.set_key(redis_key, val) - obj.hal_comp.reset_mock() - obj.update() - print("obj.hal_comp calls:") - print(obj.hal_comp.mock_calls) - print("mock_redis_client calls:") - print(mock_redis_client_obj.mock_calls) - - # total n+1 pin writes, n param reads, no param writes - assert obj.hal_comp.__setitem__.call_count == self.num_pins + 1 - assert mock_redis_client_obj.get_param.call_count == self.num_pins - mock_redis_client_obj.set_param.assert_not_called - # load_params read & written - obj.hal_comp.__getitem__.assert_any_call('load_params') - obj.hal_comp.__setitem__.assert_any_call('load_params', False) - # offsets written - for pin_o, pin_fb, redis_key, val in self.test_data: - mock_redis_client_obj.get_param.assert_any_call(redis_key) - assert mock_redis_client_obj.get_param(redis_key) == val - obj.hal_comp.__setitem__.assert_any_call(pin_o, val) - - def test_hal_offset_mgr_update_zero_all_joints_disabled( - self, obj, mock_redis_client_obj, mock_comp_obj - ): - # Test zero_all_joints command when disabled - - mock_comp_obj.setprefix(self.compname) - obj.hal_comp.set_pin('load_params', 0) - obj.hal_comp.set_pin('zero_all_joints', 1) - obj.hal_comp.set_pin('enable', 0) - for pin_o, pin_fb, redis_key, val in self.test_data: - obj.hal_comp.set_pin(pin_fb, val) - obj.hal_comp.reset_mock() - obj.update() - print("obj.hal_comp calls:") - print(obj.hal_comp.mock_calls) - print("mock_redis_client calls:") - print(mock_redis_client_obj.mock_calls) - - # total n+1 pin writes, n params read, 0 params written - assert obj.hal_comp.__setitem__.call_count == self.num_pins + 1 - assert mock_redis_client_obj.set_param.call_count == 2 - mock_redis_client_obj.set_param.assert_not_called - # zero_all_joints pin read & written - obj.hal_comp.__getitem__.assert_any_call('zero_all_joints') - obj.hal_comp.__setitem__.assert_any_call('zero_all_joints', False) - # enable read - obj.hal_comp.__getitem__.assert_any_call('enable') - # offsets read from fb pins, written to offset pins and redis - for pin_o, pin_fb, redis_key, val in self.test_data: - obj.hal_comp.__getitem__.assert_any_call(pin_fb) - obj.hal_comp.__setitem__.assert_any_call(pin_o, val) - assert obj.hal_comp[pin_o] == val - mock_redis_client_obj.set_param.assert_any_call(redis_key, val) - assert mock_redis_client_obj.get_param(redis_key) == val - - def test_hal_offset_mgr_update_zero_all_joints_enabled( - self, obj, mock_redis_client_obj - ): - # Test zero_all_joints command when enabled - - obj.hal_comp.set_pin('load_params', 0) - obj.hal_comp.set_pin('zero_all_joints', 1) - obj.hal_comp.set_pin('enable', 1) - obj.hal_comp.reset_mock() - obj.update() - print("obj.hal_comp calls:") - print(obj.hal_comp.mock_calls) - print("mock_redis_client calls:") - print(mock_redis_client_obj.mock_calls) - - # total 1 pin write - assert obj.hal_comp.__setitem__.call_count == 1 - mock_redis_client_obj.set_param.assert_not_called - mock_redis_client_obj.set_param.assert_not_called - # zero_all_joints pin read & written - obj.hal_comp.__getitem__.assert_any_call('zero_all_joints') - obj.hal_comp.__setitem__.assert_any_call('zero_all_joints', False) - # enable read - obj.hal_comp.__getitem__.assert_any_call('enable') - - def test_hal_offset_mgr_shutdown_component( - self, obj, mock_redis_client_obj - ): - # Test that redis client disconnects - obj.offset_pins[0]._redis_config # Autovivify client - obj.shutdown_component() - print("mock_redis_client calls:") - print(mock_redis_client_obj.mock_calls) - assert mock_redis_client_obj.stop.call_count == 1 diff --git a/hal_hw_interface/src/hal_hw_interface/tests/test_hal_pin_attrs.py b/hal_hw_interface/src/hal_hw_interface/tests/test_hal_pin_attrs.py index a71f6f1..327ae96 100644 --- a/hal_hw_interface/src/hal_hw_interface/tests/test_hal_pin_attrs.py +++ b/hal_hw_interface/src/hal_hw_interface/tests/test_hal_pin_attrs.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- import pytest -import hal +import machinekit.hal.pyhal as hal from hal_hw_interface.hal_pin_attrs import HalPinDir, HalPinType valid_cases = [ # Pin direction - (HalPinDir, 'IN', hal.HAL_IN), - (HalPinDir, 'OUT', hal.HAL_OUT), - (HalPinDir, 'IO', hal.HAL_IO), + (HalPinDir, "IN", hal.HAL_IN), + (HalPinDir, "OUT", hal.HAL_OUT), + (HalPinDir, "IO", hal.HAL_IO), # Pin type - (HalPinType, 'BIT', hal.HAL_BIT), - (HalPinType, 'U32', hal.HAL_U32), - (HalPinType, 'S32', hal.HAL_S32), - (HalPinType, 'FLOAT', hal.HAL_FLOAT), + (HalPinType, "BIT", hal.HAL_BIT), + (HalPinType, "U32", hal.HAL_U32), + (HalPinType, "S32", hal.HAL_S32), + (HalPinType, "FLOAT", hal.HAL_FLOAT), ] @@ -27,7 +27,7 @@ def test_hal_pin_attrs_valid_case_fixture(valid_case): assert len(valid_case) == 3 obj_type, short_name, int_val = valid_case assert obj_type in (HalPinDir, HalPinType) - assert short_name in ('IN', 'OUT', 'IO', 'BIT', 'U32', 'S32', 'FLOAT') + assert short_name in ("IN", "OUT", "IO", "BIT", "U32", "S32", "FLOAT") def test_hal_pin_attr_new_from_short_name(valid_case): @@ -40,7 +40,7 @@ def test_hal_pin_attr_new_from_short_name(valid_case): def test_hal_pin_attr_new_from_long(valid_case): # Test init from long name, e.g. 'HAL_IN' obj_type, short_name, int_val = valid_case - test_val = obj_type('HAL_' + short_name) + test_val = obj_type("HAL_" + short_name) assert test_val == int_val @@ -55,20 +55,20 @@ def test_hal_pin_attr_repr(valid_case): # Test __repr__ returns e.g. 'HAL_IN' obj_type, short_name, int_val = valid_case test_val = obj_type(int_val) - assert repr(test_val) == 'HAL_' + short_name + assert repr(test_val) == "HAL_" + short_name def test_hal_pin_attr_str(valid_case): # Test __str__ returns e.g. 'HAL_IN' obj_type, short_name, int_val = valid_case test_val = obj_type(int_val) - assert str(test_val) == 'HAL_' + short_name + assert str(test_val) == "HAL_" + short_name invalid_cases = [ # Pin direction - (HalPinDir, 'INY'), - (HalPinDir, 'in'), + (HalPinDir, "INY"), + (HalPinDir, "in"), (HalPinDir, hal.HAL_BIT), (HalPinDir, hal.HAL_U32), (HalPinDir, hal.HAL_S32), @@ -77,8 +77,8 @@ def test_hal_pin_attr_str(valid_case): (HalPinDir, None), (HalPinDir, float(hal.HAL_IN)), # Pin type - (HalPinType, 'BITY'), - (HalPinType, 'bit'), + (HalPinType, "BITY"), + (HalPinType, "bit"), (HalPinType, hal.HAL_IN), (HalPinType, hal.HAL_OUT), (HalPinType, hal.HAL_IO), diff --git a/hal_hw_interface/src/hal_hw_interface/tests/test_redis_store_hal_pin.py b/hal_hw_interface/src/hal_hw_interface/tests/test_redis_store_hal_pin.py index a8ba162..9678ff9 100644 --- a/hal_hw_interface/src/hal_hw_interface/tests/test_redis_store_hal_pin.py +++ b/hal_hw_interface/src/hal_hw_interface/tests/test_redis_store_hal_pin.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import pytest from hal_hw_interface.redis_store_hal_pin import RedisStoreHalPin # Borrow tests from ros_hal_pin @@ -6,9 +7,27 @@ class TestRedisStoreHalPin(TestRosHalPin): - default_hal_dir = HalPinDir('OUT') + default_hal_dir = HalPinDir("OUT") test_class = RedisStoreHalPin + @pytest.fixture(params=TestRosHalPin.obj_cases) + def obj(self, request, mock_comp_obj, mock_rospy, mock_redis_client_obj): + self.setup_hal_obj_base(mock_comp_obj) + mock_comp_obj.setprefix(self.compname) + attrs = dict() + params = request.param.copy() + params.setdefault("hal_dir", self.default_hal_dir) + name = params.pop("name") + attr_names = ["hal_comp", "hal_type", "hal_dir", "msg_type"] + attr_names += self.extra_attrs + for attr_name in attr_names: + if attr_name in params: + attrs[attr_name] = params.pop(attr_name) + obj = self.test_class(name, **attrs) + obj._p = request.param # Send test params in + mock_redis_client_obj.get_param.reset_mock() # Called during setup + return obj + def redis_param_key(self, obj): return "{}/{}".format(obj.compname, obj.pin_name) @@ -20,89 +39,76 @@ def set_mock_redis_param(self, obj, param): obj._redis_config.set_key(self.redis_param_key(obj), value) return value - def test_redis_store_pin_param_key(self, obj): - assert obj._redis_param_key == self.redis_param_key(obj) + def test_redis_store_pin_key(self, obj): + assert obj.key == self.redis_param_key(obj) - def test_redis_store_pin_client(self, mock_redis_client_obj, mock_comp_obj): + def test_redis_store_pin_redis_config( + self, mock_redis_client_obj, mock_comp_obj, mock_rospy + ): # Check that redis client is created and cached self.setup_hal_obj_base(mock_comp_obj) - c1 = self.test_class('test_redis_pin', 'FLOAT')._redis_config + c1 = self.test_class("test_redis_pin", "FLOAT")._redis_config assert c1 is mock_redis_client_obj - c2 = self.test_class('test_redis_pin2', 'FLOAT')._redis_config + c2 = self.test_class("test_redis_pin2", "FLOAT")._redis_config assert c1 is c2 - def test_redis_store_pin_set_pin_from_redis( + def test_redis_store_pin_update_fm_redis(self, obj): + obj._update_fm_redis(obj.key, 42) + print(self.pin_values) + assert self.pin_values[obj.pin_name] == 42 + assert obj._prev_pin_val == 42 + assert obj._prev_redis_val == 42 + # When key doesn't match, no change + obj._update_fm_redis("foo", 13) + assert self.pin_values[obj.pin_name] == 42 + + def test_redis_store_pin_ros_init( self, obj, data, mock_comp_obj, mock_redis_client_obj ): - # Fake a redis param value and set the pin from redis + # obj._redis_config.on_update_received = list() # Clear out setup entry + # Fake a redis param value & run function under test test_val = self.set_mock_redis_param(obj, data) - obj_val = obj.set_pin_from_redis() - # Check that redis was read - mock_redis_client_obj.get_param.assert_called_once_with( - '{}/{}'.format(self.compname, self.obj_test_name(obj)) - ) - # Check the returned value - assert obj_val == test_val - # Check the HAL pin value - mock_comp_obj.__setitem__.assert_called_with( - self.obj_test_name(obj), test_val - ) - - def test_redis_store_pin_set_pin_no_defaults(self, all_patches): - mock_comp_obj, mock_rospy, mock_objs, mock_redis_client_obj = ( - all_patches - ) - # Fake a redis param value and set the pin from redis - obj = self.test_class('unconfigured_redis_pin', 'FLOAT') - self.setup_hal_obj_base(mock_comp_obj) - self.set_mock_redis_param(obj, None) - mock_redis_client_obj.reset_mock() - # Call set_pin_from_redis() and check that the pin was NOT set - print("--------- Calling method") - obj.set_pin_from_redis() - print(mock_comp_obj.__setitem__.mock_calls) - mock_comp_obj.__setitem__.assert_not_called() + self.set_pin(obj, data["pin_value"]) + obj._ros_init() + + if str(obj.hal_dir) != "HAL_IN": + # Check that redis was read + print( + "get_param calls:", mock_redis_client_obj.get_param.mock_calls + ) + mock_redis_client_obj.get_param.assert_called_once_with(obj.key) + # Check the returned value + assert obj._prev_pin_val == test_val + assert obj._prev_redis_val == test_val + assert self.pin_values[obj.pin_name] == test_val + # Check the callback list + assert ( + obj._redis_config.on_update_received[-1] == obj._update_fm_redis + ) + + else: # HAL_IN pins + assert obj._prev_pin_val == self.pin_values[obj.pin_name] + assert obj._prev_redis_val is None def test_redis_store_pin_set_redis_from_pin( self, obj, data, mock_redis_client_obj ): - # Fake a HAL pin value, set redis key, clean up + # Fake values test_val = self.set_pin(obj, data) - self.set_mock_redis_param(obj, data) - mock_redis_client_obj.reset_mock() + last_val = self.set_last_value(obj, data) + obj._prev_pin_val = obj._prev_redis_val = last_val + # Call the method print("--------- Calling method") - obj_val = obj.set_redis_from_pin() - # Check the returned value - assert obj_val == test_val - # Check redis was written to - mock_redis_client_obj.set_param.assert_called_once_with( - '{}/{}'.format(self.compname, self.obj_test_name(obj)), test_val - ) - # Check if log message was printed - print(mock_redis_client_obj.get_param.mock_calls) - if data['changed']: - # Call get_param to check value and again to print log - assert mock_redis_client_obj.get_param.call_count == 2 - else: - # Only call get_param once to check value, not again to print log - assert mock_redis_client_obj.get_param.call_count == 1 + obj.update() - def test_redis_store_pin_set_redis_from_pin_first_time( - self, obj, data, mock_redis_client_obj - ): - # Fake a HAL pin value, set redis key to None, clean up - test_val = self.set_pin(obj, data) - self.set_mock_redis_param(obj, None) - mock_redis_client_obj.reset_mock() - # Call the method - obj_val = obj.set_redis_from_pin() - # Check the returned value - assert obj_val == test_val - # Check redis was written to - mock_redis_client_obj.set_param.assert_called_once_with( - '{}/{}'.format(self.compname, self.obj_test_name(obj)), test_val - ) - # Check the log message was not printed - print(mock_redis_client_obj.get_param.mock_calls) - assert mock_redis_client_obj.get_param.call_count == 1 + if str(obj.hal_dir) == "HAL_OUT": + obj._redis_config.set_param.assert_not_called() + return + + if test_val == last_val: + return + + # Check the new value + assert obj._prev_pin_val == test_val + assert obj._prev_redis_val == test_val diff --git a/hal_hw_interface/src/hal_hw_interface/tests/test_ros_hal_component.py b/hal_hw_interface/src/hal_hw_interface/tests/test_ros_hal_component.py index cf19890..d0f40f8 100644 --- a/hal_hw_interface/src/hal_hw_interface/tests/test_ros_hal_component.py +++ b/hal_hw_interface/src/hal_hw_interface/tests/test_ros_hal_component.py @@ -13,7 +13,7 @@ def obj(self, mock_comp_obj, mock_rospy, mock_objs): # Simplest test case class StubComp(self.test_class): - compname = 'stub' + compname = "stub" def setup_component(self): self.initialized = True @@ -22,50 +22,48 @@ def setup_component(self): def update(self): self.count += 1 - gp = mock_objs['rospy_get_param'] - gp.set_key('stub/update_rate', 20) - gp.set_key('stub/relative_tolerance', 1e-9) - gp.set_key('stub/absolute_tolerance', 1e-9) + gp = mock_objs["rospy_get_param"] + gp.set_key("stub/update_rate", 20) + gp.set_key("stub/relative_tolerance", 1e-9) + gp.set_key("stub/absolute_tolerance", 1e-9) return StubComp() def test_ros_hal_component_attrs(self, obj): - assert obj.compname == 'stub' + assert obj.compname == "stub" def test_ros_hal_component_init( self, obj, mock_rospy, mock_comp_obj, mock_objs ): - '''Test RosHalComponent.__init__() - ''' + """Test RosHalComponent.__init__()""" # ROS node initialized - mock_rospy['init_node'].assert_called_with(obj.compname) + mock_rospy["init_node"].assert_called_with(obj.compname) # obj.rate was created from rospy.Rate with param value - mock_objs['rospy_get_param'].assert_called_once_with( - 'stub/update_rate', 10 + mock_objs["rospy_get_param"].assert_called_once_with( + "stub/update_rate", 10 ) assert obj.update_rate == 20 - mock_objs['rospy_Rate'].assert_called_once_with(20) - assert obj.rate is mock_objs['rospy_Rate_obj'] + mock_objs["rospy_Rate"].assert_called_once_with(20) + assert obj.rate is mock_objs["rospy_Rate_obj"] # Assert obj.hal_comp is a HAL component - mock_objs['hal_comp'].assert_called_once_with('stub') + mock_objs["hal_comp"].assert_called_once_with("stub") assert obj.hal_comp == mock_comp_obj # Assert StubComp.setup_component() called - assert getattr(obj, 'initialized', False) is True + assert getattr(obj, "initialized", False) is True # Assert HAL component initialized obj.hal_comp.ready.assert_called_once_with() def test_ros_hal_component_get_param(self, obj, mock_objs): - '''Test get_param() - ''' - res = obj.get_param('relative_tolerance', 42) + """Test get_param()""" + res = obj.get_ros_param("relative_tolerance", 42) assert res == 1e-9 - res = obj.get_param('bogus_key', 88) + res = obj.get_ros_param("bogus_key", 88) assert res == 88 def test_ros_hal_component_run(self, obj, mock_objs): - '''Test run() (fixture loops three times); should call update() and + """Test run() (fixture loops three times); should call update() and rate.sleep() - ''' + """ obj.run() assert obj.count == 3 - assert mock_objs['rospy_Rate_obj'].sleep.call_count == 3 + assert mock_objs["rospy_Rate_obj"].sleep.call_count == 3 diff --git a/hal_hw_interface/src/hal_hw_interface/tests/test_ros_hal_pin.py b/hal_hw_interface/src/hal_hw_interface/tests/test_ros_hal_pin.py index 2da4667..1012d0d 100644 --- a/hal_hw_interface/src/hal_hw_interface/tests/test_ros_hal_pin.py +++ b/hal_hw_interface/src/hal_hw_interface/tests/test_ros_hal_pin.py @@ -21,74 +21,49 @@ ) from std_msgs.msg import UInt16 # For invalid test case -# List of object fixture parameter test cases to run for each class; -# contains enough attributes for superset of all classes -obj_cases = [ - dict( # 0 Test BIT, IO, names - name='reset', - hal_type='BIT', - hal_dir='IO', - sub_topic='/robot/reset', - pub_topic='/robot/reset', - service_name='/robot/reset', - ), - dict(name='bool', hal_type='BIT', hal_dir='OUT'), # 1 Test BIT OUT - dict(name='bool', hal_type='BIT'), # 2 Test BIT defaults - dict(name='u32_out', hal_type='U32', hal_dir='OUT'), # 3 Test U32 OUT - dict(name='u32_out', hal_type='U32', hal_dir='IO'), # 4 Test U32 IN - dict(name='u32_out', hal_type='U32'), # 5 Test U32 default - dict(name='s32_out', hal_type='S32'), # 6 Test S32 default - dict(name='float_io', hal_type='FLOAT', hal_dir='IO'), # 7 Test FLOAT IO -] - -# Message and pin data cases -# Values are (bit, u32, s32, float) -data_cases = [ - dict( # 0 Test random - pin_value=(False, 23, -4, 1e-9), other_value=(True, 27, 49, 3.98) - ), - dict( # 1 Test random - pin_value=(True, 0, -39958, 54.7), - other_value=(False, 199, -33399, 149.33285), - ), - dict( # 2 Test zeros -> random - pin_value=(False, 0, 0, 0.0), - other_value=(False, 0, 0, 0.0), - changed=False, - ), - dict( # 3 Test data unchanged - pin_value=(True, 199, -33399, 149.33285), - other_value=(True, 199, -33399, 149.33285), - changed=False, - ), -] - -isclose_cases = [ - (0.0, 1.0, False), - (100, 10, False), - (0.0, 0.0, True), - (1e-9, 1.5e-9, True), -] - class TestRosHalPin(object): test_class = RosHalPin default_hal_type = None - default_hal_dir = HalPinDir('IN') - compname = 'mock_hal_comp_obj' # conftest.py + default_hal_dir = HalPinDir("IN") + compname = "mock_hal_comp_obj" # conftest.py extra_attrs = [] # # Object and data fixtures # + + # List of object fixture parameter test cases to run for each class; + # contains enough attributes for superset of all classes + obj_cases = [ + dict( # 0 Test BIT, IO, names + name="reset", + hal_type="BIT", + hal_dir="IO", + sub_topic="/robot/reset", + pub_topic="/robot/reset", + service_name="/robot/reset", + ), + dict(name="bool", hal_type="BIT", hal_dir="OUT"), # 1 Test BIT OUT + dict(name="bool", hal_type="BIT"), # 2 Test BIT defaults + dict(name="u32_out", hal_type="U32", hal_dir="OUT"), # 3 Test U32 OUT + dict(name="u32_out", hal_type="U32", hal_dir="IO"), # 4 Test U32 IN + dict(name="u32_out", hal_type="U32"), # 5 Test U32 default + dict(name="s32_out", hal_type="S32"), # 6 Test S32 default + dict( + name="float_io", hal_type="FLOAT", hal_dir="IO" + ), # 7 Test FLOAT IO + ] + @pytest.fixture(params=obj_cases) def obj(self, request, mock_comp_obj, mock_rospy, mock_redis_client_obj): self.setup_hal_obj_base(mock_comp_obj) mock_comp_obj.setprefix(self.compname) attrs = dict() params = request.param.copy() - name = params.pop('name') - attr_names = ['hal_comp', 'hal_type', 'hal_dir', 'msg_type'] + params.setdefault("hal_dir", self.default_hal_dir) + name = params.pop("name") + attr_names = ["hal_comp", "hal_type", "hal_dir", "msg_type"] attr_names += self.extra_attrs for attr_name in attr_names: if attr_name in params: @@ -97,12 +72,40 @@ def obj(self, request, mock_comp_obj, mock_rospy, mock_redis_client_obj): obj._p = request.param # Send test params in return obj + # Message and pin data cases + # Values are (bit, u32, s32, float) + data_cases = [ + dict( # 0 Test random + pin_value=(False, 23, -4, 1e-9), other_value=(True, 27, 49, 3.98) + ), + dict( # 1 Test random + pin_value=(True, 0, -39958, 54.7), + other_value=(False, 199, -33399, 149.33285), + ), + dict( # 2 Test zeros -> random + pin_value=(False, 0, 0, 0.0), + other_value=(False, 0, 0, 0.0), + changed=False, + ), + dict( # 3 Test data unchanged + pin_value=(True, 199, -33399, 149.33285), + other_value=(True, 199, -33399, 149.33285), + changed=False, + ), + ] + @pytest.fixture(params=data_cases) def data(self, request): - if 'changed' not in request.param: - request.param['changed'] = True + request.param.setdefault("changed", True) return request.param + isclose_cases = [ + (0.0, 1.0, False), + (100, 10, False), + (0.0, 0.0, True), + (1e-9, 1.5e-9, True), + ] + @pytest.fixture(params=isclose_cases) def isclose_case(self, request): return request.param @@ -131,13 +134,13 @@ def get_obj_test_param(self, obj, param, default=None): return obj._p.get(param, default) def obj_test_name(self, obj): - return self.get_obj_test_param(obj, 'name') + return self.get_obj_test_param(obj, "name") def hal_dir(self, obj): - return self.get_obj_test_param(obj, 'hal_dir', self.default_hal_dir) + return self.get_obj_test_param(obj, "hal_dir", self.default_hal_dir) def hal_type(self, obj): - return self.get_obj_test_param(obj, 'hal_type') + return self.get_obj_test_param(obj, "hal_type") def msg_type(self, obj): return dict(BIT=Bool, FLOAT=Float64, U32=UInt32, S32=Int32)[ @@ -145,22 +148,22 @@ def msg_type(self, obj): ] data_indexes = { - HalPinType('BIT'): 0, - HalPinType('U32'): 1, - HalPinType('S32'): 2, - HalPinType('FLOAT'): 3, + HalPinType("BIT"): 0, + HalPinType("U32"): 1, + HalPinType("S32"): 2, + HalPinType("FLOAT"): 3, } def set_last_value(self, obj, param): index = self.data_indexes.get(obj.hal_type) - value = param['other_value'][index] + value = param["other_value"][index] obj.last_value = value return value def set_pin(self, obj, param): if isinstance(param, dict): index = self.data_indexes.get(obj.hal_type) - value = param['pin_value'][index] + value = param["pin_value"][index] else: value = param obj.hal_comp.set_pin(self.obj_test_name(obj), value) @@ -168,42 +171,43 @@ def set_pin(self, obj, param): def other_value(self, obj, param): index = self.data_indexes.get(obj.hal_type) - return param['other_value'][index] + return param["other_value"][index] def ros_name(self, obj): - return '{}/{}'.format(self.compname, self.obj_test_name(obj)) + return "{}/{}".format(self.compname, self.obj_test_name(obj)) # # Base class tests # def test_ros_hal_pin_compname(self, mock_comp_obj, all_patches): self.setup_hal_obj_base(mock_comp_obj) - obj = self.test_class('test_pin', 'BIT') + obj = self.test_class("test_pin", "BIT") mock_comp_obj.reset_mock() obj.compname print(mock_comp_obj.mock_calls) assert mock_comp_obj.getprefix.call_count == 1 - def test_ros_hal_pin_obj_fixture(self, obj, all_patches): - mock_comp_obj, mock_rospy, mock_objs = all_patches[:3] - assert hasattr(obj, '_p') + def test_ros_hal_pin_obj_fixture( + self, obj, mock_comp_obj, mock_rospy, mock_objs + ): + assert hasattr(obj, "_p") params = obj._p - assert obj.name == params['name'] + assert obj.name == params["name"] assert obj.hal_type == HalPinType( - params.get('hal_type', self.default_hal_type) + params.get("hal_type", self.default_hal_type) ) assert obj.hal_dir == HalPinDir( - params.get('hal_dir', self.default_hal_dir) + params.get("hal_dir", self.default_hal_dir) ) - if 'sub_topic' in params and hasattr(obj, 'sub_topic'): - assert obj.sub_topic == params.get('sub_topic') - if 'pub_topic' in params and hasattr(obj, 'pub_topic'): - assert obj.pub_topic == params.get('pub_topic') - if 'service_name' in params and hasattr(obj, 'service_name'): - assert obj.pub_topic == params.get('service_name') + if "sub_topic" in params and hasattr(obj, "sub_topic"): + assert obj.sub_topic == params.get("sub_topic") + if "pub_topic" in params and hasattr(obj, "pub_topic"): + assert obj.pub_topic == params.get("pub_topic") + if "service_name" in params and hasattr(obj, "service_name"): + assert obj.pub_topic == params.get("service_name") def test_ros_hal_pin_data_fixture(self, data): - for key in ('other_value', 'pin_value'): + for key in ("other_value", "pin_value"): assert key in data assert len(data[key]) == 4 assert isinstance(data[key][0], bool) @@ -221,8 +225,7 @@ def test_ros_hal_pin_isclose(self, isclose_case): assert self.test_class._isclose(a, b, 1e-9, 1e-9) is res def test_ros_hal_pin_attrs(self, obj, mock_comp_obj): - '''Test that attributes are set as expected, including defaults - ''' + """Test that attributes are set as expected, including defaults""" assert obj.pin_name == self.obj_test_name(obj) assert obj.hal_comp is mock_comp_obj assert type(obj.hal_type) is HalPinType @@ -231,15 +234,13 @@ def test_ros_hal_pin_attrs(self, obj, mock_comp_obj): assert obj.hal_dir == HalPinDir(self.hal_dir(obj)) def test_ros_hal_pin_pin_name(self, obj): - '''Test pin_name generation - ''' + """Test pin_name generation""" assert obj.pin_name == self.obj_test_name(obj) newpin_calls = 1 def test_ros_hal_pin_newpin(self, obj, mock_comp_obj): - '''Test that hal.component.newpin() is called - ''' + """Test that hal.component.newpin() is called""" print(mock_comp_obj.newpin.mock_calls) mock_comp_obj.newpin.assert_any_call( self.obj_test_name(obj), @@ -249,28 +250,33 @@ def test_ros_hal_pin_newpin(self, obj, mock_comp_obj): assert mock_comp_obj.newpin.call_count == self.newpin_calls def test_ros_hal_pin_default_attr_hal_type(self, all_patches): - '''Test default hal_type - ''' + """Test default hal_type""" # This one creates the object in the test, so `all_patches` # needed if self.default_hal_type is None: with pytest.raises(TypeError): - self.test_class('default_hal_type') + self.test_class("default_hal_type") else: - obj = self.test_class('default_hal_type') + obj = self.test_class("default_hal_type") assert obj.hal_type == self.default_hal_type class TestRosHalPinPublisher(TestRosHalPin): - default_hal_dir = HalPinDir('IN') + default_hal_dir = HalPinDir("IN") test_class = RosHalPinPublisher - extra_attrs = ['pub_topic'] + extra_attrs = ["pub_topic"] # # Helpers # def pub_topic(self, obj): - return self.get_obj_test_param(obj, 'pub_topic', self.ros_name(obj)) + return self.get_obj_test_param(obj, "pub_topic", self.ros_name(obj)) + + def set_last_value(self, obj, param): + # Set msg.data + value = super().set_last_value(obj, param) + obj._msg.data = value + return value # # Tests @@ -282,47 +288,47 @@ def test_ros_hal_pin_attrs(self, obj, mock_comp_obj): assert obj.pub_topic == self.pub_topic(obj) def test_ros_hal_pin_publisher_init(self, obj, mock_objs): - '''Test that __init__() creates rospy.Publisher object - ''' - print(mock_objs['rospy_Publisher'].mock_calls) + """Test that __init__() creates rospy.Publisher object""" + print(mock_objs["rospy_Publisher"].mock_calls) assert obj.pub_topic == self.pub_topic(obj) - mock_objs['rospy_Publisher'].assert_called_with( + mock_objs["rospy_Publisher"].assert_called_with( self.pub_topic(obj), self.msg_type(obj), queue_size=1, latch=True ) - assert obj.pub is mock_objs['rospy_Publisher_obj'] + assert obj.pub is mock_objs["rospy_Publisher_obj"] def test_ros_hal_pin_publisher_value_changed(self, obj, data, mock_objs): - '''Test _value_changed() function - ''' - assert obj.get_ros_param('relative_tolerance', 1e-9) == 1e-9 - assert obj.get_ros_param('absolute_tolerance', 1e-9) == 1e-9 + """Test _value_changed() function""" + assert obj.get_ros_param("relative_tolerance", 1e-9) == 1e-9 + assert obj.get_ros_param("absolute_tolerance", 1e-9) == 1e-9 last_value = self.set_last_value(obj, data) cur_value = self.set_pin(obj, data) same = last_value == cur_value - assert same is not data.get('changed') - assert obj._value_changed(cur_value) is data.get('changed') + assert same is not data.get("changed") + assert obj._value_changed(cur_value) is data.get("changed") def test_ros_hal_pin_publisher_update(self, obj, data): self.set_last_value(obj, data) cur_value = self.set_pin(obj, data) obj.update() - if data.get('changed'): - obj.pub.publish.assert_called_with(cur_value) + assert obj._msg.data == cur_value + print(f"pub.publish calls: {obj.pub.publish.mock_calls}") + if data.get("changed"): + obj.pub.publish.assert_called_with(obj._msg) else: obj.pub.publish.assert_not_called class TestRosHalPinSubscriber(TestRosHalPinPublisher): - default_hal_dir = HalPinDir('OUT') + default_hal_dir = HalPinDir("OUT") test_class = RosHalPinSubscriber - extra_attrs = ['pub_topic', 'sub_topic'] + extra_attrs = ["pub_topic", "sub_topic"] # # Helpers # def sub_topic(self, obj): - return self.get_obj_test_param(obj, 'sub_topic', self.ros_name(obj)) + return self.get_obj_test_param(obj, "sub_topic", self.ros_name(obj)) # # Tests @@ -334,16 +340,14 @@ def test_ros_hal_pin_attrs(self, obj, mock_comp_obj): assert obj.sub_topic == self.sub_topic(obj) def test_ros_hal_pin_subscriber_init(self, obj, mock_objs): - '''Test that __init__() creates rospy.Subscriber object - ''' - mock_objs['rospy_Subscriber'].assert_called_with( + """Test that __init__() creates rospy.Subscriber object""" + mock_objs["rospy_Subscriber"].assert_called_with( self.sub_topic(obj), self.msg_type(obj), obj._subscriber_cb ) - assert obj.sub is mock_objs['rospy_Subscriber_obj'] + assert obj.sub is mock_objs["rospy_Subscriber_obj"] def test_ros_hal_pin_subscriber_cb(self, obj, data, mock_comp_obj): - '''Test that HAL pin is set in subscriber callback - ''' + """Test that HAL pin is set in subscriber callback""" other_value = self.other_value(obj, data) obj._subscriber_cb(self.msg_type(obj)(other_value)) mock_comp_obj.__setitem__.assert_called_with( @@ -351,23 +355,22 @@ def test_ros_hal_pin_subscriber_cb(self, obj, data, mock_comp_obj): ) def test_ros_hal_pin_subscriber_bad_msg_type(self, obj): - '''Test that invalid message types are caught - ''' + """Test that invalid message types are caught""" msg = UInt16(42) with pytest.raises(HalHWInterfaceException): obj._subscriber_cb(msg) class TestRosHalPinService(TestRosHalPinPublisher): - default_hal_dir = HalPinDir('OUT') + default_hal_dir = HalPinDir("OUT") test_class = RosHalPinService - extra_attrs = ['pub_topic', 'service_name'] + extra_attrs = ["pub_topic", "service_name"] # # Helpers # def service_name(self, obj): - return self.get_obj_test_param(obj, 'service_name', self.ros_name(obj)) + return self.get_obj_test_param(obj, "service_name", self.ros_name(obj)) def service_msg_type(self, obj): return dict(BIT=SetBool, FLOAT=SetFloat64, U32=SetUInt32, S32=SetInt32)[ @@ -385,17 +388,15 @@ def test_ros_hal_pin_attrs(self, obj, mock_comp_obj): assert obj.service_msg_type == self.service_msg_type(obj) def test_ros_hal_pin_service_init(self, obj, mock_objs): - '''Test that __init__() creates rospy.Service object - ''' + """Test that __init__() creates rospy.Service object""" assert obj.service_name == self.service_name(obj) - mock_objs['rospy_Service'].assert_called_with( + mock_objs["rospy_Service"].assert_called_with( self.service_name(obj), self.service_msg_type(obj), obj._svc_cb ) - assert obj.service is mock_objs['rospy_Service_obj'] + assert obj.service is mock_objs["rospy_Service_obj"] def test_ros_hal_pin_service_cb(self, obj, data): - '''Test that the HAL pin is set during callback - ''' + """Test that the HAL pin is set during callback""" msg = self.service_msg_type(obj) call_value = self.other_value(obj, data) msg.data = call_value @@ -403,4 +404,4 @@ def test_ros_hal_pin_service_cb(self, obj, data): obj.hal_comp.__setitem__.assert_called_with( self.obj_test_name(obj), call_value ) - assert reply == (True, 'OK') + assert reply == (True, "OK") diff --git a/hal_rrbot_control/CMakeLists.txt b/hal_rrbot_control/CMakeLists.txt index 2991dbd..6d6cd68 100644 --- a/hal_rrbot_control/CMakeLists.txt +++ b/hal_rrbot_control/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 2.8.3) +cmake_minimum_required(VERSION 3.0.5) project(hal_rrbot_control) find_package( diff --git a/hal_rrbot_control/launch/hal_rrbot_simulation.launch b/hal_rrbot_control/launch/hal_rrbot_simulation.launch index d457c90..7bf11e4 100644 --- a/hal_rrbot_control/launch/hal_rrbot_simulation.launch +++ b/hal_rrbot_control/launch/hal_rrbot_simulation.launch @@ -16,7 +16,7 @@ diff --git a/hal_rrbot_control/launch/rrbot_visualize.launch b/hal_rrbot_control/launch/rrbot_visualize.launch index c2c5cd3..1125d92 100644 --- a/hal_rrbot_control/launch/rrbot_visualize.launch +++ b/hal_rrbot_control/launch/rrbot_visualize.launch @@ -6,7 +6,7 @@ diff --git a/hal_rrbot_control/package.xml b/hal_rrbot_control/package.xml index f989225..bd708b0 100644 --- a/hal_rrbot_control/package.xml +++ b/hal_rrbot_control/package.xml @@ -15,6 +15,10 @@ hal_hw_interface hal_hw_interface + xacro + controller_manager + robot_state_publisher + rviz