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(), + } + } +}