From 7e4c955a52e776ac73a030b458e5bef79c88ef0a Mon Sep 17 00:00:00 2001 From: Julien Enoch Date: Fri, 29 Sep 2023 16:29:44 +0200 Subject: [PATCH] First pull request #1 --- .cargo/config | 5 + .github/ISSUE_TEMPLATE/bug_report.yml | 38 + .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.yml | 26 + .github/release.yml | 25 + .github/workflows/Dockerfile | 42 + .github/workflows/release.yml | 349 ++ .github/workflows/rust.yml | 86 + .gitignore | 21 + CMakeLists.txt | 63 + CONTRIBUTING.md | 51 + CONTRIBUTORS.md | 8 + Cargo.lock | 4556 +++++++++++++++++ Cargo.toml | 64 + Cross.toml | 18 + DEFAULT_CONFIG.json5 | 147 + LICENSE | 459 ++ NOTICE.md | 41 + README.md | 7 +- package.xml | 38 + rust-toolchain.toml | 2 + zenoh-bridge-ros2dds/.deb/postinst | 42 + zenoh-bridge-ros2dds/.deb/postrm | 39 + .../.service/zenoh-bridge-ros2.service | 23 + zenoh-bridge-ros2dds/Cargo.toml | 72 + zenoh-bridge-ros2dds/src/main.rs | 294 ++ zenoh-plugin-ros2dds/Cargo.toml | 67 + zenoh-plugin-ros2dds/README.md | 5 + zenoh-plugin-ros2dds/build.rs | 21 + zenoh-plugin-ros2dds/src/config.rs | 347 ++ zenoh-plugin-ros2dds/src/dds_discovery.rs | 738 +++ .../src/discovered_entities.rs | 475 ++ zenoh-plugin-ros2dds/src/discovery_mgr.rs | 148 + zenoh-plugin-ros2dds/src/events.rs | 162 + zenoh-plugin-ros2dds/src/gid.rs | 179 + zenoh-plugin-ros2dds/src/lib.rs | 578 +++ zenoh-plugin-ros2dds/src/liveliness_mgt.rs | 257 + zenoh-plugin-ros2dds/src/node_info.rs | 1868 +++++++ zenoh-plugin-ros2dds/src/qos_helpers.rs | 138 + zenoh-plugin-ros2dds/src/ros2_utils.rs | 148 + zenoh-plugin-ros2dds/src/ros_discovery.rs | 644 +++ zenoh-plugin-ros2dds/src/route_publisher.rs | 344 ++ zenoh-plugin-ros2dds/src/route_subscriber.rs | 469 ++ zenoh-plugin-ros2dds/src/routes_mgr.rs | 485 ++ 44 files changed, 13592 insertions(+), 2 deletions(-) create mode 100644 .cargo/config create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/release.yml create mode 100644 .github/workflows/Dockerfile create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/rust.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTORS.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Cross.toml create mode 100644 DEFAULT_CONFIG.json5 create mode 100644 LICENSE create mode 100644 NOTICE.md create mode 100644 package.xml create mode 100644 rust-toolchain.toml create mode 100644 zenoh-bridge-ros2dds/.deb/postinst create mode 100644 zenoh-bridge-ros2dds/.deb/postrm create mode 100644 zenoh-bridge-ros2dds/.service/zenoh-bridge-ros2.service create mode 100644 zenoh-bridge-ros2dds/Cargo.toml create mode 100644 zenoh-bridge-ros2dds/src/main.rs create mode 100644 zenoh-plugin-ros2dds/Cargo.toml create mode 100644 zenoh-plugin-ros2dds/README.md create mode 100644 zenoh-plugin-ros2dds/build.rs create mode 100644 zenoh-plugin-ros2dds/src/config.rs create mode 100644 zenoh-plugin-ros2dds/src/dds_discovery.rs create mode 100644 zenoh-plugin-ros2dds/src/discovered_entities.rs create mode 100644 zenoh-plugin-ros2dds/src/discovery_mgr.rs create mode 100644 zenoh-plugin-ros2dds/src/events.rs create mode 100644 zenoh-plugin-ros2dds/src/gid.rs create mode 100644 zenoh-plugin-ros2dds/src/lib.rs create mode 100644 zenoh-plugin-ros2dds/src/liveliness_mgt.rs create mode 100644 zenoh-plugin-ros2dds/src/node_info.rs create mode 100644 zenoh-plugin-ros2dds/src/qos_helpers.rs create mode 100644 zenoh-plugin-ros2dds/src/ros2_utils.rs create mode 100644 zenoh-plugin-ros2dds/src/ros_discovery.rs create mode 100644 zenoh-plugin-ros2dds/src/route_publisher.rs create mode 100644 zenoh-plugin-ros2dds/src/route_subscriber.rs create mode 100644 zenoh-plugin-ros2dds/src/routes_mgr.rs diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..5adae5e --- /dev/null +++ b/.cargo/config @@ -0,0 +1,5 @@ +[target.x86_64-unknown-linux-musl] +rustflags = "-Ctarget-feature=-crt-static" + +[target.aarch64-unknown-linux-musl] +rustflags = "-Ctarget-feature=-crt-static" diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..3f870b7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,38 @@ +name: Report a bug +description: | + Create a bug report to help us improve Zenoh. +title: "[Bug] " +labels: ["bug"] +body: + - type: textarea + id: summary + attributes: + label: "Describe the bug" + description: | + A clear and concise description of the expected behaviour and what the bug is. + placeholder: | + E.g. zenoh peers can not automatically establish a connection. + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: To reproduce + description: "Steps to reproduce the behavior:" + placeholder: | + 1. Start a zenoh-bridge-ros2dds with config "..." + 2. Start a ROS2 node "...." + 3. See error + validations: + required: true + - type: textarea + id: system + attributes: + label: System info + description: "Please complete the following information:" + placeholder: | + - Platform: [e.g. Ubuntu 20.04 64-bit] + - CPU [e.g. AMD Ryzen 3800X] + - Zenoh version/commit [e.g. 6f172ea985d42d20d423a192a2d0d46bb0ce0d11] + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0c05a05 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://github.com/eclipse-zenoh/roadmap/discussions/categories/zenoh + about: Open to the Zenoh community. Share your feedback with the Zenoh team. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..811540e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,26 @@ +name: Request a feature +description: | + Suggest a new feature specific to this repository. NOTE: for generic Zenoh ideas use "Ask a question". +body: + - type: markdown + attributes: + value: | + **Guidelines for a good issue** + + *Is your feature request related to a problem?* + A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + + *Describe the solution you'd like* + A clear and concise description of what you want to happen. + + *Describe alternatives you've considered* + A clear and concise description of any alternative solutions or features you've considered. + + *Additional context* + Add any other context about the feature request here. + - type: textarea + id: feature + attributes: + label: "Describe the feature" + validations: + required: true diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..c402202 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,25 @@ +# +# Copyright (c) 2023 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# + +changelog: + categories: + - title: New features 🎉 + labels: + - enhancement + - title: Bug fixes 🐞 + labels: + - bug + - title: Other changes + labels: + - "*" diff --git a/.github/workflows/Dockerfile b/.github/workflows/Dockerfile new file mode 100644 index 0000000..4855a16 --- /dev/null +++ b/.github/workflows/Dockerfile @@ -0,0 +1,42 @@ +# +# Copyright (c) 2022 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# + +### +### Dockerfile creating the eclipse/zenoh-bridge-ros2dds image from cross-compiled binaries. +### It assumes that zenoh-bridge-ros2dds is installed in docker/$TARGETPLATFORM/ +### where $TARGETPLATFORM is set by buildx to a Docker supported platform such as linux/amd64 or linux/arm64 +### (see https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images) +### + + +FROM alpine:latest + +ARG TARGETPLATFORM + +RUN apk add --no-cache libgcc libstdc++ + +COPY docker/$TARGETPLATFORM/zenoh-bridge-ros2dds / + +RUN echo '#!/bin/ash' > /entrypoint.sh +RUN echo 'echo " * Starting: /zenoh-bridge-ros2dds $*"' >> /entrypoint.sh +RUN echo 'exec /zenoh-bridge-ros2dds $*' >> /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 7446/udp +EXPOSE 7447/tcp +EXPOSE 8000/tcp + +ENV RUST_LOG info + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..aa42e83 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,349 @@ +# +# Copyright (c) 2022 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# +name: Release + +on: + release: + types: [published] + schedule: + - cron: "0 1 * * 1-5" + workflow_dispatch: + +jobs: + checks: + name: Code checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Rust toolchain + run: | + rustup show + rustup component add rustfmt clippy + - name: Code format check + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -- --check + # - name: Clippy check + # uses: actions-rs/cargo@v1 + # with: + # command: clippy + # args: -- -D warnings + - name: Environment setup + id: env + shell: bash + run: | + # log some info + gcc --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + echo "GITHUB_REF=${GITHUB_REF}" + echo "GITHUB_SHA=${GITHUB_SHA:0:8}" + GIT_BRANCH=`[[ $GITHUB_REF =~ ^refs/heads/.* ]] && echo ${GITHUB_REF/refs\/heads\//} || true` + echo "GIT_BRANCH=${GIT_BRANCH}" >> $GITHUB_OUTPUT + GIT_TAG=`[[ $GITHUB_REF =~ ^refs/tags/.* ]] && echo ${GITHUB_REF/refs\/tags\//} || true` + echo "GIT_TAG=${GIT_TAG}" >> $GITHUB_OUTPUT + + sudo apt-get update + sudo apt-get install -y jq + + ZENOH_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "zenoh-plugin-ros2dds") | .version') + echo "ZENOH_VERSION=${ZENOH_VERSION}" >> $GITHUB_OUTPUT + if [ -n "${GIT_TAG}" ]; then + IS_RELEASE="true" + echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT + PKG_VERSION=${ZENOH_VERSION} + elif [ -n "${GIT_BRANCH}" ]; then + PKG_VERSION=${GIT_BRANCH}-${GITHUB_SHA:0:8} + else + PKG_VERSION=${ZENOH_VERSION}-${GITHUB_SHA:0:8} + fi + echo "PKG_VERSION=${PKG_VERSION}" >> $GITHUB_OUTPUT + cat ${GITHUB_OUTPUT} + outputs: + GIT_BRANCH: ${{ steps.env.outputs.GIT_BRANCH }} + GIT_TAG: ${{ steps.env.outputs.GIT_TAG }} + IS_RELEASE: ${{ steps.env.outputs.IS_RELEASE }} + ZENOH_VERSION: ${{ steps.env.outputs.ZENOH_VERSION }} + PKG_VERSION: ${{ steps.env.outputs.PKG_VERSION }} + + builds: + name: Build for ${{ matrix.job.target }} on ${{ matrix.job.os }} + needs: checks + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { target: x86_64-unknown-linux-gnu, arch: amd64, os: ubuntu-22.04 } + - { target: x86_64-apple-darwin, arch: darwin, os: macos-12 } + - { target: aarch64-apple-darwin, arch: darwin, os: macos-12 } + - { + target: x86_64-unknown-linux-musl, + arch: amd64, + os: ubuntu-22.04, + use-cross: true, + } + - { + target: arm-unknown-linux-gnueabi, + arch: armel, + os: ubuntu-22.04, + use-cross: true, + } + - { + target: arm-unknown-linux-gnueabihf, + arch: armhf, + os: ubuntu-22.04, + use-cross: true, + } + - { + target: armv7-unknown-linux-gnueabihf, + arch: armhf, + os: ubuntu-22.04, + use-cross: true, + } + - { + target: aarch64-unknown-linux-gnu, + arch: arm64, + os: ubuntu-22.04, + use-cross: true, + } + - { + target: aarch64-unknown-linux-musl, + arch: arm64, + os: ubuntu-22.04, + use-cross: true, + } + - { target: x86_64-pc-windows-msvc, arch: win64, os: windows-2022 } + ## + ## NOTE: cannon build for Windows GNU as not supported by cyclors + ## + # - { target: x86_64-pc-windows-gnu, arch: win64 , os: windows-2022 } + steps: + - name: Checkout source code + uses: actions/checkout@v2 + with: + fetch-depth: 500 # NOTE: get long history for git-version crate to correctly compute a version + - name: Fetch Git tags # NOTE: workaround for https://github.com/actions/checkout/issues/290 + shell: bash + run: git fetch --tags --force + - name: Install prerequisites + shell: bash + run: | + case ${{ matrix.job.target }} in + *-linux-gnu*) cargo install cargo-deb;; + esac + + case ${{ matrix.job.target }} in + arm-unknown-linux-gnueabi) + sudo apt-get -y update + sudo apt-get -y install gcc-arm-linux-gnueabi + ;; + arm*-unknown-linux-gnueabihf) + sudo apt-get -y update + sudo apt-get -y install gcc-arm-linux-gnueabihf + ;; + aarch64-unknown-linux-gnu) + sudo apt-get -y update + sudo apt-get -y install gcc-aarch64-linux-gnu + ;; + esac + + - name: Install Rust toolchain + run: | + rustup show + rustup target add ${{ matrix.job.target }} + + - name: zenoh-plugin-ros2dds > Build + uses: actions-rs/cargo@v1 + with: + use-cross: ${{ matrix.job.use-cross }} + command: build + args: --release --target=${{ matrix.job.target }} -p zenoh-plugin-ros2dds + + - name: zenoh-bridge-ros2dds > Build + uses: actions-rs/cargo@v1 + with: + use-cross: ${{ matrix.job.use-cross }} + command: build + args: --release --target=${{ matrix.job.target }} -p zenoh-bridge-ros2dds + + - name: zenoh-plugin-ros2dds > Debian package + if: contains(matrix.job.target, '-linux-gnu') + uses: actions-rs/cargo@v1 + with: + command: deb + args: --no-build --target=${{ matrix.job.target }} -p zenoh-plugin-ros2dds + + - name: zenoh-bridge-ros2dds > Debian package + if: contains(matrix.job.target, '-linux-gnu') + uses: actions-rs/cargo@v1 + with: + command: deb + args: --no-build --target=${{ matrix.job.target }} -p zenoh-bridge-ros2dds + + - name: Packaging + id: package + shell: bash + run: | + TARGET=${{ matrix.job.target }} + LIB_PKG_NAME="${GITHUB_WORKSPACE}/zenoh-plugin-ros2dds-${{ needs.checks.outputs.PKG_VERSION }}-${TARGET}.zip" + BIN_PKG_NAME="${GITHUB_WORKSPACE}/zenoh-bridge-ros2dds-${{ needs.checks.outputs.PKG_VERSION }}-${TARGET}.zip" + DEBS_PKG_NAME="${GITHUB_WORKSPACE}/zenoh-plugin-ros2dds-${{ needs.checks.outputs.PKG_VERSION }}-${TARGET}-deb-pkgs.zip" + + case ${TARGET} in + *linux*) + cd "target/${TARGET}/release/" + echo "Packaging ${LIB_PKG_NAME}:" + zip ${LIB_PKG_NAME} libzenoh_plugin*.so + echo "Packaging ${BIN_PKG_NAME}:" + zip ${BIN_PKG_NAME} zenoh-bridge-ros2dds + cd - + echo "LIB_PKG_NAME=${LIB_PKG_NAME}" >> $GITHUB_OUTPUT + echo "BIN_PKG_NAME=${BIN_PKG_NAME}" >> $GITHUB_OUTPUT + + # check if debian packages has been created and packages them in a single tgz + if [[ -d target/${TARGET}/debian ]]; then + cd target/${TARGET}/debian + echo "Packaging ${DEBS_PKG_NAME}:" + zip ${DEBS_PKG_NAME} *.deb + cd - + echo "DEBS_PKG_NAME=${DEBS_PKG_NAME}" >> $GITHUB_OUTPUT + fi + ;; + *apple*) + cd "target/${TARGET}/release/" + echo "Packaging ${LIB_PKG_NAME}:" + zip ${LIB_PKG_NAME} libzenoh_plugin*.dylib + echo "Packaging ${BIN_PKG_NAME}:" + zip ${BIN_PKG_NAME} zenoh-bridge-ros2dds + cd - + echo "LIB_PKG_NAME=${LIB_PKG_NAME}" >> $GITHUB_OUTPUT + echo "BIN_PKG_NAME=${BIN_PKG_NAME}" >> $GITHUB_OUTPUT + ;; + *windows*) + cd "target/${TARGET}/release/" + echo "Packaging ${LIB_PKG_NAME}:" + 7z -y a "${LIB_PKG_NAME}" zenoh-plugin*.dll + echo "Packaging ${BIN_PKG_NAME}:" + 7z -y a "${BIN_PKG_NAME}" zenoh-bridge-ros2dds.exe + cd - + echo "LIB_PKG_NAME=${LIB_PKG_NAME}" >> $GITHUB_OUTPUT + echo "BIN_PKG_NAME=${BIN_PKG_NAME}" >> $GITHUB_OUTPUT + ;; + esac + + - name: "Upload packages" + uses: actions/upload-artifact@master + with: + name: ${{ matrix.job.target }} + path: | + ${{ steps.package.outputs.LIB_PKG_NAME }} + ${{ steps.package.outputs.BIN_PKG_NAME }} + ${{ steps.package.outputs.DEBS_PKG_NAME }} + + docker-build: + name: Docker build and push + needs: [checks, builds] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 500 # NOTE: get long history for git-version crate to correctly compute a version + - name: Fetch Git tags # NOTE: workaround for https://github.com/actions/checkout/issues/290 + shell: bash + run: git fetch --tags --force + - name: Download packages from previous job + uses: actions/download-artifact@v2 + with: + path: PACKAGES + - name: Unzip PACKAGES + run: | + ls PACKAGES + mkdir -p docker/linux/amd + unzip PACKAGES/x86_64-unknown-linux-musl/zenoh-bridge-ros2dds-${{ needs.checks.outputs.PKG_VERSION }}-x86_64-unknown-linux-musl.zip -d docker/linux/amd64/ + mkdir -p docker/linux/arm64 + unzip PACKAGES/aarch64-unknown-linux-musl/zenoh-bridge-ros2dds-${{ needs.checks.outputs.PKG_VERSION }}-aarch64-unknown-linux-musl.zip -d docker/linux/arm64/ + tree docker + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Docker meta - set tags and labels + id: meta + uses: docker/metadata-action@v3 + with: + images: jenoch/zenoh-bridge-ros2dds + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_COM_USERNAME }} + password: ${{ secrets.DOCKER_COM_PASSWORD }} + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + platforms: linux/amd64,linux/arm64 + file: .github/workflows/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + publication: + name: Release publication + if: needs.checks.outputs.IS_RELEASE == 'true' + needs: [checks, builds] + runs-on: ubuntu-latest + steps: + - name: Download result of previous builds + uses: actions/download-artifact@v2 + with: + path: ARTIFACTS + - name: Publish as github release + uses: softprops/action-gh-release@v1 + with: + files: ARTIFACTS/*/*.* + - name: Publish to download.eclipse.org/zenoh + env: + SSH_TARGET: genie.zenoh@projects-storage.eclipse.org + ECLIPSE_BASE_DIR: /home/data/httpd/download.eclipse.org/zenoh/zenoh-plugin-ros2dds + shell: bash + run: | + echo "--- setup ssh-agent" + eval "$(ssh-agent -s)" + echo 'echo "${{ secrets.SSH_PASSPHRASE }}"' > ~/.ssh_askpass && chmod +x ~/.ssh_askpass + echo "${{ secrets.SSH_PRIVATE_KEY }}" | tr -d '\r' | DISPLAY=NONE SSH_ASKPASS=~/.ssh_askpass ssh-add - > /dev/null 2>&1 + rm -f ~/.ssh_askpass + echo "--- test ssh:" + ssh -o "StrictHostKeyChecking=no" ${SSH_TARGET} ls -al + echo "---- list artifacts to upload:" + ls -R ARTIFACTS || true + DOWNLOAD_DIR=${ECLIPSE_BASE_DIR}/${{ needs.checks.outputs.ZENOH_VERSION }} + echo "---- copy artifacts into ${DOWNLOAD_DIR}" + ssh -o "StrictHostKeyChecking=no" ${SSH_TARGET} mkdir -p ${DOWNLOAD_DIR} + cd ARTIFACTS + sha256sum */* > sha256sums.txt + scp -o "StrictHostKeyChecking=no" -r * ${SSH_TARGET}:${DOWNLOAD_DIR}/ + echo "---- cleanup identity" + ssh-add -D + - uses: actions/checkout@v2 + - name: Install Rust toolchain + run: rustup show + - name: Publish to crates.io + shell: bash + run: | + cargo login ${{ secrets.CRATES_IO_TOKEN }} + (cd zenoh-plugin-ros2dds && cargo publish) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..12ad413 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,86 @@ +# +# Copyright (c) 2022 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# +name: Rust + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: Build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + + steps: + - uses: actions/checkout@v2 + + - name: Install ACL + if: startsWith(matrix.os,'ubuntu') + run: sudo apt-get -y install libacl1-dev + + - name: Install Rust toolchain + run: | + rustup show + rustup component add rustfmt clippy + + - name: Code format check + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -- --check + + # - name: Clippy + # uses: actions-rs/cargo@v1 + # with: + # command: clippy + # args: --all --examples -- -D warnings + + - name: Build zenoh-plugin-ros2dds + uses: actions-rs/cargo@v1 + with: + command: build + args: -p zenoh-plugin-ros2dds --verbose --all-targets + + - name: Build zenoh-plugin-ros2dds (with dds_shm) + uses: actions-rs/cargo@v1 + with: + command: build + args: -p zenoh-plugin-ros2dds --features dds_shm --verbose --all-targets + + - name: Build zenoh-bridge-ros2dds + uses: actions-rs/cargo@v1 + with: + command: build + args: -p zenoh-bridge-ros2dds --verbose --all-targets + + - name: Build zenoh-bridge-ros2dds (with dds_shm) + uses: actions-rs/cargo@v1 + with: + command: build + args: -p zenoh-bridge-ros2dds --features dds_shm --verbose --all-targets + + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee7721b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Generated by Cargo +# will have compiled files and executables +**/target + +# Ignore all Cargo.lock but one at top-level, since it's committed in git. +*/**/Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# CLion project directory +.idea + +# Emacs temps +*~ + +# MacOS Related +.DS_Store + +.vscode + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..419b639 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,63 @@ +# +# Copyright (c) 2022 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# + +# ROS2 package build file (waiting for an "ament_rust" built type) +cmake_minimum_required(VERSION 3.8) +project(zenoh_bridge_ros2dds) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# find dependencies +find_package(ament_cmake REQUIRED) + +# uncomment the following section in order to fill in +# further dependencies manually. +# find_package( REQUIRED) +set(RUST_TARGET_DIR ${CMAKE_BINARY_DIR}/target) + +FILE(GLOB RUST_SRC_FILES + ${CMAKE_SOURCE_DIR}/zenoh-bridge-ros2dds/src/*.rs + ${CMAKE_SOURCE_DIR}/zenoh-plugin-ros2dds/src/*.rs + ${CMAKE_SOURCE_DIR}/Cargo.lock) + +add_custom_command( + OUTPUT + ${RUST_TARGET_DIR}/release/zenoh-bridge-ros2dds + COMMAND cargo build --release --manifest-path "${CMAKE_SOURCE_DIR}/zenoh-bridge-ros2dds/Cargo.toml" --target-dir "${RUST_TARGET_DIR}" + DEPENDS + ${RUST_SRC_FILES} +) + +add_custom_target( + build_crate ALL + DEPENDS + ${RUST_TARGET_DIR}/release/zenoh-bridge-ros2dds +) + +macro(INSTALL_ZENOH src_path dst_path) + install(FILES + ${src_path} + RENAME zenoh_bridge_ros2dds + PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE + DESTINATION ${dst_path} + ) +endmacro(INSTALL_ZENOH) + +INSTALL_ZENOH(${RUST_TARGET_DIR}/release/zenoh-bridge-ros2dds lib/${PROJECT_NAME}) + +INSTALL_ZENOH(${RUST_TARGET_DIR}/release/zenoh-bridge-ros2dds bin) + +ament_package() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1dd0b82 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing to Eclipse zenoh + +Thanks for your interest in this project. + +## Project description + +Eclipse zenoh provides is a stack designed to + 1. minimize network overhead, + 2. support extremely constrained devices, + 3. supports devices with low duty-cycle by allowing the negotiation of data exchange modes and schedules, + 4. provide a rich set of abstraction for distributing, querying and storing data along the entire system, and + 5. provide extremely low latency and high throughput. + +* https://projects.eclipse.org/projects/iot.zenoh + +## Developer resources + +Information regarding source code management, builds, coding standards, and +more. + +* https://projects.eclipse.org/projects/iot.zenoh/developer + +The project maintains the following source code repositories + +* https://github.com/eclipse-zenoh + +## Eclipse Contributor Agreement + +Before your contribution can be accepted by the project team contributors must +electronically sign the Eclipse Contributor Agreement (ECA). + +* http://www.eclipse.org/legal/ECA.php + +Commits that are provided by non-committers must have a Signed-off-by field in +the footer indicating that the author is aware of the terms by which the +contribution has been provided to the project. The non-committer must +additionally have an Eclipse Foundation account and must have a signed Eclipse +Contributor Agreement (ECA) on file. + +For more information, please see the Eclipse Committer Handbook: +https://www.eclipse.org/projects/handbook/#resources-commit + +## Contact + +Contact the project developers via the project's "dev" list. + +* https://accounts.eclipse.org/mailing-list/zenoh-dev + +Or via the Gitter channel. + +* https://gitter.im/atolab/zenoh diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..cec4c45 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,8 @@ +# Contributors to Eclipse zenoh-plugin-ros2dds + +These are the contributors to Eclipse zenoh (the initial contributors and the contributors listed in the Git log). + + +| GitHub username | Name | +| --------------- | -----------------------------| +| JEnoch | Julien Enoch | \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fc01375 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4556 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher 0.2.5", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if 1.0.0", + "cipher 0.4.4", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" +dependencies = [ + "aead", + "aes 0.6.0", + "cipher 0.2.5", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher 0.2.5", + "opaque-debug", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher 0.2.5", + "opaque-debug", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if 1.0.0", + "getrandom 0.2.10", + "once_cell", + "serde", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-dup" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7427a12b8dc09291528cfb1da2447059adb4a257388c2acd6497a79d55cf6f7c" +dependencies = [ + "futures-io", + "simple-mutex", +] + +[[package]] +name = "async-executor" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", + "tokio", +] + +[[package]] +name = "async-h1" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8101020758a4fc3a7c326cb42aa99e9fa77cbfb76987c128ad956406fe1f70a7" +dependencies = [ + "async-channel", + "async-dup", + "async-std", + "futures-core", + "http-types", + "httparse", + "log", + "pin-project", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if 1.0.0", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix 0.37.23", + "slab", + "socket2 0.4.9", + "waker-fn", +] + +[[package]] +name = "async-liveliness-monitor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "902174b1c1b8b63ed4d522448fd639c45ab86d78d75575b39e3946d465183c72" + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-process" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +dependencies = [ + "async-io", + "async-lock", + "autocfg", + "blocking", + "cfg-if 1.0.0", + "event-listener", + "futures-lite", + "rustix 0.37.23", + "signal-hook", + "windows-sys", +] + +[[package]] +name = "async-rustls" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29479d362e242e320fa8f5c831940a5b83c1679af014068196cd20d4bf497b6b" +dependencies = [ + "futures-io", + "rustls", +] + +[[package]] +name = "async-session" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345022a2eed092cd105cc1b26fd61c341e100bd5fcbbd792df4baf31c2cc631f" +dependencies = [ + "anyhow", + "async-std", + "async-trait", + "base64 0.12.3", + "bincode", + "blake3", + "chrono", + "hmac 0.8.1", + "kv-log-macro", + "rand 0.7.3", + "serde", + "serde_json", + "sha2 0.9.9", +] + +[[package]] +name = "async-sse" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bba003996b8fd22245cd0c59b869ba764188ed435392cf2796d03b805ade10" +dependencies = [ + "async-channel", + "async-std", + "http-types", + "log", + "memchr", + "pin-project-lite 0.1.12", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-attributes", + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite 0.2.13", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" + +[[package]] +name = "async-trait" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.68.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +dependencies = [ + "bitflags 2.4.0", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.33", + "which", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "blake3" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if 0.1.10", + "constant_time_eq", + "crypto-mac 0.8.0", + "digest 0.9.0", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "log", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cache-padded" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cdr" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9617422bf43fde9280707a7e90f8f7494389c182f5c70b0f67592d0f06d41dfa" +dependencies = [ + "byteorder", + "serde", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading 0.7.4", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap" +version = "4.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +dependencies = [ + "anstream", + "anstyle", + "clap_lex 0.5.1", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "const_fn" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" + +[[package]] +name = "const_format" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cookie" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" +dependencies = [ + "aes-gcm", + "base64 0.13.1", + "hkdf", + "hmac 0.10.1", + "percent-encoding", + "rand 0.8.5", + "sha2 0.9.9", + "time 0.2.27", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +dependencies = [ + "cipher 0.2.5", +] + +[[package]] +name = "cyclors" +version = "0.2.0" +source = "git+https://github.com/kydos/cyclors?branch=master#c1be4c46e652e4234bd5c25369f990ad0fc3f386" +dependencies = [ + "bincode", + "bindgen", + "cmake", + "derivative", + "libc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "dyn-clone" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "femme" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc04871e5ae3aa2952d552dae6b291b3099723bf779a8054281c1366a54613ef" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite 0.2.13", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite 0.2.13", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "git-version" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899" +dependencies = [ + "git-version-macro", + "proc-macro-hack", +] + +[[package]] +name = "git-version-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +dependencies = [ + "digest 0.9.0", + "hmac 0.10.1", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac 0.10.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "http-client" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1947510dc91e2bf586ea5ffb412caad7673264e14bb39fb9078da114a94ce1a5" +dependencies = [ + "async-trait", + "cfg-if 1.0.0", + "http-types", + "log", +] + +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel", + "async-std", + "base64 0.13.1", + "cookie", + "futures-lite", + "infer", + "pin-project-lite 0.2.13", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "url", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite 0.2.13", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.2", + "libc", + "windows-sys", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.2", + "rustix 0.38.13", + "windows-sys", +] + +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonschema" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" +dependencies = [ + "ahash", + "anyhow", + "base64 0.21.4", + "bytecount", + "clap 4.4.3", + "fancy-regex", + "fraction", + "getrandom 0.2.10", + "iso8601", + "itoa", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "time 0.3.28", + "url", + "uuid", +] + +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keyed-set" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79e110283e09081809ca488cf3a9709270c6d4d4c4a32674c39cc438366615a" +dependencies = [ + "hashbrown 0.13.2", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" +dependencies = [ + "cfg-if 1.0.0", + "windows-sys", +] + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "serde", + "value-bag", +] + +[[package]] +name = "lz4_flex" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea9b256699eda7b0387ffbc776dd625e28bde3918446381781245b7a50349d8" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.10", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.0", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.2", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_str_bytes" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" + +[[package]] +name = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pest" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bee7be22ce7918f641a33f08e3f43388c7656772244e2bbb2477f44cc9021a" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1511785c5e98d79a05e8a6bc34b4ac2168a0e3e92161862030ad84daa223141" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "pest_meta" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42f0394d3123e33353ca5e1e89092e533d2cc490389f2bd6131c43c634ebc5f" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.7", +] + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.0.0", +] + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pnet" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "130c5b738eeda2dc5796fe2671e49027e6935e817ab51b930a36ec9e6a206a64" +dependencies = [ + "ipnetwork", + "pnet_base", + "pnet_datalink", + "pnet_packet", + "pnet_sys", + "pnet_transport", +] + +[[package]] +name = "pnet_base" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5854abf0067ebbd3967f7d45ebc8976ff577ff0c7bd101c4973ae3c70f98fe" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_macros" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.33", +] + +[[package]] +name = "pnet_macros_support" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a005825396b7fe7a38a8e288dbc342d5034dac80c15212436424fef8ea90ba" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "pnet_sys" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417c0becd1b573f6d544f73671070b039051e5ad819cc64aa96377b536128d00" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pnet_transport" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2637e14d7de974ee2f74393afccbc8704f3e54e6eb31488715e72481d1662cc3" +dependencies = [ + "libc", + "pnet_base", + "pnet_packet", + "pnet_sys", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if 1.0.0", + "concurrent-queue", + "libc", + "log", + "pin-project-lite 0.2.13", + "windows-sys", +] + +[[package]] +name = "polyval" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +dependencies = [ + "cpuid-bool", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn 2.0.33", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" +dependencies = [ + "bytes", + "pin-project-lite 0.2.13", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13f81c9a9d574310b8351f8666f5a93ac3b0069c45c28ad52c10291389a7cf9" +dependencies = [ + "bytes", + "rand 0.8.5", + "ring", + "rustc-hash", + "rustls", + "rustls-native-certs", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" +dependencies = [ + "bytes", + "libc", + "socket2 0.5.4", + "tracing", + "windows-sys", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.10", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.10", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "reqwest" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +dependencies = [ + "base64 0.21.4", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite 0.2.13", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "ringbuffer-spsc" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1938faa63a2362ee1747afb2d10567d0fb1413b9cbd6198a8541485c4f773" +dependencies = [ + "array-init", + "cache-padded", +] + +[[package]] +name = "route-recognizer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e" + +[[package]] +name = "rsa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +dependencies = [ + "byteorder", + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.18", +] + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "0.38.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.7", + "windows-sys", +] + +[[package]] +name = "rustls" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.4", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "schemars" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap 2.0.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shellexpand" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simple-mutex" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38aabbeafa6f6dead8cebf246fe9fae1f9215c8d29b3a69f93bd62a9e4a3dcd6" +dependencies = [ + "event-listener", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version 0.2.3", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1 0.6.1", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "stop-token" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b" +dependencies = [ + "async-channel", + "cfg-if 1.0.0", + "futures-core", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "sval" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1" + +[[package]] +name = "sval_buffer" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_ref" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046" +dependencies = [ + "serde", + "sval", + "sval_buffer", + "sval_fmt", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "tide" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c459573f0dd2cc734b539047f57489ea875af8ee950860ded20cf93a79a1dee0" +dependencies = [ + "async-h1", + "async-session", + "async-sse", + "async-std", + "async-trait", + "femme", + "futures-util", + "http-client", + "http-types", + "kv-log-macro", + "log", + "pin-project-lite 0.2.13", + "route-recognizer", + "serde", + "serde_json", +] + +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros 0.1.1", + "version_check", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +dependencies = [ + "deranged", + "serde", + "time-core", + "time-macros 0.2.14", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +dependencies = [ + "time-core", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn 1.0.109", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "token-cell" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a2b964fdb303b08a4eab04d7c1bad2bca33f8eee334ccd28802f1041c6eb87" +dependencies = [ + "paste", +] + +[[package]] +name = "tokio" +version = "1.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite 0.2.13", + "socket2 0.5.4", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite 0.2.13", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite 0.2.13", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tungstenite" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1 0.10.5", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if 1.0.0", + "static_assertions", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "uhlc" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1eadef1fa26cbbae1276c46781e8f4d888bdda434779c18ae6c2a0e69991885" +dependencies = [ + "humantime", + "lazy_static", + "log", + "rand 0.8.5", + "serde", + "spin 0.9.8", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "universal-hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "unzip-n" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e7e85a0596447f0f2ac090e16bc4c516c6fe91771fb0c0ccf7fa3dae896b9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom 0.2.10", +] + +[[package]] +name = "validated_struct" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feef04c049b4beae3037a2a31b8da40d8cebec0b97456f24c7de0ede4ed9efed" +dependencies = [ + "json5", + "serde", + "serde_json", + "validated_struct_macros", +] + +[[package]] +name = "validated_struct_macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d4444a980afa9ef0d29c2a3f4d952ec0495a7a996a9c78b52698b71bc21edb4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unzip-n", +] + +[[package]] +name = "value-bag" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394" +dependencies = [ + "erased-serde", + "serde", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if 1.0.0", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.33", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.13", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if 1.0.0", + "windows-sys", +] + +[[package]] +name = "zenoh" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-global-executor", + "async-std", + "async-trait", + "base64 0.21.4", + "env_logger", + "event-listener", + "flume", + "form_urlencoded", + "futures", + "git-version", + "hex", + "lazy_static", + "log", + "ordered-float", + "paste", + "petgraph", + "rand 0.8.5", + "regex", + "rustc_version 0.4.0", + "serde", + "serde_json", + "socket2 0.5.4", + "stop-token", + "uhlc", + "uuid", + "vec_map", + "zenoh-buffers", + "zenoh-codec", + "zenoh-collections", + "zenoh-config", + "zenoh-core", + "zenoh-crypto", + "zenoh-link", + "zenoh-macros", + "zenoh-plugin-trait", + "zenoh-protocol", + "zenoh-result", + "zenoh-sync", + "zenoh-transport", + "zenoh-util", +] + +[[package]] +name = "zenoh-bridge-ros2dds" +version = "0.1.0-dev" +dependencies = [ + "async-liveliness-monitor", + "async-std", + "clap 3.2.25", + "env_logger", + "lazy_static", + "log", + "serde_json", + "zenoh", + "zenoh-plugin-rest", + "zenoh-plugin-ros2dds", + "zenoh-plugin-trait", +] + +[[package]] +name = "zenoh-buffers" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "zenoh-collections", +] + +[[package]] +name = "zenoh-codec" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "log", + "serde", + "uhlc", + "zenoh-buffers", + "zenoh-protocol", +] + +[[package]] +name = "zenoh-collections" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" + +[[package]] +name = "zenoh-config" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "flume", + "json5", + "num_cpus", + "serde", + "serde_json", + "serde_yaml", + "validated_struct", + "zenoh-core", + "zenoh-protocol", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-core" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-std", + "lazy_static", + "zenoh-result", +] + +[[package]] +name = "zenoh-crypto" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "aes 0.8.3", + "hmac 0.12.1", + "rand 0.8.5", + "rand_chacha 0.3.1", + "sha3", + "zenoh-result", +] + +[[package]] +name = "zenoh-ext" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-std", + "bincode", + "env_logger", + "flume", + "futures", + "log", + "serde", + "zenoh", + "zenoh-core", + "zenoh-macros", + "zenoh-result", + "zenoh-sync", + "zenoh-util", +] + +[[package]] +name = "zenoh-keyexpr" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "hashbrown 0.14.0", + "keyed-set", + "rand 0.8.5", + "schemars", + "serde", + "token-cell", + "zenoh-result", +] + +[[package]] +name = "zenoh-link" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-std", + "async-trait", + "zenoh-config", + "zenoh-link-commons", + "zenoh-link-quic", + "zenoh-link-tcp", + "zenoh-link-tls", + "zenoh-link-udp", + "zenoh-link-unixsock_stream", + "zenoh-link-ws", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-commons" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-std", + "async-trait", + "flume", + "serde", + "typenum", + "zenoh-buffers", + "zenoh-codec", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-quic" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-rustls", + "async-std", + "async-trait", + "futures", + "log", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-webpki", + "zenoh-config", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-sync", + "zenoh-util", +] + +[[package]] +name = "zenoh-link-tcp" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-std", + "async-trait", + "log", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-sync", + "zenoh-util", +] + +[[package]] +name = "zenoh-link-tls" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-rustls", + "async-std", + "async-trait", + "futures", + "log", + "rustls", + "rustls-pemfile", + "rustls-webpki", + "webpki-roots", + "zenoh-config", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-sync", + "zenoh-util", +] + +[[package]] +name = "zenoh-link-udp" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-std", + "async-trait", + "log", + "socket2 0.5.4", + "zenoh-buffers", + "zenoh-collections", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-sync", + "zenoh-util", +] + +[[package]] +name = "zenoh-link-unixsock_stream" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-std", + "async-trait", + "futures", + "log", + "nix", + "uuid", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-sync", +] + +[[package]] +name = "zenoh-link-ws" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-std", + "async-trait", + "futures-util", + "log", + "tokio", + "tokio-tungstenite", + "url", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-sync", + "zenoh-util", +] + +[[package]] +name = "zenoh-macros" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.0", + "syn 2.0.33", + "unzip-n", + "zenoh-keyexpr", +] + +[[package]] +name = "zenoh-plugin-rest" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "anyhow", + "async-std", + "base64 0.21.4", + "clap 3.2.25", + "env_logger", + "flume", + "futures", + "git-version", + "http-types", + "jsonschema", + "lazy_static", + "log", + "rustc_version 0.4.0", + "schemars", + "serde", + "serde_json", + "tide", + "zenoh", + "zenoh-plugin-trait", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-plugin-ros2dds" +version = "0.1.0-dev" +dependencies = [ + "async-std", + "async-trait", + "bincode", + "cdr", + "cyclors", + "derivative", + "env_logger", + "flume", + "futures", + "git-version", + "hex", + "lazy_static", + "log", + "regex", + "rustc_version 0.4.0", + "serde", + "serde_json", + "zenoh", + "zenoh-collections", + "zenoh-core", + "zenoh-ext", + "zenoh-plugin-trait", + "zenoh-util", +] + +[[package]] +name = "zenoh-plugin-trait" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "libloading 0.8.0", + "log", + "serde_json", + "zenoh-macros", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-protocol" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "const_format", + "hex", + "rand 0.8.5", + "serde", + "uhlc", + "uuid", + "zenoh-buffers", + "zenoh-keyexpr", + "zenoh-result", +] + +[[package]] +name = "zenoh-result" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "anyhow", +] + +[[package]] +name = "zenoh-sync" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-std", + "event-listener", + "flume", + "futures", + "tokio", + "zenoh-buffers", + "zenoh-collections", + "zenoh-core", +] + +[[package]] +name = "zenoh-transport" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-executor", + "async-global-executor", + "async-std", + "async-trait", + "flume", + "log", + "lz4_flex", + "paste", + "rand 0.8.5", + "ringbuffer-spsc", + "rsa", + "serde", + "sha3", + "zenoh-buffers", + "zenoh-codec", + "zenoh-collections", + "zenoh-config", + "zenoh-core", + "zenoh-crypto", + "zenoh-link", + "zenoh-protocol", + "zenoh-result", + "zenoh-sync", + "zenoh-util", +] + +[[package]] +name = "zenoh-util" +version = "0.10.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh?branch=master#c2bc9bd9b89b94539c6bbe12f3e1750deeda37c5" +dependencies = [ + "async-std", + "async-trait", + "clap 3.2.25", + "flume", + "futures", + "hex", + "home", + "humantime", + "lazy_static", + "libc", + "libloading 0.8.0", + "log", + "pnet", + "pnet_datalink", + "shellexpand", + "winapi", + "zenoh-core", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..de89b2e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,64 @@ +# +# Copyright (c) 2022 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# +[workspace] +resolver = "2" +members = [ + "zenoh-bridge-ros2dds", + "zenoh-plugin-ros2dds", +] + +[workspace.package] +version = "0.1.0-dev" +authors = [ + "Julien Enoch ", +] +edition = "2021" +repository = "https://github.com/eclipse-zenoh/zenoh-plugin-ros2dds" +homepage = "http://zenoh.io" +license = "EPL-2.0 OR Apache-2.0" + +[workspace.dependencies] +async-liveliness-monitor = "0.1.1" +async-std = "=1.12.0" +async-trait = "0.1.66" +bincode = "1.3.3" +cdr = "0.2.4" +clap = "3.2.23" +cyclors = { git = "https://github.com/kydos/cyclors", branch = "master" } +derivative = "2.2.0" +env_logger = "0.10.0" +flume = "0.11.0" +futures = "0.3.26" +git-version = "0.3.5" +hex = "0.4.3" +lazy_static = "1.4.0" +log = "0.4.17" +regex = "1.7.1" +rustc_version = "0.4" +serde = "1.0.154" +serde_json = "1.0.94" +zenoh = { git = "https://github.com/eclipse-zenoh/zenoh", branch = "master", features = ["unstable"] } +zenoh-collections = { git = "https://github.com/eclipse-zenoh/zenoh", branch = "master" } +zenoh-core = { git = "https://github.com/eclipse-zenoh/zenoh", branch = "master" } +zenoh-ext = { git = "https://github.com/eclipse-zenoh/zenoh", branch = "master", features = ["unstable"] } +zenoh-plugin-rest = { git = "https://github.com/eclipse-zenoh/zenoh", branch = "master", default-features = false } +zenoh-plugin-trait = { git = "https://github.com/eclipse-zenoh/zenoh", branch = "master", default-features = false } +zenoh-util = { git = "https://github.com/eclipse-zenoh/zenoh", branch = "master", default-features = false } + +[profile.release] +debug = false +lto = "fat" +codegen-units = 1 +opt-level = 3 +panic = "abort" diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..127ef5e --- /dev/null +++ b/Cross.toml @@ -0,0 +1,18 @@ +[target.x86_64-unknown-linux-musl] +image = "jenoch/rust-cross:x86_64-unknown-linux-musl" + +[target.arm-unknown-linux-gnueabi] +image = "jenoch/rust-cross:arm-unknown-linux-gnueabi" + +[target.arm-unknown-linux-gnueabihf] +image = "jenoch/rust-cross:arm-unknown-linux-gnueabihf" + +[target.armv7-unknown-linux-gnueabihf] +image = "jenoch/rust-cross:armv7-unknown-linux-gnueabihf" + +[target.aarch64-unknown-linux-gnu] +image = "jenoch/rust-cross:aarch64-unknown-linux-gnu" + +[target.aarch64-unknown-linux-musl] +image = "jenoch/rust-cross:aarch64-unknown-linux-musl" + diff --git a/DEFAULT_CONFIG.json5 b/DEFAULT_CONFIG.json5 new file mode 100644 index 0000000..3f6a754 --- /dev/null +++ b/DEFAULT_CONFIG.json5 @@ -0,0 +1,147 @@ +//// +//// This file presents the default configuration used by both the `zenoh-plugin-ros2dds` plugin and the `zenoh-bridge-ros2dds` standalone executable. +//// The "ros2" JSON5 object below can be used as such in the "plugins" part of a config file for the zenoh router (zenohd). +//// +{ + plugins: { + //// + //// ROS2 related configuration + //// All settings are optional and are unset by default - uncomment the ones you want to set + //// + ros2: { + //// + //// id: An identifier for this bridge, which must be unique in the system. + /// The bridge will use this identifier in it's administration space: `@ros2//**`. + /// This identifier will also appears in the logs of all other bridges on discovery events. + /// By default a random UUID + //// + // id: "robot-1", + + //// + //// namespace: A ROS namespace to be used by this bridge. + //// Default: "/" + //// + // namespace: "/", + + //// + //// nodename: A ROS node name to be used by this bridge. + //// Default: "zenoh_bridge_ros2dds" + //// + // namespace: "zenoh_bridge_ros2dds", + + //// + //// domain: The DDS Domain ID. By default set to 0, or to "$ROS_DOMAIN_ID" is this environment variable is defined. + //// + // domain: 0, + + //// + //// localhost_only: If set to true, the DDS discovery and traffic will occur only on the localhost interface (127.0.0.1). + //// By default set to false, unless the "ROS_LOCALHOST_ONLY=1" environment variable is defined. + //// + // localhost_only: true, + + //// + //// shm_enabled: If set to true, the DDS implementation will use Iceoryx shared memory. + //// Requires the bridge to be built with the 'dds_shm' feature for this option to valid. + //// By default set to false. + //// + // shm_enabled: false, + + //// + //// allow: Sets of 1 or more regular expression per ROS interface kind matching the interface names that must be routed via zenoh. + //// By default, all interfaces are allowed. + //// If both 'allow' and 'deny' are set an interface will be allowed if it matches only the expression in 'allow' set. + //// + // allow: { + // publishers: [".*/laser_scan", "/tf", ".*/pose"], + // subscribers: [".*/cmd_vel"], + // service_servers: [".*/.*_parameters"], + // service_clients: [], + // action_servers: [".*/rotate_absolute"], + // action_clients: [], + // }, + + //// + //// deny: Sets of 1 or more regular expression per ROS interface kind matching the interface names that must NOT be routed via zenoh. + //// By default, no interface are denied. + //// If both 'allow' and 'deny' are set an interface will be allowed if it matches only the expression in 'allow' set. + //// + // deny: { + // publishers: ["/rosout", "/parameter_events"], + // subscribers: ["/rosout"], + // service_servers: [".*/set_parameters"], + // service_clients: [".*/set_parameters"], + // action_servers: [], + // action_clients: [], + // }, + + //// + //// pub_max_frequencies: Specifies a list of maximum frequency of messages routing over zenoh for a set of topics. + //// The strings must have the format "=": + //// - "regex" is a regular expression matching a Publisher interface name + //// - "float" is the maximum frequency in Hertz; + //// if publication rate is higher, downsampling will occur when routing. + // pub_max_frequencies: [".*/laser_scan=5", "/tf=10"], + + + //// + //// reliable_routes_blocking: When true, the publications from a RELIABLE DDS Writer will be + //// routed to zenoh using the CongestionControl::Block option. + //// Meaning the routing will be blocked in case of network congestion, + //// blocking the DDS Reader and the RELIABLE DDS Writer in return. + //// When false (or for BERST_EFFORT DDS Writers), CongestionControl::Drop + //// is used, meaning the route might drop some data in case of congestion. + //// + // reliable_routes_blocking: true, + + //// + //// queries_timeout: A duration in seconds (default: 5.0 sec) that will be used as a timeout when the bridge + //// queries any other remote bridge for discovery information and for historical data for TRANSIENT_LOCAL DDS Readers it serves + //// (i.e. if the query to the remote bridge exceed the timeout, some historical samples might be not routed to the Readers, + //// but the route will not be blocked forever). + //// + // queries_timeout: 5.0, + }, + + //// + //// REST API configuration (active only if this part is defined) + //// + // rest: { + // //// + // //// The HTTP port number (for all network interfaces). + // //// You can bind on a specific interface setting a ":" string. + // //// + // http_port: 8000, + // }, + }, + + //// + //// zenoh related configuration (see zenoh documentation for more details) + //// + + //// + //// mode: The bridge's mode (peer or client) + //// + //mode: "client", + + //// + //// Which endpoints to connect to. E.g. tcp/localhost:7447. + //// By configuring the endpoints, it is possible to tell zenoh which router/peer to connect to at startup. + //// + connect: { + endpoints: [ + // "/:" + ] + }, + + //// + //// Which endpoints to listen on. E.g. tcp/localhost:7447. + //// By configuring the endpoints, it is possible to tell zenoh which are the endpoints that other routers, + //// peers, or client can use to establish a zenoh session. + //// + listen: { + endpoints: [ + // "/:" + ] + }, +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8c3fbf6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,459 @@ +apache-2.0 +epl-2.0 + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + OR + +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. + diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..01c0f1e --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,41 @@ +# Notices for Eclipse zenoh-plugin-ros2dds + +This content is produced and maintained by the Eclipse zenoh project. + + * Project home: https://projects.eclipse.org/projects/iot.zenoh + +## Trademarks + +Eclipse zenoh is trademark of the Eclipse Foundation. +Eclipse, and the Eclipse Logo are registered trademarks of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. +For more information regarding authorship of content, please consult the +listed source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the +terms of the Eclipse Public License 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +which is available at https://www.apache.org/licenses/LICENSE-2.0. + +SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + +## Source Code + +The project maintains the following source code repositories: + + * https://github.com/eclipse-zenoh/zenoh.git + * https://github.com/eclipse-zenoh/zenoh-c.git + * https://github.com/eclipse-zenoh/zenoh-java.git + * https://github.com/eclipse-zenoh/zenoh-go.git + * https://github.com/eclipse-zenoh/zenoh-python.git + * https://github.com/eclipse-zenoh/zenoh-plugin-ros2dds.git + +## Third-party Content + + *To be completed...* + diff --git a/README.md b/README.md index 1d69ed0..a879d17 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ -# zenoh-plugin-ros2dds -A Zenoh plug-in for ROS2 with a DDS RMW. +# zplugin-ros2dds + +A new Zenoh bridge for ROS 2 with a DDS RMW. + +:warning: Work in progress... diff --git a/package.xml b/package.xml new file mode 100644 index 0000000..f3e6e6d --- /dev/null +++ b/package.xml @@ -0,0 +1,38 @@ + + + + + + + zenoh_bridge_ros2dds + 0.5.0 + + Bridge between ROS2/DDS and Eclipse zenoh (https://zenoh.io). It allows the integration of zenoh applications with ROS2, + or the tunneling of ROS2 communications between nodes via the zenoh protocol at Internet scale. + + https://github.com/eclipse-zenoh/zenoh-plugin-ros2dds + ZettaScale Zenoh team + EPL-2.0 + Apache-2.0 + + ament_cmake + + cargo + clang + + + ament_cmake + + diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..743f7cd --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.72.0" diff --git a/zenoh-bridge-ros2dds/.deb/postinst b/zenoh-bridge-ros2dds/.deb/postinst new file mode 100644 index 0000000..51a2be4 --- /dev/null +++ b/zenoh-bridge-ros2dds/.deb/postinst @@ -0,0 +1,42 @@ +#!/bin/sh +# postinst script for Eclipse Zenoh bridge for DDS +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see https://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + configure) + id -u zenoh-bridge-ros2dds >/dev/null 2>&1 || sudo useradd -r -s /bin/false zenoh-bridge-ros2dds + systemctl daemon-reload + systemctl disable zenoh-bridge-ros2dds + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/zenoh-bridge-ros2dds/.deb/postrm b/zenoh-bridge-ros2dds/.deb/postrm new file mode 100644 index 0000000..fcb339b --- /dev/null +++ b/zenoh-bridge-ros2dds/.deb/postrm @@ -0,0 +1,39 @@ +#!/bin/sh +# postrm script for Eclipse Zenoh bridge for DDS +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `remove' +# * `purge' +# * `upgrade' +# * `failed-upgrade' +# * `abort-install' +# * `abort-install' +# * `abort-upgrade' +# * `disappear' +# +# for details, see https://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + userdel zenoh-bridge-ros2dds + rm -rf /etc/zenoh-bridge-ros2dds + ;; + + *) + echo "postrm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 + diff --git a/zenoh-bridge-ros2dds/.service/zenoh-bridge-ros2.service b/zenoh-bridge-ros2dds/.service/zenoh-bridge-ros2.service new file mode 100644 index 0000000..ae2895a --- /dev/null +++ b/zenoh-bridge-ros2dds/.service/zenoh-bridge-ros2.service @@ -0,0 +1,23 @@ +[Unit] +Description = Eclipse Zenoh Bridge for DDS +Documentation=https://github.com/eclipse-zenoh/zenoh-plugin-ros2dds +After=network-online.target +Wants=network-online.target + + +[Service] +Type=simple +Environment=RUST_LOG=info +ExecStart = /usr/bin/zenoh-bridge-ros2dds -c /etc/zenoh-bridge-ros2dds/conf.json5 +KillMode=mixed +KillSignal=SIGINT +RestartKillSignal=SIGINT +Restart=on-failure +PermissionsStartOnly=true +User=zenoh-bridge-ros2dds +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=zenoh-bridge-ros2dds +[Install] +WantedBy=multi-user.target + diff --git a/zenoh-bridge-ros2dds/Cargo.toml b/zenoh-bridge-ros2dds/Cargo.toml new file mode 100644 index 0000000..15b0eee --- /dev/null +++ b/zenoh-bridge-ros2dds/Cargo.toml @@ -0,0 +1,72 @@ +# +# Copyright (c) 2022 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# +[package] +name = "zenoh-bridge-ros2dds" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +categories = ["network-programming"] +description = "Zenoh bridge for ROS 2 and DDS in general" + +[features] +dds_shm = ["zenoh-plugin-ros2dds/dds_shm"] + +[dependencies] +async-std = { workspace = true, features = ["unstable", "attributes"] } +async-liveliness-monitor = { workspace = true } +clap = { workspace = true } +env_logger = { workspace = true } +lazy_static = { workspace = true } +log = { workspace = true } +serde_json = { workspace = true } +zenoh = { workspace = true } +zenoh-plugin-rest = { workspace = true } +zenoh-plugin-trait = { workspace = true } +zenoh-plugin-ros2dds = { path = "../zenoh-plugin-ros2dds/", default-features = false } + +[[bin]] +name = "zenoh-bridge-ros2dds" +path = "src/main.rs" + +[package.metadata.deb] +name = "zenoh-bridge-ros2dds" +maintainer = "zenoh-dev@eclipse.org" +copyright = "2017, 2022 ZettaScale Technology Inc." +section = "net" +license-file = ["../LICENSE", "0"] +depends = "$auto" +maintainer-scripts = ".deb" +assets = [ + # binary + [ + "target/release/zenoh-bridge-ros2dds", + "/usr/bin/", + "755", + ], + # config file + [ + "../DEFAULT_CONFIG.json5", + "/etc/zenoh-bridge-ros2dds/conf.json5", + "644", + ], + # service + [ + ".service/zenoh-bridge-ros2dds.service", + "/lib/systemd/system/zenoh-bridge-ros2dds.service", + "644", + ], +] diff --git a/zenoh-bridge-ros2dds/src/main.rs b/zenoh-bridge-ros2dds/src/main.rs new file mode 100644 index 0000000..56c913b --- /dev/null +++ b/zenoh-bridge-ros2dds/src/main.rs @@ -0,0 +1,294 @@ +use async_liveliness_monitor::LivelinessMonitor; +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use clap::{App, Arg}; +use std::time::{Duration, SystemTime}; +use zenoh::config::{Config, ModeDependentValue}; +use zenoh::prelude::*; + +lazy_static::lazy_static!( + pub static ref DEFAULT_DOMAIN_STR: String = zenoh_plugin_ros2dds::config::DEFAULT_DOMAIN.to_string(); +); + +macro_rules! insert_json5 { + ($config: expr, $args: expr, $key: expr, if $name: expr) => { + if $args.occurrences_of($name) > 0 { + $config.insert_json5($key, "true").unwrap(); + } + }; + ($config: expr, $args: expr, $key: expr, if $name: expr, $($t: tt)*) => { + if $args.occurrences_of($name) > 0 { + $config.insert_json5($key, &serde_json::to_string(&$args.value_of($name).unwrap()$($t)*).unwrap()).unwrap(); + } + }; + ($config: expr, $args: expr, $key: expr, for $name: expr, $($t: tt)*) => { + if let Some(value) = $args.values_of($name) { + $config.insert_json5($key, &serde_json::to_string(&value$($t)*).unwrap()).unwrap(); + } + }; +} + +fn parse_args() -> (Config, Option) { + let mut app = App::new("zenoh bridge for DDS") + .version(zenoh_plugin_ros2dds::GIT_VERSION) + .long_version(zenoh_plugin_ros2dds::LONG_VERSION.as_str()) + // + // zenoh related arguments: + // + .arg(Arg::from_usage( +r#"-i, --id=[HEX_STRING] \ +'The identifier (as an hexadecimal string, with odd number of chars - e.g.: 0A0B23...) that zenohd must use. +WARNING: this identifier must be unique in the system and must be 16 bytes maximum (32 chars)! +If not set, a random UUIDv4 will be used.'"#, + )) + .arg(Arg::from_usage( +r#"-m, --mode=[MODE] 'The zenoh session mode.'"#) + .possible_values(["peer", "client"]) + .default_value("peer") + ) + .arg(Arg::from_usage( +r#"-c, --config=[FILE] \ +'The configuration file. Currently, this file must be a valid JSON5 file.'"#, + )) + .arg(Arg::from_usage( +r#"-l, --listen=[ENDPOINT]... \ +'A locator on which this router will listen for incoming sessions. +Repeat this option to open several listeners.'"#, + ), + ) + .arg(Arg::from_usage( +r#"-e, --connect=[ENDPOINT]... \ +'A peer locator this router will try to connect to. +Repeat this option to connect to several peers.'"#, + )) + .arg(Arg::from_usage( +r#"--no-multicast-scouting \ +'By default the zenoh bridge listens and replies to UDP multicast scouting messages for being discovered by peers and routers. +This option disables this feature.'"# + )) + .arg(Arg::from_usage( +r#"--rest-http-port=[PORT | IP:PORT] \ +'Configures HTTP interface for the REST API (disabled by default, setting this option enables it). Accepted values:' + - a port number + - a string with format `:` (to bind the HTTP server to a specific interface)."# + )) + // + // DDS related arguments: + // + .arg(Arg::from_usage( +r#"-n, --namespace=[String] 'A ROS 2 namespace to be used by the "zenoh_bridge_dds" node'"# + )) + .arg(Arg::from_usage( +r#"-d, --domain=[ID] 'The DDS Domain ID. The default value is "$ROS_DOMAIN_ID" if defined, or "0" otherwise.'"#) + .default_value(&DEFAULT_DOMAIN_STR) + ) + .arg(Arg::from_usage( +r#"--ros-localhost-only \ +'Configure CycloneDDS to use only the localhost interface. If not set, CycloneDDS will pick the interface defined in "$CYCLONEDDS_URI" configuration, or automatically choose one. +This option is not active by default, unless the "ROS_LOCALHOST_ONLY" environment variable is set to "1".'"# + )); + + // Add option to enable DDS SHM if feature is enabled + #[cfg(feature = "dds_shm")] + { + app = app.arg(Arg::from_usage( + r#"--dds-enable-shm \ + 'Configure CycloneDDS to use Iceoryx shared memory. If not set, CycloneDDS will instead use any shared memory settings defined in "$CYCLONEDDS_URI" configuration. + This option is not active by default.'"# + )); + } + + app = app + .arg(Arg::from_usage( +r#"-a, --allow=[String]... 'A regular expression matching the set of 'partition/topic-name' that must be routed via zenoh. By default, all partitions and topics are allowed. +If both '--allow' and '--deny' are set a partition and/or topic will be allowed if it matches only the 'allow' expression. +Repeat this option to configure several topic expressions. These expressions are concatenated with '|'. +Examples of expressions: '.*/TopicA', 'Partition-?/.*', 'cmd_vel|rosout'...'"# + )) + .arg(Arg::from_usage( +r#"--deny=[String]... 'A regular expression matching the set of 'partition/topic-name' that must not be routed via zenoh. By default, no partitions and no topics are denied. +If both '--allow' and '--deny' are set a partition and/or topic will be allowed if it matches only the 'allow' expression. +Repeat this option to configure several topic expressions. These expressions are concatenated with '|'. +Examples of expressions: '.*/TopicA', 'Partition-?/.*', 'cmd_vel|rosout'...'"# + )) + .arg(Arg::from_usage( +r#"--max-frequency=[String]... 'Specifies a maximum frequency of data routing over zenoh for a set of topics. The string must have the format "=": + - "regex" is a regular expression matching the set of 'partition/topic-name' (same syntax than --allow option) + for which the data (per DDS instance) must be routed at no higher rate than the specified max frequency. + - "float" is the maximum frequency in Hertz; if publication rate is higher, downsampling will occur when routing. +Repeat this option to configure several topics expressions with a max frequency.'"# + )) + .arg(Arg::from_usage( +r#"-r, --generalise-sub=[String]... 'A list of key expression to use for generalising subscriptions (usable multiple times).'"# + )) + .arg(Arg::from_usage( +r#"-w, --generalise-pub=[String]... 'A list of key expression to use for generalising publications (usable multiple times).'"# + )) + .arg(Arg::from_usage( +r#"-f, --fwd-discovery 'When set, rather than creating a local route when discovering a local DDS entity, this discovery info is forwarded to the remote plugins/bridges. Those will create the routes, including a replica of the discovered entity.'"# + ).alias("forward-discovery") + ) + .arg(Arg::from_usage( +r#"--queries-timeout=[float]... 'A float in seconds (default: 5.0 sec) that will be used as a timeout when the bridge +queries any other remote bridge for discovery information and for historical data for TRANSIENT_LOCAL DDS Readers it serves +(i.e. if the query to the remote bridge exceed the timeout, some historical samples might be not routed to the Readers, but the route will not be blocked forever)."# + )) + .arg(Arg::from_usage( +r#"--watchdog=[PERIOD] 'Experimental!! Run a watchdog thread that monitors the bridge's async executor and reports as error log any stalled status during the specified period (default: 1.0 second)'"# + ).default_missing_value("1.0")); + let args = app.get_matches(); + + // load config file at first + let mut config = match args.value_of("config") { + Some(conf_file) => Config::from_file(conf_file).unwrap(), + None => Config::default(), + }; + // if "ros2" plugin conf is not present, add it (empty to use default config) + if config.plugin("ros2").is_none() { + config.insert_json5("plugins/ros2", "{}").unwrap(); + } + + // apply zenoh related arguments over config + // NOTE: only if args.occurrences_of()>0 to avoid overriding config with the default arg value + if args.occurrences_of("mode") > 0 { + config + .set_mode(Some(args.value_of("mode").unwrap().parse().unwrap())) + .unwrap(); + } + if let Some(endpoints) = args.values_of("connect") { + config + .connect + .endpoints + .extend(endpoints.map(|p| p.parse().unwrap())) + } + if let Some(endpoints) = args.values_of("listen") { + config + .listen + .endpoints + .extend(endpoints.map(|p| p.parse().unwrap())) + } + if args.is_present("no-multicast-scouting") { + config.scouting.multicast.set_enabled(Some(false)).unwrap(); + } + if let Some(port) = args.value_of("rest-http-port") { + config + .insert_json5("plugins/rest/http_port", &format!(r#""{port}""#)) + .unwrap(); + } + // Always add timestamps to publications (required for PublicationCache used in case of TRANSIENT_LOCAL topics) + config + .timestamping + .set_enabled(Some(ModeDependentValue::Unique(true))) + .unwrap(); + + // apply DDS related arguments over config + insert_json5!(config, args, "plugins/ros2/id", if "id",); + insert_json5!(config, args, "plugins/ros2/namespace", if "namespace",); + insert_json5!(config, args, "plugins/ros2/domain", if "domain", .parse::().unwrap()); + insert_json5!(config, args, "plugins/ros2/ros_localhost_only", if "ros-localhost-only"); + #[cfg(feature = "dds_shm")] + { + insert_json5!(config, args, "plugins/ros2/shm_enabled", if "dds-enable-shm"); + } + insert_json5!(config, args, "plugins/ros2/allow", for "allow", .collect::>()); + insert_json5!(config, args, "plugins/ros2/deny", for "deny", .collect::>()); + insert_json5!(config, args, "plugins/ros2/max_frequencies", for "max-frequency", .collect::>()); + insert_json5!(config, args, "plugins/ros2/generalise_pubs", for "generalise-pub", .collect::>()); + insert_json5!(config, args, "plugins/ros2/generalise_subs", for "generalise-sub", .collect::>()); + insert_json5!(config, args, "plugins/ros2/queries_timeout", if "queries-timeout", .parse::().unwrap()); + + let watchdog_period = if args.is_present("watchdog") { + args.value_of("watchdog").map(|s| s.parse::().unwrap()) + } else { + None + }; + + (config, watchdog_period) +} + +#[async_std::main] +async fn main() { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("z=info")).init(); + log::info!("zenoh-bridge-ros2dds {}", *zenoh_plugin_ros2dds::LONG_VERSION); + + let (config, watchdog_period) = parse_args(); + let rest_plugin = config.plugin("rest").is_some(); + + if let Some(period) = watchdog_period { + run_watchdog(period); + } + + // create a zenoh Runtime (to share with plugins) + let runtime = zenoh::runtime::Runtime::new(config).await.unwrap(); + + // start REST plugin + if rest_plugin { + use zenoh_plugin_trait::Plugin; + zenoh_plugin_rest::RestPlugin::start("rest", &runtime).unwrap(); + } + + // start DDS plugin + use zenoh_plugin_trait::Plugin; + zenoh_plugin_ros2dds::ROS2Plugin::start("ros2", &runtime).unwrap(); + async_std::future::pending::<()>().await; +} + +fn run_watchdog(period: f32) { + let sleep_time = Duration::from_secs_f32(period); + // max delta accepted for watchdog thread sleep period + let max_sleep_delta = Duration::from_millis(50); + // 1st threshold of duration since last report => debug info if exceeded + let report_threshold_1 = Duration::from_millis(10); + // 2nd threshold of duration since last report => debug warn if exceeded + let report_threshold_2 = Duration::from_millis(100); + + assert!( + sleep_time > report_threshold_2, + "Watchdog period must be greater than {} seconds", + report_threshold_2.as_secs_f32() + ); + + // Start a Liveliness Monitor thread for async_std Runtime + let (_task, monitor) = LivelinessMonitor::start(async_std::task::spawn); + std::thread::spawn(move || { + log::debug!( + "Watchdog started with period {} sec", + sleep_time.as_secs_f32() + ); + loop { + let before = SystemTime::now(); + std::thread::sleep(sleep_time); + let elapsed = SystemTime::now().duration_since(before).unwrap(); + + // Monitor watchdog thread itself + if elapsed > sleep_time + max_sleep_delta { + log::warn!( + "Watchdog thread slept more than configured: {} seconds", + elapsed.as_secs_f32() + ); + } + // check last LivelinessMonitor's report + let report = monitor.latest_report(); + if report.elapsed() > report_threshold_1 { + if report.elapsed() > sleep_time { + log::error!("Watchdog detecting async_std is stalled! No task scheduling since {} seconds", report.elapsed().as_secs_f32()); + } else if report.elapsed() > report_threshold_2 { + log::warn!("Watchdog detecting async_std was not scheduling tasks during the last {} ms", report.elapsed().as_micros()); + } else { + log::info!("Watchdog detecting async_std was not scheduling tasks during the last {} ms", report.elapsed().as_micros()); + } + } + } + }); +} diff --git a/zenoh-plugin-ros2dds/Cargo.toml b/zenoh-plugin-ros2dds/Cargo.toml new file mode 100644 index 0000000..53de6eb --- /dev/null +++ b/zenoh-plugin-ros2dds/Cargo.toml @@ -0,0 +1,67 @@ +# +# Copyright (c) 2022 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# +[package] +name = "zenoh-plugin-ros2dds" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +categories = ["network-programming", "science::robotics"] +description = "Zenoh plugin for ROS 2 and DDS in general" + +[lib] +name = "zenoh_plugin_ros2dds" +crate-type = ["cdylib", "rlib"] + +[features] +default = ["no_mangle"] +no_mangle = ["zenoh-plugin-trait/no_mangle"] +dds_shm = ["cyclors/iceoryx"] + +[dependencies] +async-std = { workspace = true, features = ["unstable", "attributes"] } +async-trait = { workspace = true } +bincode = { workspace = true } +cdr = { workspace = true } +cyclors = { workspace = true } +derivative = { workspace = true } +env_logger = { workspace = true } +flume = { workspace = true } +futures = { workspace = true } +git-version = { workspace = true } +hex = { workspace = true } +lazy_static = { workspace = true } +log = { workspace = true } +regex = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +zenoh = { workspace = true } +zenoh-collections = { workspace = true } +zenoh-core = { workspace = true } +zenoh-ext = { workspace = true } +zenoh-plugin-trait = { workspace = true } +zenoh-util = { workspace = true } + +[build-dependencies] +rustc_version = { workspace = true } + +[package.metadata.deb] +name = "zenoh-plugin-ros2dds" +maintainer = "zenoh-dev@eclipse.org" +copyright = "2017, 2022 ZettaScale Technology Inc." +section = "net" +license-file = ["../LICENSE", "0"] +depends = "zenohd (=0.10.0-dev)" diff --git a/zenoh-plugin-ros2dds/README.md b/zenoh-plugin-ros2dds/README.md new file mode 100644 index 0000000..88b37c2 --- /dev/null +++ b/zenoh-plugin-ros2dds/README.md @@ -0,0 +1,5 @@ +# zplugin-ros2dds + +A new Zenoh bridge for ROS2. + +:warning: Work in progress... diff --git a/zenoh-plugin-ros2dds/build.rs b/zenoh-plugin-ros2dds/build.rs new file mode 100644 index 0000000..8e34100 --- /dev/null +++ b/zenoh-plugin-ros2dds/build.rs @@ -0,0 +1,21 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +fn main() { + // Add rustc version to zenohd + let version_meta = rustc_version::version_meta().unwrap(); + println!( + "cargo:rustc-env=RUSTC_VERSION={}", + version_meta.short_version_string + ); +} diff --git a/zenoh-plugin-ros2dds/src/config.rs b/zenoh-plugin-ros2dds/src/config.rs new file mode 100644 index 0000000..bbbf572 --- /dev/null +++ b/zenoh-plugin-ros2dds/src/config.rs @@ -0,0 +1,347 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use regex::Regex; +use serde::{de, de::Visitor, Deserialize, Deserializer}; +use std::env; +use std::fmt; +use std::time::Duration; +use zenoh::prelude::*; + +pub const DEFAULT_NAMESPACE: &str = "/"; +pub const DEFAULT_NODENAME: &str = "zenoh_bridge_ros2dds"; +pub const DEFAULT_DOMAIN: u32 = 0; +pub const DEFAULT_RELIABLE_ROUTES_BLOCKING: bool = true; +pub const DEFAULT_TRANSIENT_LOCAL_CACHE_MULTIPLIER: usize = 10; +pub const DEFAULT_QUERIES_TIMEOUT: f32 = 5.0; +pub const DEFAULT_DDS_LOCALHOST_ONLY: bool = false; + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Config { + #[serde(default)] + pub id: Option, + #[serde(default = "default_namespace")] + pub namespace: String, + #[serde(default = "default_nodename")] + pub nodename: OwnedKeyExpr, + #[serde(default = "default_domain")] + pub domain: u32, + #[serde(default = "default_localhost_only")] + pub ros_localhost_only: bool, + #[serde(default, flatten)] + pub allowance: Option, + #[serde(default, deserialize_with = "deserialize_max_frequencies")] + pub pub_max_frequencies: Vec<(Regex, f32)>, + #[serde(default)] + #[cfg(feature = "dds_shm")] + pub shm_enabled: bool, + #[serde(default = "default_transient_local_cache_multiplier")] + pub transient_local_cache_multiplier: usize, + #[serde( + default = "default_queries_timeout", + deserialize_with = "deserialize_duration" + )] + pub queries_timeout: Duration, + #[serde(default = "default_reliable_routes_blocking")] + pub reliable_routes_blocking: bool, + #[serde(default)] + __required__: bool, + #[serde(default, deserialize_with = "deserialize_paths")] + __path__: Vec, +} + +#[derive(Deserialize, Debug)] +pub enum Allowance { + #[serde(rename = "allow")] + Allow(ROS2InterfacesRegex), + #[serde(rename = "deny")] + Deny(ROS2InterfacesRegex), +} + +impl Allowance { + pub fn is_publisher_allowed(&self, name: &str) -> bool { + use Allowance::*; + match self { + Allow(r) => r + .publishers + .as_ref() + .map(|re| re.is_match(name)) + .unwrap_or(false), + Deny(r) => r + .publishers + .as_ref() + .map(|re| !re.is_match(name)) + .unwrap_or(true), + } + } + + pub fn is_subscriber_allowed(&self, name: &str) -> bool { + use Allowance::*; + match self { + Allow(r) => r + .subscribers + .as_ref() + .map(|re| re.is_match(name)) + .unwrap_or(false), + Deny(r) => r + .subscribers + .as_ref() + .map(|re| !re.is_match(name)) + .unwrap_or(true), + } + } + + pub fn is_service_srv_allowed(&self, name: &str) -> bool { + use Allowance::*; + match self { + Allow(r) => r + .service_servers + .as_ref() + .map(|re| re.is_match(name)) + .unwrap_or(false), + Deny(r) => r + .service_servers + .as_ref() + .map(|re| !re.is_match(name)) + .unwrap_or(true), + } + } + + pub fn is_service_cli_allowed(&self, name: &str) -> bool { + use Allowance::*; + match self { + Allow(r) => r + .service_clients + .as_ref() + .map(|re| re.is_match(name)) + .unwrap_or(false), + Deny(r) => r + .service_clients + .as_ref() + .map(|re| !re.is_match(name)) + .unwrap_or(true), + } + } + + pub fn is_action_srv_allowed(&self, name: &str) -> bool { + use Allowance::*; + match self { + Allow(r) => r + .action_servers + .as_ref() + .map(|re| re.is_match(name)) + .unwrap_or(false), + Deny(r) => r + .action_servers + .as_ref() + .map(|re| !re.is_match(name)) + .unwrap_or(true), + } + } + + pub fn is_action_cli_allowed(&self, name: &str) -> bool { + use Allowance::*; + match self { + Allow(r) => r + .action_clients + .as_ref() + .map(|re| re.is_match(name)) + .unwrap_or(false), + Deny(r) => r + .action_clients + .as_ref() + .map(|re| !re.is_match(name)) + .unwrap_or(true), + } + } +} + +#[derive(Deserialize, Debug, Default)] +pub struct ROS2InterfacesRegex { + #[serde( + default, + deserialize_with = "deserialize_regex", + skip_serializing_if = "Option::is_none" + )] + pub publishers: Option, + #[serde( + default, + deserialize_with = "deserialize_regex", + skip_serializing_if = "Option::is_none" + )] + pub subscribers: Option, + #[serde( + default, + deserialize_with = "deserialize_regex", + skip_serializing_if = "Option::is_none" + )] + pub service_servers: Option, + #[serde( + default, + deserialize_with = "deserialize_regex", + skip_serializing_if = "Option::is_none" + )] + pub service_clients: Option, + #[serde( + default, + deserialize_with = "deserialize_regex", + skip_serializing_if = "Option::is_none" + )] + pub action_servers: Option, + #[serde( + default, + deserialize_with = "deserialize_regex", + skip_serializing_if = "Option::is_none" + )] + pub action_clients: Option, +} + +fn default_namespace() -> String { + DEFAULT_NAMESPACE.to_string() +} + +fn default_nodename() -> OwnedKeyExpr { + unsafe { OwnedKeyExpr::from_string_unchecked(DEFAULT_NODENAME.into()) } +} + +fn default_domain() -> u32 { + if let Ok(s) = env::var("ROS_DOMAIN_ID") { + s.parse::().unwrap_or(DEFAULT_DOMAIN) + } else { + DEFAULT_DOMAIN + } +} + +fn deserialize_paths<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct V; + impl<'de> serde::de::Visitor<'de> for V { + type Value = Vec; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a string or vector of strings") + } + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(vec![v.into()]) + } + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let mut v = if let Some(l) = seq.size_hint() { + Vec::with_capacity(l) + } else { + Vec::new() + }; + while let Some(s) = seq.next_element()? { + v.push(s); + } + Ok(v) + } + } + deserializer.deserialize_any(V) +} + +fn default_reliable_routes_blocking() -> bool { + DEFAULT_RELIABLE_ROUTES_BLOCKING +} + +fn default_localhost_only() -> bool { + env::var("ROS_LOCALHOST_ONLY").as_deref() == Ok("1") +} + +fn default_transient_local_cache_multiplier() -> usize { + DEFAULT_TRANSIENT_LOCAL_CACHE_MULTIPLIER +} + +fn default_queries_timeout() -> Duration { + Duration::from_secs_f32(DEFAULT_QUERIES_TIMEOUT) +} + +fn deserialize_duration<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let seconds: f32 = Deserialize::deserialize(deserializer)?; + Ok(Duration::from_secs_f32(seconds)) +} + +fn deserialize_regex<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(RegexVisitor) +} + +// Serde Visitor for Regex deserialization. +// It accepts either a String, either a list of Strings (that are concatenated with `|`) +struct RegexVisitor; + +impl<'de> Visitor<'de> for RegexVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(r#"either a string or a list of strings"#) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Regex::new(&format!("^{value}$")) + .map(Some) + .map_err(|e| de::Error::custom(format!("Invalid regex '{value}': {e}"))) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let mut vec: Vec = Vec::new(); + while let Some(s) = seq.next_element::()? { + vec.push(format!("^{s}$")); + } + let s: String = vec.join("|"); + Regex::new(&s) + .map(Some) + .map_err(|e| de::Error::custom(format!("Invalid regex '{s}': {e}"))) + } +} + +fn deserialize_max_frequencies<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let strs: Vec = Deserialize::deserialize(deserializer)?; + let mut result: Vec<(Regex, f32)> = Vec::with_capacity(strs.len()); + for s in strs { + let i = s + .find('=') + .ok_or_else(|| de::Error::custom(format!("Invalid 'max_frequency': {s}")))?; + let regex = Regex::new(&s[0..i]).map_err(|e| { + de::Error::custom(format!("Invalid regex for 'max_frequency': '{s}': {e}")) + })?; + let frequency: f32 = s[i + 1..].parse().map_err(|e| { + de::Error::custom(format!( + "Invalid float value for 'max_frequency': '{s}': {e}" + )) + })?; + result.push((regex, frequency)); + } + Ok(result) +} diff --git a/zenoh-plugin-ros2dds/src/dds_discovery.rs b/zenoh-plugin-ros2dds/src/dds_discovery.rs new file mode 100644 index 0000000..b37fef8 --- /dev/null +++ b/zenoh-plugin-ros2dds/src/dds_discovery.rs @@ -0,0 +1,738 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use async_std::task; +use cyclors::qos::{History, HistoryKind, Qos}; +use cyclors::*; +use flume::Sender; +use serde::{Deserialize, Serialize, Serializer}; +use std::ffi::{CStr, CString}; +use std::fmt; +use std::mem::MaybeUninit; +use std::os::raw; +use std::slice; +use std::sync::Arc; +use std::time::Duration; +use zenoh::buffers::ZBuf; +#[cfg(feature = "dds_shm")] +use zenoh::buffers::ZSlice; +use zenoh::prelude::*; +use zenoh::publication::CongestionControl; +use zenoh::Session; +use zenoh_core::SyncResolve; + +use crate::gid::Gid; + +const MAX_SAMPLES: usize = 32; + +#[derive(Debug)] +pub struct TypeInfo { + ptr: *mut dds_typeinfo_t, +} + +impl TypeInfo { + pub unsafe fn new(ptr: *const dds_typeinfo_t) -> TypeInfo { + let ptr = ddsi_typeinfo_dup(ptr); + TypeInfo { ptr } + } +} + +impl Drop for TypeInfo { + fn drop(&mut self) { + unsafe { + ddsi_typeinfo_free(self.ptr); + } + } +} + +unsafe impl Send for TypeInfo {} +unsafe impl Sync for TypeInfo {} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DdsEntity { + pub key: Gid, + pub participant_key: Gid, + pub topic_name: String, + pub type_name: String, + #[serde(skip)] + pub type_info: Option>, + pub keyless: bool, + pub qos: Qos, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DdsParticipant { + pub key: Gid, + pub qos: Qos, +} + +#[derive(Debug)] +pub enum DDSDiscoveryEvent { + DiscoveredPublication { entity: DdsEntity }, + UndiscoveredPublication { key: Gid }, + DiscoveredSubscription { entity: DdsEntity }, + UndiscoveredSubscription { key: Gid }, + DiscoveredParticipant { entity: DdsParticipant }, + UndiscoveredParticipant { key: Gid }, +} + +#[derive(Debug, Clone, Copy)] +pub enum DiscoveryType { + Participant, + Publication, + Subscription, +} + +impl fmt::Display for DiscoveryType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DiscoveryType::Participant => write!(f, "participant"), + DiscoveryType::Publication => write!(f, "publication"), + DiscoveryType::Subscription => write!(f, "subscription"), + } + } +} + +#[cfg(feature = "dds_shm")] +#[derive(Clone, Copy)] +struct IoxChunk { + ptr: *mut std::ffi::c_void, + header: *mut iceoryx_header_t, +} + +#[cfg(feature = "dds_shm")] +impl IoxChunk { + fn as_slice(&self) -> &[u8] { + unsafe { slice::from_raw_parts(self.ptr as *const u8, (*self.header).data_size as usize) } + } + + fn len(&self) -> usize { + unsafe { (*self.header).data_size as usize } + } +} + +pub struct DDSRawSample { + sdref: *mut ddsi_serdata, + data: ddsrt_iovec_t, + #[cfg(feature = "dds_shm")] + iox_chunk: Option, +} + +impl DDSRawSample { + pub unsafe fn create(serdata: *const ddsi_serdata) -> DDSRawSample { + let mut sdref: *mut ddsi_serdata = std::ptr::null_mut(); + let mut data = ddsrt_iovec_t { + iov_base: std::ptr::null_mut(), + iov_len: 0, + }; + + #[cfg(feature = "dds_shm")] + let iox_chunk: Option = match ((*serdata).iox_chunk).is_null() { + false => { + let iox_chunk_ptr = (*serdata).iox_chunk; + let header = iceoryx_header_from_chunk(iox_chunk_ptr); + + // If the Iceoryx chunk contains raw sample data this needs to be serialized before forwading to Zenoh + if (*header).shm_data_state == iox_shm_data_state_t_IOX_CHUNK_CONTAINS_RAW_DATA { + let serialized_serdata = ddsi_serdata_from_sample( + (*serdata).type_, + (*serdata).kind, + (*serdata).iox_chunk, + ); + + let size = ddsi_serdata_size(serialized_serdata); + sdref = + ddsi_serdata_to_ser_ref(serialized_serdata, 0, size as usize, &mut data); + ddsi_serdata_unref(serialized_serdata); + + // IoxChunk not needed where raw data has been serialized + None + } else { + Some(IoxChunk { + ptr: iox_chunk_ptr, + header, + }) + } + } + true => None, + }; + + // At this point sdref will be null if: + // + // * Iceoryx was not enabled/used - in this case data will contain the CDR header and payload + // * Iceoryx chunk contained serialized data - in this case data will contain the CDR header + if sdref.is_null() { + let size = ddsi_serdata_size(serdata); + sdref = ddsi_serdata_to_ser_ref(serdata, 0, size as usize, &mut data); + } + + #[cfg(feature = "dds_shm")] + return DDSRawSample { + sdref, + data, + iox_chunk, + }; + #[cfg(not(feature = "dds_shm"))] + return DDSRawSample { sdref, data }; + } + + fn data_as_slice(&self) -> &[u8] { + unsafe { + slice::from_raw_parts(self.data.iov_base as *const u8, self.data.iov_len as usize) + } + } + + pub fn payload_as_slice(&self) -> &[u8] { + unsafe { + #[cfg(feature = "dds_shm")] + { + if let Some(iox_chunk) = self.iox_chunk.as_ref() { + return iox_chunk.as_slice(); + } + } + &slice::from_raw_parts(self.data.iov_base as *const u8, self.data.iov_len as usize)[4..] + } + } + + pub fn hex_encode(&self) -> String { + let mut encoded = String::new(); + let data_encoded = hex::encode(self.data_as_slice()); + encoded.push_str(data_encoded.as_str()); + + #[cfg(feature = "dds_shm")] + { + if let Some(iox_chunk) = self.iox_chunk.as_ref() { + let iox_encoded = hex::encode(iox_chunk.as_slice()); + encoded.push_str(iox_encoded.as_str()); + } + } + + encoded + } + + pub fn len(&self) -> usize { + #[cfg(feature = "dds_shm")] + { + self.data.iov_len + self.iox_chunk.as_ref().map(IoxChunk::len).unwrap_or(0) + } + + #[cfg(not(feature = "dds_shm"))] + self.data.iov_len.try_into().unwrap() + } +} + +impl Drop for DDSRawSample { + fn drop(&mut self) { + unsafe { + ddsi_serdata_to_ser_unref(self.sdref, &self.data); + } + } +} + +impl fmt::Debug for DDSRawSample { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[cfg(feature = "dds_shm")] + { + // Where data was received via Iceoryx write both the header (contained in buf.data) and + // payload (contained in buf.iox_chunk) to the formatter. + if let Some(iox_chunk) = self.iox_chunk { + return write!( + f, + "[{:02x?}, {:02x?}]", + self.data_as_slice(), + iox_chunk.as_slice() + ); + } + } + write!(f, "{:02x?}", self.data_as_slice()) + } +} + +impl From<&DDSRawSample> for ZBuf { + fn from(buf: &DDSRawSample) -> Self { + #[cfg(feature = "dds_shm")] + { + // Where data was received via Iceoryx return both the header (contained in buf.data) and + // payload (contained in buf.iox_chunk) in a buffer. + if let Some(iox_chunk) = buf.iox_chunk { + let mut zbuf = ZBuf::default(); + zbuf.push_zslice(ZSlice::from(buf.data_as_slice().to_vec())); + zbuf.push_zslice(ZSlice::from(iox_chunk.as_slice().to_vec())); + return zbuf; + } + } + buf.data_as_slice().to_vec().into() + } +} + +impl From<&DDSRawSample> for Value { + fn from(buf: &DDSRawSample) -> Self { + ZBuf::from(buf).into() + } +} + +unsafe extern "C" fn on_data(dr: dds_entity_t, arg: *mut std::os::raw::c_void) { + let btx = Box::from_raw(arg as *mut (DiscoveryType, Sender)); + let discovery_type = btx.0; + let sender = &btx.1; + let dp = dds_get_participant(dr); + let mut dpih: dds_instance_handle_t = 0; + let _ = dds_get_instance_handle(dp, &mut dpih); + + #[allow(clippy::uninit_assumed_init)] + let mut si = MaybeUninit::<[dds_sample_info_t; MAX_SAMPLES as usize]>::uninit(); + let mut samples: [*mut ::std::os::raw::c_void; MAX_SAMPLES as usize] = + [std::ptr::null_mut(); MAX_SAMPLES as usize]; + samples[0] = std::ptr::null_mut(); + + let n = dds_take( + dr, + samples.as_mut_ptr() as *mut *mut raw::c_void, + si.as_mut_ptr() as *mut dds_sample_info_t, + MAX_SAMPLES, + MAX_SAMPLES as u32, + ); + let si = si.assume_init(); + + for i in 0..n { + match discovery_type { + DiscoveryType::Publication | DiscoveryType::Subscription => { + let sample = samples[i as usize] as *mut dds_builtintopic_endpoint_t; + if (*sample).participant_instance_handle == dpih { + // Ignore discovery of entities created by our own participant + continue; + } + let is_alive = si[i as usize].instance_state == dds_instance_state_DDS_IST_ALIVE; + let key: Gid = (*sample).key.v.into(); + + if is_alive { + let topic_name = match CStr::from_ptr((*sample).topic_name).to_str() { + Ok(s) => s, + Err(e) => { + log::warn!("Discovery of an invalid topic name: {}", e); + continue; + } + }; + if topic_name.starts_with("DCPS") { + log::debug!( + "Ignoring discovery of {} ({} is a builtin topic)", + key, + topic_name + ); + continue; + } + + let type_name = match CStr::from_ptr((*sample).type_name).to_str() { + Ok(s) => s, + Err(e) => { + log::warn!("Discovery of an invalid topic type: {}", e); + continue; + } + }; + let participant_key = (*sample).participant_key.v.into(); + let keyless = (*sample).key.v[15] == 3 || (*sample).key.v[15] == 4; + + log::debug!( + "Discovered DDS {} {} from Participant {} on {} with type {} (keyless: {})", + discovery_type, + key, + participant_key, + topic_name, + type_name, + keyless + ); + + let mut type_info: *const dds_typeinfo_t = std::ptr::null(); + let ret = dds_builtintopic_get_endpoint_type_info(sample, &mut type_info); + + let type_info = match ret { + 0 => match type_info.is_null() { + false => Some(Arc::new(TypeInfo::new(type_info))), + true => { + log::trace!( + "Type information not available for type {}", + type_name + ); + None + } + }, + _ => { + log::warn!( + "Failed to lookup type information({})", + CStr::from_ptr(dds_strretcode(ret)) + .to_str() + .unwrap_or("unrecoverable DDS retcode") + ); + None + } + }; + + // send a DDSDiscoveryEvent + let entity = DdsEntity { + key, + participant_key, + topic_name: String::from(topic_name), + type_name: String::from(type_name), + keyless, + type_info, + qos: Qos::from_qos_native((*sample).qos), + }; + + if let DiscoveryType::Publication = discovery_type { + send_discovery_event( + sender, + DDSDiscoveryEvent::DiscoveredPublication { entity }, + ); + } else { + send_discovery_event( + sender, + DDSDiscoveryEvent::DiscoveredSubscription { entity }, + ); + } + } else if let DiscoveryType::Publication = discovery_type { + send_discovery_event( + sender, + DDSDiscoveryEvent::UndiscoveredPublication { key }, + ); + } else { + send_discovery_event( + sender, + DDSDiscoveryEvent::UndiscoveredSubscription { key }, + ); + } + } + DiscoveryType::Participant => { + let sample = samples[i as usize] as *mut dds_builtintopic_participant_t; + let is_alive = si[i as usize].instance_state == dds_instance_state_DDS_IST_ALIVE; + let key: Gid = (*sample).key.v.into(); + + let mut guid = dds_builtintopic_guid { v: [0; 16] }; + let _ = dds_get_guid(dp, &mut guid); + let guid = guid.v.into(); + + if key == guid { + // Ignore discovery of entities created by our own participant + continue; + } + + if is_alive { + log::debug!("Discovered DDS Participant {})", key,); + + // Send a DDSDiscoveryEvent + let entity = DdsParticipant { + key, + qos: Qos::from_qos_native((*sample).qos), + }; + + send_discovery_event( + sender, + DDSDiscoveryEvent::DiscoveredParticipant { entity }, + ); + } else { + send_discovery_event( + sender, + DDSDiscoveryEvent::UndiscoveredParticipant { key }, + ); + } + } + } + } + dds_return_loan( + dr, + samples.as_mut_ptr() as *mut *mut raw::c_void, + MAX_SAMPLES as i32, + ); + Box::into_raw(btx); +} + +fn send_discovery_event(sender: &Sender, event: DDSDiscoveryEvent) { + if let Err(e) = sender.try_send(event) { + log::error!( + "INTERNAL ERROR sending DDSDiscoveryEvent to internal channel: {:?}", + e + ); + } +} + +pub fn run_discovery(dp: dds_entity_t, tx: Sender) { + unsafe { + let ptx = Box::new((DiscoveryType::Publication, tx.clone())); + let stx = Box::new((DiscoveryType::Subscription, tx.clone())); + let dptx = Box::new((DiscoveryType::Participant, tx)); + let sub_listener = dds_create_listener(Box::into_raw(ptx) as *mut std::os::raw::c_void); + dds_lset_data_available(sub_listener, Some(on_data)); + + let _pr = dds_create_reader( + dp, + DDS_BUILTIN_TOPIC_DCPSPUBLICATION, + std::ptr::null(), + sub_listener, + ); + + let sub_listener = dds_create_listener(Box::into_raw(stx) as *mut std::os::raw::c_void); + dds_lset_data_available(sub_listener, Some(on_data)); + let _sr = dds_create_reader( + dp, + DDS_BUILTIN_TOPIC_DCPSSUBSCRIPTION, + std::ptr::null(), + sub_listener, + ); + + let sub_listener = dds_create_listener(Box::into_raw(dptx) as *mut std::os::raw::c_void); + dds_lset_data_available(sub_listener, Some(on_data)); + let _dpr = dds_create_reader( + dp, + DDS_BUILTIN_TOPIC_DCPSPARTICIPANT, + std::ptr::null(), + sub_listener, + ); + } +} + +unsafe extern "C" fn data_forwarder_listener(dr: dds_entity_t, arg: *mut std::os::raw::c_void) { + let pa = arg as *mut (String, KeyExpr, Arc, CongestionControl); + let mut zp: *mut ddsi_serdata = std::ptr::null_mut(); + #[allow(clippy::uninit_assumed_init)] + let mut si = MaybeUninit::<[dds_sample_info_t; 1]>::uninit(); + while dds_takecdr( + dr, + &mut zp, + 1, + si.as_mut_ptr() as *mut dds_sample_info_t, + DDS_ANY_STATE, + ) > 0 + { + let si = si.assume_init(); + if si[0].valid_data { + let raw_sample = DDSRawSample::create(zp); + + if *crate::LOG_PAYLOAD { + log::trace!( + "Route Publisher (DDS:{} -> Zenoh:{}) - routing payload: {:02x?}", + &(*pa).0, + &(*pa).1, + raw_sample + ); + } else { + log::trace!( + "Route Publisher (DDS:{} -> Zenoh:{}) - routing {} bytes", + &(*pa).0, + &(*pa).1, + raw_sample.len() + ); + } + let _ = (*pa) + .2 + .put(&(*pa).1, &raw_sample) + .congestion_control((*pa).3) + .res_sync(); + } + ddsi_serdata_unref(zp); + } +} + +#[allow(clippy::too_many_arguments)] +pub fn create_forwarding_dds_reader( + dp: dds_entity_t, + topic_name: String, + type_name: String, + type_info: &Option>, + keyless: bool, + mut qos: Qos, + z_key: KeyExpr, + z: Arc, + read_period: Option, + congestion_ctrl: CongestionControl, +) -> Result { + unsafe { + let t = create_topic(dp, &topic_name, &type_name, type_info, keyless); + + match read_period { + None => { + // Use a Listener to route data as soon as it arrives + let arg = Box::new((topic_name, z_key, z, congestion_ctrl)); + let sub_listener = + dds_create_listener(Box::into_raw(arg) as *mut std::os::raw::c_void); + dds_lset_data_available(sub_listener, Some(data_forwarder_listener)); + let qos_native = qos.to_qos_native(); + let reader = dds_create_reader(dp, t, qos_native, sub_listener); + Qos::delete_qos_native(qos_native); + if reader >= 0 { + let res = dds_reader_wait_for_historical_data(reader, qos::DDS_100MS_DURATION); + if res < 0 { + log::error!( + "Error calling dds_reader_wait_for_historical_data(): {}", + CStr::from_ptr(dds_strretcode(-res)) + .to_str() + .unwrap_or("unrecoverable DDS retcode") + ); + } + Ok(reader) + } else { + Err(format!( + "Error creating DDS Reader: {}", + CStr::from_ptr(dds_strretcode(-reader)) + .to_str() + .unwrap_or("unrecoverable DDS retcode") + )) + } + } + Some(period) => { + // Use a periodic task that takes data to route from a Reader with KEEP_LAST 1 + qos.history = Some(History { + kind: HistoryKind::KEEP_LAST, + depth: 1, + }); + let qos_native = qos.to_qos_native(); + let reader = dds_create_reader(dp, t, qos_native, std::ptr::null()); + let z_key = z_key.into_owned(); + task::spawn(async move { + // loop while reader's instance handle remain the same + // (if reader was deleted, its dds_entity_t value might have been + // reused by a new entity... don't trust it! Only trust instance handle) + let mut original_handle: dds_instance_handle_t = 0; + dds_get_instance_handle(reader, &mut original_handle); + let mut handle: dds_instance_handle_t = 0; + while dds_get_instance_handle(reader, &mut handle) == DDS_RETCODE_OK as i32 { + if handle != original_handle { + break; + } + + async_std::task::sleep(period).await; + let mut zp: *mut ddsi_serdata = std::ptr::null_mut(); + #[allow(clippy::uninit_assumed_init)] + let mut si = MaybeUninit::<[dds_sample_info_t; 1]>::uninit(); + while dds_takecdr( + reader, + &mut zp, + 1, + si.as_mut_ptr() as *mut dds_sample_info_t, + DDS_ANY_STATE, + ) > 0 + { + let si = si.assume_init(); + if si[0].valid_data { + log::trace!( + "Route (periodic) data to zenoh resource with rid={}", + z_key + ); + + let raw_sample = DDSRawSample::create(zp); + + let _ = z + .put(&z_key, &raw_sample) + .congestion_control(congestion_ctrl) + .res_sync(); + } + ddsi_serdata_unref(zp); + } + } + }); + Ok(reader) + } + } + } +} + +unsafe fn create_topic( + dp: dds_entity_t, + topic_name: &str, + type_name: &str, + type_info: &Option>, + keyless: bool, +) -> dds_entity_t { + let cton = CString::new(topic_name.to_owned()).unwrap().into_raw(); + let ctyn = CString::new(type_name.to_owned()).unwrap().into_raw(); + + match type_info { + None => cdds_create_blob_topic(dp, cton, ctyn, keyless), + Some(type_info) => { + let mut descriptor: *mut dds_topic_descriptor_t = std::ptr::null_mut(); + + let ret = dds_create_topic_descriptor( + dds_find_scope_DDS_FIND_SCOPE_GLOBAL, + dp, + type_info.ptr, + 500000000, + &mut descriptor, + ); + let mut topic: dds_entity_t = 0; + if ret == (DDS_RETCODE_OK as i32) { + topic = dds_create_topic(dp, descriptor, cton, std::ptr::null(), std::ptr::null()); + assert!(topic >= 0); + dds_delete_topic_descriptor(descriptor); + } + topic + } + } +} + +pub fn create_forwarding_dds_writer( + dp: dds_entity_t, + topic_name: String, + type_name: String, + keyless: bool, + qos: Qos, +) -> Result { + let cton = CString::new(topic_name).unwrap().into_raw(); + let ctyn = CString::new(type_name).unwrap().into_raw(); + + unsafe { + let t = cdds_create_blob_topic(dp, cton, ctyn, keyless); + let qos_native = qos.to_qos_native(); + let writer: i32 = dds_create_writer(dp, t, qos_native, std::ptr::null_mut()); + Qos::delete_qos_native(qos_native); + if writer >= 0 { + Ok(writer) + } else { + Err(format!( + "Error creating DDS Writer: {}", + CStr::from_ptr(dds_strretcode(-writer)) + .to_str() + .unwrap_or("unrecoverable DDS retcode") + )) + } + } +} + +pub fn delete_dds_entity(entity: dds_entity_t) -> Result<(), String> { + unsafe { + let r = dds_delete(entity); + match r { + 0 | DDS_RETCODE_ALREADY_DELETED => Ok(()), + e => Err(format!("Error deleting DDS entity - retcode={e}")), + } + } +} + +pub fn get_guid(entity: &dds_entity_t) -> Result { + unsafe { + let mut guid = dds_guid_t { v: [0; 16] }; + let r = dds_get_guid(*entity, &mut guid); + if r == 0 { + Ok(Gid::from(guid.v)) + } else { + Err(format!("Error getting GUID of DDS entity - retcode={r}")) + } + } +} + +pub fn serialize_entity_guid(entity: &dds_entity_t, s: S) -> Result +where + S: Serializer, +{ + match get_guid(entity) { + Ok(guid) => s.serialize_str(&guid.to_string()), + Err(_) => s.serialize_str("UNKOWN_GUID"), + } +} diff --git a/zenoh-plugin-ros2dds/src/discovered_entities.rs b/zenoh-plugin-ros2dds/src/discovered_entities.rs new file mode 100644 index 0000000..e1f4adc --- /dev/null +++ b/zenoh-plugin-ros2dds/src/discovered_entities.rs @@ -0,0 +1,475 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use std::collections::HashMap; +use std::fmt::{self, Debug}; +use zenoh::prelude::r#async::AsyncResolve; +use zenoh::{prelude::*, queryable::Query}; + +use crate::events::ROS2DiscoveryEvent; +use crate::ros_discovery::NodeEntitiesInfo; +use crate::{ + dds_discovery::{DdsEntity, DdsParticipant}, + gid::Gid, + node_info::*, + ros_discovery::ParticipantEntitiesInfo, +}; + +zenoh::kedefine!( + pub(crate) ke_admin_participant: "dds/${pgid:*}", + pub(crate) ke_admin_writer: "dds/${pgid:*}/writer/${wgid:*}/${topic:**}", + pub(crate) ke_admin_reader: "dds/${pgid:*}/reader/${wgid:*}/${topic:**}", + pub(crate) ke_admin_node: "node/${node_id:**}", +); + +#[derive(Default)] +pub struct DiscoveredEntities { + participants: HashMap, + writers: HashMap, + readers: HashMap, + ros_participant_info: HashMap, + nodes_info: HashMap>, + admin_space: HashMap, +} + +impl Debug for DiscoveredEntities { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "participants: {:?}\n", + self.participants.keys().collect::>() + )?; + write!( + f, + "writers: {:?}\n", + self.writers.keys().collect::>() + )?; + write!( + f, + "readers: {:?}\n", + self.readers.keys().collect::>() + )?; + write!(f, "ros_participant_info: {:?}\n", self.ros_participant_info)?; + write!(f, "nodes_info: {:?}\n", self.nodes_info)?; + write!( + f, + "admin_space: {:?}\n", + self.admin_space.keys().collect::>() + ) + } +} + +#[derive(Debug)] +enum EntityRef { + Participant(Gid), + Writer(Gid), + Reader(Gid), + Node(Gid, String), +} + +impl DiscoveredEntities { + #[inline] + pub fn add_participant(&mut self, participant: DdsParticipant) { + self.admin_space.insert( + zenoh::keformat!(ke_admin_participant::formatter(), pgid = participant.key).unwrap(), + EntityRef::Participant(participant.key), + ); + self.participants.insert(participant.key, participant); + } + + #[inline] + pub fn remove_participant(&mut self, gid: &Gid) -> Vec { + let mut events: Vec = Vec::new(); + // Remove Participant from participants list and from admin_space + self.participants.remove(gid); + self.admin_space + .remove(&zenoh::keformat!(ke_admin_participant::formatter(), pgid = gid).unwrap()); + // Remove associated NodeInfos + if let Some(nodes) = self.nodes_info.remove(gid) { + for (name, mut node) in nodes { + log::info!("Undiscovered ROS Node {}", name); + self.admin_space.remove( + &zenoh::keformat!(ke_admin_node::formatter(), node_id = node.id_as_keyexpr(),) + .unwrap(), + ); + // return undiscovery events for this node + events.append(&mut node.remove_all_entities()); + } + } + events + } + + #[inline] + pub fn add_writer(&mut self, writer: DdsEntity) -> Option { + // insert in admin space + self.admin_space.insert( + zenoh::keformat!( + ke_admin_writer::formatter(), + pgid = writer.participant_key, + wgid = writer.key, + topic = &writer.topic_name, + ) + .unwrap(), + EntityRef::Writer(writer.key), + ); + + // Check if this Writer is present in some NodeInfo.undiscovered_writer list + let mut event: Option = None; + for (_, nodes_map) in &mut self.nodes_info { + for (_, node) in nodes_map { + if let Some(i) = node + .undiscovered_writer + .iter() + .position(|gid| gid == &writer.key) + { + // update the NodeInfo with this Writer's info + node.undiscovered_writer.remove(i); + event = node.update_with_writer(&writer); + break; + } + } + if event.is_some() { + break; + } + } + + // insert in Writers list + self.writers.insert(writer.key, writer); + event + } + + #[inline] + pub fn get_writer(&self, gid: &Gid) -> Option<&DdsEntity> { + self.writers.get(gid) + } + + #[inline] + pub fn remove_writer(&mut self, gid: &Gid) -> Option { + if let Some(writer) = self.writers.remove(gid) { + self.admin_space.remove( + &zenoh::keformat!( + ke_admin_writer::formatter(), + pgid = writer.participant_key, + wgid = writer.key, + topic = &writer.topic_name, + ) + .unwrap(), + ); + + // Remove the Writer from any NodeInfo that might use it, possibly leading to a UndiscoveredX event + for (_, nodes_map) in &mut self.nodes_info { + for (_, node) in nodes_map { + if let Some(e) = node.remove_writer(gid) { + // A Reader can be used by only 1 Node, no need to go on with loops + return Some(e); + } + } + } + } + None + } + + #[inline] + pub fn add_reader(&mut self, reader: DdsEntity) -> Option { + // insert in admin space + self.admin_space.insert( + zenoh::keformat!( + ke_admin_reader::formatter(), + pgid = reader.participant_key, + wgid = reader.key, + topic = &reader.topic_name, + ) + .unwrap(), + EntityRef::Reader(reader.key), + ); + + // Check if this Reader is present in some NodeInfo.undiscovered_reader list + let mut event = None; + for (_, nodes_map) in &mut self.nodes_info { + for (_, node) in nodes_map { + if let Some(i) = node + .undiscovered_reader + .iter() + .position(|gid| gid == &reader.key) + { + // update the NodeInfo with this Reader's info + node.undiscovered_reader.remove(i); + event = node.update_with_writer(&reader); + break; + } + } + if event.is_some() { + break; + } + } + + // insert in Readers list + self.readers.insert(reader.key, reader); + event + } + + #[inline] + pub fn get_reader(&self, gid: &Gid) -> Option<&DdsEntity> { + self.readers.get(gid) + } + + #[inline] + pub fn remove_reader(&mut self, gid: &Gid) -> Option { + if let Some(reader) = self.readers.remove(gid) { + self.admin_space.remove( + &zenoh::keformat!( + ke_admin_reader::formatter(), + pgid = reader.participant_key, + wgid = reader.key, + topic = &reader.topic_name, + ) + .unwrap(), + ); + + // Remove the Reader from any NodeInfo that might use it, possibly leading to a UndiscoveredX event + for (_, nodes_map) in &mut self.nodes_info { + for (_, node) in nodes_map { + if let Some(e) = node.remove_reader(gid) { + // A Reader can be used by only 1 Node, no need to go on with loops + return Some(e); + } + } + } + } + None + } + + pub fn update_participant_info( + &mut self, + ros_info: ParticipantEntitiesInfo, + ) -> Vec { + let mut events: Vec = Vec::new(); + let Self { + writers, + readers, + nodes_info, + admin_space, + .. + } = self; + let nodes_map = nodes_info.entry(ros_info.gid).or_insert_with(HashMap::new); + + // Remove nodes that are no longer present in ParticipantEntitiesInfo + nodes_map.retain(|name, node| { + if !ros_info.node_entities_info_seq.contains_key(name) { + log::info!("Undiscovered ROS Node {}", name); + admin_space.remove( + &zenoh::keformat!(ke_admin_node::formatter(), node_id = node.id_as_keyexpr(),) + .unwrap(), + ); + // return undiscovery events for this node + events.append(&mut node.remove_all_entities()); + false + } else { + true + } + }); + + // For each declared node in this ros_node_info + for (name, ros_node_info) in &ros_info.node_entities_info_seq { + // If node was not yet discovered, add a new NodeInfo + if !nodes_map.contains_key(name) { + log::info!("Discovered ROS Node {}", name); + match NodeInfo::create( + ros_node_info.node_namespace.clone(), + ros_node_info.node_name.clone(), + ros_info.gid, + ) { + Ok(node) => { + self.admin_space.insert( + zenoh::keformat!( + ke_admin_node::formatter(), + node_id = node.id_as_keyexpr(), + ) + .unwrap(), + EntityRef::Node(ros_info.gid, node.fullname().to_string()), + ); + nodes_map.insert(node.fullname().to_string(), node); + } + Err(e) => { + log::warn!("ROS Node has incompatible name: {e}"); + break; + } + } + }; + + // Update NodeInfo, adding resulting events to the list + let node = nodes_map.get_mut(name).unwrap(); + events.append(&mut Self::update_node_info( + node, + ros_node_info, + readers, + writers, + )); + } + + // Save ParticipantEntitiesInfo + self.ros_participant_info.insert(ros_info.gid, ros_info); + events + } + + pub fn update_node_info( + node: &mut NodeInfo, + ros_node_info: &NodeEntitiesInfo, + readers: &mut HashMap, + writers: &mut HashMap, + ) -> Vec { + let mut events = Vec::new(); + // For each declared Reader + for rgid in &ros_node_info.reader_gid_seq { + if let Some(entity) = readers.get(rgid) { + log::debug!( + "ROS Node {ros_node_info} declares Reader on {}", + entity.topic_name + ); + node.update_with_reader(entity).map(|e| events.push(e)); + } else { + log::debug!( + "ROS Node {ros_node_info} declares a not yet discovered DDS Reader: {rgid}" + ); + node.undiscovered_reader.push(*rgid); + } + } + // For each declared Writer + for wgid in &ros_node_info.writer_gid_seq { + if let Some(entity) = writers.get(wgid) { + log::debug!( + "ROS Node {ros_node_info} declares Writer on {}", + entity.topic_name + ); + node.update_with_writer(entity).map(|e| events.push(e)); + } else { + log::debug!( + "ROS Node {ros_node_info} declares a not yet discovered DDS Writer: {wgid}" + ); + node.undiscovered_writer.push(*wgid); + } + } + events + } + + fn get_entity_json_value( + &self, + entity_ref: &EntityRef, + ) -> Result, serde_json::Error> { + match entity_ref { + EntityRef::Participant(gid) => self + .participants + .get(gid) + .map(serde_json::to_value) + .map(remove_null_qos_values) + .transpose(), + EntityRef::Writer(gid) => self + .writers + .get(gid) + .map(serde_json::to_value) + .map(remove_null_qos_values) + .transpose(), + EntityRef::Reader(gid) => self + .readers + .get(gid) + .map(serde_json::to_value) + .map(remove_null_qos_values) + .transpose(), + EntityRef::Node(gid, name) => self + .nodes_info + .get(gid) + .map(|map| map.get(name)) + .flatten() + .map(serde_json::to_value) + .transpose(), + } + } + + pub async fn treat_admin_query(&self, query: &Query, admin_keyexpr_prefix: &keyexpr) { + let selector = query.selector(); + + // get the list of sub-key expressions that will match the same stored keys than + // the selector, if those keys had the admin_keyexpr_prefix. + let sub_kes = selector.key_expr.strip_prefix(admin_keyexpr_prefix); + if sub_kes.is_empty() { + log::error!("Received query for admin space: '{}' - but it's not prefixed by admin_keyexpr_prefix='{}'", selector, admin_keyexpr_prefix); + return; + } + + // For all sub-key expression + for sub_ke in sub_kes { + if sub_ke.is_wild() { + // iterate over all admin space to find matching keys and reply for each + for (ke, entity_ref) in self.admin_space.iter() { + if sub_ke.intersects(ke) { + self.send_admin_reply(query, admin_keyexpr_prefix, ke, entity_ref) + .await; + } + } + } else { + // sub_ke correspond to 1 key - just get it and reply + if let Some(entity_ref) = self.admin_space.get(sub_ke) { + self.send_admin_reply(query, admin_keyexpr_prefix, sub_ke, entity_ref) + .await; + } + } + } + } + + async fn send_admin_reply( + &self, + query: &Query, + admin_keyexpr_prefix: &keyexpr, + key_expr: &keyexpr, + entity_ref: &EntityRef, + ) { + match self.get_entity_json_value(entity_ref) { + Ok(Some(v)) => { + let admin_keyexpr = admin_keyexpr_prefix / &key_expr; + if let Err(e) = query + .reply(Ok(Sample::new(admin_keyexpr, v))) + .res_async() + .await + { + log::warn!("Error replying to admin query {:?}: {}", query, e); + } + } + Ok(None) => log::error!("INTERNAL ERROR: Dangling {:?} for {}", entity_ref, key_expr), + Err(e) => { + log::error!("INTERNAL ERROR serializing admin value as JSON: {}", e) + } + } + } +} + +// Remove any null QoS values from a serde_json::Value +fn remove_null_qos_values( + value: Result, +) -> Result { + match value { + Ok(value) => match value { + serde_json::Value::Object(mut obj) => { + let qos = obj.get_mut("qos"); + if let Some(qos) = qos { + if qos.is_object() { + qos.as_object_mut().unwrap().retain(|_, v| !v.is_null()); + } + } + Ok(serde_json::Value::Object(obj)) + } + _ => Ok(value), + }, + Err(error) => Err(error), + } +} diff --git a/zenoh-plugin-ros2dds/src/discovery_mgr.rs b/zenoh-plugin-ros2dds/src/discovery_mgr.rs new file mode 100644 index 0000000..3b6351e --- /dev/null +++ b/zenoh-plugin-ros2dds/src/discovery_mgr.rs @@ -0,0 +1,148 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use crate::dds_discovery::*; +use crate::discovered_entities::DiscoveredEntities; +use crate::events::ROS2DiscoveryEvent; +use crate::ros_discovery::*; +use async_std::task; +use cyclors::dds_entity_t; +use flume::{unbounded, Receiver, Sender}; +use futures::select; +use std::sync::Arc; +use std::sync::RwLock; +use std::time::Duration; +use zenoh::prelude::keyexpr; +use zenoh::queryable::Query; +use zenoh_core::zread; +use zenoh_core::zwrite; +use zenoh_util::{TimedEvent, Timer}; + +use crate::ChannelEvent; +use crate::ROS_DISCOVERY_INFO_POLL_INTERVAL_MS; + +pub struct DiscoveryMgr { + pub participant: dds_entity_t, + pub ros_discovery_mgr: Arc, + pub discovered_entities: Arc>, +} + +impl DiscoveryMgr { + pub fn create( + participant: dds_entity_t, + ros_discovery_mgr: Arc, + ) -> DiscoveryMgr { + DiscoveryMgr { + participant, + ros_discovery_mgr, + discovered_entities: Arc::new(RwLock::new(Default::default())), + } + } + + pub async fn run(&mut self, evt_sender: Sender) { + // run DDS discovery + let (dds_disco_snd, dds_disco_rcv): ( + Sender, + Receiver, + ) = unbounded(); + run_discovery(self.participant, dds_disco_snd); + + let ros_discovery_mgr = self.ros_discovery_mgr.clone(); + let discovered_entities = self.discovered_entities.clone(); + + task::spawn(async move { + // Timer for periodic read of "ros_discovery_info" topic + let timer = Timer::default(); + let (tx, ros_disco_timer_rcv): (Sender<()>, Receiver<()>) = unbounded(); + let ros_disco_timer_event = TimedEvent::periodic( + Duration::from_millis(ROS_DISCOVERY_INFO_POLL_INTERVAL_MS), + ChannelEvent { tx }, + ); + timer.add_async(ros_disco_timer_event).await; + + loop { + select!( + evt = dds_disco_rcv.recv_async() => { + match evt.unwrap() { + DDSDiscoveryEvent::DiscoveredParticipant {entity} => { + zwrite!(discovered_entities).add_participant(entity); + }, + DDSDiscoveryEvent::UndiscoveredParticipant {key} => { + let evts = zwrite!(discovered_entities).remove_participant(&key); + for e in evts { + if let Err(err) = evt_sender.try_send(e) { + log::error!("Internal error: failed to send DDSDiscoveryEvent to main loop: {err}"); + } + } + }, + DDSDiscoveryEvent::DiscoveredPublication{entity} => { + let e = zwrite!(discovered_entities).add_writer(entity); + if let Some(e) = e { + if let Err(err) = evt_sender.try_send(e) { + log::error!("Internal error: failed to send DDSDiscoveryEvent to main loop: {err}"); + } + } + }, + DDSDiscoveryEvent::UndiscoveredPublication{key} => { + let e = zwrite!(discovered_entities).remove_writer(&key); + if let Some(e) = e { + if let Err(err) = evt_sender.try_send(e) { + log::error!("Internal error: failed to send DDSDiscoveryEvent to main loop: {err}"); + } + } + }, + DDSDiscoveryEvent::DiscoveredSubscription {entity} => { + let e = zwrite!(discovered_entities).add_reader(entity); + if let Some(e) = e { + if let Err(err) = evt_sender.try_send(e) { + log::error!("Internal error: failed to send DDSDiscoveryEvent to main loop: {err}"); + } + } + }, + DDSDiscoveryEvent::UndiscoveredSubscription {key} => { + let e = zwrite!(discovered_entities).remove_reader(&key); + if let Some(e) = e { + if let Err(err) = evt_sender.try_send(e) { + log::error!("Internal error: failed to send DDSDiscoveryEvent to main loop: {err}"); + } + } + }, + } + } + + _ = ros_disco_timer_rcv.recv_async() => { + let infos = ros_discovery_mgr.read(); + for part_info in infos { + log::debug!("Received ros_discovery_info from {}", part_info); + let evts = zwrite!(discovered_entities).update_participant_info(part_info); + for e in evts { + if let Err(err) = evt_sender.try_send(e) { + log::error!("Internal error: failed to send DDSDiscoveryEvent to main loop: {err}"); + } + } + } + } + ) + } + }); + } + + pub fn treat_admin_query(&self, query: &Query, admin_keyexpr_prefix: &keyexpr) { + // pass query to discovered_entities + let discovered_entities = zread!(self.discovered_entities); + // TODO: find a better solution than block_on() + async_std::task::block_on( + discovered_entities.treat_admin_query(query, admin_keyexpr_prefix), + ); + } +} diff --git a/zenoh-plugin-ros2dds/src/events.rs b/zenoh-plugin-ros2dds/src/events.rs new file mode 100644 index 0000000..be9cffc --- /dev/null +++ b/zenoh-plugin-ros2dds/src/events.rs @@ -0,0 +1,162 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use std::fmt::Display; + +use cyclors::qos::Qos; +use zenoh::prelude::OwnedKeyExpr; + +use crate::node_info::*; + +/// A (local) discovery event of a ROS2 interface +#[derive(Debug)] +pub enum ROS2DiscoveryEvent { + DiscoveredMsgPub(String, MsgPub), + UndiscoveredMsgPub(String, MsgPub), + DiscoveredMsgSub(String, MsgSub), + UndiscoveredMsgSub(String, MsgSub), + DiscoveredServiceSrv(String, ServiceSrv), + UndiscoveredServiceSrv(String, ServiceSrv), + DiscoveredServiceCli(String, ServiceCli), + UndiscoveredServiceCli(String, ServiceCli), + DiscoveredActionSrv(String, ActionSrv), + UndiscoveredActionSrv(String, ActionSrv), + DiscoveredActionCli(String, ActionCli), + UndiscoveredActionCli(String, ActionCli), +} + +impl std::fmt::Display for ROS2DiscoveryEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use ROS2DiscoveryEvent::*; + match self { + DiscoveredMsgPub(node, iface) => write!(f, "Node {node} declares {iface}"), + DiscoveredMsgSub(node, iface) => write!(f, "Node {node} declares {iface}"), + DiscoveredServiceSrv(node, iface) => write!(f, "Node {node} declares {iface}"), + DiscoveredServiceCli(node, iface) => write!(f, "Node {node} declares {iface}"), + DiscoveredActionSrv(node, iface) => write!(f, "Node {node} declares {iface}"), + DiscoveredActionCli(node, iface) => write!(f, "Node {node} declares {iface}"), + UndiscoveredMsgPub(node, iface) => write!(f, "Node {node} undeclares {iface}"), + UndiscoveredMsgSub(node, iface) => write!(f, "Node {node} undeclares {iface}"), + UndiscoveredServiceSrv(node, iface) => write!(f, "Node {node} undeclares {iface}"), + UndiscoveredServiceCli(node, iface) => write!(f, "Node {node} undeclares {iface}"), + UndiscoveredActionSrv(node, iface) => write!(f, "Node {node} undeclares {iface}"), + UndiscoveredActionCli(node, iface) => write!(f, "Node {node} undeclares {iface}"), + } + } +} + +/// A (remote) announcement/retirement of a ROS2 interface +#[derive(Debug)] +pub enum ROS2AnnouncementEvent { + AnnouncedMsgPub { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + ros2_type: String, + keyless: bool, + writer_qos: Qos, + }, + RetiredMsgPub { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + }, + AnnouncedMsgSub { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + ros2_type: String, + keyless: bool, + reader_qos: Qos, + }, + RetiredMsgSub { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + }, + AnnouncedServiceSrv { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + ros2_type: String, + }, + RetiredServiceSrv { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + }, + AnnouncedServiceCli { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + ros2_type: String, + }, + RetiredServiceCli { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + }, + AnnouncedActionSrv { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + ros2_type: String, + }, + RetiredActionSrv { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + }, + AnnouncedActionCli { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + ros2_type: String, + }, + RetiredActionCli { + plugin_id: OwnedKeyExpr, + zenoh_key_expr: OwnedKeyExpr, + }, +} + +impl Display for ROS2AnnouncementEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use ROS2AnnouncementEvent::*; + match self { + AnnouncedMsgPub { zenoh_key_expr, .. } => { + write!(f, "announces Publisher {zenoh_key_expr}") + } + AnnouncedMsgSub { zenoh_key_expr, .. } => { + write!(f, "announces Subscriber {zenoh_key_expr}") + } + AnnouncedServiceSrv { zenoh_key_expr, .. } => { + write!(f, "announces Service Server {zenoh_key_expr}") + } + AnnouncedServiceCli { zenoh_key_expr, .. } => { + write!(f, "announces Service Client {zenoh_key_expr}") + } + AnnouncedActionSrv { zenoh_key_expr, .. } => { + write!(f, "announces Action Server {zenoh_key_expr}") + } + AnnouncedActionCli { zenoh_key_expr, .. } => { + write!(f, "announces Action Client {zenoh_key_expr}") + } + RetiredMsgPub { zenoh_key_expr, .. } => write!(f, "retires Publisher {zenoh_key_expr}"), + RetiredMsgSub { zenoh_key_expr, .. } => { + write!(f, "retires Subscriber {zenoh_key_expr}") + } + RetiredServiceSrv { zenoh_key_expr, .. } => { + write!(f, "retires Service Server {zenoh_key_expr}") + } + RetiredServiceCli { zenoh_key_expr, .. } => { + write!(f, "retires Service Client {zenoh_key_expr}") + } + RetiredActionSrv { zenoh_key_expr, .. } => { + write!(f, "retires Action Server {zenoh_key_expr}") + } + RetiredActionCli { zenoh_key_expr, .. } => { + write!(f, "retires Action Client {zenoh_key_expr}") + } + } + } +} diff --git a/zenoh-plugin-ros2dds/src/gid.rs b/zenoh-plugin-ros2dds/src/gid.rs new file mode 100644 index 0000000..9978631 --- /dev/null +++ b/zenoh-plugin-ros2dds/src/gid.rs @@ -0,0 +1,179 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::{fmt, ops::Deref, str::FromStr}; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Gid([u8; 16]); + +impl Gid { + pub const NOT_DISCOVERED: Gid = Gid([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); +} + +impl Default for Gid { + fn default() -> Self { + Gid::NOT_DISCOVERED + } +} + +impl Deref for Gid { + type Target = [u8; 16]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<[u8; 16]> for Gid { + fn from(key: [u8; 16]) -> Self { + Self(key) + } +} + +impl From<&[u8; 16]> for Gid { + fn from(key: &[u8; 16]) -> Self { + Self(key.clone()) + } +} + +impl Serialize for Gid { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + // serialize as an hexadecimal String + Serialize::serialize(&hex::encode(&self.0), serializer) + } else { + // serialize as a little-endian [u8; 16] + Serialize::serialize(&self.0, serializer) + } + } +} + +impl<'de> Deserialize<'de> for Gid { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + // deserialize from an hexadecimal String + let s: &str = Deserialize::deserialize(deserializer)?; + let v = hex::decode(s).map_err(|e| { + serde::de::Error::custom(format!("Failed to decode gid {s} as hex: {e}")) + })?; + if v.len() == 16 { + Ok(TryInto::<&[u8; 16]>::try_into(&v[..16]).unwrap().into()) + } else { + Err(serde::de::Error::custom(format!( + "Failed to decode gid {s} as hex: not 16 bytes" + ))) + } + } else { + // deserialize from a little-endian [u8; 16] + let bytes: [u8; 16] = Deserialize::deserialize(deserializer)?; + Ok(bytes.into()) + } + } +} + +impl fmt::Debug for Gid { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self == &Gid::NOT_DISCOVERED { + write!(f, "NOT_DISCOVERED") + } else { + let s = hex::encode(&self.0); + write!(f, "{s}") + } + } +} + +impl fmt::Display for Gid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +impl FromStr for Gid { + type Err = String; + + fn from_str(s: &str) -> Result { + let mut bytes = [0u8; 16]; + hex::decode_to_slice(s, &mut bytes).map_err(|e: hex::FromHexError| e.to_string())?; + Ok(bytes.into()) + } +} + +mod tests { + + #[test] + fn test_gid() { + use crate::gid::Gid; + use std::ops::Deref; + use std::str::FromStr; + + let str1 = "01106c8324a780d1b9e62c8f000001c1"; + let bytes1 = [ + 0x01u8, 0x10, 0x6c, 0x83, 0x24, 0xa7, 0x80, 0xd1, 0xb9, 0xe6, 0x2c, 0x8f, 0x00, 0x00, + 0x01, 0xc1, + ]; + + assert_eq!(Gid::from(bytes1.clone()).deref(), &bytes1); + assert_eq!(Gid::from(&bytes1).deref(), &bytes1); + assert_eq!(Gid::from_str(str1).unwrap().deref(), &bytes1); + assert_eq!(Gid::from(bytes1.clone()).to_string(), str1); + assert_eq!(Gid::from(&bytes1).to_string(), str1); + assert_eq!(Gid::from_str(str1).unwrap().to_string(), str1); + + let str2: &str = "01106c8324a780d1b9e62c8f00000e04"; + assert!(Gid::from_str(str2).unwrap() > Gid::from_str(str1).unwrap()); + + assert!(matches!( + Gid::from_str("01106c8324a780d1b9e62c8f00000e04aaaaaaaa"), + Err(_) + )); + } + + #[test] + fn test_serde() { + use crate::gid::Gid; + + let bytes = [ + 0x01u8, 0x10, 0x6c, 0x83, 0x24, 0xa7, 0x80, 0xd1, 0xb9, 0xe6, 0x2c, 0x8f, 0x00, 0x00, + 0x01, 0xc1, + ]; + let json: &str = "\"01106c8324a780d1b9e62c8f000001c1\""; + + assert_eq!(serde_json::to_string(&Gid::from(&bytes)).unwrap(), json); + assert_eq!( + serde_json::from_str::(json).unwrap(), + Gid::from(&bytes) + ); + + // Check that endianness doesn't impact CDR serialization (note: 4 bytes CDR header ignored in comparison) + assert_eq!( + &cdr::serialize::<_, _, cdr::CdrBe>(&Gid::from(&bytes), cdr::Infinite).unwrap()[4..], + &bytes + ); + assert_eq!( + &cdr::serialize::<_, _, cdr::CdrLe>(&Gid::from(&bytes), cdr::Infinite).unwrap()[4..], + &bytes + ); + let cdr = [ + 0x0u8, 0, 1, 0, 0x01, 0x10, 0x6c, 0x83, 0x24, 0xa7, 0x80, 0xd1, 0xb9, 0xe6, 0x2c, 0x8f, + 0x00, 0x00, 0x01, 0xc1, + ]; + assert_eq!(cdr::deserialize::(&cdr).unwrap(), Gid::from(&bytes)); + } +} diff --git a/zenoh-plugin-ros2dds/src/lib.rs b/zenoh-plugin-ros2dds/src/lib.rs new file mode 100644 index 0000000..ff312cf --- /dev/null +++ b/zenoh-plugin-ros2dds/src/lib.rs @@ -0,0 +1,578 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use async_trait::async_trait; +use cyclors::*; +use events::ROS2AnnouncementEvent; +use flume::{unbounded, Receiver, Sender}; +use futures::select; +use git_version::git_version; +use serde::ser::SerializeStruct; +use serde::{Serialize, Serializer}; +use std::collections::HashMap; +use std::env; +use std::mem::ManuallyDrop; +use std::sync::Arc; +use zenoh::liveliness::LivelinessToken; +use zenoh::plugins::{Plugin, RunningPluginTrait, Runtime, ZenohPlugin}; +use zenoh::prelude::r#async::AsyncResolve; +use zenoh::prelude::*; +use zenoh::queryable::Query; +use zenoh::Result as ZResult; +use zenoh::Session; +use zenoh_core::{bail, zerror}; +use zenoh_ext::SubscriberBuilderExt; +use zenoh_util::Timed; + +pub mod config; +mod dds_discovery; +mod discovered_entities; +mod discovery_mgr; +mod events; +mod gid; +mod liveliness_mgt; +mod node_info; +mod qos_helpers; +mod ros2_utils; +mod ros_discovery; +mod route_publisher; +mod route_subscriber; +mod routes_mgr; +use config::Config; +use dds_discovery::*; + +use crate::discovery_mgr::DiscoveryMgr; +use crate::events::ROS2DiscoveryEvent; +use crate::liveliness_mgt::{ + ke_liveliness_all, ke_liveliness_plugin, parse_ke_liveliness_pub, parse_ke_liveliness_sub, +}; +use crate::ros_discovery::RosDiscoveryInfoMgr; +use crate::routes_mgr::RoutesMgr; + +pub const GIT_VERSION: &str = git_version!(prefix = "v", cargo_prefix = "v"); + +#[macro_export] +macro_rules! ke_for_sure { + ($val:expr) => { + unsafe { keyexpr::from_str_unchecked($val) } + }; +} + +lazy_static::lazy_static!( + pub static ref LONG_VERSION: String = format!("{} built with {}", GIT_VERSION, env!("RUSTC_VERSION")); + pub static ref VERSION_JSON_VALUE: Value = + serde_json::Value::String(LONG_VERSION.clone()).into(); + static ref LOG_PAYLOAD: bool = std::env::var("Z_LOG_PAYLOAD").is_ok(); + + static ref KE_ANY_1_SEGMENT: &'static keyexpr = ke_for_sure!("*"); + static ref KE_ANY_N_SEGMENT: &'static keyexpr = ke_for_sure!("**"); + + static ref KE_PREFIX_PUB_CACHE: &'static keyexpr = ke_for_sure!("@ros2_pub_cache"); +); + +zenoh::kedefine!( + // Admin space key expressions of plugin's version + pub ke_admin_version: "${plugin_status_key:**}/__version__", + + // Admin prefix of this bridge + pub ke_admin_prefix: "@ros2/${plugin_id:*}/", +); + +// CycloneDDS' localhost-only: set network interface address (shortened form of config would be +// possible, too, but I think it is clearer to spell it out completely). +// Empty configuration fragments are ignored, so it is safe to unconditionally append a comma. +const CYCLONEDDS_CONFIG_LOCALHOST_ONLY: &str = r#","#; + +// CycloneDDS' enable-shm: enable usage of Iceoryx shared memory +#[cfg(feature = "dds_shm")] +const CYCLONEDDS_CONFIG_ENABLE_SHM: &str = r#"true,"#; + +// interval between each read/write on "ros_discovery_info" topic +const ROS_DISCOVERY_INFO_POLL_INTERVAL_MS: u64 = 100; +const ROS_DISCOVERY_INFO_PUSH_INTERVAL_MS: u64 = 100; + +zenoh_plugin_trait::declare_plugin!(ROS2Plugin); + +#[allow(clippy::upper_case_acronyms)] +pub struct ROS2Plugin; + +impl ZenohPlugin for ROS2Plugin {} +impl Plugin for ROS2Plugin { + type StartArgs = Runtime; + type RunningPlugin = zenoh::plugins::RunningPlugin; + + const STATIC_NAME: &'static str = "zenoh-plugin-ros2dds"; + + fn start(name: &str, runtime: &Self::StartArgs) -> ZResult { + // Try to initiate login. + // Required in case of dynamic lib, otherwise no logs. + // But cannot be done twice in case of static link. + let _ = env_logger::try_init(); + + let runtime_conf = runtime.config.lock(); + let plugin_conf = runtime_conf + .plugin(name) + .ok_or_else(|| zerror!("Plugin `{}`: missing config", name))?; + let config: Config = serde_json::from_value(plugin_conf.clone()) + .map_err(|e| zerror!("Plugin `{}` configuration error: {}", name, e))?; + async_std::task::spawn(run(runtime.clone(), config)); + Ok(Box::new(ROS2Plugin)) + } +} + +impl RunningPluginTrait for ROS2Plugin { + fn config_checker(&self) -> zenoh::plugins::ValidationFunction { + Arc::new(|_, _, _| bail!("ROS2Plugin does not support hot configuration changes.")) + } + + fn adminspace_getter<'a>( + &'a self, + selector: &'a Selector<'a>, + plugin_status_key: &str, + ) -> ZResult> { + let mut responses = Vec::new(); + let version_key = [plugin_status_key, "/__version__"].concat(); + if selector.key_expr.intersects(ke_for_sure!(&version_key)) { + responses.push(zenoh::plugins::Response::new( + version_key, + GIT_VERSION.into(), + )); + } + Ok(responses) + } +} + +pub async fn run(runtime: Runtime, config: Config) { + // Try to initiate login. + // Required in case of dynamic lib, otherwise no logs. + // But cannot be done twice in case of static link. + let _ = env_logger::try_init(); + log::debug!("ROS2 plugin {}", LONG_VERSION.as_str()); + log::info!("ROS2 plugin {:?}", config); + + // Check config validity + if !regex::Regex::new("/[A-Za-z0-9_/]*") + .unwrap() + .is_match(&config.namespace) + { + log::error!( + r#"Configuration error: invalid namespace "{}" must contain only alphanumeric, '_' or '/' characters and start with '/'"#, + config.namespace + ); + return; + } + if !regex::Regex::new("[A-Za-z0-9_]+") + .unwrap() + .is_match(&config.nodename) + { + log::error!( + r#"Configuration error: invalid nodename "{}" must contain only alphanumeric or '_' characters"#, + config.nodename + ); + return; + } + + // open zenoh-net Session + let zsession = match zenoh::init(runtime).res_async().await { + Ok(session) => Arc::new(session), + Err(e) => { + log::error!("Unable to init zenoh session for DDS plugin : {:?}", e); + return; + } + }; + + let plugin_id = if let Some(ref id) = config.id { + if id.contains('/') { + log::error!("The 'id' configuration must not contain any '/' character"); + return; + } + id.clone() + } else { + zsession.zid().into_keyexpr().to_owned() + }; + + // Declare plugin's liveliness token + let ke_liveliness = + zenoh::keformat!(ke_liveliness_plugin::formatter(), plugin_id = &plugin_id).unwrap(); + let member = match zsession + .liveliness() + .declare_token(ke_liveliness) + .res_async() + .await + { + Ok(member) => member, + Err(e) => { + log::error!( + "Unable to declare liveliness token for DDS plugin : {:?}", + e + ); + return; + } + }; + + // if "ros_localhost_only" is set, configure CycloneDDS to use only localhost interface + if config.ros_localhost_only { + env::set_var( + "CYCLONEDDS_URI", + format!( + "{}{}", + CYCLONEDDS_CONFIG_LOCALHOST_ONLY, + env::var("CYCLONEDDS_URI").unwrap_or_default() + ), + ); + } + + // if "enable_shm" is set, configure CycloneDDS to use Iceoryx shared memory + #[cfg(feature = "dds_shm")] + { + if config.shm_enabled { + env::set_var( + "CYCLONEDDS_URI", + format!( + "{}{}", + CYCLONEDDS_CONFIG_ENABLE_SHM, + env::var("CYCLONEDDS_URI").unwrap_or_default() + ), + ); + } + } + + // create DDS Participant + log::debug!( + "Create DDS Participant on domain {} with CYCLONEDDS_URI='{}'", + config.domain, + env::var("CYCLONEDDS_URI").unwrap_or_default() + ); + let participant = + unsafe { dds_create_participant(config.domain, std::ptr::null(), std::ptr::null()) }; + log::debug!( + "ROS2 plugin {} using DDS Participant {} created", + plugin_id, + get_guid(&participant).unwrap() + ); + + let mut ros2_plugin = ROS2PluginRuntime { + config: Arc::new(config), + zsession: &zsession, + participant, + _member: member, + plugin_id, + admin_space: HashMap::::new(), + }; + + ros2_plugin.run().await; +} + +pub struct ROS2PluginRuntime<'a> { + config: Arc, + // Note: &'a Arc here to keep the ownership of Session outside this struct + // and be able to store the publishers/subscribers it creates in this same struct. + zsession: &'a Arc, + participant: dds_entity_t, + _member: LivelinessToken<'a>, + plugin_id: OwnedKeyExpr, + // admin space: index is the admin_keyexpr (relative to admin_prefix) + // value is the JSon string to return to queries. + admin_space: HashMap, +} + +impl Serialize for ROS2PluginRuntime<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // return the plugin's config as a JSON struct + let mut s = serializer.serialize_struct("dds", 3)?; + s.serialize_field("domain", &self.config.domain)?; + s.end() + } +} + +// An reference used in admin space to point to a struct (DdsEntity or Route) stored in another map +#[derive(Debug)] +enum AdminRef { + Config, + Version, +} + +impl<'a> ROS2PluginRuntime<'a> { + async fn run(&mut self) { + // Subscribe to all liveliness info from other ROS2 plugins + let ke_liveliness_all = zenoh::keformat!( + ke_liveliness_all::formatter(), + plugin_id = "*", + remaining = "**" + ) + .unwrap(); + let liveliness_subscriber = self + .zsession + .liveliness() + .declare_subscriber(ke_liveliness_all) + .querying() + .with(zenoh::handlers::DefaultHandler {}) + .res_async() + .await + .expect("Failed to create Liveliness Subscriber"); + + // declare admin space queryable + let admin_prefix = + zenoh::keformat!(ke_admin_prefix::formatter(), plugin_id = &self.plugin_id).unwrap(); + let admin_keyexpr_expr = (&admin_prefix) / *KE_ANY_N_SEGMENT; + log::debug!("Declare admin space on {}", admin_keyexpr_expr); + let admin_queryable = self + .zsession + .declare_queryable(admin_keyexpr_expr) + .res_async() + .await + .expect("Failed to create AdminSpace queryable"); + + // add plugin's config and version in admin space + self.admin_space + .insert(&admin_prefix / ke_for_sure!("config"), AdminRef::Config); + self.admin_space + .insert(&admin_prefix / ke_for_sure!("version"), AdminRef::Version); + + // Create and start the RosDiscoveryInfoMgr (managing ros_discovery_info topic) + let ros_discovery_mgr = Arc::new( + RosDiscoveryInfoMgr::new( + self.participant, + &self.config.namespace, + &self.config.nodename, + ) + .expect("Failed to create RosDiscoveryInfoMgr"), + ); + ros_discovery_mgr.run().await; + + // Create and start DiscoveryManager + let (tx, discovery_rcv): (Sender, Receiver) = + unbounded(); + let mut discovery_mgr = DiscoveryMgr::create(self.participant, ros_discovery_mgr.clone()); + discovery_mgr.run(tx).await; + + // Create RoutesManager + let mut routes_mgr = RoutesMgr::new( + self.plugin_id.clone(), + self.config.clone(), + self.zsession, + self.participant, + discovery_mgr.discovered_entities.clone(), + ros_discovery_mgr, + admin_prefix.clone(), + ); + + loop { + select!( + evt = discovery_rcv.recv_async() => { + match evt { + Ok(evt) => { + if self.is_allowed(&evt) { + log::info!("{evt} - Allowed"); + // pass ROS2DiscoveryEvent to RoutesMgr + if let Err(e) = routes_mgr.on_ros_discovery_event(evt).await { + log::warn!("Error updating route: {e}"); + } + } else { + log::info!("{evt} - Denied per config"); + } + } + Err(e) => log::error!("Internal Error: received from DiscoveryMgr: {e}") + } + }, + + liveliness_event = liveliness_subscriber.recv_async() => { + match liveliness_event + { + Ok(evt) => { + let ke = evt.key_expr.as_keyexpr(); + if let Ok(parsed) = ke_liveliness_all::parse(ke) { + let plugin_id = parsed.plugin_id().unwrap(); + if plugin_id == self.plugin_id.as_ref() { + // ignore own announcements + continue; + } + match (parsed.remaining(), evt.kind) { + // New remote bridge detected + (None, SampleKind::Put) => { + log::info!("New ROS 2 bridge detected: {}", plugin_id); + // make all routes for a TRANSIENT_LOCAL Subscriber to query historical publications from this new plugin + routes_mgr.query_historical_all_publications(plugin_id).await; + } + // New remote bridge left + (None, SampleKind::Delete) => log::info!("Remote ROS 2 bridge left: {}", plugin_id), + // the liveliness token corresponds to a ROS2 announcement + (Some(remaining), _) => { + // parse it and pass ROS2AnnouncementEvent to RoutesMgr + match self.parse_announcement_event(ke, &remaining.as_str()[..3], evt.kind) { + Ok(evt) => { + log::info!("Remote bridge {plugin_id} {evt}"); + routes_mgr.on_ros_announcement_event(evt).await + .unwrap_or_else(|e| log::warn!("Error treating announcement event: {e}")); + }, + Err(e) => + log::warn!("Received unexpected liveliness key expression '{ke}': {e}") + } + } + } + } else { + log::warn!("Received unexpected liveliness key expression '{ke}'"); + } + }, + Err(e) => log::warn!("Error receiving liveliness event: {e}") + } + }, + + get_request = admin_queryable.recv_async() => { + if let Ok(query) = get_request { + self.treat_admin_query(&query).await; + // pass query to discovery_mgr + discovery_mgr.treat_admin_query(&query, &admin_prefix); + // pass query to discovery_mgr + routes_mgr.treat_admin_query(&query).await; + } else { + log::warn!("AdminSpace queryable was closed!"); + } + } + ) + } + } + + fn parse_announcement_event( + &self, + liveliness_ke: &keyexpr, + iface_kind: &str, + sample_kind: SampleKind, + ) -> Result { + use ROS2AnnouncementEvent::*; + log::debug!("Received liveliness event: {sample_kind} on {liveliness_ke}"); + match (iface_kind, sample_kind) { + ("MP/", SampleKind::Put) => parse_ke_liveliness_pub(liveliness_ke) + .map_err(|e| format!("Received invalid liveliness token: {e}")) + .map( + |(plugin_id, zenoh_key_expr, ros2_type, keyless, writer_qos)| AnnouncedMsgPub { + plugin_id, + zenoh_key_expr, + ros2_type, + keyless, + writer_qos, + }, + ), + ("MP/", SampleKind::Delete) => parse_ke_liveliness_pub(liveliness_ke) + .map_err(|e| format!("Received invalid liveliness token: {e}")) + .map(|(plugin_id, zenoh_key_expr, ..)| RetiredMsgPub { + plugin_id, + zenoh_key_expr, + }), + ("MS/", SampleKind::Put) => parse_ke_liveliness_sub(liveliness_ke) + .map_err(|e| format!("Received invalid liveliness token: {e}")) + .map( + |(plugin_id, zenoh_key_expr, ros2_type, keyless, reader_qos)| AnnouncedMsgSub { + plugin_id, + zenoh_key_expr, + ros2_type, + keyless, + reader_qos, + }, + ), + ("MS/", SampleKind::Delete) => parse_ke_liveliness_sub(liveliness_ke) + .map_err(|e| format!("Received invalid liveliness token: {e}")) + .map(|(plugin_id, zenoh_key_expr, ..)| RetiredMsgSub { + plugin_id, + zenoh_key_expr, + }), + _ => Err(format!("invalid ROS2 interface kind: {iface_kind}")), + } + } + + fn is_allowed(&self, evt: &ROS2DiscoveryEvent) -> bool { + if let Some(allowance) = &self.config.allowance { + use ROS2DiscoveryEvent::*; + match evt { + DiscoveredMsgPub(_, iface) => allowance.is_publisher_allowed(&iface.name), + DiscoveredMsgSub(_, iface) => allowance.is_subscriber_allowed(&iface.name), + DiscoveredServiceSrv(_, iface) => allowance.is_service_srv_allowed(&iface.name), + DiscoveredServiceCli(_, iface) => allowance.is_service_cli_allowed(&iface.name), + DiscoveredActionSrv(_, iface) => allowance.is_action_srv_allowed(&iface.name), + DiscoveredActionCli(_, iface) => allowance.is_action_cli_allowed(&iface.name), + _ => true, // only Undiscovered events remain - always allow them (in case dynamic change of config is supported) + } + } else { + // no allow/deny configured => allow all + true + } + } + + async fn treat_admin_query(&self, query: &Query) { + let query_ke = query.selector().key_expr; + if query_ke.is_wild() { + // iterate over all admin space to find matching keys and reply for each + for (ke, admin_ref) in self.admin_space.iter() { + if query_ke.intersects(ke) { + self.send_admin_reply(query, ke, admin_ref).await; + } + } + } else { + // sub_ke correspond to 1 key - just get it and reply + let own_ke: OwnedKeyExpr = query_ke.into(); + if let Some(admin_ref) = self.admin_space.get(&own_ke) { + self.send_admin_reply(query, &own_ke, admin_ref).await; + } + } + } + + async fn send_admin_reply(&self, query: &Query, key_expr: &keyexpr, admin_ref: &AdminRef) { + let value: Value = match admin_ref { + AdminRef::Version => VERSION_JSON_VALUE.clone(), + AdminRef::Config => match serde_json::to_value(self) { + Ok(v) => v.into(), + Err(e) => { + log::error!("INTERNAL ERROR serializing config as JSON: {}", e); + return; + } + }, + }; + if let Err(e) = query + .reply(Ok(Sample::new(key_expr.to_owned(), value))) + .res_async() + .await + { + log::warn!("Error replying to admin query {:?}: {}", query, e); + } + } +} + +//TODO replace when stable https://github.com/rust-lang/rust/issues/65816 +#[inline] +pub fn vec_into_raw_parts(v: Vec) -> (*mut T, usize, usize) { + let mut me = ManuallyDrop::new(v); + (me.as_mut_ptr(), me.len(), me.capacity()) +} + +struct ChannelEvent { + tx: Sender<()>, +} + +#[async_trait] +impl Timed for ChannelEvent { + async fn run(&mut self) { + if self.tx.send(()).is_err() { + log::warn!("Error sending periodic timer notification on channel"); + }; + } +} + +pub(crate) fn serialize_option_as_bool(opt: &Option, s: S) -> Result +where + S: Serializer, +{ + s.serialize_bool(opt.is_some()) +} diff --git a/zenoh-plugin-ros2dds/src/liveliness_mgt.rs b/zenoh-plugin-ros2dds/src/liveliness_mgt.rs new file mode 100644 index 0000000..b5c76f8 --- /dev/null +++ b/zenoh-plugin-ros2dds/src/liveliness_mgt.rs @@ -0,0 +1,257 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use cyclors::qos::{ + Durability, DurabilityKind, History, HistoryKind, Qos, Reliability, ReliabilityKind, + DDS_100MS_DURATION, +}; +use zenoh::prelude::{keyexpr, OwnedKeyExpr}; + +const SLASH_REPLACEMSNT_CHAR: &str = "§"; + +zenoh::kedefine!( + // Liveliness tokens key expressions + pub ke_liveliness_all: "@ros2_lv/${plugin_id:*}/${remaining:**}", + pub ke_liveliness_plugin: "@ros2_lv/${plugin_id:*}", + pub(crate) ke_liveliness_pub: "@ros2_lv/${plugin_id:*}/MP/${ke:*}/${typ:*}/${qos_ke:*}", + pub(crate) ke_liveliness_sub: "@ros2_lv/${plugin_id:*}/MS/${ke:*}/${typ:*}/${qos_ke:*}", +); + +pub(crate) fn new_ke_liveliness_pub( + plugin_id: &keyexpr, + zenoh_key_expr: &keyexpr, + ros2_type: &str, + keyless: bool, + qos: &Qos, +) -> Result { + let ke = escape_slashes(zenoh_key_expr); + let typ = escape_slashes(ros2_type); + let qos_ke = qos_to_key_expr(keyless, qos); + zenoh::keformat!(ke_liveliness_pub::formatter(), plugin_id, ke, typ, qos_ke) + .map_err(|e| e.to_string()) +} + +pub(crate) fn parse_ke_liveliness_pub( + ke: &keyexpr, +) -> Result<(OwnedKeyExpr, OwnedKeyExpr, String, bool, Qos), String> { + let parsed = ke_liveliness_pub::parse(ke) + .map_err(|e| format!("failed to parse liveliness keyexpr {ke}: {e}"))?; + let plugin_id = parsed + .plugin_id() + .map(ToOwned::to_owned) + .ok_or_else(|| format!("failed to parse liveliness keyexpr {ke}: no plugin_id"))?; + let zenoh_key_expr = parsed + .ke() + .map(|ke| unescape_slashes(ke)) + .ok_or_else(|| format!("failed to parse liveliness keyexpr {ke}: no ke"))?; + let ros2_type = parsed + .typ() + .map(|ke| unescape_slashes(ke)) + .ok_or_else(|| format!("failed to parse liveliness keyexpr {ke}: no typ"))?; + let (keyless, qos) = parsed + .qos_ke() + .ok_or_else(|| format!("failed to parse liveliness keyexpr {ke}: no typ")) + .and_then(|ke| key_expr_to_qos(ke)) + .map_err(|e| format!("failed to parse liveliness keyexpr {ke}: {e}"))?; + Ok(( + plugin_id, + zenoh_key_expr, + ros2_type.to_string(), + keyless, + qos, + )) +} + +pub(crate) fn new_ke_liveliness_sub( + plugin_id: &keyexpr, + zenoh_key_expr: &keyexpr, + ros2_type: &str, + keyless: bool, + qos: &Qos, +) -> Result { + let ke = escape_slashes(zenoh_key_expr); + let typ = escape_slashes(ros2_type); + let qos_ke = qos_to_key_expr(keyless, qos); + zenoh::keformat!(ke_liveliness_sub::formatter(), plugin_id, ke, typ, qos_ke) + .map_err(|e| e.to_string()) +} + +pub(crate) fn parse_ke_liveliness_sub( + ke: &keyexpr, +) -> Result<(OwnedKeyExpr, OwnedKeyExpr, String, bool, Qos), String> { + let parsed = ke_liveliness_sub::parse(ke) + .map_err(|e| format!("failed to parse liveliness keyexpr {ke}: {e}"))?; + let plugin_id = parsed + .plugin_id() + .map(ToOwned::to_owned) + .ok_or_else(|| format!("failed to parse liveliness keyexpr {ke}: no plugin_id"))?; + let zenoh_key_expr = parsed + .ke() + .map(|ke| unescape_slashes(ke)) + .ok_or_else(|| format!("failed to parse liveliness keyexpr {ke}: no ke"))?; + let ros2_type = parsed + .typ() + .map(|ke| unescape_slashes(ke)) + .ok_or_else(|| format!("failed to parse liveliness keyexpr {ke}: no typ"))?; + let (keyless, qos) = parsed + .qos_ke() + .ok_or_else(|| format!("failed to parse liveliness keyexpr {ke}: no typ")) + .and_then(|ke| key_expr_to_qos(ke)) + .map_err(|e| format!("failed to parse liveliness keyexpr {ke}: {e}"))?; + Ok(( + plugin_id, + zenoh_key_expr, + ros2_type.to_string(), + keyless, + qos, + )) +} + +fn escape_slashes(s: &str) -> OwnedKeyExpr { + OwnedKeyExpr::try_from(s.replace('/', SLASH_REPLACEMSNT_CHAR)).unwrap() +} + +fn unescape_slashes(ke: &keyexpr) -> OwnedKeyExpr { + OwnedKeyExpr::try_from(ke.as_str().replace(SLASH_REPLACEMSNT_CHAR, "/")).unwrap() +} + +// Serialize QoS as a KeyExpr-compatible string (for usage in liveliness keyexpr) +// NOTE: only significant Qos for ROS2 are serialized +// See https://docs.ros.org/en/rolling/Concepts/Intermediate/About-Quality-of-Service-Settings.html +// +// format: ":::," +// where each element is "" if default QoS, or an integer in case of enum, and 'K' for !keyless +pub fn qos_to_key_expr(keyless: bool, qos: &Qos) -> OwnedKeyExpr { + use std::io::Write; + let mut w: Vec = Vec::new(); + + if !keyless { + write!(w, "K").unwrap(); + } + write!(w, ":").unwrap(); + if let Some(Reliability { kind, .. }) = &qos.reliability { + write!(&mut w, "{}", *kind as isize).unwrap(); + } + write!(w, ":").unwrap(); + if let Some(Durability { kind }) = &qos.durability { + write!(&mut w, "{}", *kind as isize).unwrap(); + } + write!(w, ":").unwrap(); + if let Some(History { kind, depth }) = &qos.history { + write!(&mut w, "{},{}", *kind as isize, depth).unwrap(); + } + + unsafe { + let s: String = String::from_utf8_unchecked(w); + OwnedKeyExpr::from_string_unchecked(s) + } +} + +fn key_expr_to_qos(ke: &keyexpr) -> Result<(bool, Qos), String> { + let elts: Vec<&str> = ke.split(':').collect(); + if elts.len() != 4 { + return Err(format!("Internal Error: unexpected QoS expression: '{ke}' - 4 elements between : were expected")); + } + let mut qos = Qos::default(); + let keyless = elts[0].is_empty(); + if !elts[1].is_empty() { + match elts[1].parse::() { + Ok(i) => qos.reliability = Some(Reliability {kind: ReliabilityKind::from(&i), max_blocking_time: DDS_100MS_DURATION }), + Err(_) => return Err(format!("Internal Error: unexpected QoS expression: '{ke}' - failed to parse Reliability in 2nd element")), + } + } + if !elts[2].is_empty() { + match elts[2].parse::() { + Ok(i) => qos.durability = Some(Durability {kind: DurabilityKind::from(&i)}), + Err(_) => return Err(format!("Internal Error: unexpected QoS expression: '{ke}' - failed to parse Durability in 3d element")), + } + } + if !elts[3].is_empty() { + match elts[3].split_once(',').map(|(s1, s2)| + ( + s1.parse::(), + s2.parse::(), + ) + ) { + Some((Ok(k), Ok(depth))) => qos.history = Some(History {kind: HistoryKind::from(&k), depth }), + _ => return Err(format!("Internal Error: unexpected QoS expression: '{ke}' - failed to parse History in 4th element")), + } + } + + Ok((keyless, qos)) +} + +mod tests { + #[test] + fn test_qos_key_expr() { + use super::*; + + let mut q = Qos::default(); + assert_eq!(qos_to_key_expr(true, &q).to_string(), ":::"); + assert_eq!( + key_expr_to_qos(&qos_to_key_expr(true, &q)), + Ok((true, q.clone())) + ); + assert_eq!(qos_to_key_expr(false, &q).to_string(), "K:::"); + assert_eq!( + key_expr_to_qos(&qos_to_key_expr(false, &q)), + Ok((false, q.clone())) + ); + + q.reliability = Some(Reliability { + kind: ReliabilityKind::RELIABLE, + max_blocking_time: DDS_100MS_DURATION, + }); + assert_eq!( + qos_to_key_expr(true, &q).to_string(), + format!(":{}::", ReliabilityKind::RELIABLE as u8) + ); + assert_eq!( + key_expr_to_qos(&qos_to_key_expr(true, &q)), + Ok((true, q.clone())) + ); + assert_eq!( + key_expr_to_qos(&qos_to_key_expr(true, &q)), + Ok((true, q.clone())) + ); + q.reliability = None; + + q.durability = Some(Durability { + kind: DurabilityKind::TRANSIENT_LOCAL, + }); + assert_eq!( + qos_to_key_expr(true, &q).to_string(), + format!("::{}:", DurabilityKind::TRANSIENT_LOCAL as u8) + ); + assert_eq!( + key_expr_to_qos(&qos_to_key_expr(true, &q)), + Ok((true, q.clone())) + ); + q.durability = None; + + q.history = Some(History { + kind: HistoryKind::KEEP_LAST, + depth: 3, + }); + assert_eq!( + qos_to_key_expr(true, &q).to_string(), + format!(":::{},3", HistoryKind::KEEP_LAST as u8) + ); + assert_eq!( + key_expr_to_qos(&qos_to_key_expr(true, &q)), + Ok((true, q.clone())) + ); + q.reliability = None; + } +} diff --git a/zenoh-plugin-ros2dds/src/node_info.rs b/zenoh-plugin-ros2dds/src/node_info.rs new file mode 100644 index 0000000..427f133 --- /dev/null +++ b/zenoh-plugin-ros2dds/src/node_info.rs @@ -0,0 +1,1868 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use serde::ser::SerializeSeq; +use serde::{Serialize, Serializer}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::ops::Range; +use zenoh::prelude::{keyexpr, KeyExpr}; + +use crate::dds_discovery::DdsEntity; +use crate::events::ROS2DiscoveryEvent; +use crate::gid::Gid; +use crate::ke_for_sure; +use crate::ros2_utils::*; + +#[derive(Clone, Debug, Serialize)] +pub struct MsgPub { + pub name: String, + #[serde(rename = "type")] + pub typ: String, + #[serde(skip)] + pub writer: Gid, +} + +impl MsgPub { + pub fn create(name: String, typ: String, writer: Gid) -> Result { + check_ros_name(&name)?; + Ok(MsgPub { name, typ, writer }) + } + + pub fn name_as_keyexpr(&self) -> &keyexpr { + ke_for_sure!(&self.name[1..]) + } +} + +impl std::fmt::Display for MsgPub { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Publisher {}: {}", self.name, self.typ)?; + Ok(()) + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct MsgSub { + pub name: String, + #[serde(rename = "type")] + pub typ: String, + #[serde(skip)] + pub reader: Gid, +} + +impl MsgSub { + pub fn create(name: String, typ: String, reader: Gid) -> Result { + check_ros_name(&name)?; + Ok(MsgSub { name, typ, reader }) + } + + pub fn name_as_keyexpr(&self) -> &keyexpr { + ke_for_sure!(&self.name[1..]) + } +} + +impl std::fmt::Display for MsgSub { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Subscriber {}: {}", self.name, self.typ)?; + Ok(()) + } +} + +#[derive(Clone, Copy, Default)] +pub struct ServiceSrvEntities { + pub req_reader: Gid, + pub rep_writer: Gid, +} + +impl ServiceSrvEntities { + #[inline] + pub fn is_complete(&self) -> bool { + self.req_reader != Gid::NOT_DISCOVERED && self.rep_writer != Gid::NOT_DISCOVERED + } +} + +impl std::fmt::Debug for ServiceSrvEntities { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{reqR:{:?}, repW:{:?}}}", + self.req_reader, self.rep_writer + )?; + Ok(()) + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct ServiceSrv { + pub name: String, + #[serde(rename = "type")] + pub typ: String, + #[serde(skip)] + pub entities: ServiceSrvEntities, +} + +impl ServiceSrv { + pub fn create(name: String, typ: String) -> Result { + check_ros_name(&name)?; + Ok(ServiceSrv { + name, + typ, + entities: ServiceSrvEntities::default(), + }) + } + + pub fn name_as_keyexpr(&self) -> &keyexpr { + ke_for_sure!(&self.name[1..]) + } + + #[inline] + pub fn is_complete(&self) -> bool { + self.entities.is_complete() + } +} + +impl std::fmt::Display for ServiceSrv { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Service Server {}: {}", self.name, self.typ)?; + Ok(()) + } +} + +#[derive(Clone, Copy, Default)] +pub struct ServiceCliEntities { + pub req_writer: Gid, + pub rep_reader: Gid, +} + +impl ServiceCliEntities { + #[inline] + pub fn is_complete(&self) -> bool { + self.rep_reader != Gid::NOT_DISCOVERED && self.req_writer != Gid::NOT_DISCOVERED + } +} + +impl std::fmt::Debug for ServiceCliEntities { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{reqW:{:?}, repR:{:?}}}", + self.req_writer, self.rep_reader + )?; + Ok(()) + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct ServiceCli { + pub name: String, + #[serde(rename = "type")] + pub typ: String, + #[serde(skip)] + pub entities: ServiceCliEntities, +} + +impl ServiceCli { + pub fn create(name: String, typ: String) -> Result { + check_ros_name(&name)?; + Ok(ServiceCli { + name, + typ, + entities: ServiceCliEntities::default(), + }) + } + + pub fn name_as_keyexpr(&self) -> &keyexpr { + ke_for_sure!(&self.name[1..]) + } + + #[inline] + pub fn is_complete(&self) -> bool { + self.entities.is_complete() + } +} + +impl std::fmt::Display for ServiceCli { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Service Client {}: {}", self.name, self.typ)?; + Ok(()) + } +} + +#[derive(Clone, Copy, Default)] +pub struct ActionSrvEntities { + pub send_goal: ServiceSrvEntities, + pub cancel_goal: ServiceSrvEntities, + pub get_result: ServiceSrvEntities, + pub status_writer: Gid, + pub feedback_writer: Gid, +} + +impl ActionSrvEntities { + #[inline] + pub fn is_complete(&self) -> bool { + self.send_goal.is_complete() + && self.cancel_goal.is_complete() + && self.get_result.is_complete() + && self.status_writer != Gid::NOT_DISCOVERED + && self.feedback_writer != Gid::NOT_DISCOVERED + } +} + +impl std::fmt::Debug for ActionSrvEntities { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{send_goal{:?}, cancel_goal{:?}, get_result{:?}, statusW:{:?}, feedbackW:{:?}}}", + self.send_goal, + self.cancel_goal, + self.get_result, + self.status_writer, + self.feedback_writer, + )?; + Ok(()) + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct ActionSrv { + pub name: String, + #[serde(rename = "type")] + pub typ: String, + #[serde(skip)] + pub entities: ActionSrvEntities, +} + +impl ActionSrv { + pub fn create(name: String, typ: String) -> Result { + check_ros_name(&name)?; + Ok(ActionSrv { + name, + typ, + entities: ActionSrvEntities::default(), + }) + } + + pub fn name_as_keyexpr(&self) -> &keyexpr { + ke_for_sure!(&self.name[1..]) + } + + #[inline] + pub fn is_complete(&self) -> bool { + self.entities.is_complete() + } +} + +impl std::fmt::Display for ActionSrv { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Action Server {}: {}", self.name, self.typ)?; + Ok(()) + } +} + +#[derive(Clone, Copy, Default)] +pub struct ActionCliEntities { + pub send_goal: ServiceCliEntities, + pub cancel_goal: ServiceCliEntities, + pub get_result: ServiceCliEntities, + pub status_reader: Gid, + pub feedback_reader: Gid, +} + +impl ActionCliEntities { + #[inline] + pub fn is_complete(&self) -> bool { + self.send_goal.is_complete() + && self.cancel_goal.is_complete() + && self.get_result.is_complete() + && self.status_reader != Gid::NOT_DISCOVERED + && self.feedback_reader != Gid::NOT_DISCOVERED + } +} + +impl std::fmt::Debug for ActionCliEntities { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{send_goal{:?}, cancel_goal{:?}, get_result{:?}, statusR:{:?}, feedbackR:{:?}}}", + self.send_goal, + self.cancel_goal, + self.get_result, + self.status_reader, + self.feedback_reader, + )?; + Ok(()) + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct ActionCli { + pub name: String, + #[serde(rename = "type")] + pub typ: String, + #[serde(skip)] + pub entities: ActionCliEntities, +} + +impl ActionCli { + pub fn create(name: String, typ: String) -> Result { + check_ros_name(&name)?; + Ok(ActionCli { + name, + typ, + entities: ActionCliEntities::default(), + }) + } + + pub fn name_as_keyexpr(&self) -> &keyexpr { + ke_for_sure!(&self.name[1..]) + } + + #[inline] + pub fn is_complete(&self) -> bool { + self.entities.is_complete() + } +} + +impl std::fmt::Display for ActionCli { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Action Client {}: {}", self.name, self.typ)?; + Ok(()) + } +} + +#[derive(Serialize)] +pub struct NodeInfo { + // The node unique id is: // + #[serde(skip)] + pub id: String, + #[serde(skip)] + fullname: Range, + #[serde(skip)] + namespace: Range, + #[serde(skip)] + name: Range, + #[serde(skip)] + pub participant: Gid, + #[serde(rename = "publishers", serialize_with = "serialize_hashmap_values")] + pub msg_pub: HashMap, + #[serde(rename = "subscribers", serialize_with = "serialize_hashmap_values")] + pub msg_sub: HashMap, + #[serde( + rename = "service_servers", + serialize_with = "serialize_hashmap_values" + )] + pub service_srv: HashMap, + #[serde( + rename = "service_clients", + serialize_with = "serialize_hashmap_values" + )] + pub service_cli: HashMap, + #[serde(rename = "action_servers", serialize_with = "serialize_hashmap_values")] + pub action_srv: HashMap, + #[serde(rename = "action_clients", serialize_with = "serialize_hashmap_values")] + pub action_cli: HashMap, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub undiscovered_reader: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub undiscovered_writer: Vec, +} + +impl std::fmt::Display for NodeInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.id) + } +} + +impl std::fmt::Debug for NodeInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} (namespace={}, name={})", + self.id, + self.namespace(), + self.name() + ) + } +} + +impl NodeInfo { + pub fn create( + node_namespace: String, + node_name: String, + participant: Gid, + ) -> Result { + // Construct id as "/", keeping Ranges for namespace and name + let mut id = participant.to_string(); + let namespace_start: usize = id.len(); + id.push_str(&node_namespace); + let namespace = Range { + start: namespace_start, + end: id.len(), + }; + if node_namespace != "/" { + id.push('/') + } + let name_start = id.len(); + id.push_str(&node_name); + let name = Range { + start: name_start, + end: id.len(), + }; + let fullname = Range { + start: namespace_start, + end: id.len(), + }; + + // Check is resulting id is a valid key expression + if let Err(e) = KeyExpr::try_from(&id) { + return Err(format!( + "Incompatible ROS Node: '{id}' cannot be converted as a Zenoh key expression: {e}" + )); + } + + Ok(NodeInfo { + id, + fullname, + namespace, + name, + participant, + msg_pub: HashMap::new(), + msg_sub: HashMap::new(), + service_srv: HashMap::new(), + service_cli: HashMap::new(), + action_srv: HashMap::new(), + action_cli: HashMap::new(), + undiscovered_reader: Vec::new(), + undiscovered_writer: Vec::new(), + }) + } + + #[inline] + pub fn id(&self) -> &str { + &self.id + } + + #[inline] + pub fn fullname(&self) -> &str { + &self.id[self.fullname.clone()] + } + + #[inline] + pub fn namespace(&self) -> &str { + &self.id[self.namespace.clone()] + } + + #[inline] + pub fn name(&self) -> &str { + &self.id[self.name.clone()] + } + + #[inline] + pub fn id_as_keyexpr(&self) -> &keyexpr { + ke_for_sure!(&self.id) + } + + #[inline] + pub fn fullname_as_keyexpr(&self) -> &keyexpr { + // fullname always start with '/' - remove it + ke_for_sure!(&self.fullname()[1..]) + } + + pub fn update_with_reader(&mut self, entity: &DdsEntity) -> Option { + let topic_prefix = &entity.topic_name[..3]; + let topic_suffix = &entity.topic_name[2..]; + match topic_prefix { + "rt/" if topic_suffix.ends_with("/_action/status") => self + .update_action_cli_status_reader( + &topic_suffix[..topic_suffix.len() - 15], + &entity.key, + ), + "rt/" if topic_suffix.ends_with("/_action/feedback") => self + .update_action_cli_feedback_reader( + &topic_suffix[..topic_suffix.len() - 17], + dds_type_to_ros2_action_type(&entity.type_name), + &entity.key, + ), + "rt/" => self.update_msg_sub( + topic_suffix, + dds_type_to_ros2_message_type(&entity.type_name), + &entity.key, + ), + "rq/" if topic_suffix.ends_with("/_action/send_goalRequest") => self + .update_action_srv_send_req_reader( + &topic_suffix[..topic_suffix.len() - 25], + dds_type_to_ros2_action_type(&entity.type_name), + &entity.key, + ), + "rq/" if topic_suffix.ends_with("/_action/cancel_goalRequest") => self + .update_action_srv_cancel_req_reader( + &topic_suffix[..topic_suffix.len() - 27], + &entity.key, + ), + "rq/" if topic_suffix.ends_with("/_action/get_resultRequest") => self + .update_action_srv_result_req_reader( + &topic_suffix[..topic_suffix.len() - 26], + dds_type_to_ros2_action_type(&entity.type_name), + &entity.key, + ), + "rq/" if topic_suffix.ends_with("Request") => self.update_service_srv_req_reader( + &topic_suffix[..topic_suffix.len() - 7], + dds_type_to_ros2_service_type(&entity.type_name), + &entity.key, + ), + "rr/" if topic_suffix.ends_with("/_action/send_goalReply") => self + .update_action_cli_send_rep_reader( + &topic_suffix[..topic_suffix.len() - 23], + dds_type_to_ros2_action_type(&entity.type_name), + &entity.key, + ), + "rr/" if topic_suffix.ends_with("/_action/cancel_goalReply") => self + .update_action_cli_cancel_rep_reader( + &topic_suffix[..topic_suffix.len() - 25], + &entity.key, + ), + "rr/" if topic_suffix.ends_with("/_action/get_resultReply") => self + .update_action_cli_result_rep_reader( + &topic_suffix[..topic_suffix.len() - 24], + dds_type_to_ros2_action_type(&entity.type_name), + &entity.key, + ), + "rr/" if topic_suffix.ends_with("Reply") => self.update_service_cli_rep_reader( + &topic_suffix[..topic_suffix.len() - 5], + dds_type_to_ros2_service_type(&entity.type_name), + &entity.key, + ), + _ => { + log::warn!( + r#"ROS Node {self} uses unexpected DDS topic "{}" - ignored"#, + entity.topic_name + ); + None + } + } + } + + pub fn update_with_writer(&mut self, entity: &DdsEntity) -> Option { + let topic_prefix = &entity.topic_name[..3]; + let topic_suffix = &entity.topic_name[2..]; + match topic_prefix { + "rt/" if topic_suffix.ends_with("/_action/status") => self + .update_action_srv_status_writer( + &topic_suffix[..topic_suffix.len() - 15], + &entity.key, + ), + "rt/" if topic_suffix.ends_with("/_action/feedback") => self + .update_action_srv_feedback_writer( + &topic_suffix[..topic_suffix.len() - 17], + dds_type_to_ros2_action_type(&entity.type_name), + &entity.key, + ), + "rt/" => self.update_msg_pub( + topic_suffix, + dds_type_to_ros2_message_type(&entity.type_name), + &entity.key, + ), + "rq/" if topic_suffix.ends_with("/_action/send_goalRequest") => self + .update_action_cli_send_req_writer( + &topic_suffix[..topic_suffix.len() - 25], + dds_type_to_ros2_action_type(&entity.type_name), + &entity.key, + ), + "rq/" if topic_suffix.ends_with("/_action/cancel_goalRequest") => self + .update_action_cli_cancel_req_writer( + &topic_suffix[..topic_suffix.len() - 27], + &entity.key, + ), + "rq/" if topic_suffix.ends_with("/_action/get_resultRequest") => self + .update_action_cli_result_req_writer( + &topic_suffix[..topic_suffix.len() - 26], + dds_type_to_ros2_action_type(&entity.type_name), + &entity.key, + ), + "rq/" if topic_suffix.ends_with("Request") => self.update_service_cli_req_writer( + &topic_suffix[..topic_suffix.len() - 7], + dds_type_to_ros2_service_type(&entity.type_name), + &entity.key, + ), + "rr/" if topic_suffix.ends_with("/_action/send_goalReply") => self + .update_action_srv_send_rep_writer( + &topic_suffix[..topic_suffix.len() - 23], + dds_type_to_ros2_action_type(&entity.type_name), + &entity.key, + ), + "rr/" if topic_suffix.ends_with("/_action/cancel_goalReply") => self + .update_action_srv_cancel_rep_writer( + &topic_suffix[..topic_suffix.len() - 25], + &entity.key, + ), + "rr/" if topic_suffix.ends_with("/_action/get_resultReply") => self + .update_action_srv_result_rep_writer( + &topic_suffix[..topic_suffix.len() - 24], + dds_type_to_ros2_action_type(&entity.type_name), + &entity.key, + ), + "rr/" if topic_suffix.ends_with("Reply") => self.update_service_srv_rep_writer( + &topic_suffix[..topic_suffix.len() - 5], + dds_type_to_ros2_service_type(&entity.type_name), + &entity.key, + ), + _ => { + log::warn!( + r#"ROS Node {self} uses unexpected DDS topic "{}" - ignored"#, + entity.topic_name + ); + None + } + } + } + + // Update MsgPub, returing a ROS2DiscoveryEvent::DiscoveredMsgSub if new or changed + fn update_msg_pub( + &mut self, + name: &str, + typ: String, + writer: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredMsgPub; + let node_fullname = self.fullname().to_string(); + match self.msg_pub.entry(name.into()) { + Entry::Vacant(e) => match MsgPub::create(name.into(), typ, *writer) { + Ok(t) => { + e.insert(t.clone()); + Some(DiscoveredMsgPub(node_fullname, t)) + } + Err(e) => { + log::error!( + "ROS Node {self} declared an incompatible Publisher: {e} - ignored" + ); + None + } + }, + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result: Option = None; + if v.typ != typ { + log::warn!( + r#"ROS declaration of Publisher "{v}" changed it's type to "{typ}""# + ); + v.typ = typ; + result = Some(DiscoveredMsgPub(node_fullname.clone(), v.clone())); + } + if v.writer != *writer { + log::debug!( + r#"ROS declaration of Publisher "{v}" changed it's DDS Writer's GID from {} to {writer}"#, + v.writer + ); + v.writer = *writer; + result = Some(DiscoveredMsgPub(node_fullname, v.clone())); + } + result + } + } + } + + // Update MsgSub, returing a ROS2DiscoveryEvent::DiscoveredMsgSub if new or changed + fn update_msg_sub( + &mut self, + name: &str, + typ: String, + reader: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredMsgSub; + let node_fullname = self.fullname().to_string(); + match self.msg_sub.entry(name.into()) { + Entry::Vacant(e) => match MsgSub::create(name.into(), typ, *reader) { + Ok(t) => { + e.insert(t.clone()); + Some(DiscoveredMsgSub(node_fullname, t)) + } + Err(e) => { + log::error!( + "ROS Node {self} declared an incompatible Subscriber: {e} - ignored" + ); + None + } + }, + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result: Option = None; + if v.typ != typ { + log::warn!( + r#"ROS declaration of Subscriber "{v}" changed it's type to "{typ}""# + ); + v.typ = typ; + result = Some(DiscoveredMsgSub(node_fullname.clone(), v.clone())); + } + if v.reader != *reader { + log::debug!( + r#"ROS declaration of Subscriber "{v}" changed it's DDS Writer's GID from {} to {reader}"#, + v.reader + ); + v.reader = *reader; + result = Some(DiscoveredMsgSub(node_fullname, v.clone())); + } + result + } + } + } + + // Update ServiceSrv, returing a ROS2DiscoveryEvent::DiscoveredServiceSrv if new and complete or changed + fn update_service_srv_req_reader( + &mut self, + name: &str, + typ: String, + reader: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredServiceSrv; + let node_fullname = self.fullname().to_string(); + match self.service_srv.entry(name.into()) { + Entry::Vacant(e) => { + match ServiceSrv::create(name.into(), typ) { + Ok(mut s) => { + s.entities.req_reader = *reader; + e.insert(s); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Service Server: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + log::warn!( + r#"ROS declaration of Service Server "{v}" changed it's type to "{typ}""# + ); + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredServiceSrv(node_fullname.clone(), v.clone())) + }; + } + if v.entities.req_reader != *reader { + if v.entities.req_reader != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Service Server "{v}" changed it's Request DDS Reader's GID from {} to {reader}"#, + v.entities.req_reader + ); + } + v.entities.req_reader = *reader; + if v.is_complete() { + result = Some(DiscoveredServiceSrv(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ServiceSrv, returing a ROS2DiscoveryEvent::DiscoveredServiceSrv if new and complete or changed + fn update_service_srv_rep_writer( + &mut self, + name: &str, + typ: String, + writer: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredServiceSrv; + let node_fullname = self.fullname().to_string(); + match self.service_srv.entry(name.into()) { + Entry::Vacant(e) => { + match ServiceSrv::create(name.into(), typ) { + Ok(mut s) => { + s.entities.rep_writer = *writer; + e.insert(s); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Service Server: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + log::warn!( + r#"ROS declaration of Service Server "{v}" changed it's type to "{typ}""# + ); + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredServiceSrv(node_fullname.clone(), v.clone())) + }; + } + if v.entities.rep_writer != *writer { + if v.entities.rep_writer != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Service Server "{v}" changed it's Reply DDS Writer's GID from {} to {writer}"#, + v.entities.rep_writer + ); + } + v.entities.rep_writer = *writer; + if v.is_complete() { + result = Some(DiscoveredServiceSrv(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ServiceCli, returing a ROS2DiscoveryEvent::DiscoveredServiceCli if new and complete or changed + fn update_service_cli_rep_reader( + &mut self, + name: &str, + typ: String, + reader: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredServiceCli; + let node_fullname = self.fullname().to_string(); + match self.service_cli.entry(name.into()) { + Entry::Vacant(e) => { + match ServiceCli::create(name.into(), typ) { + Ok(mut s) => { + s.entities.rep_reader = *reader; + e.insert(s); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Service Client: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + log::warn!( + r#"ROS declaration of Service Client "{v}" changed it's type to "{typ}""# + ); + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredServiceCli(node_fullname.clone(), v.clone())) + }; + } + if v.entities.rep_reader != *reader { + if v.entities.rep_reader != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Service Client "{v}" changed it's Request DDS Reader's GID from {} to {reader}"#, + v.entities.rep_reader + ); + } + v.entities.rep_reader = *reader; + if v.is_complete() { + result = Some(DiscoveredServiceCli(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ServiceCli, returing a ROS2DiscoveryEvent::DiscoveredServiceCli if new and complete or changed + fn update_service_cli_req_writer( + &mut self, + name: &str, + typ: String, + writer: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredServiceCli; + let node_fullname = self.fullname().to_string(); + match self.service_cli.entry(name.into()) { + Entry::Vacant(e) => { + match ServiceCli::create(name.into(), typ) { + Ok(mut s) => { + s.entities.req_writer = *writer; + e.insert(s); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Service Client: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + log::warn!( + r#"ROS declaration of Service Server "{v}" changed it's type to "{typ}""# + ); + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredServiceCli(node_fullname.clone(), v.clone())) + }; + } + if v.entities.req_writer != *writer { + if v.entities.req_writer != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Service Server "{v}" changed it's Reply DDS Writer's GID from {} to {writer}"#, + v.entities.req_writer + ); + } + v.entities.req_writer = *writer; + if v.is_complete() { + result = Some(DiscoveredServiceCli(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionSrv, returing a ROS2DiscoveryEvent::DiscoveredActionSrv if new and complete or changed + fn update_action_srv_send_req_reader( + &mut self, + name: &str, + typ: String, + reader: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionSrv; + let node_fullname = self.fullname().to_string(); + match self.action_srv.entry(name.into()) { + Entry::Vacant(e) => { + match ActionSrv::create(name.into(), typ) { + Ok(mut a) => { + a.entities.send_goal.req_reader = *reader; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Server: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + if !v.typ.is_empty() { + log::warn!( + r#"ROS declaration of Action Server "{v}" changed it's type to "{typ}""# + ); + } + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname.clone(), v.clone())) + }; + } + if v.entities.send_goal.req_reader != *reader { + if v.entities.send_goal.req_reader != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Server "{v}" changed it's send_goal Request DDS Reader's GID from {} to {reader}"#, + v.entities.send_goal.req_reader + ); + } + v.entities.send_goal.req_reader = *reader; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionSrv, returing a ROS2DiscoveryEvent::DiscoveredActionSrv if new and complete or changed + fn update_action_srv_send_rep_writer( + &mut self, + name: &str, + typ: String, + writer: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionSrv; + let node_fullname = self.fullname().to_string(); + match self.action_srv.entry(name.into()) { + Entry::Vacant(e) => { + match ActionSrv::create(name.into(), typ) { + Ok(mut a) => { + a.entities.send_goal.rep_writer = *writer; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Server: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + if !v.typ.is_empty() { + log::warn!( + r#"ROS declaration of Action Server "{v}" changed it's type to "{typ}""# + ); + } + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname.clone(), v.clone())) + }; + } + if v.entities.send_goal.rep_writer != *writer { + if v.entities.send_goal.rep_writer != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Server "{v}" changed it's send_goal Reply DDS Writer's GID from {} to {writer}"#, + v.entities.send_goal.rep_writer + ); + } + v.entities.send_goal.rep_writer = *writer; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionSrv, returing a ROS2DiscoveryEvent::DiscoveredActionSrv if new and complete or changed + // NOTE: type of CancelGoal topic does not reflect the action type. + // Thus we don't update it or we create ActionCli with as an empty String as type. + fn update_action_srv_cancel_req_reader( + &mut self, + name: &str, + reader: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionSrv; + let node_fullname = self.fullname().to_string(); + match self.action_srv.entry(name.into()) { + Entry::Vacant(e) => { + match ActionSrv::create(name.into(), String::new()) { + Ok(mut a) => { + a.entities.cancel_goal.req_reader = *reader; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Server: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.entities.cancel_goal.req_reader != *reader { + if v.entities.cancel_goal.req_reader != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Server "{v}" changed it's cancel_goal Request DDS Reader's GID from {} to {reader}"#, + v.entities.cancel_goal.req_reader + ); + } + v.entities.cancel_goal.req_reader = *reader; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionSrv, returing a ROS2DiscoveryEvent::DiscoveredActionSrv if new and complete or changed + // NOTE: type of CancelGoal topic does not reflect the action type. + // Thus we don't update it or we create ActionCli with as an empty String as type. + fn update_action_srv_cancel_rep_writer( + &mut self, + name: &str, + writer: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionSrv; + let node_fullname = self.fullname().to_string(); + match self.action_srv.entry(name.into()) { + Entry::Vacant(e) => { + match ActionSrv::create(name.into(), String::new()) { + Ok(mut a) => { + a.entities.cancel_goal.rep_writer = *writer; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Server: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.entities.cancel_goal.rep_writer != *writer { + if v.entities.cancel_goal.rep_writer != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Server "{v}" changed it's cancel_goal Reply DDS Writer's GID from {} to {writer}"#, + v.entities.cancel_goal.rep_writer + ); + } + v.entities.cancel_goal.rep_writer = *writer; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionSrv, returing a ROS2DiscoveryEvent::DiscoveredActionSrv if new and complete or changed + fn update_action_srv_result_req_reader( + &mut self, + name: &str, + typ: String, + reader: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionSrv; + let node_fullname = self.fullname().to_string(); + match self.action_srv.entry(name.into()) { + Entry::Vacant(e) => { + match ActionSrv::create(name.into(), typ) { + Ok(mut a) => { + a.entities.get_result.req_reader = *reader; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Server: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + if !v.typ.is_empty() { + log::warn!( + r#"ROS declaration of Action Server "{v}" changed it's type to "{typ}""# + ); + } + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname.clone(), v.clone())) + }; + } + if v.entities.get_result.req_reader != *reader { + if v.entities.get_result.req_reader != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Server "{v}" changed it's get_result Request DDS Reader's GID from {} to {reader}"#, + v.entities.get_result.req_reader + ); + } + v.entities.get_result.req_reader = *reader; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionSrv, returing a ROS2DiscoveryEvent::DiscoveredActionSrv if new and complete or changed + fn update_action_srv_result_rep_writer( + &mut self, + name: &str, + typ: String, + writer: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionSrv; + let node_fullname = self.fullname().to_string(); + match self.action_srv.entry(name.into()) { + Entry::Vacant(e) => { + match ActionSrv::create(name.into(), typ) { + Ok(mut a) => { + a.entities.get_result.rep_writer = *writer; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Server: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + if !v.typ.is_empty() { + log::warn!( + r#"ROS declaration of Action Server "{v}" changed it's type to "{typ}""# + ); + } + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname.clone(), v.clone())) + }; + } + if v.entities.get_result.rep_writer != *writer { + if v.entities.get_result.rep_writer != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Server "{v}" changed it's get_result Reply DDS Writer's GID from {} to {writer}"#, + v.entities.get_result.rep_writer + ); + } + v.entities.get_result.rep_writer = *writer; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionSrv, returing a ROS2DiscoveryEvent::DiscoveredActionSrv if new and complete or changed + // NOTE: type of Status topic does not reflect the action type. + // Thus we don't update it or we create ActionCli with as an empty String as type. + fn update_action_srv_status_writer( + &mut self, + name: &str, + writer: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionSrv; + let node_fullname = self.fullname().to_string(); + match self.action_srv.entry(name.into()) { + Entry::Vacant(e) => { + match ActionSrv::create(name.into(), String::new()) { + Ok(mut a) => { + a.entities.status_writer = *writer; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Server: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.entities.status_writer != *writer { + if v.entities.status_writer != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Server "{v}" changed it's status DDS Writer's GID from {} to {writer}"#, + v.entities.status_writer + ); + } + v.entities.status_writer = *writer; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionSrv, returing a ROS2DiscoveryEvent::DiscoveredActionSrv if new and complete or changed + fn update_action_srv_feedback_writer( + &mut self, + name: &str, + typ: String, + writer: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionSrv; + let node_fullname = self.fullname().to_string(); + match self.action_srv.entry(name.into()) { + Entry::Vacant(e) => { + match ActionSrv::create(name.into(), typ) { + Ok(mut a) => { + a.entities.feedback_writer = *writer; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Server: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + if !v.typ.is_empty() { + log::warn!( + r#"ROS declaration of Action Server "{v}" changed it's type to "{typ}""# + ); + } + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname.clone(), v.clone())) + }; + } + if v.entities.feedback_writer != *writer { + if v.entities.feedback_writer != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Server "{v}" changed it's status DDS Writer's GID from {} to {writer}"#, + v.entities.feedback_writer + ); + } + v.entities.feedback_writer = *writer; + if v.is_complete() { + result = Some(DiscoveredActionSrv(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionCli, returing a ROS2DiscoveryEvent::DiscoveredActionCli if new and complete or changed + fn update_action_cli_send_rep_reader( + &mut self, + name: &str, + typ: String, + reader: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionCli; + let node_fullname = self.fullname().to_string(); + match self.action_cli.entry(name.into()) { + Entry::Vacant(e) => { + match ActionCli::create(name.into(), typ) { + Ok(mut a) => { + a.entities.send_goal.rep_reader = *reader; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Client: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + if !v.typ.is_empty() { + log::warn!( + r#"ROS declaration of Action Client "{v}" changed it's type to "{typ}""# + ); + } + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname.clone(), v.clone())) + }; + } + if v.entities.send_goal.rep_reader != *reader { + if v.entities.send_goal.rep_reader != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Client "{v}" changed it's send_goal Reply DDS Reader's GID from {} to {reader}"#, + v.entities.send_goal.rep_reader + ); + } + v.entities.send_goal.rep_reader = *reader; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionCli, returing a ROS2DiscoveryEvent::DiscoveredActionCli if new and complete or changed + fn update_action_cli_send_req_writer( + &mut self, + name: &str, + typ: String, + writer: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionCli; + let node_fullname = self.fullname().to_string(); + match self.action_cli.entry(name.into()) { + Entry::Vacant(e) => { + match ActionCli::create(name.into(), typ) { + Ok(mut a) => { + a.entities.send_goal.req_writer = *writer; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Client: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + if !v.typ.is_empty() { + log::warn!( + r#"ROS declaration of Action Client "{v}" changed it's type to "{typ}""# + ); + } + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname.clone(), v.clone())) + }; + } + if v.entities.send_goal.req_writer != *writer { + if v.entities.send_goal.req_writer != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Client "{v}" changed it's send_goal Request DDS Writer's GID from {} to {writer}"#, + v.entities.send_goal.req_writer + ); + } + v.entities.send_goal.req_writer = *writer; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionCli, returing a ROS2DiscoveryEvent::DiscoveredActionCli if new and complete or changed + // NOTE: type of CancelGoal topic does not reflect the action type. + // Thus we don't update it or we create ActionCli with as an empty String as type. + fn update_action_cli_cancel_rep_reader( + &mut self, + name: &str, + reader: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionCli; + let node_fullname = self.fullname().to_string(); + match self.action_cli.entry(name.into()) { + Entry::Vacant(e) => { + match ActionCli::create(name.into(), String::new()) { + Ok(mut a) => { + a.entities.cancel_goal.rep_reader = *reader; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Client: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.entities.cancel_goal.rep_reader != *reader { + if v.entities.cancel_goal.rep_reader != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Client "{v}" changed it's cancel_goal Reply DDS Reader's GID from {} to {reader}"#, + v.entities.cancel_goal.rep_reader + ); + } + v.entities.cancel_goal.rep_reader = *reader; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionCli, returing a ROS2DiscoveryEvent::DiscoveredActionCli if new and complete or changed + // NOTE: type of CancelGoal topic does not reflect the action type. + // Thus we don't update it or we create ActionCli with as an empty String as type. + fn update_action_cli_cancel_req_writer( + &mut self, + name: &str, + writer: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionCli; + let node_fullname = self.fullname().to_string(); + match self.action_cli.entry(name.into()) { + Entry::Vacant(e) => { + match ActionCli::create(name.into(), String::new()) { + Ok(mut a) => { + a.entities.cancel_goal.req_writer = *writer; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Client: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.entities.cancel_goal.req_writer != *writer { + if v.entities.cancel_goal.req_writer != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Client "{v}" changed it's cancel_goal Request DDS Writer's GID from {} to {writer}"#, + v.entities.cancel_goal.req_writer + ); + } + v.entities.cancel_goal.req_writer = *writer; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionCli, returing a ROS2DiscoveryEvent::DiscoveredActionCli if new and complete or changed + fn update_action_cli_result_rep_reader( + &mut self, + name: &str, + typ: String, + reader: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionCli; + let node_fullname = self.fullname().to_string(); + match self.action_cli.entry(name.into()) { + Entry::Vacant(e) => { + match ActionCli::create(name.into(), typ) { + Ok(mut a) => { + a.entities.get_result.rep_reader = *reader; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Client: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + if !v.typ.is_empty() { + log::warn!( + r#"ROS declaration of Action Client "{v}" changed it's type to "{typ}""# + ); + } + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname.clone(), v.clone())) + }; + } + if v.entities.get_result.rep_reader != *reader { + if v.entities.get_result.rep_reader != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Client "{v}" changed it's get_result Reply DDS Reader's GID from {} to {reader}"#, + v.entities.get_result.rep_reader + ); + } + v.entities.get_result.rep_reader = *reader; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionCli, returing a ROS2DiscoveryEvent::DiscoveredActionCli if new and complete or changed + fn update_action_cli_result_req_writer( + &mut self, + name: &str, + typ: String, + writer: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionCli; + let node_fullname = self.fullname().to_string(); + match self.action_cli.entry(name.into()) { + Entry::Vacant(e) => { + match ActionCli::create(name.into(), typ) { + Ok(mut a) => { + a.entities.get_result.req_writer = *writer; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Client: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + if !v.typ.is_empty() { + log::warn!( + r#"ROS declaration of Action Client "{v}" changed it's type to "{typ}""# + ); + } + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname.clone(), v.clone())) + }; + } + if v.entities.get_result.req_writer != *writer { + if v.entities.get_result.req_writer != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Client "{v}" changed it's get_result Request DDS Writer's GID from {} to {writer}"#, + v.entities.get_result.req_writer + ); + } + v.entities.get_result.req_writer = *writer; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionCli, returing a ROS2DiscoveryEvent::DiscoveredActionCli if new and complete or changed + // NOTE: type of Status topic does not reflect the action type. + // Thus we don't update it or we create ActionCli with as an empty String as type. + fn update_action_cli_status_reader( + &mut self, + name: &str, + reader: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionCli; + let node_fullname = self.fullname().to_string(); + match self.action_cli.entry(name.into()) { + Entry::Vacant(e) => { + match ActionCli::create(name.into(), String::new()) { + Ok(mut a) => { + a.entities.status_reader = *reader; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Client: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.entities.status_reader != *reader { + if v.entities.status_reader != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Client "{v}" changed it's status DDS Reader's GID from {} to {reader}"#, + v.entities.status_reader + ); + } + v.entities.status_reader = *reader; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname, v.clone())) + }; + } + result + } + } + } + + // Update ActionCli, returing a ROS2DiscoveryEvent::DiscoveredActionCli if new and complete or changed + fn update_action_cli_feedback_reader( + &mut self, + name: &str, + typ: String, + reader: &Gid, + ) -> Option { + use ROS2DiscoveryEvent::DiscoveredActionCli; + let node_fullname = self.fullname().to_string(); + match self.action_cli.entry(name.into()) { + Entry::Vacant(e) => { + match ActionCli::create(name.into(), typ) { + Ok(mut a) => { + a.entities.feedback_reader = *reader; + e.insert(a); + } + Err(e) => log::error!( + "ROS Node {self} declared an incompatible Action Client: {e} - ignored" + ), + } + None // discovery is not complete anyway + } + Entry::Occupied(mut e) => { + let v = e.get_mut(); + let mut result = None; + if v.typ != typ { + if !v.typ.is_empty() { + log::warn!( + r#"ROS declaration of Action Client "{v}" changed it's type to "{typ}""# + ); + } + v.typ = typ; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname.clone(), v.clone())) + }; + } + if v.entities.feedback_reader != *reader { + if v.entities.feedback_reader != Gid::NOT_DISCOVERED { + log::debug!( + r#"ROS declaration of Action Client "{v}" changed it's status DDS Reader's GID from {} to {reader}"#, + v.entities.feedback_reader + ); + } + v.entities.feedback_reader = *reader; + if v.is_complete() { + result = Some(DiscoveredActionCli(node_fullname, v.clone())) + }; + } + result + } + } + } + + // + pub fn remove_all_entities(&mut self) -> Vec { + use ROS2DiscoveryEvent::*; + let node_fullname = self.fullname().to_string(); + let mut events = Vec::new(); + + for (_, v) in self.msg_pub.drain() { + events.push(UndiscoveredMsgPub(node_fullname.clone(), v)) + } + for (_, v) in self.msg_sub.drain() { + events.push(UndiscoveredMsgSub(node_fullname.clone(), v)) + } + for (_, v) in self.service_srv.drain() { + events.push(UndiscoveredServiceSrv(node_fullname.clone(), v)) + } + for (_, v) in self.service_cli.drain() { + events.push(UndiscoveredServiceCli(node_fullname.clone(), v)) + } + for (_, v) in self.action_srv.drain() { + events.push(UndiscoveredActionSrv(node_fullname.clone(), v)) + } + for (_, v) in self.action_cli.drain() { + events.push(UndiscoveredActionCli(node_fullname.clone(), v)) + } + self.undiscovered_reader.resize(0, Gid::NOT_DISCOVERED); + self.undiscovered_writer.resize(0, Gid::NOT_DISCOVERED); + + events + } + + // Remove a DDS Reader possibly used by this node, and returns an UndiscoveredX event if + // this Reader was used by some Subscription, Service or Action + pub fn remove_reader(&mut self, reader: &Gid) -> Option { + use ROS2DiscoveryEvent::*; + let node_fullname = self.fullname().to_string(); + if let Some((name, _)) = self.msg_sub.iter().find(|(_, v)| v.reader == *reader) { + return Some(UndiscoveredMsgSub( + node_fullname, + self.msg_sub.remove(&name.clone()).unwrap(), + )); + } + if let Some((name, _)) = self + .service_srv + .iter() + .find(|(_, v)| v.entities.req_reader == *reader) + { + return Some(UndiscoveredServiceSrv( + node_fullname, + self.service_srv.remove(&name.clone()).unwrap(), + )); + } + if let Some((name, _)) = self + .service_cli + .iter() + .find(|(_, v)| v.entities.rep_reader == *reader) + { + return Some(UndiscoveredServiceCli( + node_fullname, + self.service_cli.remove(&name.clone()).unwrap(), + )); + } + if let Some((name, _)) = self.action_srv.iter().find(|(_, v)| { + v.entities.send_goal.req_reader == *reader + || v.entities.cancel_goal.req_reader == *reader + || v.entities.get_result.req_reader == *reader + }) { + return Some(UndiscoveredActionSrv( + node_fullname, + self.action_srv.remove(&name.clone()).unwrap(), + )); + } + if let Some((name, _)) = self.action_cli.iter().find(|(_, v)| { + v.entities.send_goal.rep_reader == *reader + || v.entities.cancel_goal.rep_reader == *reader + || v.entities.get_result.rep_reader == *reader + || v.entities.status_reader == *reader + || v.entities.feedback_reader == *reader + }) { + return Some(UndiscoveredActionCli( + node_fullname, + self.action_cli.remove(&name.clone()).unwrap(), + )); + } + self.undiscovered_reader.retain(|gid| gid != reader); + None + } + + // Remove a DDS Writer possibly used by this node, and returns an UndiscoveredX event if + // this Writer was used by some Subscription, Service or Action + pub fn remove_writer(&mut self, writer: &Gid) -> Option { + use ROS2DiscoveryEvent::*; + let node_fullname = self.fullname().to_string(); + if let Some((name, _)) = self.msg_pub.iter().find(|(_, v)| v.writer == *writer) { + return Some(UndiscoveredMsgPub( + node_fullname, + self.msg_pub.remove(&name.clone()).unwrap(), + )); + } + if let Some((name, _)) = self + .service_srv + .iter() + .find(|(_, v)| v.entities.rep_writer == *writer) + { + return Some(UndiscoveredServiceSrv( + node_fullname, + self.service_srv.remove(&name.clone()).unwrap(), + )); + } + if let Some((name, _)) = self + .service_cli + .iter() + .find(|(_, v)| v.entities.req_writer == *writer) + { + return Some(UndiscoveredServiceCli( + node_fullname, + self.service_cli.remove(&name.clone()).unwrap(), + )); + } + if let Some((name, _)) = self.action_srv.iter().find(|(_, v)| { + v.entities.send_goal.rep_writer == *writer + || v.entities.cancel_goal.rep_writer == *writer + || v.entities.get_result.rep_writer == *writer + || v.entities.status_writer == *writer + || v.entities.feedback_writer == *writer + }) { + return Some(UndiscoveredActionSrv( + node_fullname, + self.action_srv.remove(&name.clone()).unwrap(), + )); + } + if let Some((name, _)) = self.action_cli.iter().find(|(_, v)| { + v.entities.send_goal.req_writer == *writer + || v.entities.cancel_goal.req_writer == *writer + || v.entities.get_result.req_writer == *writer + }) { + return Some(UndiscoveredActionCli( + node_fullname, + self.action_cli.remove(&name.clone()).unwrap(), + )); + } + self.undiscovered_writer.retain(|gid| gid != writer); + None + } +} + +fn serialize_hashmap_values( + map: &HashMap, + serializer: S, +) -> Result +where + S: Serializer, +{ + let mut seq: ::SerializeSeq = serializer.serialize_seq(Some(map.len()))?; + for x in map.values() { + seq.serialize_element(x)?; + } + seq.end() +} diff --git a/zenoh-plugin-ros2dds/src/qos_helpers.rs b/zenoh-plugin-ros2dds/src/qos_helpers.rs new file mode 100644 index 0000000..81b5ce8 --- /dev/null +++ b/zenoh-plugin-ros2dds/src/qos_helpers.rs @@ -0,0 +1,138 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use cyclors::{qos::*, DDS_LENGTH_UNLIMITED}; + +pub fn get_history_or_default(qos: &Qos) -> History { + match &qos.history { + None => History::default(), + Some(history) => history.clone(), + } +} + +pub fn get_durability_service_or_default(qos: &Qos) -> DurabilityService { + match &qos.durability_service { + None => DurabilityService::default(), + Some(durability_service) => durability_service.clone(), + } +} + +pub fn partition_is_empty(partition: &Option>) -> bool { + partition + .as_ref() + .map_or(true, |partition| partition.is_empty()) +} + +pub fn partition_contains(partition: &Option>, name: &String) -> bool { + partition + .as_ref() + .map_or(false, |partition| partition.contains(name)) +} + +pub fn is_writer_reliable(reliability: &Option) -> bool { + reliability.as_ref().map_or(true, |reliability| { + reliability.kind == ReliabilityKind::RELIABLE + }) +} + +pub fn is_reader_reliable(reliability: &Option) -> bool { + reliability.as_ref().map_or(false, |reliability| { + reliability.kind == ReliabilityKind::RELIABLE + }) +} + +pub fn is_transient_local(qos: &Qos) -> bool { + qos.durability.as_ref().map_or(false, |durability| { + durability.kind == DurabilityKind::TRANSIENT_LOCAL + }) +} + +// Copy and adapt Writer's QoS for creation of a matching Reader +pub fn adapt_writer_qos_for_reader(qos: &Qos) -> Qos { + let mut reader_qos = qos.clone(); + + // Unset any writer QoS that doesn't apply to data readers + reader_qos.durability_service = None; + reader_qos.ownership_strength = None; + reader_qos.transport_priority = None; + reader_qos.lifespan = None; + reader_qos.writer_data_lifecycle = None; + reader_qos.writer_batching = None; + + // Unset proprietary QoS which shouldn't apply + reader_qos.properties = None; + reader_qos.entity_name = None; + reader_qos.ignore_local = Some(IgnoreLocal { + kind: IgnoreLocalKind::PARTICIPANT, + }); + + // Set default Reliability QoS if not set for writer + if reader_qos.reliability.is_none() { + reader_qos.reliability = Some({ + Reliability { + kind: ReliabilityKind::BEST_EFFORT, + max_blocking_time: DDS_100MS_DURATION, + } + }); + } + + reader_qos +} + +// Copy and adapt Reader's QoS for creation of a matching Writer +pub fn adapt_reader_qos_for_writer(qos: &Qos) -> Qos { + let mut writer_qos = qos.clone(); + + // Unset any reader QoS that doesn't apply to data writers + writer_qos.time_based_filter = None; + writer_qos.reader_data_lifecycle = None; + writer_qos.properties = None; + writer_qos.entity_name = None; + + // Don't match with readers with the same participant + writer_qos.ignore_local = Some(IgnoreLocal { + kind: IgnoreLocalKind::PARTICIPANT, + }); + + // if Reader is TRANSIENT_LOCAL, configure durability_service QoS with same history as the Reader. + // This is because CycloneDDS is actually using durability_service.history for transient_local historical data. + if is_transient_local(qos) { + let history = qos + .history + .as_ref() + .map_or(History::default(), |history| history.clone()); + + writer_qos.durability_service = Some(DurabilityService { + service_cleanup_delay: 60 * DDS_1S_DURATION, + history_kind: history.kind, + history_depth: history.depth, + max_samples: DDS_LENGTH_UNLIMITED, + max_instances: DDS_LENGTH_UNLIMITED, + max_samples_per_instance: DDS_LENGTH_UNLIMITED, + }); + } + // Workaround for the DDS Writer to correctly match with a FastRTPS Reader + writer_qos.reliability = match writer_qos.reliability { + Some(mut reliability) => { + reliability.max_blocking_time = reliability.max_blocking_time.saturating_add(1); + Some(reliability) + } + _ => { + let mut reliability = Reliability::default(); + reliability.max_blocking_time = reliability.max_blocking_time.saturating_add(1); + Some(reliability) + } + }; + + writer_qos +} diff --git a/zenoh-plugin-ros2dds/src/ros2_utils.rs b/zenoh-plugin-ros2dds/src/ros2_utils.rs new file mode 100644 index 0000000..5e4a23b --- /dev/null +++ b/zenoh-plugin-ros2dds/src/ros2_utils.rs @@ -0,0 +1,148 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use zenoh::prelude::KeyExpr; + +/// Convert DDS Topic type to ROS2 Message type +pub fn dds_type_to_ros2_message_type(dds_topic: &str) -> String { + let result = dds_topic.replace("::dds_::", "::").replace("::", "/"); + if result.ends_with('_') { + result[..result.len() - 1].into() + } else { + result + } +} + +/// Convert ROS2 Message type to DDS Topic type +/// +/// # Examples +pub fn ros2_message_type_to_dds_type(ros_topic: &str) -> String { + let mut result = ros_topic.replace("/", "::"); + result + .rfind(':') + .map(|pos| result.insert_str(pos + 1, "dds_::")); + result.push('_'); + result +} + +/// Convert DDS Topic type for ROS2 Service to ROS2 Service type +pub fn dds_type_to_ros2_service_type(dds_topic: &str) -> String { + dds_type_to_ros2_message_type( + dds_topic + .strip_suffix("_Request_") + .or(dds_topic.strip_suffix("_Response_")) + .unwrap_or(dds_topic), + ) +} + +/// Convert DDS Topic type for ROS2 Action to ROS2 Action type +/// Warning: can't work for "rt/.../_action/status", "rq/.../_action/cancel_goalRequest" +/// or "rr../_action/cancel_goalReply" topic, since their types are generic +pub fn dds_type_to_ros2_action_type(dds_topic: &str) -> String { + dds_type_to_ros2_message_type( + dds_topic + .strip_suffix("_SendGoal_Request_") + .or(dds_topic.strip_suffix("_SendGoal_Response_")) + .or(dds_topic.strip_suffix("_GetResult_Request_")) + .or(dds_topic.strip_suffix("_GetResult_Response_")) + .or(dds_topic.strip_suffix("_FeedbackMessage_")) + .unwrap_or(dds_topic), + ) +} + +// check if name is a ROS name: starting with '/' and useable as a key expression (removing 1st '/') +#[inline] +pub fn check_ros_name(name: &str) -> Result<(), String> { + if !name.starts_with('/') || KeyExpr::try_from("&(name[1..])").is_err() { + Err(format!( + "'{name}' cannot be converted as a Zenoh key expression" + )) + } else { + Ok(()) + } +} + +mod tests { + + #[test] + fn test_types_conversions() { + use crate::ros2_utils::*; + + assert_eq!( + dds_type_to_ros2_message_type("geometry_msgs::msg::dds_::Twist_"), + "geometry_msgs/msg/Twist" + ); + assert_eq!( + dds_type_to_ros2_message_type("rcl_interfaces::msg::dds_::Log_"), + "rcl_interfaces/msg/Log" + ); + + assert_eq!( + ros2_message_type_to_dds_type("geometry_msgs/msg/Twist"), + "geometry_msgs::msg::dds_::Twist_" + ); + assert_eq!( + ros2_message_type_to_dds_type("rcl_interfaces/msg/Log"), + "rcl_interfaces::msg::dds_::Log_" + ); + + assert_eq!( + dds_type_to_ros2_service_type("example_interfaces::srv::dds_::AddTwoInts_Request_"), + "example_interfaces/srv/AddTwoInts" + ); + assert_eq!( + dds_type_to_ros2_service_type("example_interfaces::srv::dds_::AddTwoInts_Response_"), + "example_interfaces/srv/AddTwoInts" + ); + assert_eq!( + dds_type_to_ros2_service_type("rcl_interfaces::srv::dds_::ListParameters_Request_"), + "rcl_interfaces/srv/ListParameters" + ); + assert_eq!( + dds_type_to_ros2_service_type("rcl_interfaces::srv::dds_::ListParameters_Response_"), + "rcl_interfaces/srv/ListParameters" + ); + + assert_eq!( + dds_type_to_ros2_action_type( + "example_interfaces::action::dds_::Fibonacci_SendGoal_Request_" + ), + "example_interfaces/action/Fibonacci" + ); + assert_eq!( + dds_type_to_ros2_action_type( + "example_interfaces::action::dds_::Fibonacci_SendGoal_Response_" + ), + "example_interfaces/action/Fibonacci" + ); + assert_eq!( + dds_type_to_ros2_action_type( + "example_interfaces::action::dds_::Fibonacci_GetResult_Request_" + ), + "example_interfaces/action/Fibonacci" + ); + assert_eq!( + dds_type_to_ros2_action_type( + "example_interfaces::action::dds_::Fibonacci_GetResult_Response_" + ), + "example_interfaces/action/Fibonacci" + ); + assert_eq!( + dds_type_to_ros2_action_type( + "example_interfaces::action::dds_::Fibonacci_FeedbackMessage_" + ), + "example_interfaces/action/Fibonacci" + ); + } +} diff --git a/zenoh-plugin-ros2dds/src/ros_discovery.rs b/zenoh-plugin-ros2dds/src/ros_discovery.rs new file mode 100644 index 0000000..10236d4 --- /dev/null +++ b/zenoh-plugin-ros2dds/src/ros_discovery.rs @@ -0,0 +1,644 @@ +use crate::{ChannelEvent, ROS_DISCOVERY_INFO_PUSH_INTERVAL_MS}; +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use crate::dds_discovery::{delete_dds_entity, get_guid, DDSRawSample}; +use crate::gid::Gid; +use async_std::task; +use cdr::{CdrLe, Infinite}; +use cyclors::qos::{ + Durability, History, IgnoreLocal, IgnoreLocalKind, Qos, Reliability, DDS_INFINITE_TIME, +}; +use cyclors::*; +use flume::{unbounded, Receiver, Sender}; +use futures::select; +use serde::ser::SerializeSeq; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::HashSet; +use std::convert::TryInto; +use std::sync::Arc; +use std::sync::RwLock; +use std::time::Duration; +use std::{ + collections::HashMap, + ffi::{CStr, CString}, + mem::MaybeUninit, +}; +use zenoh::buffers::ZBuf; +use zenoh::prelude::HasReader; +use zenoh_core::zwrite; +use zenoh_util::{TimedEvent, Timer}; + +pub const ROS_DISCOVERY_INFO_TOPIC_NAME: &str = "ros_discovery_info"; +const ROS_DISCOVERY_INFO_TOPIC_TYPE: &str = "rmw_dds_common::msg::dds_::ParticipantEntitiesInfo_"; + +pub struct RosDiscoveryInfoMgr { + reader: dds_entity_t, + writer: dds_entity_t, + // This bridge Node fullname, as used as index in participant_entities_info.node_entities_info_seq + node_fullname: String, + // The ParticipantEntitiesInfo to publish on "ros_discovery_info" topic when changed, + // plus a bool indicating if it changed + participant_entities_state: Arc>, +} + +impl Drop for RosDiscoveryInfoMgr { + fn drop(&mut self) { + if let Err(e) = delete_dds_entity(self.reader) { + log::warn!( + "Error dropping DDS reader on {}: {}", + ROS_DISCOVERY_INFO_TOPIC_NAME, + e + ); + } + if let Err(e) = delete_dds_entity(self.writer) { + log::warn!( + "Error dropping DDS writer on {}: {}", + ROS_DISCOVERY_INFO_TOPIC_NAME, + e + ); + } + } +} + +impl RosDiscoveryInfoMgr { + pub fn new( + participant: dds_entity_t, + namespace: &str, + node_name: &str, + ) -> Result { + let cton = CString::new(ROS_DISCOVERY_INFO_TOPIC_NAME) + .unwrap() + .into_raw(); + let ctyn = CString::new(ROS_DISCOVERY_INFO_TOPIC_TYPE) + .unwrap() + .into_raw(); + + unsafe { + // Create topic (for reader/writer creation) + let t = cdds_create_blob_topic(participant, cton, ctyn, true); + + // Create reader + let mut qos = Qos::default(); + qos.reliability = Some(Reliability { + kind: qos::ReliabilityKind::RELIABLE, + max_blocking_time: DDS_INFINITE_TIME, + }); + qos.durability = Some(Durability { + kind: qos::DurabilityKind::TRANSIENT_LOCAL, + }); + // Note: KEEP_ALL to not loose any sample (topic is keyless). A periodic task should take samples from history. + qos.history = Some(History { + kind: qos::HistoryKind::KEEP_ALL, + depth: 0, + }); + qos.ignore_local = Some(IgnoreLocal { + kind: IgnoreLocalKind::PARTICIPANT, + }); + let qos_native = qos.to_qos_native(); + let reader = dds_create_reader(participant, t, qos_native, std::ptr::null()); + Qos::delete_qos_native(qos_native); + if reader < 0 { + return Err(format!( + "Error creating DDS Reader on {}: {}", + ROS_DISCOVERY_INFO_TOPIC_NAME, + CStr::from_ptr(dds_strretcode(-reader)) + .to_str() + .unwrap_or("unrecoverable DDS retcode") + )); + } + + // Create writer + let mut qos = Qos::default(); + qos.reliability = Some(Reliability { + kind: qos::ReliabilityKind::RELIABLE, + max_blocking_time: DDS_INFINITE_TIME, + }); + qos.durability = Some(Durability { + kind: qos::DurabilityKind::TRANSIENT_LOCAL, + }); + qos.history = Some(History { + kind: qos::HistoryKind::KEEP_LAST, + depth: 1, + }); + qos.ignore_local = Some(IgnoreLocal { + kind: IgnoreLocalKind::PARTICIPANT, + }); + let qos_native = qos.to_qos_native(); + let writer = dds_create_writer(participant, t, qos_native, std::ptr::null()); + Qos::delete_qos_native(qos_native); + if writer < 0 { + return Err(format!( + "Error creating DDS Writer on {}: {}", + ROS_DISCOVERY_INFO_TOPIC_NAME, + CStr::from_ptr(dds_strretcode(-writer)) + .to_str() + .unwrap_or("unrecoverable DDS retcode") + )); + } + + drop(CString::from_raw(cton)); + drop(CString::from_raw(ctyn)); + + let gid = get_guid(&participant)?; + let mut participant_entities_info = ParticipantEntitiesInfo::new(gid); + let node_info = NodeEntitiesInfo::new(namespace.to_string(), node_name.to_string()); + let node_fullname = node_info.to_string(); + participant_entities_info + .node_entities_info_seq + .insert(node_fullname.clone(), node_info); + + Ok(RosDiscoveryInfoMgr { + reader, + writer, + node_fullname, + participant_entities_state: Arc::new(RwLock::new(( + participant_entities_info, + true, + ))), + }) + } + } + + pub async fn run(&self) { + let writer = self.writer; + let participant_entities_state = self.participant_entities_state.clone(); + task::spawn(async move { + // Timer for periodic write of "ros_discovery_info" topic + let timer = Timer::default(); + let (tx, ros_disco_timer_rcv): (Sender<()>, Receiver<()>) = unbounded(); + let ros_disco_timer_event = TimedEvent::periodic( + Duration::from_millis(ROS_DISCOVERY_INFO_PUSH_INTERVAL_MS), + ChannelEvent { tx }, + ); + timer.add_async(ros_disco_timer_event).await; + + loop { + select!( + _ = ros_disco_timer_rcv.recv_async() => { + let (ref msg, ref mut has_changed) = *zwrite!(participant_entities_state); + if *has_changed { + log::debug!("Publish update on 'ros_discovery_info': {msg:?}"); + Self::write(writer, msg).unwrap_or_else(|e| + log::error!("Failed to publish update on 'ros_discovery_info' topic: {e}") + ); + *has_changed = false; + } + + } + ) + } + }); + } + + pub fn add_dds_writer(&self, gid: Gid) { + let (ref mut info, ref mut has_changed) = *zwrite!(self.participant_entities_state); + info.node_entities_info_seq + .get_mut(&self.node_fullname) + .unwrap() + .writer_gid_seq + .insert(gid); + *has_changed = true; + } + + pub fn remove_dds_writer(&self, gid: Gid) { + let (ref mut info, ref mut has_changed) = *zwrite!(self.participant_entities_state); + info.node_entities_info_seq + .get_mut(&self.node_fullname) + .unwrap() + .writer_gid_seq + .remove(&gid); + *has_changed = true; + } + + pub fn add_dds_reader(&self, gid: Gid) { + let (ref mut info, ref mut has_changed) = *zwrite!(self.participant_entities_state); + info.node_entities_info_seq + .get_mut(&self.node_fullname) + .unwrap() + .reader_gid_seq + .insert(gid); + *has_changed = true; + } + + pub fn remove_dds_reader(&self, gid: Gid) { + let (ref mut info, ref mut has_changed) = *zwrite!(self.participant_entities_state); + info.node_entities_info_seq + .get_mut(&self.node_fullname) + .unwrap() + .reader_gid_seq + .remove(&gid); + *has_changed = true; + } + + pub fn read(&self) -> Vec { + unsafe { + let mut zp: *mut ddsi_serdata = std::ptr::null_mut(); + #[allow(clippy::uninit_assumed_init)] + let mut si = MaybeUninit::<[dds_sample_info_t; 1]>::uninit(); + // Place read samples into a map indexed by Participant gid. + // Thus we only keep the last (not deserialized) update for each + let mut map: HashMap = HashMap::new(); + while dds_takecdr( + self.reader, + &mut zp, + 1, + si.as_mut_ptr() as *mut dds_sample_info_t, + DDS_ANY_STATE, + ) > 0 + { + let si = si.assume_init(); + if si[0].valid_data { + let raw_sample = DDSRawSample::create(zp); + + // No need to deserialize the full payload. Just read the Participant gid (first 16 bytes of the payload) + let gid = hex::encode(&raw_sample.payload_as_slice()[0..16]); + + map.insert(gid, raw_sample); + } + ddsi_serdata_unref(zp); + } + + map.values() + .into_iter() + .filter_map(|sample| { + log::trace!("Deserialize ParticipantEntitiesInfo: {:?}", sample); + match cdr::deserialize_from::<_, ParticipantEntitiesInfo, _>( + ZBuf::from(sample).reader(), + cdr::size::Infinite, + ) { + Ok(i) => Some(i), + Err(e) => { + log::warn!( + "Error receiving ParticipantEntitiesInfo on ros_discovery_info: {}", + e + ); + None + } + } + }) + .collect() + } + } + + fn write(writer: dds_entity_t, info: &ParticipantEntitiesInfo) -> Result<(), String> { + unsafe { + let buf = cdr::serialize::<_, _, CdrLe>(info, Infinite) + .map_err(|e| format!("Error serializing ParticipantEntitiesInfo: {e}"))?; + + let mut sertype: *const ddsi_sertype = std::ptr::null_mut(); + let ret = dds_get_entity_sertype(writer, &mut sertype); + if ret < 0 { + return Err(format!( + "Error creating payload for ParticipantEntitiesInfo: {}", + CStr::from_ptr(dds_strretcode(ret)) + .to_str() + .unwrap_or("unrecoverable DDS retcode") + )); + } + + // As per the Vec documentation (see https://doc.rust-lang.org/std/vec/struct.Vec.html#method.into_raw_parts) + // the only way to correctly releasing it is to create a vec using from_raw_parts + // and then have its destructor do the cleanup. + // Thus, while tempting to just pass the raw pointer to cyclone and then free it from C, + // that is not necessarily safe or guaranteed to be leak free. + // TODO replace when stable https://github.com/rust-lang/rust/issues/65816 + let (ptr, len, capacity) = crate::vec_into_raw_parts(buf); + let size: ddsrt_iov_len_t = len.try_into().map_err(|e| { + format!("Error creating payload for ParticipantEntitiesInfo, excessive payload size: {e}") + })?; + + let data_out = ddsrt_iovec_t { + iov_base: ptr as *mut std::ffi::c_void, + iov_len: size, + }; + + let fwdp = ddsi_serdata_from_ser_iov( + sertype, + ddsi_serdata_kind_SDK_DATA, + 1, + &data_out, + size as usize, + ); + dds_writecdr(writer, fwdp); + drop(Vec::from_raw_parts(ptr, len, capacity)); + Ok(()) + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct NodeEntitiesInfo { + pub node_namespace: String, + pub node_name: String, + #[serde( + serialize_with = "serialize_ros_gids", + deserialize_with = "deserialize_ros_gids" + )] + pub reader_gid_seq: HashSet, + #[serde( + serialize_with = "serialize_ros_gids", + deserialize_with = "deserialize_ros_gids" + )] + pub writer_gid_seq: HashSet, +} + +impl NodeEntitiesInfo { + pub fn new(node_namespace: String, node_name: String) -> NodeEntitiesInfo { + NodeEntitiesInfo { + node_namespace, + node_name, + reader_gid_seq: HashSet::new(), + writer_gid_seq: HashSet::new(), + } + } + + pub fn full_name(&self) -> String { + format!( + "{}/{}", + if &self.node_namespace == "/" { + "" + } else { + &self.node_namespace + }, + self.node_name, + ) + } +} + +impl std::fmt::Display for NodeEntitiesInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}/{}", + if &self.node_namespace == "/" { + "" + } else { + &self.node_namespace + }, + self.node_name, + )?; + Ok(()) + } +} + +impl std::fmt::Debug for NodeEntitiesInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "Node {}/{} :", + if &self.node_namespace == "/" { + "" + } else { + &self.node_namespace + }, + self.node_name, + )?; + writeln!(f, " {} pubs:", self.writer_gid_seq.len())?; + for i in &self.writer_gid_seq { + writeln!(f, " {}", i)?; + } + writeln!(f, " {} subs:", self.reader_gid_seq.len())?; + for i in &self.reader_gid_seq { + writeln!(f, " {}", i)?; + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct ParticipantEntitiesInfo { + #[serde( + serialize_with = "serialize_ros_gid", + deserialize_with = "deserialize_ros_gid" + )] + pub gid: Gid, + #[serde( + serialize_with = "serialize_node_entities_info_seq", + deserialize_with = "deserialize_node_entities_info_seq" + )] + pub node_entities_info_seq: HashMap, +} + +impl ParticipantEntitiesInfo { + pub fn new(gid: Gid) -> Self { + ParticipantEntitiesInfo { + gid, + node_entities_info_seq: HashMap::new(), + } + } +} + +impl std::fmt::Display for ParticipantEntitiesInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "participant {} with nodes: [", self.gid)?; + for (name, _) in self.node_entities_info_seq.iter().take(1) { + write!(f, "{}", name)?; + } + for (name, _) in self.node_entities_info_seq.iter().skip(1) { + write!(f, ", {}", name)?; + } + write!(f, "]")?; + Ok(()) + } +} + +impl std::fmt::Debug for ParticipantEntitiesInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "participant {} :", self.gid)?; + for i in self.node_entities_info_seq.values() { + write!(f, "{i:?}")?; + } + Ok(()) + } +} + +const BYTES_8: [u8; 8] = [0u8, 0, 0, 0, 0, 0, 0, 0]; + +fn serialize_ros_gid(gid: &Gid, serializer: S) -> Result +where + S: Serializer, +{ + if serializer.is_human_readable() { + gid.serialize(serializer) + } else { + // Data size for gid in ROS messages in 24 bytes, while a DDS gid is 16 bytes. + // Rely on "impl Serialize for Gid" for the 16 bytes, and add the last 8 bytes. + Serialize::serialize(&(gid, &BYTES_8), serializer) + } +} + +fn deserialize_ros_gid<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + if deserializer.is_human_readable() { + // Rely on impl<'de> Deserialize<'de> for Gid + Deserialize::deserialize(deserializer) + } else { + // Data size for gid in ROS messages in 24 bytes, while a DDS gid is 16 bytes. + // Rely on "impl<'de> Deserialize<'de> for Gid" for the 16 bytes, and ignore the last 8 bytes + let (result, _ignore): (Gid, [u8; 8]) = Deserialize::deserialize(deserializer)?; + Ok(result) + } +} + +fn serialize_ros_gids(gids: &HashSet, serializer: S) -> Result +where + S: Serializer, +{ + let is_human_readable = serializer.is_human_readable(); + let mut seq: ::SerializeSeq = serializer.serialize_seq(Some(gids.len()))?; + for gid in gids { + if is_human_readable { + seq.serialize_element(gid)?; + } else { + // Data size for gid in ROS messages in 24 bytes, while a DDS gid is 16 bytes. + // Rely on "impl Serialize for Gid" for the 16 bytes, and add the last 8 bytes. + seq.serialize_element(&(gid, &BYTES_8))?; + } + } + seq.end() +} + +fn deserialize_ros_gids<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + if deserializer.is_human_readable() { + Deserialize::deserialize(deserializer) + } else { + // Data size for gid in ROS messages in 24 bytes, while a DDS gid is 16 bytes. + // Deserialize as Vec<[u8; 24]>, consider 16 bytes only for each + let ros_gids: Vec<[u8; 24]> = Deserialize::deserialize(deserializer)?; + // NOTE: a DDS gid is 16 bytes only. ignore the last 8 bytes + Ok(ros_gids + .iter() + .map(|ros_gid| { + TryInto::<&[u8; 16]>::try_into(&ros_gid[..16]) + .unwrap() + .into() + }) + .collect()) + } +} + +fn serialize_node_entities_info_seq( + entities: &HashMap, + serializer: S, +) -> Result +where + S: Serializer, +{ + let mut seq = serializer.serialize_seq(Some(entities.len()))?; + for entity in entities.values() { + seq.serialize_element(entity)?; + } + seq.end() +} + +fn deserialize_node_entities_info_seq<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let mut entities: Vec = Deserialize::deserialize(deserializer)?; + let mut map: HashMap = HashMap::with_capacity(entities.len()); + for entity in entities.drain(..) { + map.insert(entity.full_name(), entity); + } + Ok(map) +} + +mod tests { + + #[test] + fn test_serde() { + use super::*; + use std::str::FromStr; + + // ros_discovery_message sent by a component_container node started as such: + // - ros2 run rclcpp_components component_container --ros-args --remap __ns:=/TEST + // - ros2 component load /TEST/ComponentManager composition composition::Listener + // - ros2 component load /TEST/ComponentManager composition composition::Talker + let ros_discovery_info_cdr: Vec = hex::decode( + "000100000110de17b1eaf995400c9ac8000001c1000000000000000003000000\ + 060000002f5445535400000011000000436f6d706f6e656e744d616e61676572\ + 00000000040000000110de17b1eaf995400c9ac8000007040000000000000000\ + 0110de17b1eaf995400c9ac80000090400000000000000000110de17b1eaf995\ + 400c9ac800000b0400000000000000000110de17b1eaf995400c9ac800000d04\ + 0000000000000000040000000110de17b1eaf995400c9ac80000060300000000\ + 000000000110de17b1eaf995400c9ac80000080300000000000000000110de17\ + b1eaf995400c9ac800000a0300000000000000000110de17b1eaf995400c9ac8\ + 00000c030000000000000000020000002f000000090000006c697374656e6572\ + 00000000080000000110de17b1eaf995400c9ac8000010040000000000000000\ + 0110de17b1eaf995400c9ac80000120400000000000000000110de17b1eaf995\ + 400c9ac80000140400000000000000000110de17b1eaf995400c9ac800001604\ + 00000000000000000110de17b1eaf995400c9ac8000018040000000000000000\ + 0110de17b1eaf995400c9ac800001a0400000000000000000110de17b1eaf995\ + 400c9ac800001c0400000000000000000110de17b1eaf995400c9ac800001d04\ + 0000000000000000080000000110de17b1eaf995400c9ac800000e0300000000\ + 000000000110de17b1eaf995400c9ac800000f0300000000000000000110de17\ + b1eaf995400c9ac80000110300000000000000000110de17b1eaf995400c9ac8\ + 0000130300000000000000000110de17b1eaf995400c9ac80000150300000000\ + 000000000110de17b1eaf995400c9ac80000170300000000000000000110de17\ + b1eaf995400c9ac80000190300000000000000000110de17b1eaf995400c9ac8\ + 00001b030000000000000000020000002f0000000700000074616c6b65720000\ + 070000000110de17b1eaf995400c9ac80000200400000000000000000110de17\ + b1eaf995400c9ac80000220400000000000000000110de17b1eaf995400c9ac8\ + 0000240400000000000000000110de17b1eaf995400c9ac80000260400000000\ + 000000000110de17b1eaf995400c9ac80000280400000000000000000110de17\ + b1eaf995400c9ac800002a0400000000000000000110de17b1eaf995400c9ac8\ + 00002c040000000000000000090000000110de17b1eaf995400c9ac800001e03\ + 00000000000000000110de17b1eaf995400c9ac800001f030000000000000000\ + 0110de17b1eaf995400c9ac80000210300000000000000000110de17b1eaf995\ + 400c9ac80000230300000000000000000110de17b1eaf995400c9ac800002503\ + 00000000000000000110de17b1eaf995400c9ac8000027030000000000000000\ + 0110de17b1eaf995400c9ac80000290300000000000000000110de17b1eaf995\ + 400c9ac800002b0300000000000000000110de17b1eaf995400c9ac800002d03\ + 0000000000000000", + ) + .unwrap(); + + let part_info: ParticipantEntitiesInfo = cdr::deserialize(&ros_discovery_info_cdr).unwrap(); + println!("{:?}", part_info); + + assert_eq!( + part_info.gid, + Gid::from_str("0110de17b1eaf995400c9ac8000001c1").unwrap() + ); + assert_eq!(part_info.node_entities_info_seq.len(), 3); + + let node_componentmgr = part_info + .node_entities_info_seq + .get("/TEST/ComponentManager") + .unwrap(); + assert_eq!(node_componentmgr.node_namespace, "/TEST"); + assert_eq!(node_componentmgr.node_name, "ComponentManager"); + assert_eq!(node_componentmgr.reader_gid_seq.len(), 4); + assert_eq!(node_componentmgr.writer_gid_seq.len(), 4); + + let node_listener = part_info.node_entities_info_seq.get("/listener").unwrap(); + assert_eq!(node_listener.node_namespace, "/"); + assert_eq!(node_listener.node_name, "listener"); + assert_eq!(node_listener.reader_gid_seq.len(), 8); + assert_eq!(node_listener.writer_gid_seq.len(), 8); + + let node_talker = part_info.node_entities_info_seq.get("/talker").unwrap(); + assert_eq!(node_talker.node_namespace, "/"); + assert_eq!(node_talker.node_name, "talker"); + assert_eq!(node_talker.reader_gid_seq.len(), 7); + assert_eq!(node_talker.writer_gid_seq.len(), 9); + } +} diff --git a/zenoh-plugin-ros2dds/src/route_publisher.rs b/zenoh-plugin-ros2dds/src/route_publisher.rs new file mode 100644 index 0000000..4851b85 --- /dev/null +++ b/zenoh-plugin-ros2dds/src/route_publisher.rs @@ -0,0 +1,344 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use cyclors::qos::{HistoryKind, Qos}; +use cyclors::{dds_entity_t, DDS_LENGTH_UNLIMITED}; +use serde::Serialize; +use std::sync::Arc; +use std::time::Duration; +use std::{collections::HashSet, fmt}; +use zenoh::liveliness::LivelinessToken; +use zenoh::prelude::r#async::AsyncResolve; +use zenoh::prelude::*; +use zenoh_ext::{PublicationCache, SessionExt}; + +use crate::gid::Gid; +use crate::liveliness_mgt::new_ke_liveliness_pub; +use crate::ros2_utils::ros2_message_type_to_dds_type; +use crate::{dds_discovery::*, qos_helpers::*, Config}; +use crate::{serialize_option_as_bool, KE_PREFIX_PUB_CACHE}; + +enum ZPublisher<'a> { + Publisher(KeyExpr<'a>), + PublicationCache(PublicationCache<'a>), +} + +// a route from DDS to Zenoh +#[allow(clippy::upper_case_acronyms)] +#[derive(Serialize)] +pub struct RoutePublisher<'a> { + // the ROS2 Publisher name + ros2_name: String, + // the ROS2 type + ros2_type: String, + // the Zenoh key expression used for routing + zenoh_key_expr: OwnedKeyExpr, + // the zenoh session + #[serde(skip)] + zsession: &'a Arc, + // the config + #[serde(skip)] + config: Arc, + // the zenoh publisher used to re-publish to zenoh the data received by the DDS Reader + // `None` when route is created on a remote announcement and no local ROS2 Subscriber discovered yet + #[serde(rename = "is_active", serialize_with = "serialize_option_as_bool")] + zenoh_publisher: Option>, + // the local DDS Reader created to serve the route (i.e. re-publish to zenoh data coming from DDS) + #[serde(serialize_with = "serialize_entity_guid")] + dds_reader: dds_entity_t, + // if the Reader is TRANSIENT_LOCAL + transient_local: bool, + // if the topic is keyless + #[serde(skip)] + keyless: bool, + // a liveliness token associated to this route, for announcement to other plugins + #[serde(skip)] + liveliness_token: Option>, + // the list of remote routes served by this route (":"") + remote_routes: HashSet, + // the list of nodes served by this route + local_nodes: HashSet, +} + +impl Drop for RoutePublisher<'_> { + fn drop(&mut self) { + if let Err(e) = delete_dds_entity(self.dds_reader) { + log::warn!("{}: error deleting DDS Reader: {}", self, e); + } + } +} + +impl fmt::Display for RoutePublisher<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Route Publisher (ROS:{} -> Zenoh:{})", + self.ros2_name, self.zenoh_key_expr + ) + } +} + +impl RoutePublisher<'_> { + #[allow(clippy::too_many_arguments)] + pub async fn create<'a>( + config: Arc, + zsession: &'a Arc, + participant: dds_entity_t, + ros2_name: String, + ros2_type: String, + zenoh_key_expr: OwnedKeyExpr, + type_info: &Option>, + keyless: bool, + reader_qos: Qos, + ) -> Result, String> { + let transient_local = is_transient_local(&reader_qos); + log::debug!( + "Route Publisher ({ros2_name} -> {zenoh_key_expr}): creation with type {ros2_type}" + ); + + // declare the zenoh key expression (for wire optimization) + let declared_ke = zsession + .declare_keyexpr(zenoh_key_expr.clone()) + .res() + .await + .map_err(|e| { + format!("Route Publisher ({ros2_name} -> {zenoh_key_expr}): failed to declare KeyExpr: {e}") + })?; + + // CongestionControl to be used when re-publishing over zenoh: Blocking if Writer is RELIABLE (since we don't know what is remote Reader's QoS) + let congestion_ctrl = match ( + config.reliable_routes_blocking, + is_reader_reliable(&reader_qos.reliability), + ) { + (true, true) => CongestionControl::Block, + _ => CongestionControl::Drop, + }; + + let topic_name = format!("rt{ros2_name}"); + let type_name = ros2_message_type_to_dds_type(&ros2_type); + let read_period = get_read_period(&config, &zenoh_key_expr); + + // create matching DDS Reader that forwards data coming from DDS to Zenoh + let dds_reader = create_forwarding_dds_reader( + participant, + topic_name, + type_name, + type_info, + keyless, + reader_qos.clone(), + declared_ke.clone(), + zsession.clone(), + read_period, + congestion_ctrl, + )?; + + Ok(RoutePublisher { + ros2_name, + ros2_type, + dds_reader, + zenoh_key_expr, + zsession, + config, + zenoh_publisher: None, + transient_local, + keyless, + liveliness_token: None, + remote_routes: HashSet::new(), + local_nodes: HashSet::new(), + }) + } + + async fn activate<'a>( + &'a mut self, + plugin_id: &keyexpr, + discovered_writer_qos: &Qos, + ) -> Result<(), String> { + // For lifetime issue, redeclare the zenoh key expression that can't be stored in Self + let declared_ke = self + .zsession + .declare_keyexpr(self.zenoh_key_expr.clone()) + .res() + .await + .map_err(|e| { + format!( + "Route Publisher (ROS:{} -> Zenoh:{}): failed to declare KeyExpr: {e}", + self.ros2_name, self.zenoh_key_expr + ) + })?; + + // create the zenoh Publisher + // if Reader is TRANSIENT_LOCAL, use a PublicationCache to store historical data + self.zenoh_publisher = if self.transient_local { + #[allow(non_upper_case_globals)] + let history_qos = get_history_or_default(discovered_writer_qos); + let durability_service_qos = get_durability_service_or_default(discovered_writer_qos); + let mut history = match (history_qos.kind, history_qos.depth) { + (HistoryKind::KEEP_LAST, n) => { + if self.keyless { + // only 1 instance => history=n + n as usize + } else if durability_service_qos.max_instances == DDS_LENGTH_UNLIMITED { + // No limit! => history=MAX + usize::MAX + } else if durability_service_qos.max_instances > 0 { + // Compute cache size as history.depth * durability_service.max_instances + // This makes the assumption that the frequency of publication is the same for all instances... + // But as we have no way to have 1 cache per-instance, there is no other choice. + n.saturating_mul(durability_service_qos.max_instances) as usize + } else { + n as usize + } + } + (HistoryKind::KEEP_ALL, _) => usize::MAX, + }; + // In case there are several Writers served by this route, increase the cache size + history = history.saturating_mul(self.config.transient_local_cache_multiplier); + log::debug!( + "{self}: caching TRANSIENT_LOCAL publications via a PublicationCache with history={history} (computed from Reader's QoS: history=({:?},{}), durability_service.max_instances={})", + history_qos.kind, history_qos.depth, durability_service_qos.max_instances + ); + let pub_cache = self + .zsession + .declare_publication_cache(&declared_ke) + .history(history) + .queryable_prefix(*KE_PREFIX_PUB_CACHE / plugin_id) + .queryable_allowed_origin(Locality::Remote) // Note: don't reply to queries from local QueryingSubscribers + .res() + .await + .map_err(|e| { + format!( + "Failed create PublicationCache for key {} (rid={}): {e}", + self.zenoh_key_expr, declared_ke + ) + })?; + Some(ZPublisher::PublicationCache(pub_cache)) + } else { + if let Err(e) = self + .zsession + .declare_publisher(declared_ke.clone()) + .res() + .await + { + log::warn!( + "Failed to declare publisher for key {} (rid={}): {}", + self.zenoh_key_expr, + declared_ke, + e + ); + } + Some(ZPublisher::Publisher(declared_ke.clone())) + }; + + // create associated LivelinessToken + let liveliness_ke = new_ke_liveliness_pub( + plugin_id, + &self.zenoh_key_expr, + &self.ros2_type, + self.keyless, + &discovered_writer_qos, + )?; + let ros2_name = self.ros2_name.clone(); + self.liveliness_token = Some(self.zsession + .liveliness() + .declare_token(liveliness_ke) + .res() + .await + .map_err(|e| { + format!( + "Failed create LivelinessToken associated to route for Publisher {ros2_name}: {e}" + ) + })? + ); + Ok(()) + } + + fn deactivate(&mut self) { + log::debug!("{self} deactivate"); + // Drop Zenoh Publisher and Liveliness token + // The DDS Writer remains to be discovered by local ROS nodes + self.zenoh_publisher = None; + self.liveliness_token = None; + } + + #[inline] + pub fn dds_reader_guid(&self) -> Result { + get_guid(&self.dds_reader) + } + + #[inline] + pub fn add_remote_route(&mut self, plugin_id: &str, zenoh_key_expr: &keyexpr) { + self.remote_routes + .insert(format!("{plugin_id}:{zenoh_key_expr}")); + log::debug!("{self} now serving remote routes {:?}", self.remote_routes); + } + + #[inline] + pub fn remove_remote_route(&mut self, plugin_id: &str, zenoh_key_expr: &keyexpr) { + self.remote_routes + .remove(&format!("{plugin_id}:{zenoh_key_expr}")); + log::debug!("{self} now serving remote routes {:?}", self.remote_routes); + } + + #[inline] + pub fn is_serving_remote_route(&self) -> bool { + !self.remote_routes.is_empty() + } + + #[inline] + pub async fn add_local_node( + &mut self, + node: String, + plugin_id: &keyexpr, + discovered_writer_qos: &Qos, + ) { + self.local_nodes.insert(node); + log::debug!("{self} now serving local nodes {:?}", self.local_nodes); + // if 1st local node added, activate the route + if self.local_nodes.len() == 1 { + if let Err(e) = self.activate(plugin_id, discovered_writer_qos).await { + log::error!("{self} activation failed: {e}"); + } + } + } + + #[inline] + pub fn remove_local_node(&mut self, node: &str) { + self.local_nodes.remove(node); + log::debug!("{self} now serving local nodes {:?}", self.local_nodes); + // if last local node removed, deactivate the route + if self.local_nodes.is_empty() { + self.deactivate(); + } + } + + #[inline] + pub fn is_serving_local_node(&self) -> bool { + !self.local_nodes.is_empty() + } + + #[inline] + pub fn is_unused(&self) -> bool { + !self.is_serving_local_node() && !self.is_serving_remote_route() + } +} + +// Return the read period if keyexpr matches one of the "pub_max_frequencies" option +fn get_read_period(config: &Config, ke: &keyexpr) -> Option { + for (re, freq) in &config.pub_max_frequencies { + if re.is_match(ke) { + return Some(Duration::from_secs_f32(1f32 / freq)); + } + } + None +} diff --git a/zenoh-plugin-ros2dds/src/route_subscriber.rs b/zenoh-plugin-ros2dds/src/route_subscriber.rs new file mode 100644 index 0000000..a6afc6d --- /dev/null +++ b/zenoh-plugin-ros2dds/src/route_subscriber.rs @@ -0,0 +1,469 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use cyclors::{ + dds_entity_t, dds_get_entity_sertype, dds_strretcode, dds_writecdr, ddsi_serdata_from_ser_iov, + ddsi_serdata_kind_SDK_DATA, ddsi_sertype, ddsrt_iov_len_t, ddsrt_iovec_t, +}; +use serde::Serialize; +use std::collections::HashSet; +use std::convert::TryInto; +use std::sync::Arc; +use std::{ffi::CStr, fmt, time::Duration}; +use zenoh::liveliness::LivelinessToken; +use zenoh::prelude::*; +use zenoh::query::ReplyKeyExpr; +use zenoh::{prelude::r#async::AsyncResolve, subscriber::Subscriber}; +use zenoh_ext::{FetchingSubscriber, SubscriberBuilderExt}; + +use crate::gid::Gid; +use crate::liveliness_mgt::new_ke_liveliness_sub; +use crate::qos_helpers::is_transient_local; +use crate::ros2_utils::ros2_message_type_to_dds_type; +use crate::{ + dds_discovery::*, qos::Qos, vec_into_raw_parts, Config, KE_ANY_1_SEGMENT, LOG_PAYLOAD, +}; +use crate::{serialize_option_as_bool, KE_PREFIX_PUB_CACHE}; + +enum ZSubscriber<'a> { + Subscriber(Subscriber<'a, ()>), + FetchingSubscriber(FetchingSubscriber<'a, ()>), +} + +impl ZSubscriber<'_> { + fn key_expr(&self) -> &KeyExpr<'static> { + match self { + ZSubscriber::Subscriber(s) => s.key_expr(), + ZSubscriber::FetchingSubscriber(s) => s.key_expr(), + } + } +} + +// a route from Zenoh to DDS +#[allow(clippy::upper_case_acronyms)] +#[derive(Serialize)] +pub struct RouteSubscriber<'a> { + // the ROS2 Subscriber name + ros2_name: String, + // the ROS2 type + ros2_type: String, + // the Zenoh key expression used for routing + zenoh_key_expr: OwnedKeyExpr, + // the zenoh session + #[serde(skip)] + zsession: &'a Arc, + // the config + #[serde(skip)] + config: Arc, + // the zenoh subscriber receiving data to be re-published by the DDS Writer + // `None` when route is created on a remote announcement and no local ROS2 Subscriber discovered yet + #[serde(rename = "is_active", serialize_with = "serialize_option_as_bool")] + zenoh_subscriber: Option>, + // the local DDS Writer created to serve the route (i.e. re-publish to DDS data coming from zenoh) + #[serde(serialize_with = "serialize_entity_guid")] + dds_writer: dds_entity_t, + // if the Writer is TRANSIENT_LOCAL + transient_local: bool, + // if the topic is keyless + #[serde(skip)] + keyless: bool, + // a liveliness token associated to this route, for announcement to other plugins + #[serde(skip)] + liveliness_token: Option>, + // the list of remote routes served by this route (":"") + remote_routes: HashSet, + // the list of nodes served by this route + local_nodes: HashSet, +} + +impl Drop for RouteSubscriber<'_> { + fn drop(&mut self) { + if let Err(e) = delete_dds_entity(self.dds_writer) { + log::warn!("{}: error deleting DDS Reader: {}", self, e); + } + } +} + +impl fmt::Display for RouteSubscriber<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Route Subscriber (Zenoh:{} -> ROS:{})", + self.zenoh_key_expr, self.ros2_name + ) + } +} + +impl RouteSubscriber<'_> { + #[allow(clippy::too_many_arguments)] + pub async fn create<'a, 'b>( + config: Arc, + zsession: &'a Arc, + participant: dds_entity_t, + ros2_name: String, + ros2_type: String, + zenoh_key_expr: OwnedKeyExpr, + keyless: bool, + writer_qos: Qos, + ) -> Result, String> { + let transient_local = is_transient_local(&writer_qos); + log::debug!("Route Subscriber ({zenoh_key_expr} -> {ros2_name}): creation with type {ros2_type} (transient_local:{transient_local})"); + + let topic_name = format!("rt{ros2_name}"); + let type_name = ros2_message_type_to_dds_type(&ros2_type); + + let dds_writer = + create_forwarding_dds_writer(participant, topic_name, type_name, keyless, writer_qos)?; + + Ok(RouteSubscriber { + ros2_name, + ros2_type, + zenoh_key_expr, + zsession, + config, + zenoh_subscriber: None, + dds_writer, + transient_local, + keyless, + liveliness_token: None, + remote_routes: HashSet::new(), + local_nodes: HashSet::new(), + }) + } + + async fn activate( + &mut self, + config: &Config, + plugin_id: &keyexpr, + discovered_reader_qos: &Qos, + ) -> Result<(), String> { + log::debug!("{self} activate"); + // Callback routing data received by Zenoh subscriber to DDS Writer (if set) + let ros2_name = self.ros2_name.clone(); + let dds_writer = self.dds_writer; + let subscriber_callback = move |s: Sample| { + do_route_data(s, &ros2_name, dds_writer); + }; + + // create zenoh subscriber + // if Writer is TRANSIENT_LOCAL, use a QueryingSubscriber to fetch remote historical data to write + self.zenoh_subscriber = if self.transient_local { + // query all PublicationCaches on "/*/" + let query_selector: Selector = + (*KE_PREFIX_PUB_CACHE / *KE_ANY_1_SEGMENT / &self.zenoh_key_expr).into(); + log::error!("{self}: query historical data from everybody for TRANSIENT_LOCAL Reader on {query_selector}"); + { + use zenoh_core::SyncResolve; + // + println!("********* QUERY FROM {query_selector}"); + let rep = self + .zsession + .get(&query_selector) + .target(QueryTarget::All) + .consolidation(ConsolidationMode::None) + .accept_replies(ReplyKeyExpr::Any) + .res_sync() + .unwrap(); + while let Ok(reply) = rep.recv() { + match reply.sample { + Ok(sample) => println!( + ">>>>>> Received ('{}': '{:02x?}')", + sample.key_expr.as_str(), + sample.value.payload.contiguous(), + ), + Err(err) => { + println!(">> Received (ERROR: '{}')", String::try_from(&err).unwrap()) + } + } + } + // + } + + let sub = self + .zsession + .declare_subscriber(&self.zenoh_key_expr) + .callback(subscriber_callback) + .allowed_origin(Locality::Remote) // Allow only remote publications to avoid loops + .reliable() + .querying() + .query_timeout(config.queries_timeout) + .query_selector(query_selector) + .query_accept_replies(ReplyKeyExpr::Any) + .res() + .await + .map_err(|e| format!("{self}: failed to create FetchingSubscriber: {e}",))?; + Some(ZSubscriber::FetchingSubscriber(sub)) + } else { + let sub = self + .zsession + .declare_subscriber(&self.zenoh_key_expr) + .callback(subscriber_callback) + .allowed_origin(Locality::Remote) // Allow only remote publications to avoid loops + .reliable() + .res() + .await + .map_err(|e| format!("{self}: failed to create Subscriber: {e}"))?; + Some(ZSubscriber::Subscriber(sub)) + }; + + // create associated LivelinessToken + let liveliness_ke = new_ke_liveliness_sub( + plugin_id, + &self.zenoh_key_expr, + &self.ros2_type, + self.keyless, + &discovered_reader_qos, + )?; + let ros2_name = self.ros2_name.clone(); + self.liveliness_token = Some( + self.zsession + .liveliness() + .declare_token(liveliness_ke) + .res() + .await + .map_err(|e| { + format!( + "Failed create LivelinessToken associated to route for Subscriber {ros2_name} : {e}" + ) + })?, + ); + Ok(()) + } + + fn deactivate(&mut self) { + log::debug!("{self} deactivate"); + // Drop Zenoh Subscriber and Liveliness token + // The DDS Writer remains to be discovered by local ROS nodes + self.zenoh_subscriber = None; + self.liveliness_token = None; + } + + /// If this route uses a FetchingSubscriber, query for historical publications + /// using the specified Selector. Otherwise, do nothing. + pub async fn query_historical_publications<'a>( + &mut self, + plugin_id: &keyexpr, + query_timeout: Duration, + ) { + if let Some(ZSubscriber::FetchingSubscriber(sub)) = &mut self.zenoh_subscriber { + // query all PublicationCaches on "//" + let query_selector: Selector = + (*KE_PREFIX_PUB_CACHE / plugin_id / &self.zenoh_key_expr).into(); + log::error!("Route Subscriber (Zenoh:{} -> ROS:{}): query historical data from {plugin_id} for TRANSIENT_LOCAL Reader on {query_selector}", + self.zenoh_key_expr, self.ros2_name + ); + + if let Err(e) = sub + .fetch({ + let session = &self.zsession; + let query_selector = query_selector.clone(); + { + use zenoh_core::SyncResolve; + // + println!("********* FETCH FROM {query_selector}"); + let rep = session + .get(&query_selector) + .target(QueryTarget::All) + .consolidation(ConsolidationMode::None) + .accept_replies(ReplyKeyExpr::Any) + .res_sync() + .unwrap(); + while let Ok(reply) = rep.recv() { + match reply.sample { + Ok(sample) => println!( + ">>>>>> Received ('{}': '{:02x?}')", + sample.key_expr.as_str(), + sample.value.payload.contiguous(), + ), + Err(err) => println!( + ">> Received (ERROR: '{}')", + String::try_from(&err).unwrap() + ), + } + } + // + } + + move |cb| { + use zenoh_core::SyncResolve; + session + .get(&query_selector) + .target(QueryTarget::All) + .consolidation(ConsolidationMode::None) + .accept_replies(ReplyKeyExpr::Any) + .timeout(query_timeout) + .callback(cb) + .res_sync() + } + }) + .res() + .await + { + log::warn!( + "{}: query for historical publications on {} failed: {}", + self, + query_selector, + e + ); + } + } + } + + #[inline] + pub fn dds_writer_guid(&self) -> Result { + get_guid(&self.dds_writer) + } + + #[inline] + pub fn add_remote_route(&mut self, plugin_id: &str, zenoh_key_expr: &keyexpr) { + self.remote_routes + .insert(format!("{plugin_id}:{zenoh_key_expr}")); + log::debug!("{self} now serving remote routes {:?}", self.remote_routes); + } + + #[inline] + pub fn remove_remote_route(&mut self, plugin_id: &str, zenoh_key_expr: &keyexpr) { + self.remote_routes + .remove(&format!("{plugin_id}:{zenoh_key_expr}")); + log::debug!("{self} now serving remote routes {:?}", self.remote_routes); + } + + #[inline] + pub fn is_serving_remote_route(&self) -> bool { + !self.remote_routes.is_empty() + } + + #[inline] + pub async fn add_local_node( + &mut self, + entity_key: String, + config: &Config, + plugin_id: &keyexpr, + discovered_reader_qos: &Qos, + ) { + self.local_nodes.insert(entity_key); + log::debug!("{self} now serving local nodes {:?}", self.local_nodes); + // if 1st local node added, activate the route + if self.local_nodes.len() == 1 { + if let Err(e) = self + .activate(config, plugin_id, discovered_reader_qos) + .await + { + log::error!("{self} activation failed: {e}"); + } + } + } + + #[inline] + pub fn remove_local_node(&mut self, entity_key: &str) { + self.local_nodes.remove(entity_key); + log::debug!("{self} now serving local nodes {:?}", self.local_nodes); + // if last local node removed, deactivate the route + if self.local_nodes.is_empty() { + self.deactivate(); + } + } + + #[inline] + pub fn is_serving_local_node(&self) -> bool { + !self.local_nodes.is_empty() + } + + #[inline] + pub fn is_unused(&self) -> bool { + !self.is_serving_local_node() && !self.is_serving_remote_route() + } +} + +fn do_route_data(s: Sample, ros2_name: &str, data_writer: dds_entity_t) { + if *LOG_PAYLOAD { + log::trace!( + "Route Subscriber (Zenoh:{} -> ROS:{}): routing data - payload: {:?}", + s.key_expr, + &ros2_name, + s.value.payload + ); + } else { + log::trace!( + "Route Subscriber (Zenoh:{} -> ROS:{}): routing data", + s.key_expr, + &ros2_name + ); + } + + unsafe { + let bs = s.value.payload.contiguous().into_owned(); + // As per the Vec documentation (see https://doc.rust-lang.org/std/vec/struct.Vec.html#method.into_raw_parts) + // the only way to correctly releasing it is to create a vec using from_raw_parts + // and then have its destructor do the cleanup. + // Thus, while tempting to just pass the raw pointer to cyclone and then free it from C, + // that is not necessarily safe or guaranteed to be leak free. + // TODO replace when stable https://github.com/rust-lang/rust/issues/65816 + let (ptr, len, capacity) = vec_into_raw_parts(bs); + let size: ddsrt_iov_len_t = match len.try_into() { + Ok(s) => s, + Err(_) => { + log::warn!( + "Route Subscriber (Zenoh:{} -> ROS:{}): can't route data; excessive payload size ({})", + s.key_expr, + ros2_name, + len + ); + return; + } + }; + + let data_out = ddsrt_iovec_t { + iov_base: ptr as *mut std::ffi::c_void, + iov_len: size, + }; + + let mut sertype_ptr: *const ddsi_sertype = std::ptr::null_mut(); + let ret = dds_get_entity_sertype(data_writer, &mut sertype_ptr); + if ret < 0 { + log::warn!( + "Route Subscriber (Zenoh:{} -> ROS:{}): can't route data; sertype lookup failed ({})", + s.key_expr, + ros2_name, + CStr::from_ptr(dds_strretcode(ret)) + .to_str() + .unwrap_or("unrecoverable DDS retcode") + ); + return; + } + + let fwdp = ddsi_serdata_from_ser_iov( + sertype_ptr, + ddsi_serdata_kind_SDK_DATA, + 1, + &data_out, + size as usize, + ); + + let ret = dds_writecdr(data_writer, fwdp); + if ret < 0 { + log::warn!( + "Route Subscriber (Zenoh:{} -> ROS:{}): DDS write({data_writer}) failed: {}", + s.key_expr, + ros2_name, + CStr::from_ptr(dds_strretcode(ret)) + .to_str() + .unwrap_or("unrecoverable DDS retcode") + ); + return; + } + + drop(Vec::from_raw_parts(ptr, len, capacity)); + } +} diff --git a/zenoh-plugin-ros2dds/src/routes_mgr.rs b/zenoh-plugin-ros2dds/src/routes_mgr.rs new file mode 100644 index 0000000..0f0e5cd --- /dev/null +++ b/zenoh-plugin-ros2dds/src/routes_mgr.rs @@ -0,0 +1,485 @@ +// +// Copyright (c) 2022 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use crate::config::Config; +use crate::discovered_entities::DiscoveredEntities; +use crate::events::ROS2AnnouncementEvent; +use crate::events::ROS2DiscoveryEvent; +use crate::qos_helpers::adapt_reader_qos_for_writer; +use crate::qos_helpers::adapt_writer_qos_for_reader; +use crate::ros_discovery::RosDiscoveryInfoMgr; +use crate::route_publisher::RoutePublisher; +use crate::route_subscriber::RouteSubscriber; +use cyclors::dds_entity_t; +use cyclors::qos::Qos; +use serde::{Deserialize, Serialize}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::RwLock; +use zenoh::prelude::keyexpr; +use zenoh::prelude::r#async::AsyncResolve; +use zenoh::prelude::OwnedKeyExpr; +use zenoh::queryable::Query; +use zenoh::sample::Sample; +use zenoh::Session; +use zenoh_core::zread; + +use crate::ke_for_sure; + +lazy_static::lazy_static!( + static ref KE_PREFIX_ROUTE_PUBLISHER: &'static keyexpr = ke_for_sure!("route/topic/pub"); + static ref KE_PREFIX_ROUTE_SUBSCRIBER: &'static keyexpr = ke_for_sure!("route/topic/sub"); + static ref KE_PREFIX_ROUTE_SERVICE_SRV: &'static keyexpr = ke_for_sure!("route/service/srv"); + static ref KE_PREFIX_ROUTE_SERVICE_CLI: &'static keyexpr = ke_for_sure!("route/service/cli"); +); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum RouteStatus { + Routed(OwnedKeyExpr), // Routing is active, with the zenoh key expression used for the route + NotAllowed, // Routing was not allowed per configuration + CreationFailure(String), // The route creation failed + _QoSConflict, // A route was already established but with conflicting QoS +} + +#[derive(Debug)] +enum RouteRef { + PublisherRoute(String), + SubscriberRoute(String), +} + +pub struct RoutesMgr<'a> { + plugin_id: OwnedKeyExpr, + config: Arc, + zsession: &'a Arc, + participant: dds_entity_t, + discovered_entities: Arc>, + // maps of established routes - ecah map indexed by topic/service/action name + routes_publishers: HashMap>, + routes_subscribers: HashMap>, + // ros_discovery_info read/write manager + ros_discovery_mgr: Arc, + admin_prefix: OwnedKeyExpr, + // admin space: index is the admin_keyexpr (relative to admin_prefix) + admin_space: HashMap, +} + +impl<'a> RoutesMgr<'a> { + pub fn new( + plugin_id: OwnedKeyExpr, + config: Arc, + zsession: &'a Arc, + participant: dds_entity_t, + discovered_entities: Arc>, + ros_discovery_mgr: Arc, + admin_prefix: OwnedKeyExpr, + ) -> RoutesMgr<'a> { + RoutesMgr { + plugin_id, + config, + zsession, + participant, + discovered_entities, + routes_publishers: HashMap::new(), + routes_subscribers: HashMap::new(), + ros_discovery_mgr, + admin_prefix, + admin_space: HashMap::new(), + } + } + + pub async fn on_ros_discovery_event( + &mut self, + event: ROS2DiscoveryEvent, + ) -> Result<(), String> { + use ROS2DiscoveryEvent::*; + match event { + DiscoveredMsgPub(node, iface) => { + let plugin_id = self.plugin_id.clone(); + // Retrieve info on DDS Writer + let entity = { + let entities = zread!(self.discovered_entities); + entities + .get_writer(&iface.writer) + .ok_or(format!( + "Failed to get DDS info for {iface} Writer {}. Already deleted ?", + iface.writer + ))? + .clone() + }; + // Get route (create it if not yet exists) + let route = self + .get_or_create_route_publisher( + iface.name, + iface.typ, + entity.keyless, + adapt_writer_qos_for_reader(&entity.qos), + ) + .await?; + route + .add_local_node(node.into(), &plugin_id, &entity.qos) + .await; + } + + UndiscoveredMsgPub(node, iface) => { + if let Entry::Occupied(mut entry) = self.routes_publishers.entry(iface.name.clone()) + { + let route = entry.get_mut(); + route.remove_local_node(&node); + if route.is_unused() { + self.admin_space + .remove(&(*KE_PREFIX_ROUTE_PUBLISHER / iface.name_as_keyexpr())); + let route = entry.remove(); + // remove reader's GID in ros_discovery_msg + self.ros_discovery_mgr + .remove_dds_reader(route.dds_reader_guid().map_err(|e| { + format!("Failed to update ros_discovery_info message: {e}") + })?); + log::info!("{route} removed"); + } + } + } + + DiscoveredMsgSub(node, iface) => { + // Retrieve info on DDS Reader + let entity = { + let entities = zread!(self.discovered_entities); + entities + .get_reader(&iface.reader) + .ok_or(format!( + "Failed to get DDS info for {iface} Reader {}. Already deleted ?", + iface.reader + ))? + .clone() + }; + let plugin_id = self.plugin_id.clone(); + let config = self.config.clone(); + // Get route (create it if not yet exists) + let route = self + .get_or_create_route_subscriber( + iface.name, + iface.typ, + entity.keyless, + adapt_reader_qos_for_writer(&entity.qos), + ) + .await?; + route + .add_local_node(node.into(), &config, &plugin_id, &entity.qos) + .await; + } + + UndiscoveredMsgSub(node, iface) => { + if let Entry::Occupied(mut entry) = + self.routes_subscribers.entry(iface.name.clone()) + { + let route = entry.get_mut(); + route.remove_local_node(&node); + if route.is_unused() { + self.admin_space + .remove(&(*KE_PREFIX_ROUTE_SUBSCRIBER / iface.name_as_keyexpr())); + let route = entry.remove(); + // remove writer's GID in ros_discovery_msg + self.ros_discovery_mgr + .remove_dds_writer(route.dds_writer_guid().map_err(|e| { + format!("Failed to update ros_discovery_info message: {e}") + })?); + log::info!("{route} removed"); + } + } + } + DiscoveredServiceSrv(_node, iface) => { + log::info!("... TODO: create Service Server route for {}", iface.name); + } + UndiscoveredServiceSrv(_node, iface) => { + log::info!("... TODO: delete Service Server route for {}", iface.name); + } + DiscoveredServiceCli(_node, iface) => { + log::info!("... TODO: create Service Client route for {}", iface.name); + } + UndiscoveredServiceCli(_node, iface) => { + log::info!("... TODO: delete Service Client route for {}", iface.name); + } + DiscoveredActionSrv(_node, iface) => { + log::info!("... TODO: create Action Server route for {}", iface.name); + } + UndiscoveredActionSrv(_node, iface) => { + log::info!("... TODO: delete Action Server route for {}", iface.name); + } + DiscoveredActionCli(_node, iface) => { + log::info!("... TODO: create Action Client route for {}", iface.name); + } + UndiscoveredActionCli(_node, iface) => { + log::info!("... TODO: delete Action Client route for {}", iface.name); + } + } + Ok(()) + } + + pub async fn on_ros_announcement_event( + &mut self, + event: ROS2AnnouncementEvent, + ) -> Result<(), String> { + use ROS2AnnouncementEvent::*; + match event { + AnnouncedMsgPub { + plugin_id, + zenoh_key_expr, + ros2_type, + keyless, + writer_qos, + } => { + // On remote Publisher route announcement, prepare a Subscriber route + // with an associated DDS Writer allowing local ROS2 Nodes to discover it + let route = self + .get_or_create_route_subscriber( + format!("/{zenoh_key_expr}"), + ros2_type, + keyless, + writer_qos, + ) + .await?; + route.add_remote_route(&plugin_id, &zenoh_key_expr); + } + + RetiredMsgPub { + plugin_id, + zenoh_key_expr, + } => { + if let Entry::Occupied(mut entry) = + self.routes_subscribers.entry(format!("/{zenoh_key_expr}")) + { + let route = entry.get_mut(); + route.remove_remote_route(&plugin_id, &zenoh_key_expr); + if route.is_unused() { + self.admin_space + .remove(&(*KE_PREFIX_ROUTE_SUBSCRIBER / &zenoh_key_expr)); + let route = entry.remove(); + // remove writer's GID in ros_discovery_msg + self.ros_discovery_mgr + .remove_dds_writer(route.dds_writer_guid().map_err(|e| { + format!("Failed to update ros_discovery_info message: {e}") + })?); + log::info!("{route} removed"); + } + } + } + + AnnouncedMsgSub { + plugin_id, + zenoh_key_expr, + ros2_type, + keyless, + reader_qos, + } => { + // On remote Subscriber route announcement, prepare a Publisher route + // with an associated DDS Reader allowing local ROS2 Nodes to discover it + let route = self + .get_or_create_route_publisher( + format!("/{zenoh_key_expr}"), + ros2_type, + keyless, + reader_qos, + ) + .await?; + route.add_remote_route(&plugin_id, &zenoh_key_expr); + } + + RetiredMsgSub { + plugin_id, + zenoh_key_expr, + } => { + if let Entry::Occupied(mut entry) = + self.routes_publishers.entry(format!("/{zenoh_key_expr}")) + { + let route = entry.get_mut(); + route.remove_remote_route(&plugin_id, &zenoh_key_expr); + if route.is_unused() { + self.admin_space + .remove(&(*KE_PREFIX_ROUTE_PUBLISHER / &zenoh_key_expr)); + let route = entry.remove(); + // remove reader's GID in ros_discovery_msg + self.ros_discovery_mgr + .remove_dds_reader(route.dds_reader_guid().map_err(|e| { + format!("Failed to update ros_discovery_info message: {e}") + })?); + log::info!("{route} removed"); + } + } + } + + _ => log::info!("... TODO: manage {event:?}"), + } + Ok(()) + } + + pub async fn query_historical_all_publications(&mut self, plugin_id: &keyexpr) { + for route in self.routes_subscribers.values_mut() { + route + .query_historical_publications(&plugin_id, self.config.queries_timeout) + .await; + } + } + + async fn get_or_create_route_publisher( + &mut self, + ros2_name: String, + ros2_type: String, + keyless: bool, + reader_qos: Qos, + ) -> Result<&mut RoutePublisher<'a>, String> { + match self.routes_publishers.entry(ros2_name.clone()) { + Entry::Vacant(entry) => { + // ROS2 topic name => Zenoh key expr : strip '/' prefix + let zenoh_key_expr = ke_for_sure!(&ros2_name[1..]); + // create route + let route = RoutePublisher::create( + self.config.clone(), + &self.zsession, + self.participant, + ros2_name.clone(), + ros2_type, + zenoh_key_expr.to_owned(), + &None, + keyless, + reader_qos, + ) + .await?; + log::info!("{route} created"); + + // insert reference in admin_space + let admin_ke = *KE_PREFIX_ROUTE_PUBLISHER / zenoh_key_expr; + self.admin_space + .insert(admin_ke, RouteRef::PublisherRoute(ros2_name)); + + // insert reader's GID in ros_discovery_msg + self.ros_discovery_mgr.add_dds_reader( + route + .dds_reader_guid() + .map_err(|e| format!("Failed to update ros_discovery_info message: {e}"))?, + ); + + Ok(entry.insert(route)) + } + Entry::Occupied(entry) => Ok(entry.into_mut()), + } + } + + async fn get_or_create_route_subscriber( + &mut self, + ros2_name: String, + ros2_type: String, + keyless: bool, + writer_qos: Qos, + ) -> Result<&mut RouteSubscriber<'a>, String> { + match self.routes_subscribers.entry(ros2_name.clone()) { + Entry::Vacant(entry) => { + // ROS2 topic name => Zenoh key expr : strip '/' prefix + let zenoh_key_expr = ke_for_sure!(&ros2_name[1..]); + // create route + let route = RouteSubscriber::create( + self.config.clone(), + &self.zsession, + self.participant, + ros2_name.clone(), + ros2_type, + zenoh_key_expr.to_owned(), + keyless, + writer_qos, + ) + .await?; + log::info!("{route} created"); + + // insert reference in admin_space + let admin_ke = *KE_PREFIX_ROUTE_SUBSCRIBER / zenoh_key_expr; + self.admin_space + .insert(admin_ke, RouteRef::SubscriberRoute(ros2_name)); + + // insert writer's GID in ros_discovery_msg + self.ros_discovery_mgr.add_dds_writer( + route + .dds_writer_guid() + .map_err(|e| format!("Failed to update ros_discovery_info message: {e}"))?, + ); + + Ok(entry.insert(route)) + } + Entry::Occupied(entry) => Ok(entry.into_mut()), + } + } + + pub async fn treat_admin_query(&self, query: &Query) { + let selector = query.selector(); + + // get the list of sub-key expressions that will match the same stored keys than + // the selector, if those keys had the admin_keyexpr_prefix. + let sub_kes = selector.key_expr.strip_prefix(&self.admin_prefix); + if sub_kes.is_empty() { + log::error!("Received query for admin space: '{}' - but it's not prefixed by admin_keyexpr_prefix='{}'", selector, &self.admin_prefix); + return; + } + + // For all sub-key expression + for sub_ke in sub_kes { + if sub_ke.is_wild() { + // iterate over all admin space to find matching keys and reply for each + for (ke, route_ref) in self.admin_space.iter() { + if sub_ke.intersects(ke) { + self.send_admin_reply(query, ke, route_ref).await; + } + } + } else { + // sub_ke correspond to 1 key - just get it and reply + if let Some(route_ref) = self.admin_space.get(sub_ke) { + self.send_admin_reply(query, sub_ke, route_ref).await; + } + } + } + } + + async fn send_admin_reply(&self, query: &Query, key_expr: &keyexpr, route_ref: &RouteRef) { + match self.get_entity_json_value(route_ref) { + Ok(Some(v)) => { + let admin_keyexpr = &self.admin_prefix / &key_expr; + if let Err(e) = query + .reply(Ok(Sample::new(admin_keyexpr, v))) + .res_async() + .await + { + log::warn!("Error replying to admin query {:?}: {}", query, e); + } + } + Ok(None) => log::error!("INTERNAL ERROR: Dangling {:?} for {}", route_ref, key_expr), + Err(e) => { + log::error!("INTERNAL ERROR serializing admin value as JSON: {}", e) + } + } + } + + fn get_entity_json_value( + &self, + route_ref: &RouteRef, + ) -> Result, serde_json::Error> { + match route_ref { + RouteRef::PublisherRoute(ke) => self + .routes_publishers + .get(ke) + .map(serde_json::to_value) + .transpose(), + RouteRef::SubscriberRoute(ke) => self + .routes_subscribers + .get(ke) + .map(serde_json::to_value) + .transpose(), + } + } +}