diff --git a/.ci/Dockerfile b/.ci/Dockerfile new file mode 100644 index 00000000..04abb19b --- /dev/null +++ b/.ci/Dockerfile @@ -0,0 +1,193 @@ +ARG UBUNTU_VERSION="20.04" +ARG PYTHON_VERSION="3.10" + +# Start with a base image +FROM ubuntu:${UBUNTU_VERSION} +ARG UBUNTU_VERSION +ARG PYTHON_VERSION + +# Set non-interactive mode +ENV DEBIAN_FRONTEND=noninteractive + +ARG USER_UID=1001 +ARG USER_GID=1001 +ARG USERNAME=user + +WORKDIR /workspaces + +# Create a non-root user +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ + && chown -R $USER_UID:$USER_GID /workspaces \ + && apt-get update \ + && apt-get install -y sudo \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* \ + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +# Add LLVM repository for newer clang versions +RUN apt-get update && \ + apt-get install -y wget gnupg lsb-release software-properties-common && \ + wget https://apt.llvm.org/llvm.sh && \ + chmod +x llvm.sh && \ + ./llvm.sh 18 && \ + apt-get update && \ + apt-get install -y clang-format-18 clang-tidy-18 \ + && ln -s $(which clang-tidy-18) /usr/bin/clang-tidy \ + && ln -s $(which clang-format-18) /usr/bin/clang-format + +# Install necessary packages (without Python - we'll install specific version later) +# Note: pybind11-dev kept for devcontainer users (works with Ubuntu's default Python) +# For CI wheel builds, pip-installed pybind11 in venv is used for each Python version +RUN apt-get update \ + && apt-get install -y \ + bash-completion \ + build-essential \ + curl \ + doxygen \ + dpkg \ + git \ + graphviz \ + lcov \ + libeigen3-dev \ + libfmt-dev \ + libpoco-dev \ + lsb-release \ + pybind11-dev \ + rename \ + valgrind \ + wget \ + software-properties-common \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* + +# Install specific Python version +# On Ubuntu 22.04, Python 3.10 is the default system Python (packages are python3-dev, python3-venv) +# For other versions, use deadsnakes PPA (packages are python3.X-dev, python3.X-venv) +RUN set -ex && \ + # Check if Python version is already available as system default + SYSTEM_PYTHON_VERSION=$(python3 --version 2>/dev/null | grep -oP '\d+\.\d+' || echo "none") && \ + echo "System Python: $SYSTEM_PYTHON_VERSION, Requested: ${PYTHON_VERSION}" && \ + if [ "$SYSTEM_PYTHON_VERSION" = "${PYTHON_VERSION}" ]; then \ + echo "Python ${PYTHON_VERSION} is already the system default, installing dev packages..." && \ + apt-get update && \ + apt-get install -y python3-dev python3-venv && \ + apt-get clean -y && \ + rm -rf /var/lib/apt/lists/*; \ + else \ + echo "Installing Python ${PYTHON_VERSION} from deadsnakes PPA..." && \ + add-apt-repository ppa:deadsnakes/ppa -y && \ + apt-get update && \ + apt-get install -y \ + python${PYTHON_VERSION} \ + python${PYTHON_VERSION}-dev \ + python${PYTHON_VERSION}-venv && \ + (apt-get install -y python${PYTHON_VERSION}-distutils 2>/dev/null || true) && \ + apt-get clean -y && \ + rm -rf /var/lib/apt/lists/* && \ + update-alternatives --install /usr/bin/python3 python3 /usr/bin/python${PYTHON_VERSION} 1 && \ + update-alternatives --set python3 /usr/bin/python${PYTHON_VERSION}; \ + fi && \ + echo "Installed Python version:" && python3 --version + +# Install CMake; use Kitware repo on Ubuntu 20.04 for >=3.22 +RUN if [ "${UBUNTU_VERSION}" = "20.04" ]; then \ + apt-get update && \ + apt-get install -y wget gnupg ca-certificates software-properties-common && \ + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor | tee /usr/share/keyrings/kitware-archive-keyring.gpg > /dev/null && \ + echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ focal main" > /etc/apt/sources.list.d/kitware.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends cmake=3.22.2-0kitware1ubuntu20.04.1 cmake-data=3.22.2-0kitware1ubuntu20.04.1; \ + else \ + apt-get update && apt-get install -y --no-install-recommends cmake; \ + fi && \ + cmake --version && \ + dpkg --compare-versions "$(cmake --version | head -n1 | awk '{print $3}')" ge 3.22 || (echo "Error: CMake >= 3.22 is required. Got ubuntu version ${UBUNTU_VERSION}" >&2; exit 1) + +# Add the necessary 3rd party dependencies for the robot-service +# Note: the order is important, change at your own risk. +RUN git clone --depth 1 --recurse-submodules --shallow-submodules --branch boost-1.77.0 https://github.com/boostorg/boost.git \ + && cd boost \ + && ./bootstrap.sh --prefix=/usr \ + && ./b2 install \ + && cd ../.. \ + && rm -rf boost + +RUN git clone --depth 1 --branch 10.0.0 https://github.com/leethomason/tinyxml2.git \ + && cd tinyxml2 \ + && mkdir build && cd build \ + && cmake .. -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ + && make -j4 && make install \ + && cd ../.. \ + && rm -rf tinyxml2 + +RUN git clone --depth 1 --branch 1.0.2 https://github.com/ros/console_bridge.git \ + && cd console_bridge \ + && mkdir build && cd build \ + && cmake .. -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ + && make -j4 \ + && make install \ + && cd ../.. \ + && rm -rf console_bridge + +RUN git clone --depth 1 --branch 1.0.5 https://github.com/ros/urdfdom_headers.git \ + && cd urdfdom_headers \ + && mkdir build && cd build \ + && cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ + && make -j4 && make install \ + && cd ../.. \ + && rm -rf urdfdom_headers + +COPY ./urdfdom.patch /tmp/urdfdom.patch +RUN git clone --depth 1 --branch 4.0.0 https://github.com/ros/urdfdom.git \ + && cd urdfdom \ + && git apply /tmp/urdfdom.patch \ + && mkdir build && cd build \ + && cmake .. -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ + && make -j4 && make install \ + && cd ../.. \ + && rm -rf urdfdom + +RUN git clone --depth 1 --recurse-submodules --shallow-submodules --branch v5.4.3 https://github.com/assimp/assimp.git \ + && cd assimp \ + && mkdir build && cd build \ + && cmake .. -DBoost_USE_STATIC_LIBS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DASSIMP_BUILD_TESTS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ + && make -j4 && make install \ + && cd ../.. \ + && rm -rf assimp + +COPY ./pinocchio.patch /tmp/pinocchio.patch +RUN git clone --depth 1 --recurse-submodules --shallow-submodules --branch v3.4.0 https://github.com/stack-of-tasks/pinocchio.git \ + && cd pinocchio \ + && git apply /tmp/pinocchio.patch \ + && mkdir build && cd build \ + && cmake .. -DBoost_USE_STATIC_LIBS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DBUILD_PYTHON_INTERFACE=OFF -DBUILD_DOCUMENTATION=OFF -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ + && make -j4 && make install \ + && cd ../.. \ + && rm -rf pinocchio + +# Install Python wheel building tools inside a virtualenv to satisfy PEP 668 +# Note: python3 -m venv uses ensurepip to bootstrap pip automatically +RUN python3 -m venv /opt/venv && \ + /opt/venv/bin/pip install --upgrade pip setuptools wheel && \ + /opt/venv/bin/pip install \ + auditwheel \ + build \ + cibuildwheel \ + twine \ + patchelf \ + flake8 \ + numpy \ + pybind11 \ + cmake && \ + echo "Python version in venv:" && \ + /opt/venv/bin/python --version && \ + /opt/venv/bin/pip --version && \ + chown -R $USER_UID:$USER_GID /opt/venv + +# Expose the virtualenv via env var for downstream scripts (e.g., Jenkinsfile) +ENV VIRTUAL_ENV="/opt/venv" +ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" + +USER $USERNAME diff --git a/.ci/Dockerfile.focal b/.ci/Dockerfile.focal deleted file mode 100644 index 65a75503..00000000 --- a/.ci/Dockerfile.focal +++ /dev/null @@ -1,143 +0,0 @@ -# Start with a base image -FROM ubuntu:20.04 - -# Set non-interactive mode -ENV DEBIAN_FRONTEND=noninteractive - -ARG USER_UID=1001 -ARG USER_GID=1001 -ARG USERNAME=user - -WORKDIR /workspaces - -# Create a non-root user -RUN groupadd --gid $USER_GID $USERNAME \ - && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ - && chown -R $USER_UID:$USER_GID /workspaces \ - && apt-get update \ - && apt-get install -y sudo \ - && apt-get clean -y \ - && rm -rf /var/lib/apt/lists/* \ - && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ - && chmod 0440 /etc/sudoers.d/$USERNAME - -# Add LLVM repository for newer clang versions -RUN apt-get update && \ - apt-get install -y wget gnupg lsb-release software-properties-common && \ - wget https://apt.llvm.org/llvm.sh && \ - chmod +x llvm.sh && \ - ./llvm.sh 14 - -# Install necessary packages -RUN apt-get update \ - && apt-get install -y \ - bash-completion \ - build-essential \ - clang-14 \ - clang-format-14 \ - clang-tidy-14 \ - curl \ - doxygen \ - dpkg \ - git \ - graphviz \ - lcov \ - libeigen3-dev \ - libpoco-dev \ - lsb-release \ - libfmt-dev \ - python3-dev \ - python3-pip \ - python3-venv \ - python3-distutils \ - python3-numpy \ - pybind11-dev \ - rename \ - valgrind \ - wget \ - && apt-get clean -y \ - && rm -rf /var/lib/apt/lists/* \ - && ln -s $(which clang-tidy-14) /usr/bin/clang-tidy \ - && ln -s $(which clang-format-14) /usr/bin/clang-format - -RUN apt-get update && \ - apt-get install -y wget gnupg && \ - wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc | apt-key add - && \ - apt-add-repository 'deb https://apt.kitware.com/ubuntu/ focal main' && \ - apt-get update && \ - apt-get install -y cmake=3.22.2-0kitware1ubuntu20.04.1 cmake-data=3.22.2-0kitware1ubuntu20.04.1 - -# Add the necessary 3rd party dependencies for the robot-service -# Note: the order is important, change at your own risk. -RUN git clone --recursive --branch boost-1.77.0 https://github.com/boostorg/boost.git \ - && cd boost \ - && ./bootstrap.sh --prefix=/usr \ - && ./b2 install \ - && cd ../.. \ - && rm -rf boost - -RUN git clone --branch 10.0.0 https://github.com/leethomason/tinyxml2.git \ - && cd tinyxml2 \ - && mkdir build && cd build \ - && cmake .. -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ - && make -j4 && make install \ - && cd ../.. \ - && rm -rf tinyxml2 - -RUN git clone --branch 1.0.2 https://github.com/ros/console_bridge.git \ - && cd console_bridge \ - && mkdir build && cd build \ - && cmake .. -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ - && make -j4 \ - && make install \ - && cd ../.. \ - && rm -rf console_bridge - -RUN git clone --branch 1.0.5 https://github.com/ros/urdfdom_headers.git \ - && cd urdfdom_headers \ - && mkdir build && cd build \ - && cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ - && make -j4 && make install \ - && cd ../.. \ - && rm -rf urdfdom_headers - -COPY ./urdfdom.patch /tmp/urdfdom.patch -RUN git clone --branch 4.0.0 https://github.com/ros/urdfdom.git \ - && cd urdfdom \ - && git apply /tmp/urdfdom.patch \ - && mkdir build && cd build \ - && cmake .. -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ - && make -j4 && make install \ - && cd ../.. \ - && rm -rf urdfdom - -RUN git clone --recursive --branch v5.4.3 https://github.com/assimp/assimp.git \ - && cd assimp \ - && mkdir build && cd build \ - && cmake .. -DBoost_USE_STATIC_LIBS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DASSIMP_BUILD_TESTS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ - && make -j4 && make install \ - && cd ../.. \ - && rm -rf assimp - -COPY ./pinocchio.patch /tmp/pinocchio.patch -RUN git clone --recursive --branch v3.4.0 https://github.com/stack-of-tasks/pinocchio.git \ - && cd pinocchio \ - && git apply /tmp/pinocchio.patch \ - && mkdir build && cd build \ - && cmake .. -DBoost_USE_STATIC_LIBS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF -DBUILD_PYTHON_INTERFACE=OFF -DBUILD_DOCUMENTATION=OFF -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=/usr/lib \ - && make -j4 && make install \ - && cd ../.. \ - && rm -rf pinocchio - -# Install Python wheel building tools -RUN python3 -m pip install --upgrade pip setuptools wheel && \ - python3 -m pip install \ - auditwheel \ - build \ - cibuildwheel \ - twine \ - patchelf \ - flake8 \ - pybind11 - -USER $USERNAME diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index be50bfc9..f4a4b36d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,10 +1,10 @@ { - "name": "libfranka_container", - "dockerComposeFile": "./docker-compose.yml", + "name": "libfranka", + "dockerComposeFile": "docker-compose.yml", "service": "libfranka_project", "workspaceFolder": "/workspaces", "remoteUser": "user", - "initializeCommand": "echo \"USER_UID=$(id -u $USER)\nUSER_GID=$(id -g $USER)\" > .devcontainer/.env", + "initializeCommand": "bash -c 'ENV_FILE=\".devcontainer/.env\"; UBUNTU_VERSION=$(cat devcontainer_distro 2>/dev/null || echo 22.04); if [ ! -f \"$ENV_FILE\" ]; then echo -e \"UBUNTU_VERSION=${UBUNTU_VERSION}\\nUSER_UID=$(id -u)\\nUSER_GID=$(id -g)\" > \"$ENV_FILE\"; else sed -i.bak \"s/^UBUNTU_VERSION=.*/UBUNTU_VERSION=${UBUNTU_VERSION}/\" \"$ENV_FILE\"; sed -i.bak \"s/^USER_UID=.*/USER_UID=$(id -u)/\" \"$ENV_FILE\"; sed -i.bak \"s/^USER_GID=.*/USER_GID=$(id -g)/\" \"$ENV_FILE\"; rm -f \"$ENV_FILE.bak\"; fi'", "customizations": { "vscode": { "extensions": [ diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index bb17d660..435be07e 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -2,11 +2,12 @@ services: libfranka_project: build: context: ../.ci/ - dockerfile: ../.ci/Dockerfile.focal + dockerfile: Dockerfile args: - USER_UID: ${USER_UID} - USER_GID: ${USER_GID} - container_name: libfranka + USER_UID: ${USER_UID:-1000} + USER_GID: ${USER_GID:-1000} + UBUNTU_VERSION: ${UBUNTU_VERSION:-22.04} + container_name: libfranka-${UBUNTU_VERSION:-22.04} network_mode: "host" shm_size: 512m privileged: true diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 00000000..1c333d46 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,110 @@ +name: Build and Push Docker Images + +on: + push: + branches: [main, master] + paths: + - '.ci/Dockerfile' + - '.ci/*.patch' + - '.github/workflows/docker-image.yml' + workflow_dispatch: + inputs: + force_rebuild: + description: 'Force rebuild all images' + required: false + default: 'false' + type: boolean + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/pylibfranka-build + +jobs: + build-and-push: + name: Build ${{ matrix.name }} image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + strategy: + fail-fast: false + matrix: + include: + # Python wheel build images + - python-version: '3.9' + ubuntu-version: '20.04' # Ubuntu 20.04 for glibc 2.31 compatibility + name: 'Python 3.9' + tag-prefix: 'py' + - python-version: '3.10' + ubuntu-version: '22.04' + name: 'Python 3.10' + tag-prefix: 'py' + - python-version: '3.11' + ubuntu-version: '22.04' + name: 'Python 3.11' + tag-prefix: 'py' + - python-version: '3.12' + ubuntu-version: '22.04' + name: 'Python 3.12' + tag-prefix: 'py' + # Debian package build images (Ubuntu version specific) + - python-version: '3.8' + ubuntu-version: '20.04' + name: 'Ubuntu 20.04' + tag-prefix: 'ubuntu' + - python-version: '3.10' + ubuntu-version: '22.04' + name: 'Ubuntu 22.04' + tag-prefix: 'ubuntu' + - python-version: '3.12' + ubuntu-version: '24.04' + name: 'Ubuntu 24.04' + tag-prefix: 'ubuntu' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ matrix.tag-prefix }}${{ matrix.tag-prefix == 'py' && matrix.python-version || matrix.ubuntu-version }} + type=sha,prefix=${{ matrix.tag-prefix }}${{ matrix.tag-prefix == 'py' && matrix.python-version || matrix.ubuntu-version }}- + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: .ci/ + file: .ci/Dockerfile + build-args: | + UBUNTU_VERSION=${{ matrix.ubuntu-version }} + PYTHON_VERSION=${{ matrix.python-version }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=docker-${{ matrix.tag-prefix }}${{ matrix.tag-prefix == 'py' && matrix.python-version || matrix.ubuntu-version }} + cache-to: type=gha,mode=max,scope=docker-${{ matrix.tag-prefix }}${{ matrix.tag-prefix == 'py' && matrix.python-version || matrix.ubuntu-version }} + + - name: Verify pushed image + env: + IMAGE_TAG: ${{ matrix.tag-prefix }}${{ matrix.tag-prefix == 'py' && matrix.python-version || matrix.ubuntu-version }} + run: | + echo "Verifying image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG}" + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} + docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} python3 --version diff --git a/.github/workflows/libfranka-build.yml b/.github/workflows/libfranka-build.yml index da923cee..5288f386 100644 --- a/.github/workflows/libfranka-build.yml +++ b/.github/workflows/libfranka-build.yml @@ -1,14 +1,36 @@ -name: Build the libfranka Debian Package +name: Build libfranka Debian Packages on: push: tags: - '*' workflow_dispatch: + inputs: + release_tag: + description: 'Tag name for release (leave empty for artifacts only)' + required: false + type: string + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/pylibfranka-build jobs: build-deb: runs-on: ubuntu-latest + permissions: + contents: write + packages: read + strategy: + fail-fast: false + matrix: + include: + - ubuntu_version: "20.04" + python_version: "3.8" + - ubuntu_version: "22.04" + python_version: "3.10" + - ubuntu_version: "24.04" + python_version: "3.12" steps: - name: Checkout repository @@ -17,43 +39,104 @@ jobs: submodules: 'recursive' fetch-depth: 0 - - name: Build Docker image - uses: docker/build-push-action@v4 + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 with: - context: .ci/ - file: .ci/Dockerfile.focal - tags: libfranka-build:latest - push: false - load: true + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Pull pre-built Docker image + run: | + echo "Pulling pre-built image for Ubuntu ${{ matrix.ubuntu_version }}..." + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:ubuntu${{ matrix.ubuntu_version }} + docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:ubuntu${{ matrix.ubuntu_version }} libfranka-build:${{ matrix.ubuntu_version }} - name: Build and package in container uses: addnab/docker-run-action@v3 with: - image: libfranka-build:latest + image: libfranka-build:${{ matrix.ubuntu_version }} options: -v ${{ github.workspace }}:/workspaces run: | cd /workspaces - cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF -B build -S . + cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -B build -S . cmake --build build -- -j$(nproc) cd build cpack -G DEB - # Upload to artifacts for manual workflow runs - - name: Upload Debian package (manual runs) - if: github.event_name == 'workflow_dispatch' + - name: Generate SHA256 checksums + run: | + cd build + for deb in *.deb; do + if [ -f "$deb" ]; then + sha256sum "$deb" > "${deb}.sha256" + echo "Generated checksum for $deb:" + cat "${deb}.sha256" + fi + done + + - name: List generated files + run: | + echo "Generated packages and checksums:" + ls -lh build/*.deb build/*.sha256 + + - name: Upload Debian package and checksum uses: actions/upload-artifact@v4 with: - name: libfranka-deb - path: build/*.deb + name: libfranka-${{ matrix.ubuntu_version }} + path: | + build/*.deb + build/*.sha256 + retention-days: 30 - # Create release for tag pushes - - name: Create GitHub Release and Upload Assets - if: startsWith(github.ref, 'refs/tags/') + release: + name: Create GitHub Release + needs: [build-deb] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '') + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get release info + id: get_release_info + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + TAG="${{ github.event.inputs.release_tag }}" + else + TAG=${GITHUB_REF#refs/tags/} + fi + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "Release tag: $TAG" + shell: bash + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: packages/ + pattern: libfranka-* + merge-multiple: true + + - name: Generate checksums summary + run: | + echo "Packages to upload:" + ls -la packages/ + echo "" + echo "Checksums:" + cat packages/*.sha256 + + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - files: build/*.deb - generate_release_notes: true + tag_name: ${{ steps.get_release_info.outputs.tag }} + name: libfranka ${{ steps.get_release_info.outputs.tag }} draft: false prerelease: false + files: packages/* + generate_release_notes: true + fail_on_unmatched_files: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pylibfranka-docs.yml b/.github/workflows/pylibfranka-docs.yml index bb07c9ba..1b40cc0a 100644 --- a/.github/workflows/pylibfranka-docs.yml +++ b/.github/workflows/pylibfranka-docs.yml @@ -5,9 +5,16 @@ on: tags: - '*' +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/pylibfranka-build + jobs: sphinx-docs: runs-on: ubuntu-latest + permissions: + contents: write + packages: read steps: - name: Checkout repository @@ -16,14 +23,18 @@ jobs: submodules: 'recursive' fetch-depth: 0 # Fetch all history including tags for version detection - - name: Build Docker Image - uses: docker/build-push-action@v4 + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 with: - context: .ci/ - file: .ci/Dockerfile.focal - tags: pylibfranka:latest - push: false - load: true + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Pull pre-built Docker image + run: | + echo "Pulling pre-built image..." + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:py3.10 + docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:py3.10 pylibfranka:latest - name: Setup Dependencies uses: addnab/docker-run-action@v3 @@ -52,7 +63,7 @@ jobs: cd pylibfranka/docs # Install Sphinx requirements - pip3 install -r requirements.txt --user + pip3 install -r requirements.txt # Get version from installed package (synced from CMakeLists.txt) VERSION=$(python3 -c "import pylibfranka; print(pylibfranka.__version__)") diff --git a/.github/workflows/pylibfranka-wheels.yml b/.github/workflows/pylibfranka-wheels.yml new file mode 100644 index 00000000..f5393839 --- /dev/null +++ b/.github/workflows/pylibfranka-wheels.yml @@ -0,0 +1,360 @@ +name: Build and Publish pylibfranka Wheels + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' # Also trigger on tags like 0.19.0 + pull_request: + branches: ['main', 'master'] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/pylibfranka-build + +jobs: + build_wheels: + name: Build wheels for Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + permissions: + contents: write + packages: read + strategy: + fail-fast: false + matrix: + include: + - python-version: '3.9' + python-tag: 'cp39' + ubuntu-version: '20.04' + manylinux-tag: 'manylinux_2_31_x86_64' + - python-version: '3.10' + python-tag: 'cp310' + ubuntu-version: '22.04' + manylinux-tag: 'manylinux_2_34_x86_64' + - python-version: '3.11' + python-tag: 'cp311' + ubuntu-version: '22.04' + manylinux-tag: 'manylinux_2_34_x86_64' + - python-version: '3.12' + python-tag: 'cp312' + ubuntu-version: '22.04' + manylinux-tag: 'manylinux_2_34_x86_64' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: 'recursive' + fetch-depth: 0 + + - name: Get Package Version + id: get_version + run: | + VERSION=$(grep -oP 'set\(libfranka_VERSION\s+\K[\d.]+' CMakeLists.txt) + if [ -z "$VERSION" ]; then + echo "Error: Could not extract version from CMakeLists.txt" + exit 1 + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Package version: $VERSION" + shell: bash + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Pull pre-built Docker image + run: | + echo "Pulling pre-built image for Python ${{ matrix.python-version }}..." + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:py${{ matrix.python-version }} + docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:py${{ matrix.python-version }} pylibfranka-build:py${{ matrix.python-version }} + + - name: Build Wheel for Python ${{ matrix.python-version }} + uses: addnab/docker-run-action@v3 + with: + image: pylibfranka-build:py${{ matrix.python-version }} + options: -v ${{ github.workspace }}:/workspace + run: | + cd /workspace + + # Verify Python version + echo "Python version:" + python3 --version + + # Clean previous builds + rm -rf build dist *.egg-info + + # Build the wheel (version is auto-extracted from CMakeLists.txt by setup.py) + echo "Building wheel for Python ${{ matrix.python-version }}..." + python3 -m build --wheel + + # Create wheelhouse directory + mkdir -p wheelhouse + + echo "Built wheel:" + ls -la dist/*.whl + + - name: Repair Wheels with Auditwheel + uses: addnab/docker-run-action@v3 + with: + image: pylibfranka-build:py${{ matrix.python-version }} + options: -v ${{ github.workspace }}:/workspace + run: | + cd /workspace + mkdir -p wheelhouse + + echo "Repairing wheels with auditwheel..." + for whl in dist/*.whl; do + if [ -f "$whl" ]; then + echo "Repairing: $whl" + echo "Target platform: ${{ matrix.manylinux-tag }}" + # Use the manylinux tag from matrix for this Python version + auditwheel repair "$whl" -w wheelhouse/ --plat ${{ matrix.manylinux-tag }} || \ + auditwheel repair "$whl" -w wheelhouse/ || \ + cp "$whl" wheelhouse/ + fi + done + + echo "Final wheels in wheelhouse:" + ls -la wheelhouse/ + + - name: Test Installation + uses: addnab/docker-run-action@v3 + with: + image: python:${{ matrix.python-version }}-slim + options: -v ${{ github.workspace }}:/workspace + run: | + # Install the wheel from wheelhouse + pip install /workspace/wheelhouse/*.whl + + # IMPORTANT: Change to /tmp to avoid importing from local pylibfranka folder + cd /tmp + + # Test import and version + python -c " + import pylibfranka + print('pylibfranka imported successfully') + print(f'Version: {pylibfranka.__version__}') + + # Verify key classes are available + from pylibfranka import Robot, Gripper, Model, RobotState + print('All core classes imported successfully') + + # Test creating objects + from pylibfranka import JointPositions, JointVelocities, Torques + jp = JointPositions([0.0] * 7) + print(f'JointPositions: {jp.q}') + print('All tests passed!') + " + + - name: Upload Wheel Artifact + uses: actions/upload-artifact@v4 + with: + name: wheel-python${{ matrix.python-version }} + path: wheelhouse/*.whl + retention-days: 30 + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: 'recursive' + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build setuptools wheel + + - name: Build source distribution + # Version is auto-extracted from CMakeLists.txt by setup.py + run: python -m build --sdist + + - name: Upload sdist artifact + uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + retention-days: 30 + + release: + name: Create GitHub Release + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get release info + id: get_release_info + run: | + TAG=${GITHUB_REF#refs/tags/} + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "version=$TAG" >> $GITHUB_OUTPUT + shell: bash + + - name: Download all wheel artifacts + uses: actions/download-artifact@v4 + with: + path: dist/ + pattern: wheel-* + merge-multiple: true + + - name: Download sdist artifact + uses: actions/download-artifact@v4 + with: + name: sdist + path: dist/ + + - name: List distributions + run: | + echo "Distributions to upload:" + ls -la dist/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.get_release_info.outputs.tag }} + name: libfranka ${{ steps.get_release_info.outputs.version }} + draft: false + prerelease: false + files: dist/* + body: | + # libfranka ${{ steps.get_release_info.outputs.version }} + + C++ library and Python bindings for controlling Franka robots. + + --- + + ## libfranka (C++ Library) + + ### Installation from Debian Package + Download the appropriate `.deb` file for your Ubuntu version from the assets below and install: + + **Ubuntu 20.04 (Focal):** + ```bash + sudo dpkg -i libfranka_${{ steps.get_release_info.outputs.version }}_focal_amd64.deb + sudo apt-get install -f # Install dependencies if needed + ``` + + **Ubuntu 22.04 (Jammy):** + ```bash + sudo dpkg -i libfranka_${{ steps.get_release_info.outputs.version }}_jammy_amd64.deb + sudo apt-get install -f # Install dependencies if needed + ``` + + **Ubuntu 24.04 (Noble):** + ```bash + sudo dpkg -i libfranka_${{ steps.get_release_info.outputs.version }}_noble_amd64.deb + sudo apt-get install -f # Install dependencies if needed + ``` + + ### Available Debian Packages + | Ubuntu Version | Package | + |----------------|---------| + | 20.04 (Focal) | `libfranka_${{ steps.get_release_info.outputs.version }}_focal_amd64.deb` | + | 22.04 (Jammy) | `libfranka_${{ steps.get_release_info.outputs.version }}_jammy_amd64.deb` | + | 24.04 (Noble) | `libfranka_${{ steps.get_release_info.outputs.version }}_noble_amd64.deb` | + + --- + + ## pylibfranka (Python Bindings) + + Python bindings for libfranka, providing easy-to-use Python interfaces for controlling Franka robots. + + ### Installation + + #### From PyPI (Recommended) + ```bash + pip install pylibfranka==${{ steps.get_release_info.outputs.version }} + ``` + + #### From GitHub Release + Download the appropriate wheel for your Python version and install: + ```bash + pip install pylibfranka-${{ steps.get_release_info.outputs.version }}-cp310-cp310-manylinux_2_34_x86_64.whl + ``` + + ### Platform Compatibility + + | Ubuntu Version | Supported Python Versions | + |----------------|---------------------------| + | 20.04 (Focal) | Python 3.9 only | + | 22.04 (Jammy) | Python 3.9, 3.10, 3.11, 3.12 | + | 24.04 (Noble) | Python 3.9, 3.10, 3.11, 3.12 | + + **Note:** Ubuntu 20.04 users must use Python 3.9 due to glibc compatibility requirements. + + ### Available Wheels + - Python 3.9: `cp39-cp39-manylinux_2_31_x86_64.whl` (Ubuntu 20.04+) + - Python 3.10: `cp310-cp310-manylinux_2_34_x86_64.whl` (Ubuntu 22.04+) + - Python 3.11: `cp311-cp311-manylinux_2_34_x86_64.whl` (Ubuntu 22.04+) + - Python 3.12: `cp312-cp312-manylinux_2_34_x86_64.whl` (Ubuntu 22.04+) + - Source: `pylibfranka-${{ steps.get_release_info.outputs.version }}.tar.gz` + + ### Quick Start + ```python + import pylibfranka + print(f"pylibfranka version: {pylibfranka.__version__}") + + robot = pylibfranka.Robot("172.16.0.2") + state = robot.read_once() + ``` + + --- + + ## Documentation + - [libfranka Documentation](https://frankarobotics.github.io/libfranka/) + - [pylibfranka Examples](https://github.com/frankarobotics/libfranka/tree/main/pylibfranka/examples) + - [pylibfranka API Documentation](https://frankarobotics.github.io/libfranka/pylibfranka/latest) + - [pylibfranka README](https://github.com/frankarobotics/libfranka/blob/main/pylibfranka/README.md) + + publish_pypi: + name: Publish to PyPI + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + # Publish automatically on tags + if: startsWith(github.ref, 'refs/tags/') + permissions: + id-token: write # Required for Trusted Publisher OIDC + + steps: + - name: Download all wheel artifacts + uses: actions/download-artifact@v4 + with: + path: dist/ + pattern: wheel-* + merge-multiple: true + + - name: Download sdist artifact + uses: actions/download-artifact@v4 + with: + name: sdist + path: dist/ + + - name: List distributions + run: | + echo "Distributions to upload:" + ls -la dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true + verbose: true diff --git a/.gitignore b/.gitignore index a943e170..46ea99e3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ __pycache__/ version_info.properties pylibfranka/docs/_build/ pylibfranka/_version.py +pylibfranka/VERSION diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4fe0a2dd..ab912562 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: # C++ formatting with clang-format - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v16.0.6 + rev: v18.1.8 hooks: - id: clang-format types_or: [c++, c, cuda] diff --git a/CHANGELOG.md b/CHANGELOG.md index 636d7188..5d47e070 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,39 @@ # CHANGELOG -All notable changes to libfranka and pylibfranka will be documented in this file. +All notable changes to libfranka in this file. + +## [0.20.2] +### libfranka - C++ +- Fix the github workflow to push all the debian packages from 20.4, 22.04 and 24.04 +### pylibfranka - Python +#### Added +- Automated publishing to PyPI via GitHub Actions workflow. When a version tag is pushed, the workflow automatically builds wheels for Python 3.9, 3.10, 3.11, and 3.12, and publishes them to PyPI. Users can now install pylibfranka directly from PyPI using `pip install pylibfranka`. +- Fix the pylibfranka pybind error with std::nullopt + +## [0.20.1] + +### libfranka - C++ + +- Fixed tinyxml2 dependency for ros users using `rosdep install` + +## [0.20.0] +### libfranka - C++ +#### Changed +- Hotfix to avoid torques discontinuity false positives due to robot state float precision change (was again reverted). +- Breaking change: Fixed a wrong torque discontinuity trigger by reverting the float change within the robot state back to doubles. + +### pylibfranka - Python +#### Added +- Async control python bindings +- Async joint positions control example from C++. + +## [0.19.0] +### libfranka - C++ +#### Changed +- To support franka_ros2, we added an option for the async position control to base the `getFeedback` function on a robot state received via `franka_hardware` instead of querying the robot directly. +- Format libfranka debian package to inlclude ubuntu code name and arch: libfranka_VERSION_CODENAME_ARCH.deb +- Added build containers to support Ubuntu 22.04 and 24.04 + ## [0.18.2] Requires Franka Research 3 System Version >= 5.9.0 @@ -15,7 +48,6 @@ Requires Franka Research 3 System Version >= 5.9.0 #### Fixed - Fixed a compile issue with TinyXML2 dependency (see [github](https://github.com/frankarobotics/libfranka/issues/215)) - ### pylibfranka - Python #### Added - Added Joint and Cartesian Velocity controller examples. diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c4905f2..078ce6db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.11) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -set(libfranka_VERSION 0.18.2) +set(libfranka_VERSION 0.20.3) project(libfranka VERSION ${libfranka_VERSION} @@ -75,6 +75,9 @@ if(BUILD_COVERAGE) set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --coverage") endif() +# Suppress false positive warnings about string operations (aka GetRobotModel) +add_compile_options(-Wno-stringop-overflow -Wno-array-bounds) + ## Load dependencies include(FetchFMT) @@ -157,6 +160,7 @@ target_link_libraries(franka PUBLIC fmt::fmt ) + ## Compile pylibfranka if (GENERATE_PYLIBFRANKA) add_subdirectory(pylibfranka) @@ -219,12 +223,19 @@ endif() ## Packaging include(SetVersionFromGit) set_version_from_git(PACKAGE_VERSION PACKAGE_TAG) + +# Get architecture execute_process(COMMAND dpkg --print-architecture OUTPUT_VARIABLE PACKAGE_ARCH OUTPUT_STRIP_TRAILING_WHITESPACE) +# Get Ubuntu codename (focal, jammy, noble) +execute_process(COMMAND lsb_release -cs OUTPUT_VARIABLE UBUNTU_CODENAME OUTPUT_STRIP_TRAILING_WHITESPACE) + set(CPACK_PACKAGE_VENDOR "Franka Robotics GmbH") set(CPACK_GENERATOR "DEB;TGZ") set(CPACK_PACKAGE_VERSION ${libfranka_VERSION}) -set(CPACK_PACKAGE_FILE_NAME ${PROJECT_NAME}_${PACKAGE_TAG}_${PACKAGE_ARCH}) + +# Updated filename format: libfranka_VERSION_CODENAME_ARCH.deb +set(CPACK_PACKAGE_FILE_NAME ${PROJECT_NAME}_${PACKAGE_TAG}_${UBUNTU_CODENAME}_${PACKAGE_ARCH}) set(CPACK_PACKAGE_VERSION ${PACKAGE_TAG}) set(CPACK_PACKAGE_ARCHITECTURE ${PACKAGE_ARCH}) set(CPACK_SYSTEM_NAME ${CMAKE_HOST_SYSTEM_PROCESSOR}) @@ -277,16 +288,32 @@ if(CLANG_TIDY_PROG) string(REPLACE ";" "\"},{\"name\":\"" TIDY_LINE_FILTER "${TIDY_FILES}") set(TIDY_LINE_FILTER "[{\"name\":\"${TIDY_LINE_FILTER}\"}]") + # Disable noisy checks reported in tidy summary + # - misc-include-cleaner: requires direct include of providing headers + # - performance-avoid-endl: prefer \n over std::endl (OK in examples) + # - misc-const-correctness: suggests const qualifiers for locals + set(CLANG_TIDY_DISABLED_CHECKS "-misc-include-cleaner,-performance-avoid-endl,-misc-const-correctness,-readability-redundant-member-init") + add_custom_target(tidy COMMAND ${CLANG_TIDY_PROG} -p=${CMAKE_BINARY_DIR} - -line-filter=${TIDY_LINE_FILTER} ${SOURCES} + -quiet + -line-filter=${TIDY_LINE_FILTER} + -checks=${CLANG_TIDY_DISABLED_CHECKS} + --extra-arg=-Wno-unknown-warning-option + --extra-arg=-Wno-unused-command-line-argument + ${SOURCES} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Running clang-tidy" VERBATIM ) add_custom_target(check-tidy COMMAND scripts/fail-on-output.sh ${CLANG_TIDY_PROG} -p=${CMAKE_BINARY_DIR} - -line-filter=${TIDY_LINE_FILTER} ${SOURCES} + -quiet + -line-filter=${TIDY_LINE_FILTER} + -checks=${CLANG_TIDY_DISABLED_CHECKS} + --extra-arg=-Wno-unknown-warning-option + --extra-arg=-Wno-unused-command-line-argument + ${SOURCES} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Running clang-tidy" VERBATIM diff --git a/Jenkinsfile b/Jenkinsfile index 0b813048..ddd5a935 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,3 +1,5 @@ +def PYTHON_VERSION_BY_UBUNTU = ['20.04': '3.9', '22.04': '3.10', '24.04': '3.12'] + pipeline { libraries { lib('fe-pipeline-steps@1.0.0') @@ -12,7 +14,7 @@ pipeline { } options { parallelsAlwaysFailFast() - timeout(time: 1, unit: 'HOURS') + timeout(time: 2, unit: 'HOURS') } environment { VERSION = feDetermineVersionFromGit() @@ -21,24 +23,41 @@ pipeline { stages { stage('Matrix') { matrix { + axes { + axis { + name 'UBUNTU_VERSION' + values '20.04', '22.04', '24.04' + } + } + environment { + DISTRO = "${['20.04': 'focal', '22.04': 'jammy', '24.04': 'noble'][UBUNTU_VERSION]}" + PYTHON_VERSION = "${PYTHON_VERSION_BY_UBUNTU[UBUNTU_VERSION]}" + } agent { dockerfile { dir ".ci" - filename "Dockerfile.${env.DISTRO}" - reuseNode true + filename "Dockerfile" + reuseNode false + additionalBuildArgs "--pull --build-arg UBUNTU_VERSION=${env.UBUNTU_VERSION} --build-arg PYTHON_VERSION=${PYTHON_VERSION_BY_UBUNTU[env.UBUNTU_VERSION]} --tag libfranka:${env.UBUNTU_VERSION}" args '--privileged ' + '--cap-add=SYS_PTRACE ' + '--security-opt seccomp=unconfined ' + '--shm-size=2g ' } } - axes { - axis { - name 'DISTRO' - values 'focal' - } - } stages { + stage('Init Distro') { + steps { + script { + if (!env.DISTRO) { + error "Unknown UBUNTU_VERSION=${env.UBUNTU_VERSION}" + } + if (!env.PYTHON_VERSION) { + error "Unknown PYTHON_VERSION for UBUNTU_VERSION=${env.UBUNTU_VERSION}" + } + } + } + } stage('Setup') { stages { stage('Notify Stash') { @@ -50,7 +69,20 @@ pipeline { } stage('Clean Workspace') { steps { - sh "rm -rf build-*${DISTRO}" + script { + def distro = DISTRO_VERSIONS[env.UBUNTU_VERSION] + withEnv(["DISTRO=${distro}"]) { + sh ''' + # Clean build dirs for this distro axis + rm -rf build-*${DISTRO} + rm -rf install-*${DISTRO} + # Remove corrupted googletest fetch content if present + rm -rf build-release.${DISTRO}/_deps/gtest-src build-release.${DISTRO}/_deps/gtest-build || true + rm -rf build-debug.${DISTRO}/_deps/gtest-src build-debug.${DISTRO}/_deps/gtest-build || true + rm -rf build-coverage.${DISTRO}/_deps/gtest-src build-coverage.${DISTRO}/_deps/gtest-build || true + ''' + } + } } } } @@ -61,10 +93,12 @@ pipeline { steps { dir("build-debug.${env.DISTRO}") { sh ''' + rm -rf CMakeCache.txt CMakeFiles _deps || true cmake -DCMAKE_BUILD_TYPE=Debug -DSTRICT=ON -DBUILD_COVERAGE=OFF \ -DBUILD_DOCUMENTATION=OFF -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON \ - -DGENERATE_PYLIBFRANKA=ON .. + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DGENERATE_PYLIBFRANKA=ON .. make -j$(nproc) + cmake --install . --prefix ../install-debug.${DISTRO} ''' } } @@ -73,36 +107,51 @@ pipeline { steps { dir("build-release.${env.DISTRO}") { sh ''' + rm -rf CMakeCache.txt CMakeFiles _deps || true cmake -DCMAKE_BUILD_TYPE=Release -DSTRICT=ON -DBUILD_COVERAGE=OFF \ -DBUILD_DOCUMENTATION=ON -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON \ - -DGENERATE_PYLIBFRANKA=ON .. + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DGENERATE_PYLIBFRANKA=ON .. make -j$(nproc) + cmake --install . --prefix ../install-release.${DISTRO} ''' } } } stage('Build examples (debug)') { steps { - dir("build-debug-examples.${env.DISTRO}") { - sh "cmake -DFranka_DIR:PATH=../build-debug.${env.DISTRO} ../examples" - sh 'make -j$(nproc)' + script { + def distro = DISTRO_VERSIONS[env.UBUNTU_VERSION] + dir("build-debug-examples.${distro}") { + sh "cmake -DCMAKE_PREFIX_PATH=../install-debug.${distro} ../examples" + sh 'make -j$(nproc)' + } } } } stage('Build examples (release)') { steps { - dir("build-release-examples.${env.DISTRO}") { - sh "cmake -DFranka_DIR:PATH=../build-release.${env.DISTRO} ../examples" - sh 'make -j$(nproc)' + script { + def distro = DISTRO_VERSIONS[env.UBUNTU_VERSION] + dir("build-release-examples.${distro}") { + sh "cmake -DCMAKE_PREFIX_PATH=../install-release.${distro} ../examples" + sh 'make -j$(nproc)' + } } } } stage('Build coverage') { + when { + not { + environment name: 'UBUNTU_VERSION', value: '24.04' + } + } steps { dir("build-coverage.${env.DISTRO}") { sh ''' + rm -rf CMakeCache.txt CMakeFiles _deps || true cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_COVERAGE=ON \ - -DBUILD_DOCUMENTATION=OFF -DBUILD_EXAMPLES=OFF -DBUILD_TESTS=ON .. + -DBUILD_DOCUMENTATION=OFF -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DBUILD_EXAMPLES=OFF -DBUILD_TESTS=ON .. make -j$(nproc) ''' } @@ -115,7 +164,7 @@ pipeline { dir("build-lint.${env.DISTRO}") { catchError(buildResult: env.UNSTABLE, stageResult: env.UNSTABLE) { sh ''' - cmake -DBUILD_COVERAGE=OFF -DBUILD_DOCUMENTATION=OFF -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON .. + cmake -DBUILD_COVERAGE=OFF -DBUILD_DOCUMENTATION=OFF -DBUILD_EXAMPLES=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DBUILD_TESTS=ON .. make check-tidy -j$(nproc) ''' } @@ -127,7 +176,7 @@ pipeline { dir("build-format.${env.DISTRO}") { catchError(buildResult: env.UNSTABLE, stageResult: env.UNSTABLE) { sh ''' - cmake -DBUILD_COVERAGE=OFF -DBUILD_DOCUMENTATION=OFF -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON .. + cmake -DBUILD_COVERAGE=OFF -DBUILD_DOCUMENTATION=OFF -DBUILD_EXAMPLES=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DBUILD_TESTS=ON .. make check-format -j$(nproc) ''' } @@ -135,11 +184,16 @@ pipeline { } } stage('Coverage') { + when { + not { + environment name: 'UBUNTU_VERSION', value: '24.04' + } + } steps { dir("build-coverage.${env.DISTRO}") { catchError(buildResult: env.UNSTABLE, stageResult: env.UNSTABLE) { sh ''' - cmake -DBUILD_COVERAGE=ON -DBUILD_DOCUMENTATION=OFF -DBUILD_EXAMPLES=OFF -DBUILD_TESTS=ON .. + cmake -DBUILD_COVERAGE=ON -DBUILD_DOCUMENTATION=OFF -DBUILD_EXAMPLES=OFF -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DBUILD_TESTS=ON .. make coverage -j$(nproc) ''' publishHTML([allowMissing: false, @@ -163,18 +217,21 @@ pipeline { echo "[Debug Tests] ASLR status: $(cat /proc/sys/kernel/randomize_va_space)" ''' - dir("build-debug.${env.DISTRO}") { - sh ''' - echo "[Debug Tests] Running tests..." - ctest -V - ''' - } + script { + def distro = DISTRO_VERSIONS[env.UBUNTU_VERSION] + dir("build-debug.${distro}") { + sh ''' + echo "[Debug Tests] Running tests..." + ctest -V + ''' + } - dir("build-release.${env.DISTRO}") { - sh ''' - echo "[Release Tests] Running tests..." - ctest -V - ''' + dir("build-release.${distro}") { + sh ''' + echo "[Release Tests] Running tests..." + ctest -V + ''' + } } sh ''' @@ -189,8 +246,8 @@ pipeline { post { always { catchError(buildResult: env.UNSTABLE, stageResult: env.UNSTABLE) { - junit "build-release.${env.DISTRO}/test_results/*.xml" - junit "build-debug.${env.DISTRO}/test_results/*.xml" + junit "build-release.${DISTRO_VERSIONS[env.UBUNTU_VERSION]}/test_results/*.xml" + junit "build-debug.${DISTRO_VERSIONS[env.UBUNTU_VERSION]}/test_results/*.xml" } } } @@ -202,60 +259,79 @@ pipeline { } stage('Publish') { steps { - dir("build-release.${env.DISTRO}") { - catchError(buildResult: env.UNSTABLE, stageResult: env.UNSTABLE) { - sh 'cpack' + script { + def distro = DISTRO_VERSIONS[env.UBUNTU_VERSION] + dir("build-release.${distro}") { + catchError(buildResult: env.UNSTABLE, stageResult: env.UNSTABLE) { + sh 'cpack' - // Publish Debian packages with Git commit hash in the name - fePublishDebian('*.deb', 'fci', "deb.distribution=${env.DISTRO};deb.component=main;deb.architecture=amd64") + // Publish Debian packages with Git commit hash in the name + fePublishDebian('*.deb', 'fci', "deb.distribution=${distro};deb.component=main;deb.architecture=amd64") - dir('doc') { - sh 'mv docs/*/html/ html/' - sh 'tar cfz ../libfranka-docs.tar.gz html' + dir('doc') { + sh 'mv docs/*/html/ html/' + sh 'tar cfz ../libfranka-docs.tar.gz html' + } + sh "rename -e 's/(.tar.gz)\$/-${distro}\$1/' *.tar.gz" + publishHTML([allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'doc/html', + reportFiles: 'index.html', + reportName: "API Documentation (${distro})"]) } - sh "rename -e 's/(.tar.gz)\$/-${env.DISTRO}\$1/' *.tar.gz" - publishHTML([allowMissing: false, - alwaysLinkToLastBuild: false, - keepAll: true, - reportDir: 'doc/html', - reportFiles: 'index.html', - reportName: "API Documentation (${env.DISTRO})"]) } } // Build and publish pylibfranka documentation catchError(buildResult: env.UNSTABLE, stageResult: env.UNSTABLE) { - sh ''' - # Install pylibfranka from root (builds against libfranka in build-release.focal) - export LD_LIBRARY_PATH="${WORKSPACE}/build-release.${DISTRO}:${LD_LIBRARY_PATH:-}" - pip3 install . --user - ''' - - dir('pylibfranka/docs') { - sh ''' - # Add sphinx to PATH - export PATH="$HOME/.local/bin:$PATH" + script { + def distro = DISTRO_VERSIONS[env.UBUNTU_VERSION] + withEnv(["DISTRO=${distro}"]) { + sh ''' + # Install pylibfranka from root (builds against libfranka in build-release.${DISTRO}) + export LD_LIBRARY_PATH="${WORKSPACE}/build-release.${DISTRO}:${LD_LIBRARY_PATH:-}" + if [ -n "$VIRTUAL_ENV" ]; then + python3 -m pip install . + else + python3 -m pip install . --user + fi + ''' - # Install Sphinx and dependencies - pip3 install -r requirements.txt --user + dir('pylibfranka/docs') { + sh ''' + # Install Sphinx and dependencies (respect virtualenv if present) + if [ -n "$VIRTUAL_ENV" ]; then + python3 -m pip install -r requirements.txt + else + # Add sphinx to PATH for --user installs + export PATH="$HOME/.local/bin:$PATH" + python3 -m pip install -r requirements.txt --user + fi - # Set locale - export LC_ALL=C.UTF-8 - export LANG=C.UTF-8 + # Set locale + export LC_ALL=C.UTF-8 + export LANG=C.UTF-8 - # Add libfranka to library path - export LD_LIBRARY_PATH="${WORKSPACE}/build-release.${DISTRO}:${LD_LIBRARY_PATH:-}" + # Add libfranka to library path + export LD_LIBRARY_PATH="${WORKSPACE}/build-release.${DISTRO}:${LD_LIBRARY_PATH:-}" - # Build the documentation - make html - ''' + # Build the documentation only on Ubuntu 20.04 + if [ "${UBUNTU_VERSION}" = "20.04" ]; then + make html + else + echo "Skipping docs build on ${UBUNTU_VERSION}" + fi + ''' - publishHTML([allowMissing: false, - alwaysLinkToLastBuild: false, - keepAll: true, - reportDir: '_build/html', - reportFiles: 'index.html', - reportName: "pylibfranka Documentation (${env.DISTRO})"]) + publishHTML([allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: '_build/html', + reportFiles: 'index.html', + reportName: "pylibfranka Documentation (${distro})"]) + } + } } } } diff --git a/README.rst b/README.rst index fa21fdad..d7cca5e5 100644 --- a/README.rst +++ b/README.rst @@ -16,203 +16,429 @@ Key Features - **Low-level control**: Access precise motion control for research robots. - **Real-time communication**: Interact with the robot in real-time. +- **Multi-platform support**: Ubuntu 20.04, 22.04, and 24.04 LTS -Getting Started ---------------- - -.. _system-requirements: 1. System Requirements ~~~~~~~~~~~~~~~~~~~~~~ Before using **libfranka**, ensure your system meets the following requirements: -- **Operating System**: `Linux with PREEMPT_RT patched kernel `_ (Ubuntu 16.04 or later, Ubuntu 22.04 recommended) -- **Compiler**: GCC 7 or later -- **CMake**: Version 3.10 or later -- **Robot**: Franka Robotics robot with FCI feature installed +**Operating System**: + - Ubuntu 20.04 LTS (Focal Fossa) + - Ubuntu 22.04 LTS (Jammy Jellyfish) + - Ubuntu 24.04 LTS (Noble Numbat) + - `Linux with PREEMPT_RT patched kernel `_ recommended for real-time control -.. _installing-dependencies: +**Build Tools** (for building from source): + - GCC 9 or later + - CMake 3.22 or later + - Git -2. Installing dependencies -~~~~~~~~~~~~~~~~~~~~~~~~~~ +**Hardware**: + - Franka Robotics robot with FCI feature installed + - Network connection to robot (1000BASE-T Ethernet recommended) -.. code-block:: bash +.. _installation-debian-package: - sudo apt-get update - sudo apt-get install -y build-essential cmake git libpoco-dev libeigen3-dev libfmt-dev +2. Installation from Debian Package (Recommended) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The easiest way to install **libfranka** is by using the pre-built Debian packages +published on GitHub. + +Supported Platforms +^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 28 18 18 26 -3. Install from Debian Package - Generic Pattern -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * - Ubuntu Version + - Codename + - Architecture + - Status + * - 20.04 LTS + - focal + - amd64 + - Supported + * - 22.04 LTS + - jammy + - amd64 + - Supported + * - 24.04 LTS + - noble + - amd64 + - Supported -.. note:: +Quick Install +^^^^^^^^^^^^^ - The installation packages currently only support Ubuntu 20 (focal). Please ensure you are using this version to avoid compatibility issues. +substitute `` with the desired version number (e.g., `0.19.0`). And +substitute `` with your Ubuntu codename (e.g., `focal`, `jammy`, `noble`). you can find it by running `lsb_release -cs`. -**Check your architecture:** .. code-block:: bash - dpkg --print-architecture + # Download package + wget https://github.com/frankarobotics/libfranka/releases/download//libfranka___amd64.deb -**Download and install:** + # Download checksum + wget https://github.com/frankarobotics/libfranka/releases/download//libfranka___amd64.deb.sha256 + + # Verify package integrity + sha256sum -c libfranka___amd64.deb.sha256 + + # Install package + sudo dpkg -i libfranka___amd64.deb + +Example +^^^^^^^ + +The following example installs **libfranka 0.19.0** on **Ubuntu 22.04 (Jammy)**: .. code-block:: bash - wget https://github.com/frankarobotics/libfranka/releases/download//libfranka___.deb - sudo dpkg -i libfranka___.deb -Replace ```` with the desired release version (e.g., ``0.18.2``, with ``focal`` and ```` with your system architecture (e.g., ``amd64`` or ``arm64``). + wget https://github.com/frankarobotics/libfranka/releases/download/0.19.0/libfranka_0.19.0_jammy_amd64.deb + wget https://github.com/frankarobotics/libfranka/releases/download/0.19.0/libfranka_0.19.0_jammy_amd64.deb.sha256 + sha256sum -c libfranka_0.19.0_jammy_amd64.deb.sha256 + sudo dpkg -i libfranka_0.19.0_jammy_amd64.deb + +.. tip:: + + All released versions, packages, and checksums are available on the + `GitHub Releases page `_. -**Example for version 0.18.2 on amd64:** + +.. _building-with-docker: + +3. Building with Docker (Development Environment) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Docker provides a consistent, isolated build environment. This method is ideal for: + +- Development and testing +- Building for multiple Ubuntu versions +- Avoiding dependency conflicts on your host system + +Prerequisites +^^^^^^^^^^^^^ + +- Docker installed on your system +- Visual Studio Code with Dev Containers extension (optional, for VS Code users) + +Clone the Repository +^^^^^^^^^^^^^^^^^^^^ .. code-block:: bash - wget https://github.com/frankarobotics/libfranka/releases/download/0.18.2/libfranka_0.18.2_focal_amd64.deb - sudo dpkg -i libfranka_0.18.2_focal_amd64.deb + git clone --recurse-submodules https://github.com/frankarobotics/libfranka.git + cd libfranka + git checkout 0.19.0 # or your desired version -.. _building-in-docker: -4. Building libfranka Inside Docker -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Method A: Using Visual Studio Code (Recommended) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you prefer to build **libfranka** inside a Docker container, you can use the provided Docker setup. This ensures a consistent build environment and avoids dependency conflicts on your host system. +1. **Install Visual Studio Code** -Docker creates a self-contained environment, which is helpful if: + Download from https://code.visualstudio.com/ -- Your system doesn't meet the requirements -- You want to avoid installing dependencies on your main system -- You prefer a clean, reproducible setup +2. **Install the Dev Containers extension** -If you haven't already, clone the **libfranka** repository: + - Open VS Code + - Go to Extensions (Ctrl+Shift+X) + - Search for "Dev Containers" and install it + +3. **Configure Ubuntu version** (optional, defaults to 20.04) + + Edit ``devcontainer_distro`` file in the project root and specify the desired Ubuntu version: .. code-block:: bash - git clone --recurse-submodules https://github.com/frankarobotics/libfranka.git - cd libfranka - git checkout + 22.04 # Ubuntu 22.04 (default) + -Using Docker command line -^^^^^^^^^^^^^^^^^^^^^^^^^ +4. **Open in container** -1. **Build the Docker image**: + - Open the project in VS Code: ``code .`` + - Press ``Ctrl+Shift+P`` + - Select "Dev Containers: Reopen in Container" + - Wait for the container to build + +5. **Build libfranka** + + Open a terminal in VS Code and run: .. code-block:: bash - cd .ci - docker build -t libfranka:latest -f Dockerfile.focal . - cd .. + mkdir build && cd build + cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF .. + cmake --build . -- -j$(nproc) -2. **Run the Docker container**: +6. **Create Debian package** (optional) .. code-block:: bash - docker run --rm -it -v $(pwd):/workspace libfranka:latest + cd build + cpack -G DEB - If you already have a build folder, you must remove it first to avoid issues: + The package will be in the ``build/`` directory. To install on your host system: .. code-block:: bash - rm -rf /workspace/build - mkdir -p /workspace/build - cd /workspace/build - cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF /workspace - make + # On host system (outside container) + cd libfranka/build + sudo dpkg -i libfranka*.deb + +Method B: Using Docker Command Line +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - To generate a Debian package: +1. **Build the Docker image** .. code-block:: bash - sudo cpack -G DEB + # For Ubuntu 20.04 + docker build --build-arg UBUNTU_VERSION=20.04 -t libfranka-build:20.04 .ci/ + + # For Ubuntu 22.04 + docker build --build-arg UBUNTU_VERSION=22.04 -t libfranka-build:22.04 .ci/ + + # For Ubuntu 24.04 + docker build --build-arg UBUNTU_VERSION=24.04 -t libfranka-build:24.04 .ci/ + +2. **Run the container and build** - Exit the Docker container by typing ``exit`` in the terminal. + .. code-block:: bash + + docker run --rm -it -v $(pwd):/workspaces -w /workspaces libfranka-build:20.04 -3. **Install libfranka on your host system**: + # Inside container: + mkdir build && cd build + cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF .. + cmake --build . -- -j$(nproc) + cpack -G DEB + exit - Inside the libfranka build folder ``cd build`` on your host system, run: +3. **Install on host system** .. code-block:: bash + cd build sudo dpkg -i libfranka*.deb +.. _building-from-source: -Using Visual Studio Code -^^^^^^^^^^^^^^^^^^^^^^^^ +4. Building from Source (Advanced) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can also build **libfranka** inside Docker using **VS Code** with the **Dev Containers** extension. This provides an integrated development environment inside the container. +Prerequisites +^^^^^^^^^^^^^ -1. **Install Visual Studio Code**: +**System packages:** - - Download and install **Visual Studio Code** from the official website: https://code.visualstudio.com/. - - Follow the installation instructions for your operating system. +.. code-block:: bash -2. **Open the Project in VS Code**: + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + cmake \ + git \ + wget \ + libeigen3-dev \ + libpoco-dev \ + libfmt-dev \ + pybind11-dev + +**Ubuntu 20.04:** ensure CMake >= 3.22: - Inside the libfranka folder, open a new terminal and run: +.. code-block:: bash - .. code-block:: bash + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor -o /usr/share/keyrings/kitware-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ focal main" | sudo tee /etc/apt/sources.list.d/kitware.list + sudo apt-get update + sudo apt-get install -y cmake - code . +Remove Existing Installations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - This will open the project in VS Code. +.. code-block:: bash -3. **Install the Dev Containers Extension**: + sudo apt-get remove -y "*libfranka*" - Install the "Dev Containers" extension in VS Code from the Extensions Marketplace. +Build Dependencies from Source +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -4. **Open the Project in a Dev Container**: +Follow these steps **in order**. All dependencies are built with static linking. - - Open the **Command Palette** (``Ctrl+Shift+P``). - - Select **Dev Containers: Reopen in Container**. - - VS Code will build the Docker image and start a container based on the provided ``.ci/Dockerfile``. +**1. Boost 1.77.0** -5. **Build libfranka**: +.. code-block:: bash - - Open a terminal in VS Code. - - Run the following commands: + git clone --depth 1 --recurse-submodules --shallow-submodules \ + --branch boost-1.77.0 https://github.com/boostorg/boost.git + cd boost + ./bootstrap.sh --prefix=/usr/local + sudo ./b2 install -j$(nproc) + cd .. && rm -rf boost - .. code-block:: bash +**2. TinyXML2** - mkdir build && cd build - cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF .. - make +.. code-block:: bash -6. **Install libfranka**: + git clone --depth 1 --branch 10.0.0 https://github.com/leethomason/tinyxml2.git + cd tinyxml2 + mkdir build && cd build - If you want to install **libfranka** inside the container, you can run: + cmake .. \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr/local - .. code-block:: bash + make -j$(nproc) + sudo make install - sudo make install + cd ../.. && rm -rf tinyxml2 - If you want to install **libfranka** on your host system, you can run: +**3. console_bridge** - .. code-block:: bash +.. code-block:: bash - sudo cpack -G DEB + git clone --depth 1 --branch 1.0.2 https://github.com/ros/console_bridge.git + cd console_bridge + mkdir build && cd build - Then in a new terminal on your host system, navigate to the libfranka ``build`` folder and run: + cmake .. \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr/local - .. code-block:: bash + make -j$(nproc) + sudo make install - sudo dpkg -i libfranka*.deb + cd ../.. + rm -rf console_bridge + + +**4. urdfdom_headers** + +.. code-block:: bash + + git clone --depth 1 --branch 1.0.5 https://github.com/ros/urdfdom_headers.git + cd urdfdom_headers + mkdir build && cd build + + cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr/local + + make -j$(nproc) + sudo make install + + cd ../.. + rm -rf urdfdom_headers + + +**5. urdfdom (with patch)** + +.. code-block:: bash + + wget https://raw.githubusercontent.com/frankarobotics/libfranka/main/.ci/urdfdom.patch + + git clone --depth 1 --branch 4.0.0 https://github.com/ros/urdfdom.git + cd urdfdom + + git apply ../urdfdom.patch + + mkdir build && cd build + cmake .. \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr/local + + make -j$(nproc) + sudo make install + cd ../.. + rm -rf urdfdom + + +**6. Assimp** + +.. code-block:: bash + + git clone --depth 1 --recurse-submodules --shallow-submodules \ + --branch v5.4.3 https://github.com/assimp/assimp.git + cd assimp && mkdir build && cd build + cmake .. -DBoost_USE_STATIC_LIBS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DBUILD_SHARED_LIBS=OFF -DASSIMP_BUILD_TESTS=OFF \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local + make -j$(nproc) && sudo make install + cd ../.. && rm -rf assimp + +**7. Pinocchio (with patch)** + +.. code-block:: bash + + wget https://raw.githubusercontent.com/frankarobotics/libfranka/main/.ci/pinocchio.patch + git clone --depth 1 --recurse-submodules --shallow-submodules \ + --branch v3.4.0 https://github.com/stack-of-tasks/pinocchio.git + cd pinocchio && git apply ../pinocchio.patch + mkdir build && cd build + cmake .. -DBoost_USE_STATIC_LIBS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DBUILD_SHARED_LIBS=OFF -DBUILD_PYTHON_INTERFACE=OFF \ + -DBUILD_DOCUMENTATION=OFF -DBUILD_TESTING=OFF \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local + make -j$(nproc) && sudo make install + cd ../.. && rm -rf pinocchio + +Build libfranka +^^^^^^^^^^^^^^^ + +.. code-block:: bash + + git clone --recurse-submodules https://github.com/frankarobotics/libfranka.git + cd libfranka + git checkout 0.19.0 + git submodule update --init --recursive + + mkdir build && cd build + cmake -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=OFF \ + -DCMAKE_INSTALL_PREFIX=/usr/local .. + cmake --build . -- -j$(nproc) + +Create Debian Package (Optional) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Inside the build folder, run: + +.. code-block:: bash + + cpack -G DEB + sudo dpkg -i libfranka*.deb .. _verifying-installation: -Verify the installation on your local system -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To verify its installation, you can run: +5. Verifying Installation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After installation (any method), verify that libfranka is properly installed: + +**Check library file:** .. code-block:: bash - ls /usr/lib/libfranka.so + ls -l /usr/lib/libfranka.so Expected output: .. code-block:: text - /usr/lib/libfranka.so + /usr/lib/libfranka.so -> libfranka.so.0.19.0 -Check the installed headers: +**Check header files:** .. code-block:: bash @@ -222,14 +448,11 @@ Expected output: .. code-block:: text - active_control_base.h active_torque_control.h control_tools.h errors.h - gripper_state.h lowpass_filter.h robot.h robot_state.h - active_control.h async_control control_types.h exception.h - joint_velocity_limits.h model.h robot_model_base.h vacuum_gripper.h - active_motion_generator.h commands duration.h gripper.h - logging rate_limiting.h robot_model.h vacuum_gripper_state.h + active_control_base.h active_torque_control.h control_tools.h + gripper_state.h lowpass_filter.h robot.h + ... -You can check the version of the installed library: +**Check installed package version:** .. code-block:: bash @@ -239,131 +462,343 @@ Expected output: .. code-block:: text - ii libfranka 0.18.2-9-g722bf63 amd64 libfranka built using CMake + ii libfranka 0.19.0 amd64 libfranka - Franka Robotics C++ library +.. _usage: -.. _building-from-source: +6. Usage +~~~~~~~~ -5. Building and Installation from Source -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +After installation, configure your system for real-time control and run example programs: -Before building and installing from source, please uninstall existing installations of libfranka to avoid conflicts: +System Setup +^^^^^^^^^^^^ + +1. **Network configuration**: Follow `Minimum System and Network Requirements `_ +2. **Real-time kernel**: See `Setting up the Real-Time Kernel `_ +3. **Getting started**: Read the `Getting Started Manual `_ + +Running Examples +^^^^^^^^^^^^^^^^ + +If you built from source, example programs are in the ``build/examples/`` directory: .. code-block:: bash - sudo apt-get remove "*libfranka*" + cd build/examples + ./communication_test -Clone the Repository -^^^^^^^^^^^^^^^^^^^^ +Replace ```` with your robot's IP address (e.g., ``192.168.1.1``). -You can clone the repository and choose the version you need by selecting a specific tag: +For more examples, see the `Usage Examples documentation `_. + +.. _pylibfranka: + +7. Pylibfranka (Python Bindings) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Pylibfranka** provides Python bindings for libfranka, allowing robot control with Python. + +Installation +^^^^^^^^^^^^ + +Pylibfranka is included in the libfranka Debian packages. For manual installation: .. code-block:: bash - git clone --recurse-submodules https://github.com/frankarobotics/libfranka.git - cd libfranka + # Build with Python bindings enabled + cd libfranka/build + cmake -DGENERATE_PYLIBFRANKA=ON .. + cmake --build . -- -j$(nproc) + sudo cmake --install . + +Documentation +^^^^^^^^^^^^^ + +- `API Documentation `_ -List available tags +.. _development-information: + +8. Development Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Contributing to libfranka +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you're contributing to libfranka development: + +**Install pre-commit hooks:** .. code-block:: bash - git tag -l + pip install pre-commit + pre-commit install + +This automatically runs code formatting and linting checks before each commit. -Checkout a specific tag (e.g., 0.15.0) +**Run checks manually:** .. code-block:: bash - git checkout 0.15.0 + pre-commit run --all-files -Update submodules +Build Options +^^^^^^^^^^^^^ + +Customize your build with CMake options: + +.. list-table:: + :header-rows: 1 + :widths: 35 50 15 + + * - Option + - Description + - Default + * - ``CMAKE_BUILD_TYPE`` + - Build type (Release/Debug) + - Release + * - ``BUILD_TESTS`` + - Build unit tests + - ON + * - ``BUILD_EXAMPLES`` + - Build example programs + - ON + * - ``GENERATE_PYLIBFRANKA`` + - Build Python bindings + - OFF + * - ``CMAKE_INSTALL_PREFIX`` + - Installation directory + - /usr/local + +Example: .. code-block:: bash - git submodule update + cmake -DCMAKE_BUILD_TYPE=Debug \ + -DBUILD_TESTS=ON \ + -DGENERATE_PYLIBFRANKA=ON \ + .. + +.. _troubleshooting: + +Troubleshooting +~~~~~~~~~~~~~~~ + +**Network connection issues** + +See the `Troubleshooting Network `_ guide. + +License +------- + +``libfranka`` is licensed under the `Apache 2.0 license `_. + +pylibfranka Installation and Usage Guide +----------------------------------------- + +This document provides comprehensive instructions for installing and using pylibfranka, a Python binding for libfranka that enables control of Franka Robotics robots. + +Table of Contents +~~~~~~~~~~~~~~~~~ + +- `Installation`_ +- `Examples`_ +- `Troubleshooting`_ + +.. _installation: + +Installation +~~~~~~~~~~~~ + +From PyPI (Recommended) +~~~~~~~~~~~~~~~~~~~~~~~ -Create a build directory and navigate to it +The easiest way to install pylibfranka is via pip. Pre-built wheels include all necessary dependencies. .. code-block:: bash - mkdir build - cd build + pip install pylibfranka -Configure the project and build +Platform Compatibility +~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Ubuntu Version + - Supported Python Versions + * - 20.04 (Focal) + - Python 3.9 only + * - 22.04 (Jammy) + - Python 3.9, 3.10, 3.11, 3.12 + * - 24.04 (Noble) + - Python 3.9, 3.10, 3.11, 3.12 + +**Note:** Ubuntu 20.04 users must use Python 3.9 due to glibc compatibility requirements. + +From Source +~~~~~~~~~~~ + +If you need to build from source (e.g., for development or unsupported platforms): + +Prerequisites +~~~~~~~~~~~~~ + +- Python 3.9 or newer +- CMake 3.16 or newer +- C++ compiler with C++17 support +- Eigen3 development headers +- Poco development headers + +**Disclaimer: If you are using the provided devcontainer, you can skip the prerequisites installation as they are already included in the container.** + +Installing Prerequisites on Ubuntu/Debian +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + sudo apt-get update + sudo apt-get install -y build-essential cmake libeigen3-dev libpoco-dev python3-dev + +Build and Install +~~~~~~~~~~~~~~~~~ + +From the root folder, you can install `pylibfranka` using pip: .. code-block:: bash - cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/opt/openrobots/lib/cmake -DBUILD_TESTS=OFF .. - make + pip install . + + +This will install pylibfranka in your current Python environment. -.. _installing-debian-package: +.. _examples: -Installing libfranka as a Debian Package (Optional but recommended) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Examples +^^^^^^^^ -Building a Debian package is optional but recommended for easier installation and management. In the build folder, execute: +pylibfranka comes with three example scripts that demonstrate how to use the library to control a Franka robot. + +Joint Position Example +~~~~~~~~~~~~~~~~~~~~~~ + +This example demonstrates how to use an external control loop with pylibfranka to move the robot joints. + +To run the example: .. code-block:: bash - cpack -G DEB + cd examples + python3 joint_position_example.py --ip + +Where `` is the IP address of your Franka robot. If not specified, it defaults to "localhost". -This command creates a Debian package named libfranka--.deb. You can then install it with: +The active control example: +- Sets collision behavior parameters +- Starts joint position control with CartesianImpedance controller mode +- Moves the robot using an external control loop +- Performs a simple motion of selected joints + +Print Robot State Example +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This example shows how to read and display the complete state of the robot. + +To run the example: .. code-block:: bash - sudo dpkg -i libfranka*.deb + cd examples + python3 print_robot_state.py --ip [--rate ] [--count ] -Installing via a Debian package simplifies the process compared to building from source every time. Additionally the package integrates better with -system tools and package managers, which can help manage updates and dependencies more effectively. +Where: +- `--ip` is the robot's IP address (defaults to "localhost") +- `--rate` is the frequency at which to print the state in Hz (defaults to 0.5) +- `--count` is the number of state readings to print (defaults to 1, use 0 for continuous) -.. _usage: +The print robot state example: -6. Usage -~~~~~~~~ +- Connects to the robot +- Reads the complete robot state +- Prints detailed information about: + + - Joint positions, velocities, and torques + - End effector pose and velocities + - External forces and torques + - Robot mode and error states + - Mass and inertia properties -After installation, check the `Minimum System and Network Requirements `_ for network settings, -the `Setting up the Real-Time Kernel `_ for system setup, -and the `Getting Started Manual `_ for initial steps. Once configured, -you can control the robot using the example applications provided in the examples folder (`Usage Examples `_). +Joint Impedance Control Example +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To run a sample program, navigate to the build folder and execute the following command: +This example demonstrates how to implement a joint impedance controller that renders a spring-damper system to move the robot through a sequence of target joint configurations. + +To run the example: .. code-block:: bash - ./examples/communication_test + cd examples + python3 joint_impedance_example.py --ip -.. _pylibfranka: +Where `--ip` is the robot's IP address (defaults to "localhost"). -7. Pylibfranka -~~~~~~~~~~~~~~ +The joint impedance example: -Pylibfranka is a Python binding for libfranka, allowing you to control Franka robots using Python. It is included in the libfranka repository and -can be built alongside libfranka. For more details, see ``pylibfranka`` and its `README `_. -The `generated API documentation `_ offers an overview of its capabilities. +- Implements a minimum jerk trajectory generator for smooth joint motion +- Uses a spring-damper system for compliant control +- Moves through a sequence of predefined joint configurations: -.. _development-information: + - Home position (slightly bent arm) + - Extended arm pointing forward + - Arm pointing to the right + - Arm pointing to the left + - Return to home position -8. Development Information -~~~~~~~~~~~~~~~~~~~~~~~~~~ +- Includes position holding with dwell time between movements +- Compensates for Coriolis effects + +And more control examples +~~~~~~~~~~~~~~~~~~~~~~~~~ -If you actively contribute to this repository, you should install and set up pre-commit hooks: +There are more control examples to discover. All of them can be executed in a similar way: .. code-block:: bash - pip install pre-commit - pre-commit install + cd examples + python3 other_example.py --ip + +Gripper Control Example +~~~~~~~~~~~~~~~~~~~~~~~ -This will install pre-commit and set up the git hooks to automatically run checks before each commit. -The hooks will help maintain code quality by running various checks like code formatting, linting, and other validations. +This example demonstrates how to control the Franka gripper, including homing, grasping, and reading gripper state. -To manually run the pre-commit checks on all files: +To run the example: .. code-block:: bash - pre-commit run --all-files + cd examples + python3 move_gripper.py --robot_ip [--width ] [--homing <0|1>] [--speed ] [--force ] -This will build the C++ extension and install the Python package. +Where: +- `--robot_ip` is the robot's IP address (required) +- `--width` is the object width to grasp in meters (defaults to 0.005) +- `--homing` enables/disables gripper homing (0 or 1, defaults to 1) +- `--speed` is the gripper speed (defaults to 0.1) +- `--force` is the gripper force in N (defaults to 60) -License -------- +The gripper example: -``libfranka`` is licensed under the `Apache 2.0 license `_. +- Connects to the gripper +- Performs homing to estimate maximum grasping width +- Reads and displays gripper state including: + + - Current width + - Maximum width + - Grasp status + - Temperature + - Timestamp + +- Attempts to grasp an object with specified parameters +- Verifies successful grasping +- Releases the object diff --git a/devcontainer_distro b/devcontainer_distro new file mode 100644 index 00000000..dcdf6284 --- /dev/null +++ b/devcontainer_distro @@ -0,0 +1 @@ +22.04 diff --git a/docs/installation.rst b/docs/installation.rst index 7a243a46..6d0d85f2 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -5,35 +5,52 @@ Build / Installation .. note:: - The installation currently only support Ubuntu 20.04. Please ensure you are using this version to avoid compatibility issues. + **libfranka** supports Ubuntu 20.04 (focal), 22.04 (jammy), and 24.04 (noble) LTS versions. Pre-built **amd64** Debian packages are available for all supported versions. .. _libfranka_installation_debian_package: + Debian Package ~~~~~~~~~~~~~~ -**libfranka** releases are provided as pre-built Debian packages. +**libfranka** releases are provided as pre-built Debian packages for multiple Ubuntu LTS versions. You can find the packaged artifacts on the `libfranka releases `_ page on GitHub. -Download the Debian package: + +Installation Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Step 1: Check your Ubuntu version** .. code-block:: bash - wget https://github.com/frankarobotics/libfranka/releases/download/0.18.2/libfranka_0.18.2_focal_amd64.deb + lsb_release -a + +**Step 2: Download and install the appropriate package** + -Install the package on your system: +For the supported ubuntu versions, use the following pattern: .. code-block:: bash - sudo dpkg -i libfranka_0.18.2_focal_amd64.deb + # Replace with your desired version and Ubuntu codename + VERSION=0.19.0 + CODENAME=focal # or jammy, noble -For other versions or architectures, use the following pattern: + wget https://github.com/frankarobotics/libfranka/releases/download/${VERSION}/libfranka_${VERSION}_${CODENAME}_amd64.deb + sudo dpkg -i libfranka_${VERSION}_${CODENAME}_amd64.deb -.. code-block:: bash +.. tip:: + + This is the recommended installation method for **libfranka** if you do not need to modify the source code. + +Verify Installation +^^^^^^^^^^^^^^^^^^^ - wget https://github.com/frankarobotics/libfranka/releases/download//libfranka___.deb - sudo dpkg -i libfranka___.deb +After installation, verify that libfranka is correctly installed: + +.. code-block:: bash -This is the recommended installation method for **libfranka** if you do not need to modify the source code. + dpkg -l | grep libfranka .. _libfranka_installation_docker: Inside docker container diff --git a/docs/real_time_kernel.rst b/docs/real_time_kernel.rst index 4825b620..b455e10e 100644 --- a/docs/real_time_kernel.rst +++ b/docs/real_time_kernel.rst @@ -11,8 +11,14 @@ Different kernel versions are compatible with specific Ubuntu releases. The tabl +----------------+-------------------------+ | Kernel Version | Ubuntu Version | +================+=========================+ +| Pro Kernel | 24.04 (Noble Numbat) | ++----------------+-------------------------+ +| 6.8.0 | 24.04 (Noble Numbat) | ++----------------+-------------------------+ | Pro Kernel | 22.04 (Jammy Jellyfish) | +----------------+-------------------------+ +| 6.8.0 | 22.04 (Jammy Jellyfish) | ++----------------+-------------------------+ | 5.9.1 | 20.04 (Focal Fossa) | +----------------+-------------------------+ | 5.4.19 | 18.04 (Bionic Beaver) | @@ -37,25 +43,6 @@ Then decide which kernel version to use. Check your current version with ``uname Real-time patches are only available for select versions: `RT Kernel Patches `_. Choose the version closest to your current kernel. Use ``curl`` to download the sources. -.. note:: - Ubuntu 16.04 (tested with kernel 4.14.12): - - .. code-block:: bash - - curl -LO https://www.kernel.org/pub/linux/kernel/v4.x/linux-4.14.12.tar.xz - curl -LO https://www.kernel.org/pub/linux/kernel/v4.x/linux-4.14.12.tar.sign - curl -LO https://www.kernel.org/pub/linux/kernel/projects/rt/4.14/older/patch-4.14.12-rt10.patch.xz - curl -LO https://www.kernel.org/pub/linux/kernel/projects/rt/4.14/older/patch-4.14.12-rt10.patch.sign - -.. note:: - Ubuntu 18.04 (tested with kernel 5.4.19): - - .. code-block:: bash - - curl -LO https://www.kernel.org/pub/linux/kernel/v5.x/linux-5.4.19.tar.xz - curl -LO https://www.kernel.org/pub/linux/kernel/v5.x/linux-5.4.19.tar.sign - curl -LO https://www.kernel.org/pub/linux/kernel/projects/rt/5.4/older/patch-5.4.19-rt10.patch.xz - curl -LO https://www.kernel.org/pub/linux/kernel/projects/rt/5.4/older/patch-5.4.19-rt10.patch.sign .. note:: Ubuntu 20.04 (tested with kernel 5.9.1): @@ -68,7 +55,7 @@ Choose the version closest to your current kernel. Use ``curl`` to download the curl -LO https://www.kernel.org/pub/linux/kernel/projects/rt/5.9/patch-5.9.1-rt20.patch.sign .. note:: - Ubuntu 22.04: We recommend using the `Ubuntu Pro real-time kernel `_. + Ubuntu 22.04 and 24.04: We recommend using the `Ubuntu Pro real-time kernel `_. After enabling it, you can skip directly to :ref:`installation-real-time`. If you prefer not to use Ubuntu Pro, you can patch manually (tested with kernel 6.8.0): diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 5cf55e3f..270a6b00 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.4) +cmake_minimum_required(VERSION 3.5) project(libfranka-examples CXX) diff --git a/examples/motion_with_control.cpp b/examples/motion_with_control.cpp index bc23ce22..2aaf78a1 100644 --- a/examples/motion_with_control.cpp +++ b/examples/motion_with_control.cpp @@ -37,7 +37,7 @@ class Controller { dq_buffer_ = std::vector(dq_filter_size_ * 7, 0.0); } - inline franka::Torques step(const franka::RobotState& state) { + franka::Torques step(const franka::RobotState& state) { updateDQFilter(state); std::array tau_J_d; // NOLINT(readability-identifier-naming) @@ -66,8 +66,8 @@ class Controller { size_t dq_current_filter_position_{0}; size_t dq_filter_size_; - const std::array K_P_; // NOLINT(readability-identifier-naming) - const std::array K_D_; // NOLINT(readability-identifier-naming) + std::array K_P_; // NOLINT(readability-identifier-naming) + std::array K_D_; // NOLINT(readability-identifier-naming) std::array dq_d_; std::vector dq_buffer_; diff --git a/examples/motion_with_control_external_control_loop.cpp b/examples/motion_with_control_external_control_loop.cpp index b746b7e3..849ecada 100644 --- a/examples/motion_with_control_external_control_loop.cpp +++ b/examples/motion_with_control_external_control_loop.cpp @@ -40,7 +40,7 @@ class Controller { dq_buffer_ = std::vector(dq_filter_size_ * 7, 0.0); } - inline franka::Torques step(const franka::RobotState& state) { + franka::Torques step(const franka::RobotState& state) { updateDQFilter(state); std::array tau_J_d; // NOLINT(readability-identifier-naming) @@ -69,8 +69,8 @@ class Controller { size_t dq_current_filter_position_{0}; size_t dq_filter_size_; - const std::array K_P_; // NOLINT(readability-identifier-naming) - const std::array K_D_; // NOLINT(readability-identifier-naming) + std::array K_P_; // NOLINT(readability-identifier-naming) + std::array K_D_; // NOLINT(readability-identifier-naming) std::array dq_d_; std::vector dq_buffer_; diff --git a/include/franka/active_motion_generator.h b/include/franka/active_motion_generator.h index 6f91d9a1..c5014920 100644 --- a/include/franka/active_motion_generator.h +++ b/include/franka/active_motion_generator.h @@ -21,6 +21,8 @@ namespace franka { template class ActiveMotionGenerator : public ActiveControl { public: + ~ActiveMotionGenerator() override = default; + /** * Updates the motion generator commands of an active control * @@ -57,7 +59,7 @@ class ActiveMotionGenerator : public ActiveControl { std::unique_lock control_lock, research_interface::robot::Move::ControllerMode controller_type) : ActiveControl(robot_impl, motion_id, std::move(control_lock)), - controller_type_(controller_type){}; + controller_type_(controller_type) {}; bool isTorqueControlFinished(const std::optional& control_input); diff --git a/include/franka/active_torque_control.h b/include/franka/active_torque_control.h index 75b7a617..0bcb75ac 100644 --- a/include/franka/active_torque_control.h +++ b/include/franka/active_torque_control.h @@ -20,6 +20,8 @@ namespace franka { */ class ActiveTorqueControl : public ActiveControl { public: + ~ActiveTorqueControl() override = default; + /** * Updates the joint-level based torque commands of an active joint effort control * @@ -50,7 +52,7 @@ class ActiveTorqueControl : public ActiveControl { ActiveTorqueControl(std::shared_ptr robot_impl, uint32_t motion_id, std::unique_lock control_lock) - : ActiveControl(std::move(robot_impl), motion_id, std::move(control_lock)){}; + : ActiveControl(std::move(robot_impl), motion_id, std::move(control_lock)) {}; }; } // namespace franka diff --git a/include/franka/async_control/async_position_control_handler.hpp b/include/franka/async_control/async_position_control_handler.hpp index dc903c9a..1528cd4f 100644 --- a/include/franka/async_control/async_position_control_handler.hpp +++ b/include/franka/async_control/async_position_control_handler.hpp @@ -81,9 +81,10 @@ class AsyncPositionControlHandler { /** * Retrieves feedback about the current control state. * + * @param robot_state Optional robot state to use for feedback calculation. * @return TargetFeedback containing the current status and joint positions. */ - auto getTargetFeedback() -> TargetFeedback; + auto getTargetFeedback(const std::optional& robot_state = {}) -> TargetFeedback; /** * Stops the asynchronous position control. @@ -103,6 +104,7 @@ class AsyncPositionControlHandler { std::shared_ptr robot_; std::unique_ptr active_robot_control_; TargetStatus control_status_{TargetStatus::kIdle}; + franka::RobotState current_robot_state_{}; std::array target_position_{}; diff --git a/include/franka/control_types.h b/include/franka/control_types.h index d5019f7a..965b4959 100644 --- a/include/franka/control_types.h +++ b/include/franka/control_types.h @@ -16,14 +16,17 @@ namespace franka { /** * Available controller modes for a franka::Robot. */ -enum class ControllerMode { kJointImpedance, kCartesianImpedance }; +enum class ControllerMode { // NOLINT(performance-enum-size) + kJointImpedance, + kCartesianImpedance +}; /** * Used to decide whether to enforce realtime mode for a control loop thread. * * @see Robot::Robot */ -enum class RealtimeConfig { kEnforce, kIgnore }; +enum class RealtimeConfig { kEnforce, kIgnore }; // NOLINT(performance-enum-size) /** * Helper type for control and motion generation loops. diff --git a/include/franka/errors.h b/include/franka/errors.h index a3b6f768..7957af99 100644 --- a/include/franka/errors.h +++ b/include/franka/errors.h @@ -67,182 +67,218 @@ struct Errors { /** * True if the robot moved past the joint limits. */ - const bool& joint_position_limits_violation; + const bool& + joint_position_limits_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the robot moved past any of the virtual walls. */ - const bool& cartesian_position_limits_violation; + const bool& + cartesian_position_limits_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the robot would have collided with itself. */ - const bool& self_collision_avoidance_violation; + const bool& + self_collision_avoidance_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the robot exceeded joint velocity limits. */ - const bool& joint_velocity_violation; + const bool& + joint_velocity_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the robot exceeded Cartesian velocity limits. */ - const bool& cartesian_velocity_violation; + const bool& + cartesian_velocity_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the robot exceeded safety threshold during force control. */ - const bool& force_control_safety_violation; + const bool& + force_control_safety_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if a collision was detected, i.e.\ the robot exceeded a torque threshold in a joint * motion. */ - const bool& joint_reflex; + const bool& joint_reflex; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if a collision was detected, i.e.\ the robot exceeded a torque threshold in a Cartesian * motion. */ - const bool& cartesian_reflex; + const bool& cartesian_reflex; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if internal motion generator did not reach the goal pose. */ - const bool& max_goal_pose_deviation_violation; + const bool& + max_goal_pose_deviation_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if internal motion generator deviated from the path. */ - const bool& max_path_pose_deviation_violation; + const bool& + max_path_pose_deviation_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if Cartesian velocity profile for internal motions was exceeded. */ - const bool& cartesian_velocity_profile_safety_violation; + const bool& + cartesian_velocity_profile_safety_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if an external joint position motion generator was started with a pose too far from the * current pose. */ - const bool& joint_position_motion_generator_start_pose_invalid; + const bool& + joint_position_motion_generator_start_pose_invalid; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if an external joint motion generator would move into a joint limit. */ - const bool& joint_motion_generator_position_limits_violation; + const bool& + joint_motion_generator_position_limits_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if an external joint motion generator exceeded velocity limits. */ - const bool& joint_motion_generator_velocity_limits_violation; + const bool& + joint_motion_generator_velocity_limits_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if commanded velocity in joint motion generators is discontinuous (target values are too * far apart). */ - const bool& joint_motion_generator_velocity_discontinuity; + const bool& + joint_motion_generator_velocity_discontinuity; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if commanded acceleration in joint motion generators is discontinuous (target values are * too far apart). */ - const bool& joint_motion_generator_acceleration_discontinuity; + const bool& + joint_motion_generator_acceleration_discontinuity; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if an external Cartesian position motion generator was started with a pose too far from * the current pose. */ - const bool& cartesian_position_motion_generator_start_pose_invalid; + const bool& + cartesian_position_motion_generator_start_pose_invalid; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if an external Cartesian motion generator would move into an elbow limit. */ - const bool& cartesian_motion_generator_elbow_limit_violation; + const bool& + cartesian_motion_generator_elbow_limit_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if an external Cartesian motion generator would move with too high velocity. */ - const bool& cartesian_motion_generator_velocity_limits_violation; + const bool& + cartesian_motion_generator_velocity_limits_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if commanded velocity in Cartesian motion generators is discontinuous (target values are * too far apart). */ - const bool& cartesian_motion_generator_velocity_discontinuity; + const bool& + cartesian_motion_generator_velocity_discontinuity; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if commanded acceleration in Cartesian motion generators is discontinuous (target values * are too far apart). */ - const bool& cartesian_motion_generator_acceleration_discontinuity; + const bool& + cartesian_motion_generator_acceleration_discontinuity; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if commanded elbow values in Cartesian motion generators are inconsistent. */ - const bool& cartesian_motion_generator_elbow_sign_inconsistent; + const bool& + cartesian_motion_generator_elbow_sign_inconsistent; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the first elbow value in Cartesian motion generators is too far from initial one. */ - const bool& cartesian_motion_generator_start_elbow_invalid; + const bool& + cartesian_motion_generator_start_elbow_invalid; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the joint position limits would be exceeded after IK calculation. */ - const bool& cartesian_motion_generator_joint_position_limits_violation; + const bool& + cartesian_motion_generator_joint_position_limits_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the joint velocity limits would be exceeded after IK calculation. */ - const bool& cartesian_motion_generator_joint_velocity_limits_violation; + const bool& + cartesian_motion_generator_joint_velocity_limits_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the joint velocity in Cartesian motion generators is discontinuous after IK * calculation. */ - const bool& cartesian_motion_generator_joint_velocity_discontinuity; + const bool& + cartesian_motion_generator_joint_velocity_discontinuity; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the joint acceleration in Cartesian motion generators is discontinuous after IK * calculation. */ - const bool& cartesian_motion_generator_joint_acceleration_discontinuity; + const bool& + cartesian_motion_generator_joint_acceleration_discontinuity; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the Cartesian pose is not a valid transformation matrix. */ - const bool& cartesian_position_motion_generator_invalid_frame; + const bool& + cartesian_position_motion_generator_invalid_frame; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if desired force exceeds the safety thresholds. */ - const bool& force_controller_desired_force_tolerance_violation; + const bool& + force_controller_desired_force_tolerance_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the torque set by the external controller is discontinuous. */ - const bool& controller_torque_discontinuity; + const bool& + controller_torque_discontinuity; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the start elbow sign was inconsistent. * * Applies only to motions started from Desk. */ - const bool& start_elbow_sign_inconsistent; + const bool& + start_elbow_sign_inconsistent; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if minimum network communication quality could not be held during a motion. */ - const bool& communication_constraints_violation; + const bool& + communication_constraints_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if commanded values would result in exceeding the power limit. */ - const bool& power_limit_violation; + const bool& power_limit_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the robot is overloaded for the required motion. * * Applies only to motions started from Desk. */ - const bool& joint_p2p_insufficient_torque_for_planning; + const bool& + joint_p2p_insufficient_torque_for_planning; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the measured torque signal is out of the safe range. */ - const bool& tau_j_range_violation; + const bool& tau_j_range_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if an instability is detected. */ - const bool& instability_detected; + const bool& instability_detected; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the robot is in joint position limits violation error and the user guides the robot * further towards the limit. */ - const bool& joint_move_in_wrong_direction; + const bool& + joint_move_in_wrong_direction; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the generated motion violates a joint limit. */ - const bool& cartesian_spline_motion_generator_violation; + const bool& + cartesian_spline_motion_generator_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the generated motion violates a joint limit. */ - const bool& joint_via_motion_generator_planning_joint_limit_violation; + const bool& + joint_via_motion_generator_planning_joint_limit_violation; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the gravity vector could not be initialized by measureing the base acceleration. */ - const bool& base_acceleration_initialization_timeout; + const bool& + base_acceleration_initialization_timeout; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * True if the base acceleration O_ddP_O cannot be determined. */ - const bool& base_acceleration_invalid_reading; + const bool& + base_acceleration_invalid_reading; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) }; /** diff --git a/include/franka/exception.h b/include/franka/exception.h index 92425347..2a8cf392 100644 --- a/include/franka/exception.h +++ b/include/franka/exception.h @@ -57,11 +57,11 @@ struct IncompatibleVersionException : public Exception { /** * Control's protocol version. */ - const uint16_t server_version; + const uint16_t server_version; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) /** * libfranka protocol version. */ - const uint16_t library_version; + const uint16_t library_version; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) }; /** @@ -82,7 +82,8 @@ struct ControlException : public Exception { /** * Vector of states and commands logged just before the exception occurred. */ - const std::vector log; + const std::vector + log; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) }; /** diff --git a/include/franka/model.h b/include/franka/model.h index b6ba1e57..66209a7f 100644 --- a/include/franka/model.h +++ b/include/franka/model.h @@ -19,7 +19,7 @@ namespace franka { /** * Enumerates the seven joints, the flange, and the end effector of a robot. */ -enum class Frame { +enum class Frame { // NOLINT(performance-enum-size) kJoint1, kJoint2, kJoint3, diff --git a/include/franka/rate_limiting.h b/include/franka/rate_limiting.h index 4185462a..189b7a7e 100644 --- a/include/franka/rate_limiting.h +++ b/include/franka/rate_limiting.h @@ -37,6 +37,7 @@ constexpr double kTolNumberPacketsLost = 0.0; * Factor for the definition of rotational limits using the Cartesian Pose interface */ constexpr double kFactorCartesianRotationPoseInterface = 0.99; + /** * Maximum torque rate */ diff --git a/include/franka/robot_model.h b/include/franka/robot_model.h index 513510d8..1129deac 100644 --- a/include/franka/robot_model.h +++ b/include/franka/robot_model.h @@ -3,18 +3,11 @@ #pragma once #include -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include +#include +#include + #include "franka/robot_model_base.h" /** diff --git a/include/franka/robot_state.h b/include/franka/robot_state.h index 2bc9a6e7..aae3f02f 100644 --- a/include/franka/robot_state.h +++ b/include/franka/robot_state.h @@ -18,7 +18,7 @@ namespace franka { /** * Describes the robot's current mode. */ -enum class RobotMode { +enum class RobotMode { // NOLINT(performance-enum-size) kOther, kIdle, kMove, diff --git a/include/franka/vacuum_gripper.h b/include/franka/vacuum_gripper.h index 1064b95c..0303c075 100644 --- a/include/franka/vacuum_gripper.h +++ b/include/franka/vacuum_gripper.h @@ -35,7 +35,7 @@ class VacuumGripper { /** * Vacuum production setup profile. */ - enum class ProductionSetupProfile { kP0, kP1, kP2, kP3 }; + enum class ProductionSetupProfile { kP0, kP1, kP2, kP3 }; // NOLINT(performance-enum-size) /** * Establishes a connection with a vacuum gripper connected to a robot. diff --git a/package.xml b/package.xml index 3d96b007..b40c5f25 100644 --- a/package.xml +++ b/package.xml @@ -4,7 +4,7 @@ schematypens="http://www.w3.org/2001/XMLSchema"?> libfranka - 0.18.2 + 0.20.3 libfranka is a C++ library for Franka Robotics research robots Franka Robotics GmbH Apache 2.0 @@ -20,10 +20,12 @@ fmt libpoco-dev pinocchio + tinyxml2 fmt libpoco-dev pinocchio + tinyxml2 doxygen graphviz diff --git a/pylibfranka/.github/workflows/wheels.yml b/pylibfranka/.github/workflows/wheels.yml deleted file mode 100644 index 91244e70..00000000 --- a/pylibfranka/.github/workflows/wheels.yml +++ /dev/null @@ -1,119 +0,0 @@ -name: Build Wheels - -on: - push: - branches: [ '*' ] - tags: [ 'v*' ] # Trigger on version tags - pull_request: - branches: [ '*' ] - -jobs: - build_and_publish: - name: Build and Publish - runs-on: ubuntu-latest - permissions: - contents: write # Needed to create releases - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - submodules: 'recursive' - - - name: Get Package Version - id: get_package_version - run: ./scripts/get_version.sh - shell: bash - - - name: Build Docker Image - uses: docker/build-push-action@v4 - with: - context: . - file: ./Dockerfile - tags: pylibfranka:latest - push: false - load: true - - - name: Setup Dependencies - uses: addnab/docker-run-action@v3 - with: - image: pylibfranka:latest - options: -v ${{ github.workspace }}:/workspace - run: | - cd /workspace - ./scripts/setup_dependencies.sh - - - name: Build Package - uses: addnab/docker-run-action@v3 - with: - image: pylibfranka:latest - options: -v ${{ github.workspace }}:/workspace - run: | - cd /workspace - ./scripts/build_package.sh - - - name: Repair Wheels with Auditwheel - uses: addnab/docker-run-action@v3 - with: - image: pylibfranka:latest - options: -v ${{ github.workspace }}:/workspace - run: | - cd /workspace - ./scripts/repair_wheels.sh - - - name: Test Installation - uses: addnab/docker-run-action@v3 - with: - image: python:3.10-slim - options: -v ${{ github.workspace }}:/workspace - run: | - cd /workspace - /workspace/scripts/test_installation.sh "-manylinux_2_34_x86_64.whl" - - - name: Lint Code - uses: addnab/docker-run-action@v3 - with: - image: pylibfranka:latest - options: -v ${{ github.workspace }}:/workspace - run: | - cd /workspace - ./scripts/lint_code.sh - - - name: Upload wheels as artifacts - uses: actions/upload-artifact@v4 - with: - name: wheels - path: ./wheelhouse/*.whl - - - name: Get release info - if: startsWith(github.ref, 'refs/tags/') - id: get_release_info - run: | - # Extract tag name from GITHUB_REF (e.g., refs/tags/v1.0.0 -> v1.0.0) - TAG=${GITHUB_REF#refs/tags/} - echo "tag=$TAG" >> $GITHUB_OUTPUT - # Extract version without 'v' prefix (e.g., v1.0.0 -> 1.0.0) - VERSION=${TAG#v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - shell: bash - - - name: Create GitHub Release - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.get_release_info.outputs.tag }} - name: Release ${{ steps.get_release_info.outputs.tag }} - draft: false - prerelease: false - files: ./wheelhouse/*.whl - body: | - # pylibfranka ${{ steps.get_release_info.outputs.version }} - Python bindings for libfranka, providing easy-to-use Python interfaces for controlling Franka robots. - - ## Installation - ```bash - pip install pylibfranka==${{ steps.get_release_info.outputs.version }} - ``` - - ## Documentation - See the [README](https://github.com/frankaemika/pylibfranka#readme) for more information. diff --git a/pylibfranka/CMakeLists.txt b/pylibfranka/CMakeLists.txt index 784270f7..efcb92f7 100644 --- a/pylibfranka/CMakeLists.txt +++ b/pylibfranka/CMakeLists.txt @@ -20,6 +20,9 @@ find_package(pinocchio REQUIRED) # Create Python module pybind11_add_module(_pylibfranka src/pylibfranka.cpp + src/async_control.cpp + src/gripper.cpp + # add more source files as needed for next splits ) # Include directories diff --git a/pylibfranka/Jenkinsfile b/pylibfranka/Jenkinsfile deleted file mode 100644 index 12248d62..00000000 --- a/pylibfranka/Jenkinsfile +++ /dev/null @@ -1,150 +0,0 @@ -pipeline { - libraries { - lib('fe-pipeline-steps@1.5.0') - } - agent none - - triggers { - pollSCM('H/5 * * * *') - } - options { - buildDiscarder(logRotator(numToKeepStr: '30')) - parallelsAlwaysFailFast() - timeout(time: 1, unit: 'HOURS') - } - - stages { - stage('Prepare') { - agent { - dockerfile { - filename './Dockerfile' - label 'docker1' - reuseNode true - } - } - steps { - script { - notifyBitbucket() - env.VERSION = feDetermineVersionFromGit() - } - feSetupPip() - sh './scripts/setup_dependencies.sh' - } - } - - stage('Get Package Version') { - agent { - dockerfile { - filename './Dockerfile' - label 'docker1' - reuseNode true - } - } - steps { - sh './scripts/get_version.sh' - script { - def props = readProperties file: 'version_info.properties' - env.PACKAGE_VERSION = props.PACKAGE_VERSION - } - } - } - - stage('Build') { - agent { - dockerfile { - filename './Dockerfile' - label 'docker1' - reuseNode true - } - } - steps { - sh './scripts/build_package.sh' - stash includes: 'dist/*.whl', name: 'built-wheels' - } - } - - stage('Vendoring') { - agent { - dockerfile { - filename './Dockerfile' - label 'docker1' - reuseNode true - } - } - steps { - unstash 'built-wheels' - sh './scripts/repair_wheels.sh' - stash includes: 'wheelhouse/*.whl', name: 'wheels' - } - } - - stage('Test') { - agent { - docker { - image 'python:3.10-slim' - label 'docker1' - reuseNode true - args '-v $WORKSPACE:/workspace -e HOME=/workspace/.test_home --user root' - } - } - steps { - // Unstash the wheels from the build stage - unstash 'wheels' - catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') { - sh ''' - cd /workspace - # Ensure the HOME directory exists and is writable - mkdir -p $HOME - mkdir -p $HOME/.cache - - # Run the test script with manylinux wheel pattern - /workspace/scripts/test_installation.sh "-manylinux_2_34_x86_64.whl" - ''' - } - } - } - - stage('Analyze') { - parallel { - stage('Lint') { - agent { - dockerfile { - filename './Dockerfile' - label 'docker1' - reuseNode true - } - } - steps { - catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') { - sh './scripts/lint_code.sh' - } - } - } - } - } - - stage('Publish') { - agent { - dockerfile { - filename './Dockerfile' - label 'docker1' - reuseNode true - } - } - steps { - fePublishPip("wheelhouse/*.whl", "tools") - } - post { - success { - fePublishBuildInfo() - } - always { - cleanWs() - script { - notifyBitbucket() - } - } - } - } - } -} diff --git a/pylibfranka/README.md b/pylibfranka/README.md index 8aea8329..44bedfa8 100644 --- a/pylibfranka/README.md +++ b/pylibfranka/README.md @@ -3,9 +3,9 @@ This document provides comprehensive instructions for installing and using pylibfranka, a Python binding for libfranka that enables control of Franka Robotics robots. ## Table of Contents -- [Prerequisites](#prerequisites) - [Installation](#installation) - - [Installation from Source](#installation-from-source) + - [From PyPI (Recommended)](#from-pypi-recommended) + - [From Source](#from-source) - [Examples](#examples) - [Joint Position Example](#joint-position-example) - [Print Robot State Example](#print-robot-state-example) @@ -13,11 +13,33 @@ This document provides comprehensive instructions for installing and using pylib - [Gripper Control Example](#gripper-control-example) - [Troubleshooting](#troubleshooting) -## Prerequisites +## Installation -Before installing pylibfranka, ensure you have the following prerequisites: +### From PyPI (Recommended) -- Python 3.8 or newer +The easiest way to install pylibfranka is via pip. Pre-built wheels include all necessary dependencies. + +```bash +pip install pylibfranka +``` + +#### Platform Compatibility + +| Ubuntu Version | Supported Python Versions | +|----------------|---------------------------| +| 20.04 (Focal) | Python 3.9 only | +| 22.04 (Jammy) | Python 3.9, 3.10, 3.11, 3.12 | +| 24.04 (Noble) | Python 3.9, 3.10, 3.11, 3.12 | + +**Note:** Ubuntu 20.04 users must use Python 3.9 due to glibc compatibility requirements. + +### From Source + +If you need to build from source (e.g., for development or unsupported platforms): + +#### Prerequisites + +- Python 3.9 or newer - CMake 3.16 or newer - C++ compiler with C++17 support - Eigen3 development headers @@ -25,27 +47,21 @@ Before installing pylibfranka, ensure you have the following prerequisites: **Disclaimer: If you are using the provided devcontainer, you can skip the prerequisites installation as they are already included in the container.** -### Installing Prerequisites on Ubuntu/Debian +#### Installing Prerequisites on Ubuntu/Debian ```bash sudo apt-get update sudo apt-get install -y build-essential cmake libeigen3-dev libpoco-dev python3-dev ``` -#### Installing pylibfranka via PIP +#### Build and Install -From the root folder, you can install `pylibfranka` (therefore, NOT in the build folder) using pip: +From the root folder, you can install `pylibfranka` using pip: ```bash pip install . ``` -or - -```bash -pip3 install . -``` - This will install pylibfranka in your current Python environment. ## Examples @@ -161,3 +177,14 @@ The gripper example: - Attempts to grasp an object with specified parameters - Verifies successful grasping - Releases the object + +### Async Joint Position Control Example + +You can also use the async API to control the robot in a low-rate fashion, e.g. 50Hz. +This allows you to specify joint position setpoints without the need for a blocking loop. + +```bash +cd examples +python3 async_position_control.py --ip +``` + diff --git a/pylibfranka/__init__.py b/pylibfranka/__init__.py index 4a31b474..ee4114d2 100644 --- a/pylibfranka/__init__.py +++ b/pylibfranka/__init__.py @@ -28,6 +28,7 @@ RobotMode, RobotState, Torques, + AsyncPositionControlHandler ) from ._version import __version__ @@ -55,4 +56,5 @@ "RobotMode", "RobotState", "Torques", + "AsyncPositionControlHandler" ] diff --git a/pylibfranka/examples/async_position_control.py b/pylibfranka/examples/async_position_control.py new file mode 100644 index 00000000..89acca60 --- /dev/null +++ b/pylibfranka/examples/async_position_control.py @@ -0,0 +1,124 @@ +# Copyright (c) 2026 Franka Robotics GmbH +# Apache-2.0 + +# This example demonstrates asynchronous position control of a Franka robot using the +# pylibfranka library. It connects to the robot, sets up an asynchronous position control +# handler, and continuously updates joint position targets in a loop until interrupted, leveraging +# the latest low-rate control API. + +import signal +import sys +import time +import math +import argparse +import threading +from datetime import timedelta + +import pylibfranka as franka +from example_common import setDefaultBehaviour + +kDefaultMaximumVelocities = [0.655, 0.655, 0.655, 0.655, 1.315, 1.315, 1.315] +kDefaultGoalTolerance = 10.0 + +motion_finished = False + + +def signal_handler(sig, frame): + global motion_finished + if sig == signal.SIGINT: + motion_finished = True + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--ip", type=str, default="localhost", help="Robot IP address") + args = parser.parse_args() + + signal.signal(signal.SIGINT, signal_handler) + + try: + robot = franka.Robot(args.ip, franka.RealtimeConfig.kIgnore) + except Exception as e: + print(f"Could not connect to robot: {e}") + sys.exit(-1) + + setDefaultBehaviour(robot) + + initial_position = [0, + -math.pi / 4, + 0, + -3 * math.pi / 4, + 0, + math.pi / 2, + math.pi / 4] + + time_elapsed = 0.0 + direction = 1.0 + time_since_last_log = 0.0 + + def calculate_joint_position_target(period_sec): + nonlocal time_elapsed, direction, time_since_last_log + + time_elapsed += period_sec + + target_positions = [ + initial_position[i] + direction * 0.25 + for i in range(7) + ] + + time_since_last_log += period_sec + if time_since_last_log >= 1.0: + direction *= -1.0 + time_since_last_log = 0.0 + + return franka.AsyncPositionControlHandler.JointPositionTarget( + joint_positions=target_positions + ) + + joint_position_control_configuration = \ + franka.AsyncPositionControlHandler.Configuration( + maximum_joint_velocities=kDefaultMaximumVelocities, + goal_tolerance=kDefaultGoalTolerance + ) + + result = franka.AsyncPositionControlHandler.configure(robot, + joint_position_control_configuration) + + if result.error_message is not None: + print(result.error_message) + sys.exit(-1) + + position_control_handler = result.handler + target_feedback = position_control_handler.get_target_feedback() + + time_step = 0.020 # 20 ms, 50 Hz + + global motion_finished + while not motion_finished: + loop_start = time.monotonic() + + target_feedback = position_control_handler.get_target_feedback() + if target_feedback.error_message is not None: + print(target_feedback.error_message) + sys.exit(-1) + + next_target = calculate_joint_position_target(time_step) + command_result = position_control_handler.set_joint_position_target(next_target) + + if command_result.error_message is not None: + print(command_result.error_message) + sys.exit(-1) + + if time_elapsed > 10.0: + position_control_handler.stop_control() + motion_finished = True + print("Control finished") + break + + sleep_time = time_step - (time.monotonic() - loop_start) + if sleep_time > 0: + time.sleep(sleep_time) + + +if __name__ == "__main__": + main() diff --git a/pylibfranka/include/pygripper.h b/pylibfranka/include/pygripper.h deleted file mode 100644 index 8199f348..00000000 --- a/pylibfranka/include/pygripper.h +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2025 Franka Robotics GmbH -// Use of this source code is governed by the Apache-2.0 license, see LICENSE - -/** - * @file pygripper.h - * @brief Python bindings for the Franka Robotics Gripper Control - * - * This header file provides C++ class that wraps the Franka Robotics Gripper Control - * for use in Python through pybind11. It offers: - * - Gripper homing and movement control - * - Grasping functionality with configurable parameters - * - State reading and control methods - */ - -#pragma once - -#include -#include -#include -#include - -namespace pylibfranka { - -class PyGripper { - public: - explicit PyGripper(const std::string& franka_address); - ~PyGripper() = default; - - bool homing(); - bool grasp(double width, - double speed, - double force, - double epsilon_inner = 0.005, - double epsilon_outer = 0.005); - - franka::GripperState readOnce(); - - bool stop(); - bool move(double width, double speed); - franka::Gripper::ServerVersion serverVersion(); - - private: - std::unique_ptr gripper_; -}; - -} // namespace pylibfranka diff --git a/pylibfranka/include/pylibfranka.h b/pylibfranka/include/pylibfranka.h deleted file mode 100644 index ebe54d04..00000000 --- a/pylibfranka/include/pylibfranka.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) 2025 Franka Robotics GmbH -// Use of this source code is governed by the Apache-2.0 license, see LICENSE - -/** - * @file pylibfranka.h - * @brief Python bindings for the Franka Robotics Robot Control Library - * - * This header file provides C++ classes that wrap the Franka Robotics Robot Control Library - * for use in Python through pybind11. It offers: - * PyRobot: A wrapper for the Franka robot control functionality, providing: - * - Active control methods (torque, joint position, joint velocity control) - * - Configuration methods (collision behavior, impedance settings, etc.) - * - State reading and control methods - */ - -#pragma once - -// C++ standard library headers -#include -#include -#include -#include - -// Third-party library headers -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace pylibfranka { - -class PyRobot { - public: - explicit PyRobot(const std::string& robot_ip_address, - franka::RealtimeConfig realtime_config = franka::RealtimeConfig::kEnforce); - ~PyRobot() = default; - - // Active control methods - /** - * Starts torque control mode. - */ - auto startTorqueControl() -> std::unique_ptr; - - /** - * Starts the joint position control mode. - * @param control_type The type of controller to use (JointImpedance or CartesianImpedance). - */ - auto startJointPositionControl(franka::ControllerMode controller_mode) - -> std::unique_ptr; - - /** - * Starts the joint velocity control mode. - * @param control_type The type of controller to use (JointImpedance or CartesianImpedance). - */ - auto startJointVelocityControl(franka::ControllerMode controller_mode) - -> std::unique_ptr; - - /** - * Starts the Cartesian pose control mode. - * @param control_type The type of controller to use (JointImpedance or CartesianImpedance). - */ - auto startCartesianPoseControl(franka::ControllerMode controller_mode) - -> std::unique_ptr; - - /** - * Starts the Cartesian velocity control mode. - * @param control_type The type of controller to use (JointImpedance or CartesianImpedance). - */ - auto startCartesianVelocityControl(franka::ControllerMode controller_mode) - -> std::unique_ptr; - - // Configuration methods - void setCollisionBehavior(const std::array& lower_torque_thresholds, - const std::array& upper_torque_thresholds, - const std::array& lower_force_thresholds, - const std::array& upper_force_thresholds); - - void setJointImpedance(const std::array& K_theta); - void setCartesianImpedance(const std::array& K_x); - void setK(const std::array& EE_T_K); - void setEE(const std::array& NE_T_EE); - void setLoad(double load_mass, - const std::array& F_x_Cload, - const std::array& load_inertia); - void automaticErrorRecovery(); - - // State methods - franka::RobotState readOnce(); - void stop(); - - std::unique_ptr loadModel(); - - private: - std::unique_ptr robot_; -}; - -} // namespace pylibfranka diff --git a/pylibfranka/include/pylibfranka/async_control.hpp b/pylibfranka/include/pylibfranka/async_control.hpp new file mode 100644 index 00000000..6c04fb96 --- /dev/null +++ b/pylibfranka/include/pylibfranka/async_control.hpp @@ -0,0 +1,15 @@ +// Copyright (c) 2026 Franka Robotics GmbH +// Use of this source code is governed by the Apache-2.0 license, see LICENSE + +// Expose async position control handler python bindings + +#pragma once +#include + +namespace py = pybind11; + +namespace pylibfranka { + +void bind_async_control(py::module& m); + +} diff --git a/pylibfranka/include/pylibfranka/gripper.hpp b/pylibfranka/include/pylibfranka/gripper.hpp new file mode 100644 index 00000000..4cff936c --- /dev/null +++ b/pylibfranka/include/pylibfranka/gripper.hpp @@ -0,0 +1,15 @@ +// Copyright (c) 2026 Franka Robotics GmbH +// Use of this source code is governed by the Apache-2.0 license, see LICENSE + +// Expose gripper-related python bindings + +#pragma once +#include + +namespace py = pybind11; + +namespace pylibfranka { + +void bind_gripper(py::module& m); + +} \ No newline at end of file diff --git a/pylibfranka/scripts/build_package.sh b/pylibfranka/scripts/build_package.sh index c1413046..515da60f 100755 --- a/pylibfranka/scripts/build_package.sh +++ b/pylibfranka/scripts/build_package.sh @@ -1,23 +1,22 @@ #!/bin/bash set -e -echo "Building package..." +echo "Building pylibfranka package..." -# Ensure user-level Python packages are in PATH -export PATH=$HOME/.local/bin:$PATH +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$PROJECT_ROOT" -# Set up CMake for pybind11 +export PATH=$HOME/.local/bin:$PATH export pybind11_DIR=/usr/lib/cmake/pybind11 -# Install the package in development mode echo "Installing package..." -pip install . +pip install . --no-build-isolation || pip install . -# Build the wheel echo "Building wheel..." python3 -m build --wheel -# Create wheelhouse directory mkdir -p wheelhouse echo "Package build complete!" diff --git a/pylibfranka/scripts/get_version.sh b/pylibfranka/scripts/get_version.sh deleted file mode 100755 index 43cbb25c..00000000 --- a/pylibfranka/scripts/get_version.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -set -e - -# Get Package Version from CMakeLists.txt (single source of truth) -echo "Getting package version from CMakeLists.txt..." -PACKAGE_VERSION=$(grep -oP 'set\(libfranka_VERSION\s+\K[\d.]+' CMakeLists.txt) - -if [ -z "$PACKAGE_VERSION" ]; then - echo "Error: Could not extract version from CMakeLists.txt" - exit 1 -fi - -echo "Package version: $PACKAGE_VERSION" - -# Export for use in pipelines -echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> version_info.properties -export PACKAGE_VERSION -echo "PACKAGE_VERSION=$PACKAGE_VERSION" diff --git a/pylibfranka/scripts/lint_code.sh b/pylibfranka/scripts/lint_code.sh deleted file mode 100755 index 20173249..00000000 --- a/pylibfranka/scripts/lint_code.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -e - -echo "Running code linting..." - -# Ensure user-level Python packages are in PATH -export PATH=$HOME/.local/bin:$PATH - -# Install flake8 if not already installed -if ! command -v flake8 &> /dev/null; then - echo "Installing flake8..." - python3 -m pip install --user flake8 -fi - -# Run linting (allow it to fail without stopping the build) -echo "Running flake8 on pylibfranka and examples..." -flake8 pylibfranka examples || echo "Linting issues found but continuing" - -echo "Linting complete!" diff --git a/pylibfranka/scripts/repair_wheels.sh b/pylibfranka/scripts/repair_wheels.sh index 92d08c16..5577483e 100755 --- a/pylibfranka/scripts/repair_wheels.sh +++ b/pylibfranka/scripts/repair_wheels.sh @@ -3,31 +3,85 @@ set -e echo "Repairing wheels with auditwheel..." +# Get script and project directories +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$PROJECT_ROOT" + # Ensure user-level Python packages are in PATH export PATH=$HOME/.local/bin:$PATH # Create wheelhouse directory if it doesn't exist mkdir -p wheelhouse -# Try different approaches to run auditwheel +# Target platform for manylinux wheels +PLATFORM="${1:-manylinux_2_17_x86_64}" + +# Function to run auditwheel +run_auditwheel() { + local wheel="$1" + local platform="$2" + + echo "Repairing: $wheel for $platform" + + # Try with specified platform first + if auditwheel repair "$wheel" -w wheelhouse/ --plat "$platform" 2>/dev/null; then + return 0 + fi + + # Fallback to auto-detection + echo "Trying auto-detection..." + if auditwheel repair "$wheel" -w wheelhouse/ 2>/dev/null; then + return 0 + fi + + # Final fallback: copy as-is + echo "Warning: auditwheel failed, copying wheel as-is" + cp "$wheel" wheelhouse/ + return 0 +} + +# Find auditwheel if command -v auditwheel &> /dev/null; then - echo "Using system auditwheel" - auditwheel repair dist/*.whl -w wheelhouse/ -elif [ -f $HOME/.local/bin/auditwheel ]; then - echo "Using user-local auditwheel" - $HOME/.local/bin/auditwheel repair dist/*.whl -w wheelhouse/ + AUDITWHEEL="auditwheel" +elif [ -f "$HOME/.local/bin/auditwheel" ]; then + AUDITWHEEL="$HOME/.local/bin/auditwheel" else - echo "Installing auditwheel and running repair" - python3 -m pip install --user auditwheel - $HOME/.local/bin/auditwheel repair dist/*.whl -w wheelhouse/ + echo "Installing auditwheel..." + PIP_USER_FLAG="" + if [ -z "$VIRTUAL_ENV" ]; then + PIP_USER_FLAG="--user" + fi + python3 -m pip install $PIP_USER_FLAG auditwheel patchelf + AUDITWHEEL="$HOME/.local/bin/auditwheel" fi -# If auditwheel fails, copy the wheel directly as fallback -if [ ! -f wheelhouse/*.whl ]; then - echo "auditwheel failed, copying wheel directly" - cp dist/*.whl wheelhouse/ +# Check for wheels in dist directory +if [ ! -d dist ] || [ -z "$(ls -A dist/*.whl 2>/dev/null)" ]; then + echo "Error: No wheels found in dist/ directory" + echo "Run ./scripts/build_package.sh first" + exit 1 fi +# Repair each wheel +for whl in dist/*.whl; do + if [ -f "$whl" ]; then + run_auditwheel "$whl" "$PLATFORM" + fi +done + # List the final wheels +echo "" echo "Final wheels in wheelhouse:" ls -la wheelhouse/ + +# Show wheel contents for verification +echo "" +echo "Wheel contents (showing bundled libraries):" +for whl in wheelhouse/*.whl; do + if [ -f "$whl" ]; then + echo "--- $whl ---" + unzip -l "$whl" | grep -E "\.so|\.libs" | head -20 || true + fi +done diff --git a/pylibfranka/scripts/setup_dependencies.sh b/pylibfranka/scripts/setup_dependencies.sh index d5319dcd..d6975577 100755 --- a/pylibfranka/scripts/setup_dependencies.sh +++ b/pylibfranka/scripts/setup_dependencies.sh @@ -1,29 +1,95 @@ #!/bin/bash set -e -echo "Setting up dependencies..." +echo "Setting up dependencies for pylibfranka..." -# Update package lists (allow failure) -sudo apt-get update || true +# Get script and project directories +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Update package lists (allow failure for non-root users) +echo "Updating package lists..." +sudo apt-get update 2>/dev/null || apt-get update 2>/dev/null || true # Install system dependencies -sudo apt-get install -y libeigen3-dev libpoco-dev pybind11-dev || true +echo "Installing system dependencies..." +sudo apt-get install -y \ + build-essential \ + cmake \ + libeigen3-dev \ + libpoco-dev \ + libfmt-dev \ + pybind11-dev \ + python3-dev \ + python3-pip \ + patchelf \ + 2>/dev/null || \ +apt-get install -y \ + build-essential \ + cmake \ + libeigen3-dev \ + libpoco-dev \ + libfmt-dev \ + pybind11-dev \ + python3-dev \ + python3-pip \ + patchelf \ + 2>/dev/null || true # Install Python dependencies echo "Installing Python dependencies..." -python3 -m pip install --user auditwheel setuptools build wheel patchelf pybind11 numpy cmake +# Use --user only if not in a virtualenv (virtualenv doesn't support --user) +PIP_USER_FLAG="" +if [ -z "$VIRTUAL_ENV" ]; then + PIP_USER_FLAG="--user" +fi +python3 -m pip install $PIP_USER_FLAG --upgrade \ + pip \ + setuptools \ + wheel \ + build \ + auditwheel \ + patchelf \ + pybind11 \ + numpy \ + cmake \ + twine -# Add user-level Python bin to PATH -export PATH=$HOME/.local/bin:$PATH +# Add user-level Python bin to PATH (only needed for --user installs) +export PATH="$HOME/.local/bin:$PATH" -# Verify auditwheel is available -if command -v auditwheel &> /dev/null; then - echo "auditwheel found at: $(which auditwheel)" -else - echo "WARNING: auditwheel not found in PATH" -fi +# Verify tools are available +echo "" +echo "Verifying installed tools..." + +check_tool() { + local tool="$1" + if command -v "$tool" &> /dev/null; then + echo "✓ $tool found at: $(which $tool)" + return 0 + elif [ -f "$HOME/.local/bin/$tool" ]; then + echo "✓ $tool found at: $HOME/.local/bin/$tool" + return 0 + else + echo "✗ $tool not found" + return 1 + fi +} + +check_tool cmake +check_tool python3 +check_tool pip +check_tool auditwheel +check_tool patchelf # Set up CMake for pybind11 export pybind11_DIR=/usr/lib/cmake/pybind11 +echo "" echo "Dependencies setup complete!" +echo "" +echo "To build the wheel, run:" +echo " cd $PROJECT_ROOT" +echo " ./pylibfranka/scripts/build_package.sh" +echo " ./pylibfranka/scripts/repair_wheels.sh" +echo " ./pylibfranka/scripts/test_installation.sh" \ No newline at end of file diff --git a/pylibfranka/scripts/test_installation.sh b/pylibfranka/scripts/test_installation.sh index f89dafbf..9e892c31 100755 --- a/pylibfranka/scripts/test_installation.sh +++ b/pylibfranka/scripts/test_installation.sh @@ -1,39 +1,164 @@ #!/bin/bash set -e +# Test pylibfranka wheel installation +# Usage: ./test_installation.sh [wheel_pattern] +# +# Examples: +# ./test_installation.sh # Install any wheel from wheelhouse/ +# ./test_installation.sh manylinux_2_17_x86_64.whl # Install specific platform wheel +# ./test_installation.sh cp310 # Install Python 3.10 wheel + # Accept wheel pattern as first argument, default to all wheels WHEEL_PATTERN=${1:-"*.whl"} +echo "===========================================" +echo "Testing pylibfranka installation" +echo "===========================================" +echo "" + +# Get script and project directories +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +WHEELHOUSE="$PROJECT_ROOT/wheelhouse" + +if [ ! -d "$WHEELHOUSE" ]; then + echo "Error: wheelhouse directory not found at $WHEELHOUSE" + exit 1 +fi + echo "Installing wheel with pattern: $WHEEL_PATTERN" +echo "" # Find wheels matching the pattern if [[ "$WHEEL_PATTERN" == "*.whl" ]]; then - # Install all wheels (default behavior) - pip install wheelhouse/*.whl + # Install first available wheel + MATCHING_WHEEL=$(ls -1 "$WHEELHOUSE"/*.whl 2>/dev/null | head -1) else # Find specific wheel matching pattern echo "Available wheels:" - ls -la wheelhouse/ + ls -la "$WHEELHOUSE"/*.whl + echo "" echo "Filtering for wheels matching: $WHEEL_PATTERN" - MATCHING_WHEEL=$(find wheelhouse/ -name "*${WHEEL_PATTERN}" | head -1) - if [ -z "$MATCHING_WHEEL" ]; then - echo "No wheel found matching pattern: $WHEEL_PATTERN" - exit 1 - fi - echo "Found wheel: $MATCHING_WHEEL" - pip install "$MATCHING_WHEEL" + MATCHING_WHEEL=$(find "$WHEELHOUSE" -name "*${WHEEL_PATTERN}*" -name "*.whl" | head -1) fi -# Test the installation +if [ -z "$MATCHING_WHEEL" ] || [ ! -f "$MATCHING_WHEEL" ]; then + echo "Error: No wheel found matching pattern: $WHEEL_PATTERN" + echo "Available wheels in $WHEELHOUSE:" + ls -la "$WHEELHOUSE"/*.whl 2>/dev/null || echo " (none)" + exit 1 +fi + +echo "Installing: $MATCHING_WHEEL" +pip install --force-reinstall "$MATCHING_WHEEL" + +echo "" +echo "===========================================" echo "Testing pylibfranka import..." -cd / -python -c " +echo "===========================================" + +# IMPORTANT: Change to a neutral directory to avoid importing from local pylibfranka folder +# The project has a pylibfranka/ folder which would shadow the installed package +cd /tmp +echo "Testing from directory: $(pwd)" +echo "" + +python3 << 'EOF' +import sys +import os + +print(f"Python version: {sys.version}") +print(f"Python executable: {sys.executable}") +print(f"Current directory: {os.getcwd()}") +print() + +# Test basic import +print("Testing basic import...") import pylibfranka -print('pylibfranka imported successfully') -try: - print(f'Version: {pylibfranka.__version__}') -except AttributeError: - print('No version attribute found') -" +print(f"✓ pylibfranka imported successfully") +print(f" Version: {pylibfranka.__version__}") +print(f" Location: {pylibfranka.__file__}") + +# Verify we're NOT importing from the local project folder +if "/workspace" in pylibfranka.__file__ or "/workspaces" in pylibfranka.__file__: + print(f"✗ ERROR: Importing from project folder instead of installed package!") + print(f" This means the test is not testing the wheel correctly.") + sys.exit(1) +print(f"✓ Confirmed: importing from installed package (not local folder)") +print() + +# Test core classes +print("Testing core classes...") +from pylibfranka import ( + Robot, + Gripper, + Model, + RobotState, + GripperState, + Duration, + Errors, + RobotMode, + ControllerMode, + RealtimeConfig, +) +print("✓ All core classes imported successfully") +print() + +# Test control types +print("Testing control types...") +from pylibfranka import ( + JointPositions, + JointVelocities, + CartesianPose, + CartesianVelocities, + Torques, +) +print("✓ All control types imported successfully") +print() + +# Test exceptions +print("Testing exceptions...") +from pylibfranka import ( + FrankaException, + CommandException, + ControlException, + NetworkException, + RealtimeException, + InvalidOperationException, +) +print("✓ All exception types imported successfully") +print() + +# Test creating objects (without connecting to robot) +print("Testing object creation...") +jp = JointPositions([0.0] * 7) +print(f"✓ JointPositions created: {jp.q}") + +jv = JointVelocities([0.0] * 7) +print(f"✓ JointVelocities created: {jv.dq}") + +t = Torques([0.0] * 7) +print(f"✓ Torques created: {t.tau_J}") + +# Duration class exists (constructor may vary by version) +print(f"✓ Duration class available: {Duration}") +print() + +# Verify version matches expected format +import re +if re.match(r'^\d+\.\d+\.\d+', pylibfranka.__version__): + print(f"✓ Version format valid: {pylibfranka.__version__}") +else: + print(f"✗ Warning: Unexpected version format: {pylibfranka.__version__}") + sys.exit(1) + +print() +print("=========================================") +print("All tests passed!") +print("=========================================") +EOF +echo "" echo "Installation test complete!" diff --git a/pylibfranka/src/async_control.cpp b/pylibfranka/src/async_control.cpp new file mode 100644 index 00000000..7bde8889 --- /dev/null +++ b/pylibfranka/src/async_control.cpp @@ -0,0 +1,114 @@ +// Copyright (c) 2026 Franka Robotics GmbH +// Use of this source code is governed by the Apache-2.0 license, see LICENSE + +#include +#include + +namespace pylibfranka { + +void bind_async_control(py::module& m) { + auto async_position_control_handler = + py::class_>( + m, "AsyncPositionControlHandler", R"pbdoc( + Handler for asynchronous joint position control + )pbdoc") + .def_static("configure", &franka::AsyncPositionControlHandler::configure, R"pbdoc( + Configure an AsyncPositionControlHandler with the given configuration. + + @param configuration Configuration parameters + @return ConfigurationResult containing the handler or an error message + )pbdoc") + .def("set_joint_position_target", + &franka::AsyncPositionControlHandler::setJointPositionTarget, R"pbdoc( + Set a new joint position target. + + @param target Joint position target + @return CommandResult containing the motion UUID, success flag, and error message + )pbdoc") + .def("get_target_feedback", &franka::AsyncPositionControlHandler::getTargetFeedback, + py::arg("robot_state") = py::none(), R"pbdoc( + Get feedback on the current target. + + @param robot_state Optional current robot state for more detailed feedback + @return TargetFeedback containing status and error message + )pbdoc") + .def("stop_control", &franka::AsyncPositionControlHandler::stopControl, R"pbdoc( + Stop the position control handler. + )pbdoc"); + + py::class_(async_position_control_handler, + "Configuration", R"pbdoc( + Configuration parameters for AsyncPositionControlHandler. + )pbdoc") + .def(py::init([](const std::vector& maximum_joint_velocities, double goal_tolerance) { + if (maximum_joint_velocities.size() != franka::Robot::kNumJoints) { + throw std::invalid_argument("joint_velocities must have exactly" + + std::to_string(franka::Robot::kNumJoints) + " values"); + } + franka::AsyncPositionControlHandler::Configuration config{ + .maximum_joint_velocities = maximum_joint_velocities, + .goal_tolerance = goal_tolerance}; + return config; + }), + py::arg("maximum_joint_velocities"), py::arg("goal_tolerance"), R"pbdoc( + Create Configuration. + + @param maximum_joint_velocities Maximum joint velocities [rad/s] (7,) + @param goal_tolerance Goal tolerance [rad] + )pbdoc") + .def_readwrite("maximum_joint_velocities", + &franka::AsyncPositionControlHandler::Configuration::maximum_joint_velocities) + .def_readwrite("goal_tolerance", + &franka::AsyncPositionControlHandler::Configuration::goal_tolerance); + + py::class_( + async_position_control_handler, "ConfigurationResult", R"pbdoc( + Result of configuring an AsyncPositionControlHandler. + )pbdoc") + .def_readwrite("handler", &franka::AsyncPositionControlHandler::ConfigurationResult::handler) + .def_readwrite("error_message", + &franka::AsyncPositionControlHandler::ConfigurationResult::error_message); + + py::class_( + async_position_control_handler, "JointPositionTarget", R"pbdoc( + Joint position target for AsyncPositionControlHandler. + )pbdoc") + .def(py::init([](const std::vector& joints) { + if (joints.size() != franka::Robot::kNumJoints) { + throw std::invalid_argument("joint_positions must have exactly" + + std::to_string(franka::Robot::kNumJoints) + " values"); + } + franka::AsyncPositionControlHandler::JointPositionTarget tgt; + std::copy(joints.begin(), joints.end(), tgt.joint_positions.begin()); + return tgt; + }), + py::arg("joint_positions"), R"pbdoc( + Create JointPositionTarget. + + @param joint_positions Target joint positions [rad] (7,) + )pbdoc") + .def_readwrite("joint_positions", + &franka::AsyncPositionControlHandler::JointPositionTarget::joint_positions); + + py::class_(async_position_control_handler, + "CommandResult", R"pbdoc( + Result of setting a joint position target. + )pbdoc") + .def_readwrite("motion_uuid", + &franka::AsyncPositionControlHandler::CommandResult::motion_uuid) + .def_readwrite("was_successful", + &franka::AsyncPositionControlHandler::CommandResult::was_successful) + .def_readwrite("error_message", + &franka::AsyncPositionControlHandler::CommandResult::error_message); + + py::class_(async_position_control_handler, + "TargetFeedback", R"pbdoc( + Feedback from a target position command. + )pbdoc") + .def_readwrite("status", &franka::AsyncPositionControlHandler::TargetFeedback::status) + .def_readwrite("error_message", + &franka::AsyncPositionControlHandler::TargetFeedback::error_message); +} + +} // namespace pylibfranka \ No newline at end of file diff --git a/pylibfranka/src/gripper.cpp b/pylibfranka/src/gripper.cpp new file mode 100644 index 00000000..e58604cf --- /dev/null +++ b/pylibfranka/src/gripper.cpp @@ -0,0 +1,96 @@ +// Copyright (c) 2026 Franka Robotics GmbH +// Use of this source code is governed by the Apache-2.0 license, see LICENSE + +#include +#include + +namespace pylibfranka { + +void bind_gripper(py::module& m) { + // Bind franka::GripperState + py::class_(m, "GripperState", R"pbdoc( + Current state of the Franka Hand gripper. + )pbdoc") + .def_readwrite("width", &franka::GripperState::width, "Current gripper width [m]") + .def_readwrite("max_width", &franka::GripperState::max_width, "Maximum gripper width [m]") + .def_readwrite("is_grasped", &franka::GripperState::is_grasped, "True if object is grasped") + .def_readwrite("temperature", &franka::GripperState::temperature, "Gripper temperature [°C]") + .def_readwrite("time", &franka::GripperState::time, "Timestamp"); + + // Bind franka::Gripper + py::class_(m, "Gripper", R"pbdoc( + Interface for controlling a Franka Hand gripper. + )pbdoc") + .def(py::init(), R"pbdoc( + Connect to gripper. + + Establishes connection to the Franka Hand gripper at the specified address. + + @param franka_address IP address or hostname of the robot + :raises NetworkException: if the connection cannot be established + )pbdoc") + .def("homing", &franka::Gripper::homing, R"pbdoc( + Perform homing to find maximum width. + + Homing moves the gripper fingers to the maximum width to calibrate the position. + This should be performed after powering on the gripper or when the gripper state is uncertain. + + @return True if successful + :raises CommandException: if the command fails + :raises NetworkException: if the connection is lost + )pbdoc") + .def("grasp", &franka::Gripper::grasp, py::arg("width"), py::arg("speed"), py::arg("force"), + py::arg("epsilon_inner") = 0.005, py::arg("epsilon_outer") = 0.005, R"pbdoc( + Grasp an object. + + The gripper closes at the specified speed and force until an object is detected + or the target width is reached. The grasp is considered successfulsetCurrentThreadToHighestSchedulerPriority if the final + width is within the specified tolerances. + + @param width Target grasp width [m] + @param speed Closing speed [m/s] + @param force Grasping force [N] + @param epsilon_inner Inner tolerance for grasp check [m] (default: 0.005) + @param epsilon_outer Outer tolerance for grasp check [m] (default: 0.005) + @return True if grasp successful + :raises CommandException: if the command fails + :raises NetworkException: if the connection is lost + )pbdoc") + .def("read_once", &franka::Gripper::readOnce, R"pbdoc( + Read current gripper state once. + + Queries the gripper for its current state including width, temperature, + and grasp status. + + @return Current gripper state + :raises NetworkException: if the connection is lost + )pbdoc") + .def("stop", &franka::Gripper::stop, R"pbdoc( + Stop current gripper motion. + + Stops any ongoing gripper movement immediately. + + @return True if successful + :raises CommandException: if the command fails + :raises NetworkException: if the connection is lost + )pbdoc") + .def("move", &franka::Gripper::move, R"pbdoc( + Move gripper fingers to a specific width. + + Moves the gripper to the specified width at the given speed. Use this for + opening and closing the gripper without force control. + + @param width Target width [m] + @param speed Movement speed [m/s] + @return True if successful + :raises CommandException: if the command fails + :raises NetworkException: if the connection is lost + )pbdoc") + .def("server_version", &franka::Gripper::serverVersion, R"pbdoc( + Get gripper server version. + + @return Server version information + )pbdoc"); +} + +} // namespace pylibfranka \ No newline at end of file diff --git a/pylibfranka/src/pylibfranka.cpp b/pylibfranka/src/pylibfranka.cpp index 7641c8fc..610056de 100644 --- a/pylibfranka/src/pylibfranka.cpp +++ b/pylibfranka/src/pylibfranka.cpp @@ -1,19 +1,35 @@ // Copyright (c) 2025 Franka Robotics GmbH // Use of this source code is governed by the Apache-2.0 license, see LICENSE -#include "pylibfranka.h" -#include "pygripper.h" - // C++ standard library headers +#include +#include +#include +#include // Third-party library headers -#include -#include #include #include #include + +// Libfranka +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// common #include +// Pylibfranka +#include +#include + namespace py = pybind11; namespace { @@ -38,111 +54,6 @@ auto convertControllerMode(franka::ControllerMode mode) namespace pylibfranka { -PyGripper::PyGripper(const std::string& franka_address) - : gripper_(std::make_unique(franka_address)) {} - -bool PyGripper::homing() { - return gripper_->homing(); -} - -bool PyGripper::grasp(double width, - double speed, - double force, - double epsilon_inner, - double epsilon_outer) { - return gripper_->grasp(width, speed, force, epsilon_inner, epsilon_outer); -} - -franka::GripperState PyGripper::readOnce() { - return gripper_->readOnce(); -} - -bool PyGripper::stop() { - return gripper_->stop(); -} -bool PyGripper::move(double width, double speed) { - return gripper_->move(width, speed); -} - -franka::Gripper::ServerVersion PyGripper::serverVersion() { - return gripper_->serverVersion(); -} - -PyRobot::PyRobot(const std::string& franka_address, franka::RealtimeConfig realtime_config) - : robot_(std::make_unique(franka_address, realtime_config)) {} - -auto PyRobot::startTorqueControl() -> std::unique_ptr { - return robot_->startTorqueControl(); -} - -auto PyRobot::startJointPositionControl(franka::ControllerMode controller_mode) - -> std::unique_ptr { - return robot_->startJointPositionControl(convertControllerMode(controller_mode)); -} - -auto PyRobot::startJointVelocityControl(franka::ControllerMode controller_mode) - -> std::unique_ptr { - return robot_->startJointVelocityControl(convertControllerMode(controller_mode)); -} - -auto PyRobot::startCartesianPoseControl(franka::ControllerMode controller_mode) - -> std::unique_ptr { - return robot_->startCartesianPoseControl(convertControllerMode(controller_mode)); -} - -auto PyRobot::startCartesianVelocityControl(franka::ControllerMode controller_mode) - -> std::unique_ptr { - return robot_->startCartesianVelocityControl(convertControllerMode(controller_mode)); -} - -void PyRobot::setCollisionBehavior(const std::array& lower_torque_thresholds, - const std::array& upper_torque_thresholds, - const std::array& lower_force_thresholds, - const std::array& upper_force_thresholds) { - robot_->setCollisionBehavior(lower_torque_thresholds, upper_torque_thresholds, - lower_torque_thresholds, upper_torque_thresholds, - lower_force_thresholds, upper_force_thresholds, - lower_force_thresholds, upper_force_thresholds); -} - -void PyRobot::setJointImpedance(const std::array& K_theta) { - robot_->setJointImpedance(K_theta); -} - -void PyRobot::setCartesianImpedance(const std::array& K_x) { - robot_->setCartesianImpedance(K_x); -} - -void PyRobot::setK(const std::array& EE_T_K) { - robot_->setK(EE_T_K); -} - -void PyRobot::setEE(const std::array& NE_T_EE) { - robot_->setEE(NE_T_EE); -} - -void PyRobot::setLoad(double load_mass, - const std::array& F_x_Cload, - const std::array& load_inertia) { - robot_->setLoad(load_mass, F_x_Cload, load_inertia); -} - -void PyRobot::automaticErrorRecovery() { - robot_->automaticErrorRecovery(); -} - -franka::RobotState PyRobot::readOnce() { - return robot_->readOnce(); -} - -std::unique_ptr PyRobot::loadModel() { - return std::make_unique(robot_->loadModel()); -} - -void PyRobot::stop() { - robot_->stop(); -} - PYBIND11_MODULE(_pylibfranka, m) { m.doc() = "Python bindings for Franka Robotics Robot Control Library"; @@ -586,8 +497,9 @@ PYBIND11_MODULE(_pylibfranka, m) { "End effector twist [m/s, rad/s] (6,)") .def_readwrite("motion_finished", &franka::CartesianVelocities::motion_finished, "Set to True to finish motion"); - // Bind PyRobot - py::class_(m, "Robot", R"pbdoc( + + // Bind Robot + py::class_>(m, "Robot", R"pbdoc( Main interface for controlling a Franka robot. Provides real-time control capabilities including torque, position, @@ -600,171 +512,118 @@ PYBIND11_MODULE(_pylibfranka, m) { @param robot_ip_address IP address or hostname of the robot @param realtime_config Real-time scheduling requirements (default: kEnforce) )pbdoc") - .def("start_torque_control", &PyRobot::startTorqueControl, R"pbdoc( + .def("start_torque_control", &franka::Robot::startTorqueControl, R"pbdoc( Start torque control mode. @return ActiveControlBase interface for sending torque commands )pbdoc") - .def("start_joint_position_control", &PyRobot::startJointPositionControl, R"pbdoc( + .def( + "start_joint_position_control", + [](franka::Robot& self, franka::ControllerMode mode) { + return self.startJointPositionControl(convertControllerMode(mode)); + }, + R"pbdoc( Start joint position control mode. @param control_type Controller mode (JointImpedance or CartesianImpedance) @return ActiveControlBase interface for sending position commands )pbdoc") - .def("start_joint_velocity_control", &PyRobot::startJointVelocityControl, R"pbdoc( + .def( + "start_joint_velocity_control", + [](franka::Robot& self, franka::ControllerMode mode) { + return self.startJointVelocityControl(convertControllerMode(mode)); + }, + R"pbdoc( Start joint velocity control mode. @param control_type Controller mode (JointImpedance or CartesianImpedance) @return ActiveControlBase interface for sending velocity commands )pbdoc") - .def("start_cartesian_pose_control", &PyRobot::startCartesianPoseControl, R"pbdoc( + .def( + "start_cartesian_pose_control", + [](franka::Robot& self, franka::ControllerMode mode) { + return self.startCartesianPoseControl(convertControllerMode(mode)); + }, + R"pbdoc( Start Cartesian pose control mode. @param control_type Controller mode (JointImpedance or CartesianImpedance) @return ActiveControlBase interface for sending Cartesian pose commands )pbdoc") - .def("start_cartesian_velocity_control", &PyRobot::startCartesianVelocityControl, R"pbdoc( + .def( + "start_cartesian_velocity_control", + [](franka::Robot& self, franka::ControllerMode mode) { + return self.startCartesianVelocityControl(convertControllerMode(mode)); + }, + R"pbdoc( Start Cartesian velocity control mode. @param control_type Controller mode (JointImpedance or CartesianImpedance) @return ActiveControlBase interface for sending Cartesian velocity commands )pbdoc") - .def("set_collision_behavior", &PyRobot::setCollisionBehavior, R"pbdoc( - Configure collision detection thresholds. + .def("set_collision_behavior", + py::overload_cast&, const std::array&, + const std::array&, const std::array&>( + &franka::Robot::setCollisionBehavior), + py::arg("lower_torque_thresholds"), py::arg("upper_torque_thresholds"), + py::arg("lower_force_thresholds"), py::arg("upper_force_thresholds"), + R"pbdoc( + Configure collision detection thresholds. - @param lower_torque_thresholds Lower torque thresholds [Nm] (7,) - @param upper_torque_thresholds Upper torque thresholds [Nm] (7,) - @param lower_force_thresholds Lower Cartesian force thresholds [N, Nm] (6,) - @param upper_force_thresholds Upper Cartesian force thresholds [N, Nm] (6,) + @param lower_torque_thresholds Lower torque thresholds [Nm] (7,) + @param upper_torque_thresholds Upper torque thresholds [Nm] (7,) + @param lower_force_thresholds Lower Cartesian force thresholds [N, Nm] (6,) + @param upper_force_thresholds Upper Cartesian force thresholds [N, Nm] (6,) )pbdoc") - .def("set_joint_impedance", &PyRobot::setJointImpedance, R"pbdoc( + .def("set_joint_impedance", &franka::Robot::setJointImpedance, R"pbdoc( Set joint impedance for internal controller. @param K_theta Joint stiffness values [Nm/rad] (7,) )pbdoc") - .def("set_cartesian_impedance", &PyRobot::setCartesianImpedance, R"pbdoc( + .def("set_cartesian_impedance", &franka::Robot::setCartesianImpedance, R"pbdoc( Set Cartesian impedance for internal controller. @param K_x Cartesian stiffness values [N/m, Nm/rad] (6,) )pbdoc") - .def("set_K", &PyRobot::setK, R"pbdoc( + .def("set_K", &franka::Robot::setK, R"pbdoc( Set stiffness frame K in end effector frame. @param EE_T_K Homogeneous transformation matrix (16,) in column-major order )pbdoc") - .def("set_EE", &PyRobot::setEE, R"pbdoc( + .def("set_EE", &franka::Robot::setEE, R"pbdoc( Set end effector frame relative to nominal end effector frame. @param NE_T_EE Homogeneous transformation matrix (16,) in column-major order )pbdoc") - .def("set_load", &PyRobot::setLoad, R"pbdoc( + .def("set_load", &franka::Robot::setLoad, R"pbdoc( Set external load parameters. @param load_mass Mass of the external load [kg] @param F_x_Cload Center of mass of load in flange frame [m] (3,) @param load_inertia Inertia tensor of load [kg*m²] (9,) in column-major order )pbdoc") - .def("automatic_error_recovery", &PyRobot::automaticErrorRecovery, R"pbdoc( + .def("automatic_error_recovery", &franka::Robot::automaticErrorRecovery, R"pbdoc( Attempt automatic error recovery. )pbdoc") - .def("read_once", &PyRobot::readOnce, R"pbdoc( + .def("read_once", &franka::Robot::readOnce, R"pbdoc( Read current robot state once. @return Current robot state )pbdoc") - .def("load_model", &PyRobot::loadModel, R"pbdoc( + .def("load_model", py::overload_cast<>(&franka::Robot::loadModel), R"pbdoc( Load robot dynamics model. @return Model object for computing dynamics quantities )pbdoc") - .def("stop", &PyRobot::stop, R"pbdoc( + .def("stop", &franka::Robot::stop, R"pbdoc( Stop currently running motion. )pbdoc"); - // Bind franka::GripperState - py::class_(m, "GripperState", R"pbdoc( - Current state of the Franka Hand gripper. - )pbdoc") - .def_readwrite("width", &franka::GripperState::width, "Current gripper width [m]") - .def_readwrite("max_width", &franka::GripperState::max_width, "Maximum gripper width [m]") - .def_readwrite("is_grasped", &franka::GripperState::is_grasped, "True if object is grasped") - .def_readwrite("temperature", &franka::GripperState::temperature, "Gripper temperature [°C]") - .def_readwrite("time", &franka::GripperState::time, "Timestamp"); + // Gripper bindings + bind_gripper(m); - // Bind PyGripper - py::class_(m, "Gripper", R"pbdoc( - Interface for controlling a Franka Hand gripper. - )pbdoc") - .def(py::init(), R"pbdoc( - Connect to gripper. - - Establishes connection to the Franka Hand gripper at the specified address. - - @param franka_address IP address or hostname of the robot - :raises NetworkException: if the connection cannot be established - )pbdoc") - .def("homing", &PyGripper::homing, R"pbdoc( - Perform homing to find maximum width. - - Homing moves the gripper fingers to the maximum width to calibrate the position. - This should be performed after powering on the gripper or when the gripper state is uncertain. - - @return True if successful - :raises CommandException: if the command fails - :raises NetworkException: if the connection is lost - )pbdoc") - .def("grasp", &PyGripper::grasp, py::arg("width"), py::arg("speed"), py::arg("force"), - py::arg("epsilon_inner") = 0.005, py::arg("epsilon_outer") = 0.005, R"pbdoc( - Grasp an object. - - The gripper closes at the specified speed and force until an object is detected - or the target width is reached. The grasp is considered successful if the final - width is within the specified tolerances. - - @param width Target grasp width [m] - @param speed Closing speed [m/s] - @param force Grasping force [N] - @param epsilon_inner Inner tolerance for grasp check [m] (default: 0.005) - @param epsilon_outer Outer tolerance for grasp check [m] (default: 0.005) - @return True if grasp successful - :raises CommandException: if the command fails - :raises NetworkException: if the connection is lost - )pbdoc") - .def("read_once", &PyGripper::readOnce, R"pbdoc( - Read current gripper state once. - - Queries the gripper for its current state including width, temperature, - and grasp status. - - @return Current gripper state - :raises NetworkException: if the connection is lost - )pbdoc") - .def("stop", &PyGripper::stop, R"pbdoc( - Stop current gripper motion. - - Stops any ongoing gripper movement immediately. - - @return True if successful - :raises CommandException: if the command fails - :raises NetworkException: if the connection is lost - )pbdoc") - .def("move", &PyGripper::move, R"pbdoc( - Move gripper fingers to a specific width. - - Moves the gripper to the specified width at the given speed. Use this for - opening and closing the gripper without force control. - - @param width Target width [m] - @param speed Movement speed [m/s] - @return True if successful - :raises CommandException: if the command fails - :raises NetworkException: if the connection is lost - )pbdoc") - .def("server_version", &PyGripper::serverVersion, R"pbdoc( - Get gripper server version. - - @return Server version information - )pbdoc"); + // Async Control bindings + bind_async_control(m); } } // namespace pylibfranka diff --git a/pyproject.toml b/pyproject.toml index 06ad92e1..80af29bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,28 +14,35 @@ build-backend = "setuptools.build_meta" [project] name = "pylibfranka" dynamic = ["version"] -description = "Python bindings for libfranka with automatic dependency bundling" +description = "Python bindings for libfranka - Control Franka robots with Python" +readme = "pylibfranka/README.md" authors = [{name = "Franka Robotics GmbH", email = "info@franka.de"}] license = {text = "Apache-2.0"} -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "numpy>=1.19.0", ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", + "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.8", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", - "Topic :: Scientific/Engineering :: Robotics", + "Programming Language :: Python :: 3.12", + "Programming Language :: C++", + "Topic :: Software Development :: Libraries :: Python Modules", ] [project.urls] Homepage = "https://github.com/frankarobotics/libfranka" +Documentation = "https://frankarobotics.github.io/libfranka/pylibfranka/latest" Repository = "https://github.com/frankarobotics/libfranka" Issues = "https://github.com/frankarobotics/libfranka/issues" +Changelog = "https://github.com/frankarobotics/libfranka/blob/main/CHANGELOG.md" [project.optional-dependencies] dev = [ @@ -53,37 +60,37 @@ packages = ["pylibfranka"] zip-safe = false [tool.setuptools.dynamic] -version = {attr = "pylibfranka._version.__version__"} +version = {file = "pylibfranka/VERSION"} [tool.setuptools.package-data] pylibfranka = ["*.so", "*.pyd", "_pylibfranka*"] [tool.cibuildwheel] -# Build only on modern Python versions -build = "cp38-* cp39-* cp310-* cp311-*" -skip = "pp* *-manylinux_i686" +# Build for Python 3.9 through 3.12 (3.8 EOL) +build = "cp39-* cp310-* cp311-* cp312-*" +skip = "pp* *-manylinux_i686 *-musllinux*" # Install system dependencies before building before-all = [ "yum install -y eigen3-devel poco-devel || true", "apt-get update && apt-get install -y libeigen3-dev libpoco-dev || true", - "pip install pybind11 cmake" + "pip install pybind11 cmake numpy" ] # Build requirements build-verbosity = 1 [tool.cibuildwheel.linux] -# Use manylinux2014 for better compatibility -manylinux-x86_64-image = "manylinux2014" -manylinux-aarch64-image = "manylinux2014" +# Use manylinux_2_17 for broader compatibility +manylinux-x86_64-image = "manylinux_2_17" +manylinux-aarch64-image = "manylinux_2_17" -# Let auditwheel bundle dependencies automatically -repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel} --lib-sdir ." +# Repair wheel to bundle shared libraries with unique hashes +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel} --plat manylinux_2_17_x86_64" [tool.black] line-length = 100 -target-version = ["py38", "py39", "py310", "py311"] +target-version = ["py39", "py310", "py311", "py312"] [tool.isort] profile = "black" diff --git a/setup.py b/setup.py index 574232c3..71e6603d 100644 --- a/setup.py +++ b/setup.py @@ -7,56 +7,43 @@ from setuptools import Extension, setup from setuptools.command.build_ext import build_ext +# Root directory of the project +ROOT_DIR = Path(__file__).parent + def get_version(): """Extract version from CMakeLists.txt.""" - cmake_file = Path(__file__).parent / "CMakeLists.txt" + cmake_file = ROOT_DIR / "CMakeLists.txt" if cmake_file.exists(): - with open(cmake_file, 'r') as f: + with open(cmake_file, "r", encoding="utf-8") as f: content = f.read() - match = re.search(r'set\(libfranka_VERSION\s+(\d+\.\d+\.\d+)\)', content) + match = re.search(r"set\(libfranka_VERSION\s+(\d+\.\d+\.\d+)\)", content) if match: return match.group(1) return "0.0.0" -def update_version_file(): - """Update _version.py with current version from CMakeLists.txt.""" - version = get_version() - version_file = Path(__file__).parent / "pylibfranka" / "_version.py" - - if version_file.exists(): - # Read current content - with open(version_file, 'r') as f: - content = f.read() - - # Update the version line (matches the pattern with AUTO-GENERATED comment) - new_content = re.sub( - r'__version__ = "[^"]*" # AUTO-GENERATED', - f'__version__ = "{version}" # AUTO-GENERATED', - content - ) - - # Write the updated content - with open(version_file, 'w') as f: - f.write(new_content) - else: - # Create the file if it doesn't exist - with open(version_file, 'w') as f: - f.write(f'''"""Version information for pylibfranka.""" +def write_version_files(version): + """Write version to VERSION file and _version.py for runtime access.""" + pylibfranka_dir = ROOT_DIR / "pylibfranka" -__all__ = ['__version__'] + # Write VERSION file (used by pyproject.toml for build metadata) + version_file = pylibfranka_dir / "VERSION" + version_file.write_text(f"{version}\n") + # Write _version.py (used at runtime for pylibfranka.__version__) + version_py = pylibfranka_dir / "_version.py" + version_py.write_text( + '"""Version information for pylibfranka."""\n\n' + "__all__ = ['__version__']\n\n" + "# Version is auto-generated from CMakeLists.txt during build\n" + f'__version__ = "{version}"\n' + ) -# Version will be written here by pip install . command -# or setup.py during build/install -# DO NOT EDIT THIS LINE - it is automatically replaced -__version__ = "{version}" # AUTO-GENERATED -''') - -# Update version file immediately when setup.py is loaded -update_version_file() +# Extract and write version files before setuptools processes pyproject.toml +_version = get_version() +write_version_files(_version) class CMakeExtension(Extension): @@ -65,14 +52,22 @@ def __init__(self, name, sourcedir=""): self.sourcedir = os.path.abspath(sourcedir) +def get_pybind11_cmake_dir(): + """Get pybind11 CMake directory from pip-installed pybind11.""" + try: + import pybind11 + + return pybind11.get_cmake_dir() + except (ImportError, AttributeError): + # Fallback to system pybind11 or let CMake find it + return None + + class CMakeBuild(build_ext): def run(self): - # Ensure version file is updated (redundant but safe) - update_version_file() - # Call parent run super().run() - + def build_extension(self, ext): extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) @@ -92,6 +87,11 @@ def build_extension(self, ext): "-DCMAKE_BUILD_TYPE=Release", ] + # Use pip-installed pybind11 if available (ensures Python version compatibility) + pybind11_dir = get_pybind11_cmake_dir() + if pybind11_dir: + cmake_args.append(f"-Dpybind11_DIR={pybind11_dir}") + subprocess.check_call(["cmake", ext.sourcedir] + cmake_args, cwd=build_temp) subprocess.check_call( ["cmake", "--build", ".", "--config", "Release", "--parallel"], @@ -113,9 +113,9 @@ def build_extension(self, ext): setup( name="pylibfranka", - version=get_version(), + version=_version, packages=["pylibfranka"], - python_requires=">=3.8", + python_requires=">=3.9", install_requires=["numpy>=1.19.0"], ext_modules=[CMakeExtension("pylibfranka._pylibfranka")], cmdclass={ @@ -123,6 +123,6 @@ def build_extension(self, ext): }, zip_safe=False, package_data={ - "pylibfranka": ["*.so", "*.pyd"], + "pylibfranka": ["*.so", "*.pyd", "VERSION"], }, ) diff --git a/src/async_control/async_position_control_handler.cpp b/src/async_control/async_position_control_handler.cpp index 3b54d538..40b20fb2 100644 --- a/src/async_control/async_position_control_handler.cpp +++ b/src/async_control/async_position_control_handler.cpp @@ -37,18 +37,13 @@ auto AsyncPositionControlHandler::configure(const std::shared_ptr& robot, result.handler = std::shared_ptr(new AsyncPositionControlHandler( std::move(active_robot_control), configuration.goal_tolerance)); - - return result; + logging::logInfo("Async position control initialized successfully."); } catch (const std::exception& e) { result.error_message = e.what(); logging::logError("Error while constructing AsyncPositionControlHandler: {}", result.error_message.value()); - - return result; } - logging::logInfo("Async position control initialized successfully."); - return result; } @@ -85,7 +80,8 @@ auto AsyncPositionControlHandler::setJointPositionTarget(const JointPositionTarg return result; } -auto AsyncPositionControlHandler::getTargetFeedback() -> TargetFeedback { +auto AsyncPositionControlHandler::getTargetFeedback(const std::optional& robot_state) + -> TargetFeedback { TargetFeedback feedback{}; if (control_status_ == TargetStatus::kAborted || active_robot_control_ == nullptr) { @@ -99,13 +95,19 @@ auto AsyncPositionControlHandler::getTargetFeedback() -> TargetFeedback { } try { - auto [robot_state, duration] = active_robot_control_->readOnce(); + if (robot_state.has_value()) { + current_robot_state_ = robot_state.value(); + } else { + std::tie(current_robot_state_, std::ignore) = active_robot_control_->readOnce(); + } - switch (robot_state.robot_mode) { + switch (current_robot_state_.robot_mode) { case RobotMode::kMove: { - Eigen::Map current_position(robot_state.q_d.data(), Robot::kNumJoints); + Eigen::Map current_position(current_robot_state_.q_d.data(), + Robot::kNumJoints); Eigen::Map target_position(target_position_.data(), Robot::kNumJoints); - Eigen::Map current_velocity(robot_state.dq_d.data(), Robot::kNumJoints); + Eigen::Map current_velocity(current_robot_state_.dq_d.data(), + Robot::kNumJoints); auto position_error = (current_position - target_position).norm(); auto current_speed = current_velocity.norm(); @@ -143,10 +145,9 @@ auto AsyncPositionControlHandler::getTargetFeedback() -> TargetFeedback { auto AsyncPositionControlHandler::stopControl() -> void { try { - // ToDo(kuhn_an): Stop motion gracefully - // auto motion_finished = JointPositions{target_position_}; - // motion_finished.motion_finished = true; - // active_robot_control_->writeOnce(motion_finished); + auto motion_finished = JointPositions{target_position_}; + motion_finished.motion_finished = true; + active_robot_control_->writeOnce(motion_finished); active_robot_control_.reset(); logging::logInfo("Async position control stopped successfully."); diff --git a/src/control_loop.h b/src/control_loop.h index 83c8e1b8..109c4bae 100644 --- a/src/control_loop.h +++ b/src/control_loop.h @@ -105,11 +105,17 @@ class ControlLoop { research_interface::robot::MotionGeneratorCommand& motion_generation_command) -> bool; private: - RobotControl& robot_; - const MotionGeneratorCallback motion_callback_; // NOLINT(readability-identifier-naming) - const ControlCallback control_callback_; // NOLINT(readability-identifier-naming) - const bool limit_rate_; // NOLINT(readability-identifier-naming) - const double cutoff_frequency_; // NOLINT(readability-identifier-naming) + RobotControl& robot_; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + const MotionGeneratorCallback + motion_callback_; /* NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members, + readability-identifier-naming) */ + const ControlCallback + control_callback_; /* NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members, + readability-identifier-naming) */ + const bool limit_rate_; /* NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members, + readability-identifier-naming) */ + const double cutoff_frequency_; /* NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members, + readability-identifier-naming) */ uint32_t motion_id_ = 0; bool initialized_filter_{false}; diff --git a/src/network.h b/src/network.h index 967f6ac7..be18e955 100644 --- a/src/network.h +++ b/src/network.h @@ -16,6 +16,13 @@ #include #include +#ifdef __APPLE__ +#include + #ifndef TCP_KEEPIDLE + #define TCP_KEEPIDLE TCP_KEEPALIVE + #endif +#endif + #include #include @@ -254,7 +261,7 @@ typename T::Response Network::tcpBlockingReceiveResponse(uint32_t command_id, using namespace std::literals::chrono_literals; // NOLINT(google-build-using-namespace) std::unique_lock lock(tcp_mutex_, std::defer_lock); decltype(received_responses_)::const_iterator received_response; - do { + do { // NOLINT(cppcoreguidelines-avoid-do-while) lock.lock(); tcpReadFromBuffer(kTimeout); received_response = received_responses_.find(command_id); @@ -287,7 +294,7 @@ Network::tcpBlockingReceiveResponse( using namespace std::literals::chrono_literals; // NOLINT(google-build-using-namespace) std::unique_lock lock(tcp_mutex_, std::defer_lock); decltype(received_responses_)::const_iterator received_response; - do { + do { // NOLINT(cppcoreguidelines-avoid-do-while) lock.lock(); tcpReadFromBuffer(kTimeout); received_response = received_responses_.find(command_id); diff --git a/src/robot.cpp b/src/robot.cpp index e3fd1fb2..3a31c4e2 100644 --- a/src/robot.cpp +++ b/src/robot.cpp @@ -182,9 +182,6 @@ void Robot::read(std::function read_callback) { } RobotState Robot::readOnce() { - std::unique_lock control_lock(control_mutex_, std::try_to_lock); - assertOwningLock(control_lock); - return impl_->readOnce(); } @@ -349,7 +346,7 @@ Model Robot::loadModel(std::unique_ptr robot_model) { return impl_->loadModel(std::move(robot_model)); } -Robot::Robot(std::shared_ptr robot_impl) : impl_(std::move(robot_impl)){}; +Robot::Robot(std::shared_ptr robot_impl) : impl_(std::move(robot_impl)) {}; template std::unique_ptr Robot::startControl( const research_interface::robot::Move::ControllerMode& controller_type); diff --git a/src/robot_impl.cpp b/src/robot_impl.cpp index 82b2fd5c..646f8506 100644 --- a/src/robot_impl.cpp +++ b/src/robot_impl.cpp @@ -144,7 +144,10 @@ research_interface::robot::RobotCommand Robot::Impl::sendRobotCommand( return robot_command; } - robot_command.message_id = message_id_; + { + std::lock_guard lock(message_id_mutex_); + robot_command.message_id = message_id_; + } if (motion_command.has_value()) { if (current_move_motion_generator_mode_ == research_interface::robot::MotionGeneratorMode::kIdle || @@ -182,7 +185,12 @@ research_interface::robot::RobotCommand Robot::Impl::sendRobotCommand( research_interface::robot::RobotState Robot::Impl::receiveRobotState() { research_interface::robot::RobotState latest_accepted_state; - latest_accepted_state.message_id = message_id_; + auto last_message_id = 0U; + { + std::lock_guard lock(message_id_mutex_); + latest_accepted_state.message_id = message_id_; + last_message_id = message_id_; + } // If states are already available on the socket, use the one with the most recent message ID. research_interface::robot::RobotState received_state{}; @@ -193,7 +201,7 @@ research_interface::robot::RobotState Robot::Impl::receiveRobotState() { } // If there was no valid state on the socket, we need to wait. - while (latest_accepted_state.message_id == message_id_) { + while (latest_accepted_state.message_id == last_message_id) { received_state = network_->udpBlockingReceive(); if (received_state.message_id > latest_accepted_state.message_id) { latest_accepted_state = received_state; @@ -208,7 +216,11 @@ void Robot::Impl::updateState(const research_interface::robot::RobotState& robot robot_mode_ = robot_state.robot_mode; motion_generator_mode_ = robot_state.motion_generator_mode; controller_mode_ = robot_state.controller_mode; - message_id_ = robot_state.message_id; + + { + std::lock_guard lock(message_id_mutex_); + message_id_ = robot_state.message_id; + } } Robot::ServerVersion Robot::Impl::serverVersion() const noexcept { @@ -454,7 +466,7 @@ void Robot::Impl::cancelMotion(uint32_t motion_id) { } research_interface::robot::RobotState robot_state; - do { + do { // NOLINT(cppcoreguidelines-avoid-do-while) robot_state = receiveRobotState(); } while (motionGeneratorRunning() || controllerRunning()); diff --git a/src/robot_impl.h b/src/robot_impl.h index 2ec31a32..9ddb3593 100644 --- a/src/robot_impl.h +++ b/src/robot_impl.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include #include @@ -238,6 +239,7 @@ class Robot::Impl : public RobotControl { bool controllerRunning() const noexcept; private: + mutable std::mutex message_id_mutex_; RobotState current_state_; template diff --git a/src/robot_model.cpp b/src/robot_model.cpp index c59eb44d..a3780a9b 100644 --- a/src/robot_model.cpp +++ b/src/robot_model.cpp @@ -1,8 +1,26 @@ // Copyright (c) 2025 Franka Robotics GmbH // Use of this source code is governed by the Apache-2.0 license, see LICENSE #include "franka/robot_model.h" + #include +// Ignore warnings from Pinocchio includes +#if defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + namespace franka { RobotModel::RobotModel(const std::string& urdf) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6d010234..1a688ecc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -100,7 +100,11 @@ get_target_property(TESTS run_all_tests SOURCES) set(ASan_FLAGS -g -O1 -fsanitize=address) set(UBSan_FLAGS -g -O1 -fsanitize=undefined -fno-sanitize=alignment -fno-sanitize-recover=undefined) set(TSan_FLAGS -g -O1 -fsanitize=thread) -set(SANITIZERS ASan UBSan TSan) +set(SANITIZERS + ASan + UBSan + # TSan # ThreadSanitizer tests currently not supported due g++ ASLR issues on latest Linux kernel (6.5+). This is fixed with clang, consider enabling if we switch to clang +) foreach(sanitizer ${SANITIZERS}) set(sanitizer_flags "${${sanitizer}_FLAGS}") diff --git a/test/active_motion_generator_tests.cpp b/test/active_motion_generator_tests.cpp index b536cac4..ee7ccb6f 100644 --- a/test/active_motion_generator_tests.cpp +++ b/test/active_motion_generator_tests.cpp @@ -27,7 +27,7 @@ class ActiveMotionGeneratorTest : public ::testing::Test { std::move(std::make_unique("127.0.0.1", robot::kCommandPort)), 0, RealtimeConfig::kIgnore)), - robot_(RobotMock(robot_impl_mock_)){}; + robot_(RobotMock(robot_impl_mock_)) {}; std::unique_ptr startControl( research_interface::robot::Move::ControllerMode controller_mode); diff --git a/test/active_torque_control_tests.cpp b/test/active_torque_control_tests.cpp index b0f8b426..f0e11c8a 100644 --- a/test/active_torque_control_tests.cpp +++ b/test/active_torque_control_tests.cpp @@ -26,7 +26,7 @@ class ActiveTorqueControlTest : public ::testing::Test { std::move(std::make_unique("127.0.0.1", robot::kCommandPort)), 0, RealtimeConfig::kIgnore)), - robot(RobotMock(robot_impl_mock)){}; + robot(RobotMock(robot_impl_mock)) {}; std::unique_ptr startTorqueControl() { EXPECT_CALL(*robot_impl_mock, diff --git a/test/control_loop_tests.cpp b/test/control_loop_tests.cpp index 073bb6e1..5af99076 100644 --- a/test/control_loop_tests.cpp +++ b/test/control_loop_tests.cpp @@ -756,14 +756,12 @@ TYPED_TEST(ControlLoops, CanNotConstructWithoutMotionCallback) { StrictMock robot; setupJointVelocityLimitsMock(robot); - EXPECT_THROW(typename TestFixture::Loop loop( - robot, - [](const RobotState&, Duration) { - return Torques({0, 1, 2, 3, 4, 5, 6}); - }, - typename TestFixture::MotionGeneratorCallback(), TestFixture::kLimitRate, - getCutoffFreq(TestFixture::kFilter)), - std::invalid_argument); + EXPECT_THROW( + typename TestFixture::Loop loop( + robot, [](const RobotState&, Duration) { return Torques({0, 1, 2, 3, 4, 5, 6}); }, + typename TestFixture::MotionGeneratorCallback(), TestFixture::kLimitRate, + getCutoffFreq(TestFixture::kFilter)), + std::invalid_argument); EXPECT_THROW( typename TestFixture::Loop loop(robot, ControllerMode::kCartesianImpedance, @@ -792,10 +790,7 @@ TYPED_TEST(ControlLoops, CanConstructWithMotionAndControllerCallback) { .WillOnce(Return(100)); EXPECT_NO_THROW(typename TestFixture::Loop( - robot, - [](const RobotState&, Duration) { - return Torques({0, 1, 2, 3, 4, 5, 6}); - }, + robot, [](const RobotState&, Duration) { return Torques({0, 1, 2, 3, 4, 5, 6}); }, std::bind(&TestFixture::createMotion, this), TestFixture::kLimitRate, getCutoffFreq(TestFixture::kFilter))); } @@ -823,10 +818,7 @@ TYPED_TEST(ControlLoops, CanConstructWithControlCallback) { .WillOnce(Return(200)); EXPECT_NO_THROW(typename TestFixture::Loop( - robot, - [](const RobotState&, Duration) { - return Torques({0, 1, 2, 3, 4, 5, 6}); - }, + robot, [](const RobotState&, Duration) { return Torques({0, 1, 2, 3, 4, 5, 6}); }, TestFixture::kLimitRate, getCutoffFreq(TestFixture::kFilter))); } diff --git a/test/mock_robot.h b/test/mock_robot.h index c36f4773..a1921bea 100644 --- a/test/mock_robot.h +++ b/test/mock_robot.h @@ -9,5 +9,5 @@ using namespace franka; class RobotMock : public Robot { public: ~RobotMock() = default; - RobotMock(std::shared_ptr robot_impl) : Robot(robot_impl){}; + RobotMock(std::shared_ptr robot_impl) : Robot(robot_impl) {}; }; diff --git a/test/mock_robot_impl.h b/test/mock_robot_impl.h index 40fa13b7..8e9c8f51 100644 --- a/test/mock_robot_impl.h +++ b/test/mock_robot_impl.h @@ -13,7 +13,7 @@ class RobotImplMock : public Robot::Impl { public: virtual ~RobotImplMock() = default; RobotImplMock(std::unique_ptr network, size_t log_size, RealtimeConfig realtime_config) - : Robot::Impl(std::move(network), log_size, realtime_config){}; + : Robot::Impl(std::move(network), log_size, realtime_config) {}; MOCK_METHOD(uint32_t, startMotion, (research_interface::robot::Move::ControllerMode, diff --git a/test/robot_command_tests.cpp b/test/robot_command_tests.cpp index e36cade1..5ffadef2 100644 --- a/test/robot_command_tests.cpp +++ b/test/robot_command_tests.cpp @@ -30,6 +30,8 @@ using research_interface::robot::SetNEToEE; using research_interface::robot::StopMove; const std::string kExpectedModelString = "test_string"; +constexpr research_interface::robot::Move::Deviation kDefaultDeviation1(1, 2, 3); +constexpr research_interface::robot::Move::Deviation kDefaultDeviation2(4, 5, 6); template class Command : public ::testing::Test { @@ -152,8 +154,8 @@ bool Command::compare(const StopMove::Request&, const StopMove::Reques template <> Move::Request Command::getExpected() { return Move::Request(Move::ControllerMode::kJointImpedance, - Move::MotionGeneratorMode::kJointVelocity, Move::Deviation(1, 2, 3), - Move::Deviation(4, 5, 6)); + Move::MotionGeneratorMode::kJointVelocity, kDefaultDeviation1, + kDefaultDeviation2); } template <> @@ -390,8 +392,8 @@ TEST_F(MoveCommand, CanReceiveMotionStarted) { franka::RealtimeConfig::kIgnore); Move::Request request(Move::ControllerMode::kJointImpedance, - Move::MotionGeneratorMode::kJointVelocity, Move::Deviation(1, 2, 3), - Move::Deviation(4, 5, 6)); + Move::MotionGeneratorMode::kJointVelocity, kDefaultDeviation1, + kDefaultDeviation2); server .waitForCommand( @@ -412,8 +414,8 @@ TEST_P(MoveCommand, CanReceiveErrorResponses) { franka::RealtimeConfig::kIgnore); Move::Request request(Move::ControllerMode::kJointImpedance, - Move::MotionGeneratorMode::kJointVelocity, Move::Deviation(1, 2, 3), - Move::Deviation(4, 5, 6)); + Move::MotionGeneratorMode::kJointVelocity, kDefaultDeviation1, + kDefaultDeviation2); server .waitForCommand<::Move>([this](const Move::Request& request) -> Move::Response { diff --git a/test/robot_tests.cpp b/test/robot_tests.cpp index 52b8b228..dac6495a 100644 --- a/test/robot_tests.cpp +++ b/test/robot_tests.cpp @@ -437,9 +437,11 @@ TEST_F(RobotTests, ThrowsIfConflictingOperationIsRunning) { InvalidOperationException); EXPECT_THROW(default_robot.read(std::function()), InvalidOperationException); - EXPECT_THROW(default_robot.readOnce(), InvalidOperationException); EXPECT_THROW(default_robot.startTorqueControl(), InvalidOperationException); + // Technically, readOnce is possible but due to the other call blocking, it will time out. + EXPECT_THROW(default_robot.readOnce(), NetworkException); + default_server.ignoreUdpBuffer(); run = false;