├── zenoh-plugin-ros1 ├── README.md ├── src │ ├── ros_to_zenoh_bridge │ │ ├── bridge_type.rs │ │ ├── topic_descriptor.rs │ │ ├── rosclient_test_helpers.rs │ │ ├── topic_utilities.rs │ │ ├── bridging_mode.rs │ │ ├── ros1_master_ctrl.rs │ │ ├── topic_mapping.rs │ │ ├── zenoh_client.rs │ │ ├── ros1_client.rs │ │ ├── topic_bridge.rs │ │ ├── aloha_declaration.rs │ │ ├── environment.rs │ │ ├── ros1_to_zenoh_bridge_impl.rs │ │ ├── aloha_subscription.rs │ │ ├── bridges_storage.rs │ │ ├── discovery.rs │ │ ├── resource_cache.rs │ │ └── abstract_bridge.rs │ ├── ros_to_zenoh_bridge.rs │ └── lib.rs ├── build.rs ├── tests │ ├── rosmaster_test.rs │ ├── aloha_declaration_test.rs │ └── discovery_test.rs ├── examples │ ├── ros1_standalone_sub.rs │ ├── ros1_standalone_pub.rs │ ├── ros1_sub.rs │ ├── ros1_pub.rs │ └── ros1_service.rs └── Cargo.toml ├── rust-toolchain.toml ├── ros_pubsub.png ├── .gitmodules ├── .markdownlint.yaml ├── .cargo └── config.toml ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ ├── release.yml │ └── bug_report.yml ├── workflows │ ├── update-release-project.yml │ ├── check-labels.yml │ ├── Dockerfile │ ├── ci.yml │ └── release.yml └── release.yml ├── .pre-commit-config.yaml ├── .gitignore ├── Cross.toml ├── CONTRIBUTORS.md ├── zenoh-bridge-ros1 ├── .service │ └── zenoh-bridge-ros1.service ├── .deb │ ├── postrm │ └── postinst └── Cargo.toml ├── NOTICE.md ├── Dockerfile ├── CONTRIBUTING.md ├── Cargo.toml ├── README.md └── DEFAULT_CONFIG.json5 /zenoh-plugin-ros1/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85.0" 3 | -------------------------------------------------------------------------------- /ros_pubsub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclipse-zenoh/zenoh-plugin-ros1/HEAD/ros_pubsub.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rosrust"] 2 | path = rosrust 3 | url = https://github.com/ZettaScaleLabs/rosrust.git 4 | branch = feature/fix_bugs 5 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, # Line length limitation 3 | "MD033": false, # Enable Inline HTML 4 | "MD041": false, # Allow first line heading 5 | "MD045": false, # Allow Images have no alternate text 6 | } -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RUST_BACKTRACE = { value = "full", force = true } 3 | 4 | [target.x86_64-unknown-linux-musl] 5 | rustflags = "-Ctarget-feature=-crt-static" 6 | 7 | [target.aarch64-unknown-linux-musl] 8 | rustflags = "-Ctarget-feature=-crt-static" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/eclipse-zenoh/roadmap/discussions/categories/zenoh 5 | about: Open to the Zenoh community. Share your feedback with the Zenoh team. 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: fmt 5 | name: fmt 6 | entry: cargo fmt -p zenoh-plugin-ros1 -p zenoh-bridge-ros1 -- --config "unstable_features=true,imports_granularity=Crate,group_imports=StdExternalCrate" 7 | language: system 8 | types: [rust] 9 | -------------------------------------------------------------------------------- /.github/workflows/update-release-project.yml: -------------------------------------------------------------------------------- 1 | name: Update release project 2 | 3 | on: 4 | issues: 5 | types: [opened, edited, labeled] 6 | pull_request_target: 7 | types: [closed] 8 | branches: 9 | - main 10 | 11 | jobs: 12 | main: 13 | uses: eclipse-zenoh/zenoh/.github/workflows/update-release-project.yml@main 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target 4 | 5 | # Ignore all Cargo.lock but one at top-level, since it's committed in git. 6 | */**/Cargo.lock 7 | 8 | # These are backup files generated by rustfmt 9 | **/*.rs.bk 10 | 11 | # CLion project directory 12 | .idea 13 | 14 | # Emacs temps 15 | *~ 16 | 17 | # MacOS Related 18 | .DS_Store 19 | 20 | .vscode 21 | -------------------------------------------------------------------------------- /.github/workflows/check-labels.yml: -------------------------------------------------------------------------------- 1 | name: Check required labels 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, synchronize, reopened, labeled] 6 | branches: ["**"] 7 | 8 | jobs: 9 | check-labels: 10 | name: Check PR labels 11 | uses: eclipse-zenoh/ci/.github/workflows/check-labels.yml@main 12 | secrets: 13 | github-token: ${{ secrets.GITHUB_TOKEN }} 14 | permissions: 15 | pull-requests: write 16 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-musl] 2 | image = "jenoch/rust-cross:x86_64-unknown-linux-musl" 3 | 4 | [target.arm-unknown-linux-gnueabi] 5 | image = "jenoch/rust-cross:arm-unknown-linux-gnueabi" 6 | 7 | [target.arm-unknown-linux-gnueabihf] 8 | image = "jenoch/rust-cross:arm-unknown-linux-gnueabihf" 9 | 10 | [target.armv7-unknown-linux-gnueabihf] 11 | image = "jenoch/rust-cross:armv7-unknown-linux-gnueabihf" 12 | 13 | [target.aarch64-unknown-linux-gnu] 14 | image = "jenoch/rust-cross:aarch64-unknown-linux-gnu" 15 | 16 | [target.aarch64-unknown-linux-musl] 17 | image = "jenoch/rust-cross:aarch64-unknown-linux-musl" 18 | 19 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors to Eclipse zenoh-plugin-ros1 2 | 3 | These are the contributors to Eclipse zenoh-plugin-ros1 (the initial contributors and the contributors listed in the Git log). 4 | 5 | | GitHub username | Name | 6 | | --------------- | ------------------------------| 7 | | yellowhatter | Dmitrii Bannov (ZettaScale) | 8 | | OlivierHecart | Olivier Hécart (ZettaScale) | 9 | | JEnoch | Julien Enoch (ZettaScale) | 10 | | Mallets | Luca Cominardi (ZettaScale) | 11 | | gabrik | Gabriele Baldoni (ZettaScale) | 12 | | imstevenpmwork | Steven Palma (ZettaScale) | 13 | -------------------------------------------------------------------------------- /zenoh-bridge-ros1/.service/zenoh-bridge-ros1.service: -------------------------------------------------------------------------------- 1 | 2 | [Unit] 3 | Description = Eclipse Zenoh Bridge for ROS1 4 | Documentation=https://github.com/eclipse-zenoh/zenoh-plugin-ros1 5 | After=network-online.target 6 | Wants=network-online.target 7 | 8 | 9 | [Service] 10 | Type=simple 11 | Environment=RUST_LOG=info 12 | ExecStart = /usr/bin/zenoh-bridge-ros1 -c /etc/zenoh-bridge-ros1/conf.json5 13 | KillMode=mixed 14 | KillSignal=SIGINT 15 | RestartKillSignal=SIGINT 16 | Restart=on-failure 17 | RestartSec=2 18 | PermissionsStartOnly=true 19 | User=zenoh-bridge-ros1 20 | StandardOutput=syslog 21 | StandardError=syslog 22 | SyslogIdentifier=zenoh-bridge-ros1 23 | [Install] 24 | WantedBy=multi-user.target 25 | 26 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/bridge_type.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use strum_macros::Display; 16 | 17 | #[derive(Clone, Copy, PartialEq, Eq, Display)] 18 | pub enum BridgeType { 19 | Publisher, 20 | Subscriber, 21 | Service, 22 | Client, 23 | } 24 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/build.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | fn main() { 15 | // Add rustc version to zenohd 16 | let version_meta = rustc_version::version_meta().unwrap(); 17 | println!( 18 | "cargo:rustc-env=RUSTC_VERSION={}", 19 | version_meta.short_version_string 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/topic_descriptor.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use super::resource_cache::{DataType, Md5, TopicName}; 16 | 17 | #[derive(Clone, PartialEq, Eq, Debug, Hash)] 18 | pub struct TopicDescriptor { 19 | pub name: TopicName, 20 | pub datatype: DataType, 21 | pub md5: Md5, 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Request a feature 2 | description: | 3 | Suggest a new feature specific to this repository. NOTE: for generic Zenoh ideas use "Ask a question". 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Guidelines for a good issue** 9 | 10 | *Is your feature request related to a problem?* 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | *Describe the solution you'd like* 14 | A clear and concise description of what you want to happen. 15 | 16 | *Describe alternatives you've considered* 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | *Additional context* 20 | Add any other context about the feature request here. 21 | - type: textarea 22 | id: feature 23 | attributes: 24 | label: "Describe the feature" 25 | validations: 26 | required: true 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/release.yml: -------------------------------------------------------------------------------- 1 | name: Add an issue to the next release 2 | description: | 3 | Add an issue as part of next release. 4 | This will be added to the current release project. 5 | You must be a contributor to use this template. 6 | labels: ["release"] 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | **Guidelines for a good issue** 12 | 13 | *Is your release item related to a problem?* 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | *Describe the solution you'd like* 17 | A clear and concise description of what you want to happen. 18 | 19 | *Describe alternatives you've considered* 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | *Additional context* 23 | Add any other context about the release item request here. 24 | - type: textarea 25 | id: item 26 | attributes: 27 | label: "Describe the release item" 28 | validations: 29 | required: true 30 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2023 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | 15 | changelog: 16 | categories: 17 | - title: Breaking changes 💥 18 | labels: 19 | - breaking-change 20 | - title: New features 🎉 21 | labels: 22 | - enhancement 23 | - new feature 24 | - title: Bug fixes 🐞 25 | labels: 26 | - bug 27 | - title: Documentation 📝 28 | labels: 29 | - documentation 30 | - title: Dependencies 👷 31 | labels: 32 | - dependencies 33 | - title: Other changes 34 | labels: 35 | - "*" 36 | exclude: 37 | labels: 38 | - internal -------------------------------------------------------------------------------- /zenoh-bridge-ros1/.deb/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postrm script for Eclipse Zenoh bridge for MQTT 3 | # see: dh_installdeb(1) 4 | 5 | set -e 6 | 7 | # summary of how this script can be called: 8 | # * `remove' 9 | # * `purge' 10 | # * `upgrade' 11 | # * `failed-upgrade' 12 | # * `abort-install' 13 | # * `abort-install' 14 | # * `abort-upgrade' 15 | # * `disappear' 16 | # 17 | # for details, see https://www.debian.org/doc/debian-policy/ or 18 | # the debian-policy package 19 | 20 | 21 | case "$1" in 22 | purge|remove|abort-install|disappear) 23 | userdel zenoh-bridge-ros1 24 | rm -rf /etc/zenoh-bridge-ros1 25 | ;; 26 | 27 | *) 28 | echo "postrm called with unknown argument \`$1'" >&2 29 | exit 1 30 | ;; 31 | esac 32 | 33 | # dh_installdeb will replace this with shell code automatically 34 | # generated by other debhelper scripts. 35 | 36 | #DEBHELPER# 37 | 38 | exit 0 39 | 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Report a bug 2 | description: | 3 | Create a bug report to help us improve Zenoh. 4 | title: "[Bug] " 5 | labels: ["bug"] 6 | body: 7 | - type: textarea 8 | id: summary 9 | attributes: 10 | label: "Describe the bug" 11 | description: | 12 | A clear and concise description of the expected behaviour and what the bug is. 13 | placeholder: | 14 | E.g. zenoh peers can not automatically establish a connection. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: reproduce 19 | attributes: 20 | label: To reproduce 21 | description: "Steps to reproduce the behavior:" 22 | placeholder: | 23 | 1. Start a subscriber "..." 24 | 2. Start a publisher "...." 25 | 3. See error 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: system 30 | attributes: 31 | label: System info 32 | description: "Please complete the following information:" 33 | placeholder: | 34 | - Platform: [e.g. Ubuntu 20.04 64-bit] 35 | - CPU [e.g. AMD Ryzen 3800X] 36 | - Zenoh version/commit [e.g. 6f172ea985d42d20d423a192a2d0d46bb0ce0d11] 37 | validations: 38 | required: true 39 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/rosclient_test_helpers.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::time::Duration; 16 | 17 | use rosrust::{Publisher, RawMessage, Subscriber}; 18 | 19 | use super::{ros1_client::Ros1Client, test_helpers::wait_sync}; 20 | 21 | pub fn wait_for_rosclient_to_connect(rosclient: &Ros1Client) -> bool { 22 | wait_sync(|| rosclient.state().is_ok(), Duration::from_secs(10)) 23 | } 24 | 25 | pub fn wait_for_publishers(subscriber: &Subscriber, count: usize) -> bool { 26 | wait_sync( 27 | || subscriber.publisher_count() == count, 28 | Duration::from_secs(10), 29 | ) 30 | } 31 | 32 | pub fn wait_for_subscribers(publisher: &Publisher, count: usize) -> bool { 33 | wait_sync( 34 | || publisher.subscriber_count() == count, 35 | Duration::from_secs(10), 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /zenoh-bridge-ros1/.deb/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postinst script for Eclipse Zenoh bridge for MQTT 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `configure' 10 | # * `abort-upgrade' 11 | # * `abort-remove' `in-favour' 12 | # 13 | # * `abort-remove' 14 | # * `abort-deconfigure' `in-favour' 15 | # `removing' 16 | # 17 | # for details, see https://www.debian.org/doc/debian-policy/ or 18 | # the debian-policy package 19 | 20 | 21 | case "$1" in 22 | configure) 23 | id -u zenoh-bridge-ros1 >/dev/null 2>&1 || sudo useradd -r -s /bin/false zenoh-bridge-ros1 24 | systemctl daemon-reload 25 | systemctl disable zenoh-bridge-ros1 26 | ;; 27 | 28 | abort-upgrade|abort-remove|abort-deconfigure) 29 | ;; 30 | 31 | *) 32 | echo "postinst called with unknown argument \`$1'" >&2 33 | exit 1 34 | ;; 35 | esac 36 | 37 | # dh_installdeb will replace this with shell code automatically 38 | # generated by other debhelper scripts. 39 | 40 | #DEBHELPER# 41 | 42 | exit 0 43 | -------------------------------------------------------------------------------- /.github/workflows/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | 15 | ### 16 | ### Dockerfile creating the eclipse/zenoh-bridge-ros1 image from cross-compiled binaries. 17 | ### It assumes that zenoh-bridge-ros1 is installed in docker/$TARGETPLATFORM/ 18 | ### where $TARGETPLATFORM is set by buildx to a Docker supported platform such as linux/amd64 or linux/arm64 19 | ### (see https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images) 20 | ### 21 | 22 | 23 | FROM alpine:latest 24 | 25 | ARG TARGETPLATFORM 26 | 27 | RUN apk add --no-cache libgcc libstdc++ 28 | 29 | COPY docker/$TARGETPLATFORM/zenoh-bridge-ros1 / 30 | 31 | RUN echo '#!/bin/ash' > /entrypoint.sh 32 | RUN echo 'echo " * Starting: /zenoh-bridge-ros1 $*"' >> /entrypoint.sh 33 | RUN echo 'exec /zenoh-bridge-ros1 $*' >> /entrypoint.sh 34 | RUN chmod +x /entrypoint.sh 35 | 36 | EXPOSE 7446/udp 37 | EXPOSE 7447/tcp 38 | EXPOSE 8000/tcp 39 | 40 | ENV RUST_LOG info 41 | 42 | ENTRYPOINT ["/entrypoint.sh"] 43 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | # Notices for Eclipse zenoh-plugin-ros1 2 | 3 | This content is produced and maintained by the Eclipse zenoh project. 4 | 5 | * Project home: https://projects.eclipse.org/projects/iot.zenoh 6 | 7 | ## Trademarks 8 | 9 | Eclipse zenoh is trademark of the Eclipse Foundation. 10 | Eclipse, and the Eclipse Logo are registered trademarks of the Eclipse Foundation. 11 | 12 | ## Copyright 13 | 14 | All content is the property of the respective authors or their employers. 15 | For more information regarding authorship of content, please consult the 16 | listed source code repository logs. 17 | 18 | ## Declared Project Licenses 19 | 20 | This program and the accompanying materials are made available under the 21 | terms of the Eclipse Public License 2.0 which is available at 22 | http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 23 | which is available at https://www.apache.org/licenses/LICENSE-2.0. 24 | 25 | SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 26 | 27 | ## Source Code 28 | 29 | The project maintains the following source code repositories: 30 | 31 | * https://github.com/eclipse-zenoh/zenoh.git 32 | * https://github.com/eclipse-zenoh/zenoh-c.git 33 | * https://github.com/eclipse-zenoh/zenoh-java.git 34 | * https://github.com/eclipse-zenoh/zenoh-go.git 35 | * https://github.com/eclipse-zenoh/zenoh-python.git 36 | * https://github.com/eclipse-zenoh/zenoh-plugin-dds.git 37 | 38 | ## Third-party Content 39 | 40 | *To be completed...* 41 | 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | 15 | 16 | ### 17 | ### Build part 18 | ### 19 | FROM rust:slim-buster as builder 20 | 21 | WORKDIR /usr/src/zenoh-plugin-ros1 22 | 23 | # List of installed tools: 24 | # * for zenoh-plugin-ros1 25 | # - git 26 | # - clang 27 | RUN apt-get update && apt-get -y install git clang build-essential 28 | 29 | COPY . . 30 | # if exists, copy .git directory to be used by git-version crate to determine the version 31 | COPY .gi? .git/ 32 | 33 | RUN cargo install --locked --path zenoh-bridge-ros1 34 | 35 | 36 | ### 37 | ### Run part 38 | ### 39 | FROM debian:buster-slim 40 | 41 | COPY --from=builder /usr/local/cargo/bin/zenoh-bridge-ros1 /usr/local/bin/zenoh-bridge-ros1 42 | RUN ldconfig -v 43 | 44 | RUN echo '#!/bin/bash' > /entrypoint.sh 45 | RUN echo 'echo " * Starting: zenoh-bridge-ros1 $*"' >> /entrypoint.sh 46 | RUN echo 'exec zenoh-bridge-ros1 $*' >> /entrypoint.sh 47 | RUN chmod +x /entrypoint.sh 48 | 49 | EXPOSE 7446/udp 50 | EXPOSE 7447/tcp 51 | EXPOSE 8000/tcp 52 | 53 | ENV RUST_LOG info 54 | 55 | ENTRYPOINT ["/entrypoint.sh"] 56 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/topic_utilities.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use zenoh::key_expr::{ 16 | format::{kedefine, keformat}, 17 | keyexpr, OwnedKeyExpr, 18 | }; 19 | 20 | use super::{environment::Environment, topic_descriptor::TopicDescriptor}; 21 | 22 | kedefine!( 23 | pub ros_mapping_format: "${data_type:*}/${md5:*}/${bridge_ns:*}/${topic:**}", 24 | ); 25 | 26 | pub fn make_topic_key(topic: &TopicDescriptor) -> &str { 27 | topic.name.trim_start_matches('/').trim_end_matches('/') 28 | } 29 | 30 | pub fn make_zenoh_key(topic: &TopicDescriptor) -> OwnedKeyExpr { 31 | let mut formatter = ros_mapping_format::formatter(); 32 | keformat!( 33 | formatter, 34 | bridge_ns = Environment::bridge_namespace().get(), 35 | data_type = hex::encode(topic.datatype.as_bytes()), 36 | md5 = topic.md5.clone(), 37 | topic = make_topic_key(topic) 38 | ) 39 | .unwrap() 40 | } 41 | 42 | pub fn make_topic(datatype: &str, md5: &str, topic_name: &keyexpr) -> TopicDescriptor { 43 | let mut name = topic_name.to_string(); 44 | name.insert(0, '/'); 45 | TopicDescriptor { 46 | name, 47 | datatype: datatype.to_string(), 48 | md5: md5.to_string(), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Eclipse zenoh 2 | 3 | Thanks for your interest in this project. 4 | 5 | ## Project description 6 | 7 | Eclipse zenoh provides is a stack designed to 8 | 1. minimize network overhead, 9 | 2. support extremely constrained devices, 10 | 3. supports devices with low duty-cycle by allowing the negotiation of data exchange modes and schedules, 11 | 4. provide a rich set of abstraction for distributing, querying and storing data along the entire system, and 12 | 5. provide extremely low latency and high throughput. 13 | 14 | * https://projects.eclipse.org/projects/iot.zenoh 15 | 16 | ## Developer resources 17 | 18 | Information regarding source code management, builds, coding standards, and 19 | more. 20 | 21 | * https://projects.eclipse.org/projects/iot.zenoh/developer 22 | 23 | The project maintains the following source code repositories 24 | 25 | * https://github.com/eclipse-zenoh 26 | 27 | ## Eclipse Contributor Agreement 28 | 29 | Before your contribution can be accepted by the project team contributors must 30 | electronically sign the Eclipse Contributor Agreement (ECA). 31 | 32 | * http://www.eclipse.org/legal/ECA.php 33 | 34 | Commits that are provided by non-committers must have a Signed-off-by field in 35 | the footer indicating that the author is aware of the terms by which the 36 | contribution has been provided to the project. The non-committer must 37 | additionally have an Eclipse Foundation account and must have a signed Eclipse 38 | Contributor Agreement (ECA) on file. 39 | 40 | For more information, please see the Eclipse Committer Handbook: 41 | https://www.eclipse.org/projects/handbook/#resources-commit 42 | 43 | ## Contact 44 | 45 | Contact the project developers via the project's "dev" list. 46 | 47 | * https://accounts.eclipse.org/mailing-list/zenoh-dev 48 | 49 | Or via the Discord server. 50 | 51 | * https://discord.gg/vSDSpqnbkm 52 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/tests/rosmaster_test.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::time::Duration; 16 | 17 | use serial_test::serial; 18 | use zenoh_plugin_ros1::ros_to_zenoh_bridge::ros1_master_ctrl::Ros1MasterCtrl; 19 | 20 | #[tokio::test(flavor = "multi_thread")] 21 | #[serial(ROS1)] 22 | async fn start_and_stop_master() { 23 | Ros1MasterCtrl::with_ros1_master() 24 | .await 25 | .expect("Error starting rosmaster"); 26 | 27 | Ros1MasterCtrl::without_ros1_master().await; 28 | } 29 | 30 | #[tokio::test(flavor = "multi_thread")] 31 | #[serial(ROS1)] 32 | async fn start_and_stop_master_and_check_connectivity() { 33 | // start rosmaster 34 | Ros1MasterCtrl::with_ros1_master() 35 | .await 36 | .expect("Error starting rosmaster"); 37 | 38 | // start ros1 client 39 | let ros1_client = rosrust::api::Ros::new_raw( 40 | "http://localhost:11311/", 41 | &rosrust::api::resolve::hostname(), 42 | &rosrust::api::resolve::namespace(), 43 | "start_and_stop_master_and_check_connectivity_client", 44 | ) 45 | .unwrap(); 46 | 47 | // wait for correct status from rosmaster.... 48 | let mut has_rosmaster = false; 49 | for _i in 0..1000 { 50 | if ros1_client.state().is_ok() { 51 | has_rosmaster = true; 52 | break; 53 | } 54 | std::thread::sleep(Duration::from_millis(10)); 55 | } 56 | 57 | // stop rosmaster 58 | Ros1MasterCtrl::without_ros1_master().await; 59 | 60 | // check if there was a status from rosmaster... 61 | if !has_rosmaster { 62 | panic!("cannot see rosmaster!"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/examples/ros1_standalone_sub.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use tokio::sync::mpsc::unbounded_channel; 16 | use zenoh_plugin_ros1::ros_to_zenoh_bridge::environment::Environment; 17 | 18 | #[tokio::main] 19 | async fn main() { 20 | let (sender, mut receiver) = unbounded_channel(); 21 | ctrlc::set_handler(move || { 22 | tracing::info!("Catching Ctrl+C..."); 23 | sender.send(()).expect("Error handling Ctrl+C signal") 24 | }) 25 | .expect("Error setting Ctrl+C handler"); 26 | 27 | // initiate logging 28 | zenoh::try_init_log_from_env(); 29 | 30 | // create ROS1 node and subscriber 31 | print!("Creating ROS1 Node..."); 32 | let ros1_node = rosrust::api::Ros::new_raw( 33 | Environment::ros_master_uri().get().as_str(), 34 | &rosrust::api::resolve::hostname(), 35 | &rosrust::api::resolve::namespace(), 36 | (Environment::ros_name().get() + "_test_subscriber_node").as_str(), 37 | ) 38 | .expect("Error creating ROS1 Node!"); 39 | println!(" OK!"); 40 | print!("Creating ROS1 Subscriber..."); 41 | #[allow(unused_variables)] 42 | let ros1_subscriber = ros1_node 43 | .subscribe("/some/ros/topic", 0, |msg: rosrust::RawMessage| { 44 | println!("ROS Subscriber: got message!") 45 | }) 46 | .expect("Error creating ROS1 Subscriber!"); 47 | println!(" OK!"); 48 | 49 | println!("Running subscription, press Ctrl+C to exit..."); 50 | // wait Ctrl+C 51 | receiver 52 | .recv() 53 | .await 54 | .expect("Error receiving Ctrl+C signal"); 55 | tracing::info!("Caught Ctrl+C, stopping..."); 56 | } 57 | -------------------------------------------------------------------------------- /zenoh-bridge-ros1/Cargo.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | [package] 15 | name = "zenoh-bridge-ros1" 16 | version = { workspace = true } 17 | authors = { workspace = true } 18 | edition = { workspace = true } 19 | repository = { workspace = true } 20 | homepage = { workspace = true } 21 | license = { workspace = true } 22 | categories = { workspace = true } 23 | description = "Zenoh bridge for ROS1" 24 | publish = false 25 | 26 | [dependencies] 27 | clap = { workspace = true } 28 | ctrlc = { workspace = true } 29 | tracing = { workspace = true } 30 | lazy_static = { workspace = true } 31 | serde_json = { workspace = true } 32 | tokio = { workspace = true } 33 | zenoh = { workspace = true } 34 | zenoh-plugin-trait = { workspace = true } 35 | zenoh-plugin-ros1 = { workspace = true } 36 | zenoh-plugin-rest = { workspace = true } 37 | [[bin]] 38 | name = "zenoh-bridge-ros1" 39 | path = "src/main.rs" 40 | 41 | [package.metadata.deb] 42 | name = "zenoh-bridge-ros1" 43 | maintainer = "zenoh-dev@eclipse.org" 44 | copyright = "2017, 2022 ZettaScale Technology Inc." 45 | section = "net" 46 | license-file = ["../LICENSE", "0"] 47 | depends = "$auto" 48 | maintainer-scripts = ".deb" 49 | assets = [ 50 | # binary 51 | [ 52 | "target/release/zenoh-bridge-ros1", 53 | "/usr/bin/", 54 | "755", 55 | ], 56 | # config file 57 | [ 58 | "../DEFAULT_CONFIG.json5", 59 | "/etc/zenoh-bridge-ros1/conf.json5", 60 | "644", 61 | ], 62 | # service 63 | [ 64 | ".service/zenoh-bridge-ros1.service", 65 | "/lib/systemd/system/zenoh-bridge-ros1.service", 66 | "644", 67 | ], 68 | ] 69 | conf-files = ["/etc/zenoh-bridge-ros1/conf.json5"] 70 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/examples/ros1_standalone_pub.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use tokio::sync::mpsc::unbounded_channel; 16 | use zenoh_plugin_ros1::ros_to_zenoh_bridge::environment::Environment; 17 | 18 | #[tokio::main] 19 | async fn main() { 20 | let (sender, mut receiver) = unbounded_channel(); 21 | ctrlc::set_handler(move || { 22 | tracing::info!("Catching Ctrl+C..."); 23 | sender.send(()).expect("Error handling Ctrl+C signal") 24 | }) 25 | .expect("Error setting Ctrl+C handler"); 26 | 27 | // initiate logging 28 | zenoh::try_init_log_from_env(); 29 | 30 | // create ROS1 node and publisher 31 | print!("Creating ROS1 Node..."); 32 | let ros1_node = rosrust::api::Ros::new_raw( 33 | Environment::ros_master_uri().get().as_str(), 34 | &rosrust::api::resolve::hostname(), 35 | &rosrust::api::resolve::namespace(), 36 | (Environment::ros_name().get() + "_test_source_node").as_str(), 37 | ) 38 | .expect("Error creating ROS1 Node!"); 39 | println!(" OK!"); 40 | print!("Creating ROS1 Publisher..."); 41 | let ros1_publisher = ros1_node 42 | .publish::("/some/ros/topic", 0) 43 | .expect("Error creating ROS1 Publisher!"); 44 | println!(" OK!"); 45 | 46 | println!("Running publication, press Ctrl+C to exit..."); 47 | 48 | // run test loop publishing data to ROS topic... 49 | let working_loop = move || { 50 | let data: Vec = (0..10).collect(); 51 | while receiver.try_recv().is_err() { 52 | println!("ROS Publisher: publishing data..."); 53 | ros1_publisher 54 | .send(rosrust::RawMessage(data.clone())) 55 | .unwrap(); 56 | std::thread::sleep(core::time::Duration::from_secs(1)); 57 | } 58 | tracing::info!("Caught Ctrl+C, stopping..."); 59 | }; 60 | tokio::task::spawn_blocking(working_loop).await.unwrap(); 61 | } 62 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | [workspace] 15 | resolver = "2" 16 | members = ["zenoh-bridge-ros1", "zenoh-plugin-ros1", "rosrust/rosrust"] 17 | exclude = ["rosrust"] 18 | 19 | [workspace.package] 20 | version = "1.0.0-dev" 21 | authors = [ 22 | "Dmitrii Bannov ", 23 | "Luca Cominardi ", 24 | ] 25 | edition = "2021" 26 | repository = "https://github.com/eclipse-zenoh/zenoh-plugin-ros1" 27 | homepage = "http://zenoh.io" 28 | license = " EPL-2.0 OR Apache-2.0" 29 | categories = ["network-programming"] 30 | 31 | [workspace.dependencies] 32 | atoi = "2.0.0" 33 | async-trait = "0.1" 34 | clap = "3.2.23" 35 | ctrlc = "3.2.5" 36 | futures = "0.3.24" 37 | git-version = "0.3.5" 38 | lazy_static = "1.4.0" 39 | serde = "1.0.147" 40 | serde_derive = "1.0.147" 41 | serde_json = "1.0.114" 42 | async-global-executor = "2.3.1" 43 | rand = "0.8.5" 44 | strum = "0.24" 45 | strum_macros = "0.24" 46 | duration-string = "0.3.0" 47 | flume = "0.11" 48 | hex = "0.4.3" 49 | xml-rpc = "0.0.12" 50 | rustc_version = "0.4" 51 | test-case = { version = "3.3" } 52 | tokio = { version = "1.35.1", features = ["process"] } 53 | tracing = "0.1" 54 | zenoh = { version = "1.0.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = [ 55 | "internal", 56 | "internal_config", 57 | "unstable", 58 | "plugins", 59 | ] } 60 | zenoh-config = { version = "1.0.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", default-features = false } 61 | zenoh-plugin-rest = { version = "1.0.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", default-features = false, features=["static_plugin"]} 62 | zenoh-plugin-trait = { version = "1.0.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", default-features = false } 63 | zenoh-plugin-ros1 = { version = "1.0.0-dev", path = "zenoh-plugin-ros1", default-features = false } 64 | 65 | [profile.release] 66 | debug = false 67 | lto = "fat" 68 | codegen-units = 1 69 | opt-level = 3 70 | panic = "abort" 71 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/bridging_mode.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use serde::{Deserialize, Serialize}; 16 | use strum_macros::{Display, EnumString}; 17 | 18 | use super::{bridge_type::BridgeType, environment::Environment}; 19 | 20 | #[derive(PartialEq, Eq, EnumString, Clone, Display)] 21 | #[strum(serialize_all = "snake_case")] 22 | #[derive(Serialize, Deserialize, Debug)] 23 | #[serde(rename_all = "snake_case")] 24 | pub enum BridgingMode { 25 | LazyAuto, 26 | Auto, 27 | Disabled, 28 | } 29 | 30 | pub(super) fn bridging_mode(entity_type: BridgeType, topic_name: &str) -> BridgingMode { 31 | macro_rules! calcmode { 32 | ($custom_modes:expr, $default_mode:expr, $topic_name:expr) => { 33 | $custom_modes 34 | .get() 35 | .modes 36 | .get($topic_name) 37 | .map_or_else(|| $default_mode.get(), |v| v.clone()) 38 | }; 39 | } 40 | 41 | match entity_type { 42 | BridgeType::Publisher => { 43 | calcmode!( 44 | Environment::publisher_topic_custom_bridging_mode(), 45 | Environment::publisher_bridging_mode(), 46 | topic_name 47 | ) 48 | } 49 | BridgeType::Subscriber => { 50 | calcmode!( 51 | Environment::subscriber_topic_custom_bridging_mode(), 52 | Environment::subscriber_bridging_mode(), 53 | topic_name 54 | ) 55 | } 56 | BridgeType::Service => { 57 | calcmode!( 58 | Environment::service_topic_custom_bridging_mode(), 59 | Environment::service_bridging_mode(), 60 | topic_name 61 | ) 62 | } 63 | BridgeType::Client => { 64 | calcmode!( 65 | Environment::client_topic_custom_bridging_mode(), 66 | Environment::client_bridging_mode(), 67 | topic_name 68 | ) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/Cargo.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | [package] 15 | name = "zenoh-plugin-ros1" 16 | version = { workspace = true } 17 | authors = { workspace = true } 18 | edition = { workspace = true } 19 | repository = { workspace = true } 20 | homepage = { workspace = true } 21 | license = { workspace = true } 22 | categories = ["network-programming"] 23 | description = "Zenoh plugin for bidging ROS1" 24 | 25 | [lib] 26 | name = "zenoh_plugin_ros1" 27 | crate-type = ["cdylib", "rlib"] 28 | 29 | [features] 30 | dynamic_plugin = [] 31 | test = [] 32 | stats = ["zenoh/stats"] 33 | default = [ 34 | "dynamic_plugin", 35 | "test", 36 | ] # TODO: https://zettascale.atlassian.net/browse/ZEN-291 37 | 38 | [dependencies] 39 | atoi = { workspace = true } 40 | flume = { workspace = true } 41 | futures = { workspace = true } 42 | git-version = { workspace = true } 43 | lazy_static = { workspace = true } 44 | test-case = { workspace = true } 45 | tokio = { workspace = true } 46 | tracing = { workspace = true } 47 | serde = { workspace = true } 48 | serde_json = { workspace = true } 49 | async-global-executor = { workspace = true } 50 | async-trait = { workspace = true } 51 | rand = { workspace = true } 52 | strum = { workspace = true } 53 | strum_macros = { workspace = true } 54 | duration-string = { workspace = true } 55 | zenoh = { workspace = true } 56 | zenoh-config = { workspace = true } 57 | zenoh-plugin-trait = { workspace = true } 58 | hex = { workspace = true } 59 | xml-rpc = { workspace = true } 60 | rosrust = { path = "../rosrust/rosrust" } 61 | 62 | [dev-dependencies] 63 | serial_test = "0.10.0" 64 | multiset = "0.0.5" 65 | ctrlc = { workspace = true } 66 | # TODO: https://zettascale.atlassian.net/browse/ZEN-291 67 | # zenoh-plugin-ros1 = { path = ".", features = ["test"]} 68 | 69 | [build-dependencies] 70 | rustc_version = { workspace = true } 71 | 72 | [package.metadata.deb] 73 | name = "zenoh-plugin-ros1" 74 | maintainer = "zenoh-dev@eclipse.org" 75 | copyright = "2017, 2022 ZettaScale Technology Inc." 76 | section = "net" 77 | license-file = ["../LICENSE", "0"] 78 | depends = "zenohd (=1.0.0~dev-1)" 79 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/ros1_master_ctrl.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use atoi::atoi; 16 | use tokio::{ 17 | process::{Child, Command}, 18 | sync::Mutex, 19 | }; 20 | use tracing::error; 21 | use zenoh::{ 22 | internal::{bail, zasynclock, zerror}, 23 | Result as ZResult, 24 | }; 25 | 26 | use crate::ros_to_zenoh_bridge::environment::Environment; 27 | 28 | lazy_static::lazy_static! { 29 | static ref ROSMASTER: Mutex> = Mutex::new(None); 30 | } 31 | 32 | pub struct Ros1MasterCtrl; 33 | impl Ros1MasterCtrl { 34 | pub async fn with_ros1_master() -> ZResult<()> { 35 | let uri = Environment::ros_master_uri().get(); 36 | let splitted: Vec<&str> = uri.split(':').collect(); 37 | if splitted.len() != 3 { 38 | bail!("Unable to parse port from ros_master_uri!") 39 | } 40 | let port_str = splitted[2].trim_matches(|v: char| !char::is_numeric(v)); 41 | let port = 42 | atoi::(port_str.as_bytes()).ok_or("Unable to parse port from ros_master_uri!")?; 43 | 44 | let mut locked = zasynclock!(ROSMASTER); 45 | assert!(locked.is_none()); 46 | let child = Command::new("rosmaster") 47 | .arg(format!("-p {}", port).as_str()) 48 | .stdout(std::process::Stdio::piped()) 49 | .stderr(std::process::Stdio::piped()) 50 | .spawn() 51 | .map_err(|e| zerror!("Error starting rosmaster process: {}", e))?; 52 | let _ = locked.insert(child); 53 | Ok(()) 54 | } 55 | 56 | pub async fn without_ros1_master() { 57 | let mut locked = zasynclock!(ROSMASTER); 58 | assert!(locked.is_some()); 59 | match locked.take() { 60 | Some(mut child) => match child.kill().await { 61 | Ok(_) => {} 62 | Err(e) => error!("Error sending kill cmd to child rosmaster: {}", e), 63 | }, 64 | None => match Command::new("killall").arg("rosmaster").spawn() { 65 | Ok(_) => {} 66 | Err(e) => error!( 67 | "Error executing killall command to stop foreign rosmaster: {}", 68 | e 69 | ), 70 | }, 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/examples/ros1_sub.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use zenoh_plugin_ros1::ros_to_zenoh_bridge::{ 16 | environment::Environment, ros1_master_ctrl::Ros1MasterCtrl, Ros1ToZenohBridge, 17 | }; 18 | 19 | #[tokio::main] 20 | async fn main() { 21 | // initiate logging 22 | zenoh::try_init_log_from_env(); 23 | 24 | // You need to have ros1 installed within your system and have "rosmaster" command available, otherwise this code will fail. 25 | // start ROS1 master... 26 | print!("Starting ROS1 Master..."); 27 | Ros1MasterCtrl::with_ros1_master() 28 | .await 29 | .expect("Error starting rosmaster!"); 30 | 31 | // create bridge 32 | print!("Starting Bridge..."); 33 | let _bridge = Ros1ToZenohBridge::new_with_own_session(zenoh::Config::default()).await; 34 | println!(" OK!"); 35 | 36 | // create ROS1 node and subscriber 37 | print!("Creating ROS1 Node..."); 38 | let ros1_node = rosrust::api::Ros::new_raw( 39 | Environment::ros_master_uri().get().as_str(), 40 | &rosrust::api::resolve::hostname(), 41 | &rosrust::api::resolve::namespace(), 42 | (Environment::ros_name().get() + "_test_subscriber_node").as_str(), 43 | ) 44 | .unwrap(); 45 | println!(" OK!"); 46 | print!("Creating ROS1 Subscriber..."); 47 | #[allow(unused_variables)] 48 | let ros1_subscriber = ros1_node 49 | .subscribe("/some/ros/topic", 0, |msg: rosrust::RawMessage| { 50 | println!("ROS Subscriber: got message!") 51 | }) 52 | .unwrap(); 53 | println!(" OK!"); 54 | 55 | // create Zenoh session and publisher 56 | print!("Creating Zenoh Session..."); 57 | let mut config = zenoh::Config::default(); 58 | config.set_mode(Some(zenoh_config::WhatAmI::Peer)).unwrap(); 59 | let zenoh_session = zenoh::open(config).await.unwrap(); 60 | println!(" OK!"); 61 | print!("Creating Zenoh Publisher..."); 62 | let zenoh_publisher = zenoh_session 63 | .declare_publisher("some/ros/topic") 64 | .congestion_control(zenoh::qos::CongestionControl::Block) 65 | .await 66 | .unwrap(); 67 | println!(" OK!"); 68 | 69 | // here bridge will expose our test ROS topic to Zenoh so that our ROS1 subscriber will get data published in Zenoh 70 | println!("Running bridge, press Ctrl+C to exit..."); 71 | 72 | // run test loop publishing data to Zenoh... 73 | let data: Vec = (0..10).collect(); 74 | loop { 75 | println!("Zenoh Publisher: publishing data..."); 76 | zenoh_publisher.put(data.clone()).await.unwrap(); 77 | tokio::time::sleep(core::time::Duration::from_secs(1)).await; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/examples/ros1_pub.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use zenoh_plugin_ros1::ros_to_zenoh_bridge::{ 16 | environment::Environment, ros1_master_ctrl::Ros1MasterCtrl, Ros1ToZenohBridge, 17 | }; 18 | 19 | #[tokio::main] 20 | async fn main() { 21 | // initiate logging 22 | zenoh::try_init_log_from_env(); 23 | 24 | // You need to have ros1 installed within your system and have "rosmaster" command available, otherwise this code will fail. 25 | // start ROS1 master... 26 | print!("Starting ROS1 Master..."); 27 | Ros1MasterCtrl::with_ros1_master() 28 | .await 29 | .expect("Error starting rosmaster!"); 30 | 31 | // create bridge 32 | print!("Starting Bridge..."); 33 | let mut config = zenoh::Config::default(); 34 | config.set_mode(Some(zenoh_config::WhatAmI::Peer)).unwrap(); 35 | let _bridge = Ros1ToZenohBridge::new_with_own_session(config).await; 36 | println!(" OK!"); 37 | 38 | // create ROS1 node and publisher 39 | print!("Creating ROS1 Node..."); 40 | let ros1_node = rosrust::api::Ros::new_raw( 41 | Environment::ros_master_uri().get().as_str(), 42 | &rosrust::api::resolve::hostname(), 43 | &rosrust::api::resolve::namespace(), 44 | (Environment::ros_name().get() + "_test_source_node").as_str(), 45 | ) 46 | .unwrap(); 47 | println!(" OK!"); 48 | print!("Creating ROS1 Publisher..."); 49 | let ros1_publisher = ros1_node 50 | .publish::("/some/ros/topic", 0) 51 | .unwrap(); 52 | println!(" OK!"); 53 | 54 | // create Zenoh session and subscriber 55 | print!("Creating Zenoh Session..."); 56 | let zenoh_session = zenoh::open(zenoh::Config::default()).await.unwrap(); 57 | println!(" OK!"); 58 | print!("Creating Zenoh Subscriber..."); 59 | zenoh_session 60 | .declare_subscriber("some/ros/topic") 61 | .callback(|_data| println!("Zenoh Subscriber: got data!")) 62 | .background() 63 | .await 64 | .unwrap(); 65 | println!(" OK!"); 66 | 67 | // here bridge will expose our test ROS topic to Zenoh so that our subscriber will get data published within it 68 | println!("Running bridge, press Ctrl+C to exit..."); 69 | 70 | // run test loop publishing data to ROS topic... 71 | let working_loop = move || { 72 | let data: Vec = (0..10).collect(); 73 | loop { 74 | println!("ROS Publisher: publishing data..."); 75 | ros1_publisher 76 | .send(rosrust::RawMessage(data.clone())) 77 | .unwrap(); 78 | std::thread::sleep(core::time::Duration::from_secs(1)); 79 | } 80 | }; 81 | tokio::task::spawn_blocking(working_loop).await.unwrap(); 82 | } 83 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/examples/ros1_service.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use zenoh_plugin_ros1::ros_to_zenoh_bridge::{ 16 | environment::Environment, ros1_master_ctrl::Ros1MasterCtrl, Ros1ToZenohBridge, 17 | }; 18 | 19 | #[tokio::main] 20 | async fn main() { 21 | // initiate logging 22 | zenoh::try_init_log_from_env(); 23 | 24 | // You need to have ros1 installed within your system and have "rosmaster" command available, otherwise this code will fail. 25 | // start ROS1 master... 26 | print!("Starting ROS1 Master..."); 27 | Ros1MasterCtrl::with_ros1_master() 28 | .await 29 | .expect("Error starting rosmaster!"); 30 | 31 | // create bridge 32 | print!("Starting Bridge..."); 33 | let _bridge = Ros1ToZenohBridge::new_with_own_session(zenoh::Config::default()).await; 34 | println!(" OK!"); 35 | 36 | // create ROS1 node and service 37 | print!("Creating ROS1 Node..."); 38 | let ros1_node = rosrust::api::Ros::new_raw( 39 | Environment::ros_master_uri().get().as_str(), 40 | &rosrust::api::resolve::hostname(), 41 | &rosrust::api::resolve::namespace(), 42 | (Environment::ros_name().get() + "_test_service_node").as_str(), 43 | ) 44 | .unwrap(); 45 | println!(" OK!"); 46 | print!("Creating ROS1 Service..."); 47 | #[allow(unused_variables)] 48 | let ros1_service = ros1_node 49 | .service::( 50 | "/some/ros/topic/", 51 | |query| -> rosrust::ServiceResult { 52 | println!("ROS Service: got query, sending reply..."); 53 | Ok(query) 54 | }, 55 | ) 56 | .unwrap(); 57 | println!(" OK!"); 58 | 59 | // create Zenoh session 60 | print!("Creating Zenoh Session..."); 61 | let zenoh_session = zenoh::open(zenoh::Config::default()).await.unwrap(); 62 | println!(" OK!"); 63 | 64 | // here bridge will expose our test ROS topic to Zenoh so that our ROS1 subscriber will get data published in Zenoh 65 | println!("Running bridge, press Ctrl+C to exit..."); 66 | 67 | // run test loop querying Zenoh... 68 | let data: Vec = (0..10).collect(); 69 | loop { 70 | println!("Zenoh: sending query..."); 71 | let reply = zenoh_session 72 | .get("some/ros/topic") 73 | .payload(data.clone()) 74 | .await 75 | .unwrap(); 76 | let result = reply.recv_async().await; 77 | match result { 78 | Ok(val) => { 79 | println!("Zenoh: got reply!"); 80 | assert!(data == val.result().unwrap().payload().to_bytes().as_ref()); 81 | } 82 | Err(e) => { 83 | println!("Zenoh got error: {}", e); 84 | } 85 | } 86 | tokio::time::sleep(core::time::Duration::from_secs(1)).await; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/topic_mapping.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::collections::HashSet; 16 | 17 | use super::{resource_cache::Ros1ResourceCache, topic_descriptor::TopicDescriptor}; 18 | use crate::ros_to_zenoh_bridge::ros1_client; 19 | 20 | #[derive(Debug)] 21 | pub struct Ros1TopicMapping { 22 | pub published: HashSet, 23 | pub subscribed: HashSet, 24 | pub serviced: HashSet, 25 | } 26 | impl Ros1TopicMapping { 27 | pub fn topic_mapping( 28 | ros1_client: &ros1_client::Ros1Client, 29 | ros1_service_cache: &mut Ros1ResourceCache, 30 | ) -> rosrust::api::error::Response { 31 | match ros1_client.state() { 32 | Ok(state_val) => Ok(Ros1TopicMapping::new(state_val, ros1_service_cache)), 33 | Err(e) => Err(e), 34 | } 35 | } 36 | 37 | // PRIVATE: 38 | fn new( 39 | state: rosrust::api::SystemState, 40 | ros1_service_cache: &mut Ros1ResourceCache, 41 | ) -> Ros1TopicMapping { 42 | let mut result = Ros1TopicMapping { 43 | published: HashSet::new(), 44 | subscribed: HashSet::default(), 45 | serviced: HashSet::new(), 46 | }; 47 | 48 | // run through subscriber topics, resolve all available data formats 49 | // and fill 'result.subscribed' with unique combinations of topic_name + format descriptions 50 | for subscriber_topic in state.subscribers { 51 | for node in subscriber_topic.connections { 52 | if let Ok((datatype, md5)) = ros1_service_cache 53 | .resolve_subscriber_parameters(subscriber_topic.name.clone(), node) 54 | { 55 | result.subscribed.insert(TopicDescriptor { 56 | name: subscriber_topic.name.clone(), 57 | datatype, 58 | md5, 59 | }); 60 | } 61 | } 62 | } 63 | 64 | // run through publisher topics, resolve all available data formats 65 | // and fill 'result.published' with unique combinations of topic_name + format descriptions 66 | for publisher_topic in state.publishers { 67 | for node in publisher_topic.connections { 68 | if let Ok((datatype, md5)) = ros1_service_cache 69 | .resolve_publisher_parameters(publisher_topic.name.clone(), node) 70 | { 71 | result.published.insert(TopicDescriptor { 72 | name: publisher_topic.name.clone(), 73 | datatype, 74 | md5, 75 | }); 76 | } 77 | } 78 | } 79 | 80 | // run through service topics, resolve all available data formats 81 | // and fill 'result.serviced' with unique combinations of topic_name + format descriptions 82 | for service_topic in state.services { 83 | for node in service_topic.connections { 84 | if let Ok((datatype, md5)) = 85 | ros1_service_cache.resolve_service_parameters(service_topic.name.clone(), node) 86 | { 87 | result.serviced.insert(TopicDescriptor { 88 | name: service_topic.name.clone(), 89 | datatype, 90 | md5, 91 | }); 92 | } 93 | } 94 | } 95 | result 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::sync::{ 16 | atomic::{AtomicBool, Ordering::Relaxed}, 17 | Arc, 18 | }; 19 | 20 | use tokio::task::JoinHandle; 21 | use tracing::error; 22 | use zenoh::{self, Result as ZResult, Session}; 23 | 24 | use self::{environment::Environment, ros1_to_zenoh_bridge_impl::work_cycle}; 25 | use crate::spawn_runtime; 26 | 27 | #[cfg(feature = "test")] 28 | pub mod aloha_declaration; 29 | #[cfg(feature = "test")] 30 | pub mod aloha_subscription; 31 | #[cfg(feature = "test")] 32 | pub mod bridge_type; 33 | #[cfg(feature = "test")] 34 | pub mod bridging_mode; 35 | #[cfg(feature = "test")] 36 | pub mod discovery; 37 | #[cfg(feature = "test")] 38 | pub mod resource_cache; 39 | #[cfg(feature = "test")] 40 | pub mod ros1_client; 41 | #[cfg(feature = "test")] 42 | pub mod ros1_to_zenoh_bridge_impl; 43 | #[cfg(feature = "test")] 44 | pub mod rosclient_test_helpers; 45 | #[cfg(feature = "test")] 46 | pub mod test_helpers; 47 | #[cfg(feature = "test")] 48 | pub mod topic_descriptor; 49 | #[cfg(feature = "test")] 50 | pub mod topic_utilities; 51 | #[cfg(feature = "test")] 52 | pub mod zenoh_client; 53 | 54 | #[cfg(not(feature = "test"))] 55 | mod aloha_declaration; 56 | #[cfg(not(feature = "test"))] 57 | mod aloha_subscription; 58 | #[cfg(not(feature = "test"))] 59 | mod bridge_type; 60 | #[cfg(not(feature = "test"))] 61 | mod bridging_mode; 62 | #[cfg(not(feature = "test"))] 63 | mod discovery; 64 | #[cfg(not(feature = "test"))] 65 | mod resource_cache; 66 | #[cfg(not(feature = "test"))] 67 | mod ros1_client; 68 | #[cfg(not(feature = "test"))] 69 | mod ros1_to_zenoh_bridge_impl; 70 | #[cfg(not(feature = "test"))] 71 | mod topic_descriptor; 72 | #[cfg(not(feature = "test"))] 73 | mod topic_utilities; 74 | #[cfg(not(feature = "test"))] 75 | mod zenoh_client; 76 | 77 | mod abstract_bridge; 78 | mod bridges_storage; 79 | mod topic_bridge; 80 | mod topic_mapping; 81 | 82 | pub mod environment; 83 | pub mod ros1_master_ctrl; 84 | 85 | pub struct Ros1ToZenohBridge { 86 | flag: Arc, 87 | task_handle: Box>, 88 | } 89 | impl Ros1ToZenohBridge { 90 | pub async fn new_with_own_session(config: zenoh::config::Config) -> ZResult { 91 | let session = zenoh::open(config).await?; 92 | Ok(Self::new_with_external_session(session)) 93 | } 94 | 95 | pub fn new_with_external_session(session: Session) -> Self { 96 | let flag = Arc::new(AtomicBool::new(true)); 97 | Self { 98 | flag: flag.clone(), 99 | task_handle: Box::new(spawn_runtime(Self::run(session, flag))), 100 | } 101 | } 102 | 103 | pub async fn stop(&mut self) { 104 | self.flag.store(false, Relaxed); 105 | self.async_await().await; 106 | } 107 | 108 | //PRIVATE: 109 | async fn run(session: Session, flag: Arc) { 110 | if let Err(e) = work_cycle( 111 | Environment::ros_master_uri().get().as_str(), 112 | session, 113 | flag, 114 | |_v| {}, 115 | |_status| {}, 116 | ) 117 | .await 118 | { 119 | error!("Error occured while running the bridge: {e}") 120 | } 121 | } 122 | 123 | async fn async_await(&mut self) { 124 | self.task_handle 125 | .as_mut() 126 | .await 127 | .expect("Unable to complete the task"); 128 | } 129 | } 130 | impl Drop for Ros1ToZenohBridge { 131 | fn drop(&mut self) { 132 | self.flag.store(false, Relaxed); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2023 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | name: CI 15 | 16 | on: 17 | push: 18 | branches: ["**"] 19 | pull_request: 20 | branches: ["**"] 21 | 22 | jobs: 23 | check: 24 | name: Run checks on ${{ matrix.os }} 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | # Revert back to ubuntu-latest once we sort out deps 30 | os: [ubuntu-22.04] 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | with: 35 | submodules: recursive 36 | 37 | - name: Install Rust toolchain 38 | run: | 39 | rustup show 40 | rustup component add rustfmt clippy 41 | 42 | # zenoh-plugin-ros1 is EOL. Let's deactivate all unnecessary steps 43 | # - name: Code format check 44 | # run: cargo fmt -p zenoh-plugin-ros1 -p zenoh-bridge-ros1 --check -- --config "unstable_features=true,imports_granularity=Crate,group_imports=StdExternalCrate" 45 | # env: 46 | # CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 47 | 48 | # - name: Clippy 49 | # run: cargo clippy -- -D warnings 50 | # env: 51 | # CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 52 | 53 | # - name: Clippy All targets 54 | # run: cargo clippy --all-targets -- -D warnings 55 | # env: 56 | # CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 57 | 58 | - name: Check 59 | run: cargo check 60 | env: 61 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 62 | 63 | - name: Check All targets 64 | run: cargo check --all-targets 65 | env: 66 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 67 | 68 | test: 69 | name: Run tests on ${{ matrix.os }} 70 | runs-on: ${{ matrix.os }} 71 | strategy: 72 | fail-fast: false 73 | matrix: 74 | # Revert back to ubuntu-latest once we sort out deps 75 | os: [ubuntu-22.04] 76 | 77 | steps: 78 | - uses: actions/checkout@v4 79 | with: 80 | submodules: recursive 81 | 82 | - name: Install Rust toolchain 83 | run: rustup show 84 | 85 | - name: Build 86 | run: cargo build --verbose --all-targets 87 | env: 88 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 89 | 90 | - name: Install ROS 91 | run: sudo apt install -y ros-base 92 | 93 | - name: Run tests 94 | run: cargo test --verbose 95 | env: 96 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 97 | 98 | - name: Run doctests 99 | run: cargo test --doc 100 | env: 101 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 102 | 103 | markdown_lint: 104 | runs-on: ubuntu-latest 105 | steps: 106 | - uses: actions/checkout@v4 107 | - uses: DavidAnson/markdownlint-cli2-action@v18 108 | with: 109 | config: '.markdownlint.yaml' 110 | globs: '**/README.md' 111 | 112 | # NOTE: In GitHub repository settings, the "Require status checks to pass 113 | # before merging" branch protection rule ensures that commits are only merged 114 | # from branches where specific status checks have passed. These checks are 115 | # specified manually as a list of workflow job names. Thus we use this extra 116 | # job to signal whether all CI checks have passed. 117 | ci: 118 | name: CI status checks 119 | runs-on: ubuntu-latest 120 | needs: [check, test, markdown_lint] 121 | if: always() 122 | steps: 123 | - name: Check whether all jobs pass 124 | run: echo '${{ toJson(needs) }}' | jq -e 'all(.result == "success")' 125 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/zenoh_client.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::fmt::Display; 16 | 17 | use tracing::debug; 18 | use zenoh::{ 19 | handlers::FifoChannelHandler, 20 | key_expr::KeyExpr, 21 | qos::{CongestionControl, Reliability}, 22 | query::Selector, 23 | sample::{Locality, Sample}, 24 | Result as ZResult, Session, 25 | }; 26 | 27 | pub struct ZenohClient { 28 | session: Session, 29 | } 30 | 31 | impl ZenohClient { 32 | // PUBLIC 33 | pub fn new(session: Session) -> ZenohClient { 34 | ZenohClient { session } 35 | } 36 | 37 | pub async fn subscribe<'b, C, TryIntoKeyExpr>( 38 | &self, 39 | key_expr: TryIntoKeyExpr, 40 | callback: C, 41 | ) -> ZResult> 42 | where 43 | C: Fn(Sample) + Send + Sync + 'static, 44 | TryIntoKeyExpr: TryInto> + Display, 45 | >>::Error: Into, 46 | { 47 | debug!("Creating Subscriber on {}", key_expr); 48 | 49 | self.session 50 | .declare_subscriber(key_expr) 51 | .callback(callback) 52 | .allowed_origin(Locality::Remote) 53 | .await 54 | } 55 | 56 | pub async fn publish<'b: 'static, TryIntoKeyExpr>( 57 | &self, 58 | key_expr: TryIntoKeyExpr, 59 | ) -> ZResult> 60 | where 61 | TryIntoKeyExpr: TryInto> + Display, 62 | >>::Error: Into, 63 | { 64 | debug!("Creating Publisher on {}", key_expr); 65 | 66 | self.session 67 | .declare_publisher(key_expr) 68 | .reliability(Reliability::Reliable) 69 | .allowed_destination(Locality::Remote) 70 | .congestion_control(CongestionControl::Block) 71 | .await 72 | } 73 | 74 | pub async fn make_queryable<'b, Callback, TryIntoKeyExpr>( 75 | &self, 76 | key_expr: TryIntoKeyExpr, 77 | callback: Callback, 78 | ) -> ZResult> 79 | where 80 | Callback: Fn(zenoh::query::Query) + Send + Sync + 'static, 81 | TryIntoKeyExpr: TryInto> + Display, 82 | >>::Error: Into, 83 | { 84 | debug!("Creating Queryable on {}", key_expr); 85 | 86 | self.session 87 | .declare_queryable(key_expr) 88 | .allowed_origin(Locality::Remote) 89 | .callback(callback) 90 | .await 91 | } 92 | 93 | #[cfg(feature = "test")] 94 | pub async fn make_query<'b, Callback, IntoSelector>( 95 | &self, 96 | selector: IntoSelector, 97 | callback: Callback, 98 | data: Vec, 99 | ) -> ZResult<()> 100 | where 101 | Callback: Fn(zenoh::query::Reply) + Send + Sync + 'static, 102 | IntoSelector: TryInto> + Display, 103 | >>::Error: Into, 104 | { 105 | debug!("Creating Query on {}", selector); 106 | 107 | self.session 108 | .get(selector) 109 | .payload(data) 110 | .callback(callback) 111 | .allowed_destination(Locality::Remote) 112 | .await 113 | } 114 | 115 | pub async fn make_query_sync<'b, IntoSelector>( 116 | &self, 117 | selector: IntoSelector, 118 | data: Vec, 119 | ) -> ZResult> 120 | where 121 | IntoSelector: TryInto> + Display, 122 | >>::Error: Into, 123 | { 124 | debug!("Creating Query on {}", selector); 125 | 126 | self.session 127 | .get(selector) 128 | .payload(data) 129 | .allowed_destination(Locality::Remote) 130 | .await 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/ros1_client.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use rosrust::{self, RawMessageDescription}; 16 | use tracing::debug; 17 | use zenoh::{internal::zerror, Result as ZResult}; 18 | 19 | use super::topic_descriptor::TopicDescriptor; 20 | 21 | pub struct Ros1Client { 22 | pub ros: rosrust::api::Ros, 23 | } 24 | 25 | impl Ros1Client { 26 | // PUBLIC 27 | pub fn new(name: &str, master_uri: &str) -> ZResult { 28 | Ok(Ros1Client { 29 | ros: rosrust::api::Ros::new_raw( 30 | master_uri, 31 | &rosrust::api::resolve::hostname(), 32 | &rosrust::api::resolve::namespace(), 33 | name, 34 | ) 35 | .map_err(|e| zerror!("{e}"))?, 36 | }) 37 | } 38 | 39 | pub fn subscribe( 40 | &self, 41 | topic: &TopicDescriptor, 42 | callback: F, 43 | ) -> rosrust::api::error::Result 44 | where 45 | T: rosrust::Message, 46 | F: Fn(T) + Send + 'static, 47 | { 48 | let description = RawMessageDescription { 49 | msg_definition: "*".to_string(), 50 | md5sum: topic.md5.clone(), 51 | msg_type: topic.datatype.clone(), 52 | }; 53 | self.ros.subscribe_with_ids_and_headers( 54 | &topic.name, 55 | 0, 56 | move |data, _| callback(data), 57 | |_| (), 58 | Some(description), 59 | ) 60 | } 61 | 62 | pub fn publish( 63 | &self, 64 | topic: &TopicDescriptor, 65 | ) -> rosrust::api::error::Result> { 66 | let description = RawMessageDescription { 67 | msg_definition: "*".to_string(), 68 | md5sum: topic.md5.clone(), 69 | msg_type: topic.datatype.clone(), 70 | }; 71 | self.ros 72 | .publish_with_description(&topic.name, 0, description) 73 | } 74 | 75 | pub fn client( 76 | &self, 77 | topic: &TopicDescriptor, 78 | ) -> rosrust::api::error::Result> { 79 | self.ros.client::(&topic.name) 80 | } 81 | 82 | pub fn service( 83 | &self, 84 | topic: &TopicDescriptor, 85 | handler: F, 86 | ) -> rosrust::api::error::Result 87 | where 88 | T: rosrust::ServicePair, 89 | F: Fn(T::Request) -> rosrust::ServiceResult + Send + Sync + 'static, 90 | { 91 | let description = RawMessageDescription { 92 | msg_definition: "*".to_string(), 93 | md5sum: topic.md5.clone(), 94 | msg_type: topic.datatype.clone(), 95 | }; 96 | self.ros 97 | .service_with_description::(&topic.name, handler, description) 98 | } 99 | 100 | pub fn state(&self) -> rosrust::api::error::Response { 101 | self.filter(self.ros.state()) 102 | } 103 | 104 | // PRIVATE 105 | /** 106 | * Filter out topics, which are published\subscribed\serviced only by the bridge itself 107 | */ 108 | fn filter( 109 | &self, 110 | mut state: rosrust::api::error::Response, 111 | ) -> rosrust::api::error::Response { 112 | debug!("system state before filter: {:#?}", state); 113 | if let Ok(value) = state.as_mut() { 114 | let name = self.ros.name(); 115 | 116 | let retain_lambda = |x: &rosrust::api::TopicData| { 117 | x.connections.len() > 1 || (x.connections.len() == 1 && x.connections[0] != name) 118 | }; 119 | 120 | value.publishers.retain(retain_lambda); 121 | value.subscribers.retain(retain_lambda); 122 | value.services.retain(retain_lambda); 123 | } 124 | debug!("system state after filter: {:#?}", state); 125 | state 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/topic_bridge.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::{fmt::Display, sync::Arc}; 16 | 17 | use tracing::error; 18 | 19 | use super::{ 20 | abstract_bridge::AbstractBridge, 21 | bridge_type::BridgeType, 22 | bridging_mode::BridgingMode, 23 | discovery::{LocalResource, LocalResources}, 24 | ros1_client, 25 | topic_descriptor::TopicDescriptor, 26 | zenoh_client, 27 | }; 28 | 29 | pub struct TopicBridge { 30 | topic: TopicDescriptor, 31 | b_type: BridgeType, 32 | ros1_client: Arc, 33 | zenoh_client: Arc, 34 | 35 | bridging_mode: BridgingMode, 36 | required_on_ros1_side: bool, 37 | required_on_zenoh_side: bool, 38 | 39 | declaration_interface: Arc, 40 | declaration: Option, 41 | 42 | bridge: Option, 43 | } 44 | 45 | impl Display for TopicBridge { 46 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 47 | write!(f, "{}:{:?}", self.b_type, self.topic) 48 | } 49 | } 50 | 51 | impl TopicBridge { 52 | pub fn new( 53 | topic: TopicDescriptor, 54 | b_type: BridgeType, 55 | declaration_interface: Arc, 56 | ros1_client: Arc, 57 | zenoh_client: Arc, 58 | bridging_mode: BridgingMode, 59 | ) -> Self { 60 | Self { 61 | topic, 62 | b_type, 63 | ros1_client, 64 | zenoh_client, 65 | bridging_mode, 66 | required_on_ros1_side: false, 67 | required_on_zenoh_side: false, 68 | declaration_interface, 69 | declaration: None, 70 | bridge: None, 71 | } 72 | } 73 | 74 | pub async fn set_present_in_ros1(&mut self, present: bool) -> bool { 75 | let recalc = self.required_on_ros1_side != present; 76 | self.required_on_ros1_side = present; 77 | if recalc { 78 | self.recalc_state().await; 79 | } 80 | recalc 81 | } 82 | 83 | pub async fn set_has_complementary_in_zenoh(&mut self, present: bool) -> bool { 84 | let recalc = self.required_on_zenoh_side != present; 85 | self.required_on_zenoh_side = present; 86 | if recalc { 87 | self.recalc_state().await; 88 | } 89 | recalc 90 | } 91 | 92 | pub fn is_bridging(&self) -> bool { 93 | self.bridge.is_some() 94 | } 95 | 96 | pub fn is_actual(&self) -> bool { 97 | self.required_on_ros1_side || self.required_on_zenoh_side 98 | } 99 | 100 | //PRIVATE: 101 | async fn recalc_state(&mut self) { 102 | self.recalc_declaration().await; 103 | self.recalc_bridging().await; 104 | } 105 | 106 | async fn recalc_declaration(&mut self) { 107 | match (self.required_on_ros1_side, &self.declaration) { 108 | (true, None) => { 109 | match self 110 | .declaration_interface 111 | .declare_with_type(&self.topic, self.b_type) 112 | .await 113 | { 114 | Ok(decl) => self.declaration = Some(decl), 115 | Err(e) => error!("{self}: error declaring discovery: {e}"), 116 | } 117 | } 118 | (false, Some(_)) => { 119 | self.declaration = None; 120 | } 121 | (_, _) => {} 122 | } 123 | } 124 | 125 | async fn recalc_bridging(&mut self) { 126 | let is_discovered_client = self.b_type == BridgeType::Client && self.required_on_zenoh_side; 127 | 128 | let is_required = is_discovered_client 129 | || match (self.required_on_zenoh_side, self.required_on_ros1_side) { 130 | (true, true) => true, 131 | (false, false) => false, 132 | (_, _) => self.bridging_mode == BridgingMode::Auto, 133 | }; 134 | 135 | match (is_required, self.bridge.as_ref()) { 136 | (true, Some(_)) => {} 137 | (true, None) => self.create_bridge().await, 138 | (false, _) => self.bridge = None, 139 | } 140 | } 141 | 142 | async fn create_bridge(&mut self) { 143 | match AbstractBridge::new( 144 | self.b_type, 145 | &self.topic, 146 | &self.ros1_client, 147 | &self.zenoh_client, 148 | ) 149 | .await 150 | { 151 | Ok(val) => { 152 | self.bridge = Some(val); 153 | } 154 | Err(e) => { 155 | self.bridge = None; 156 | error!("{self}: error creating bridge: {e}"); 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/aloha_declaration.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::{ 16 | sync::{ 17 | atomic::{AtomicBool, AtomicUsize}, 18 | Arc, 19 | }, 20 | time::Duration, 21 | }; 22 | 23 | use zenoh::{ 24 | internal::buffers::ZBuf, 25 | key_expr::OwnedKeyExpr, 26 | qos::{CongestionControl, Priority, Reliability}, 27 | sample::Locality, 28 | Session, 29 | }; 30 | 31 | use crate::spawn_runtime; 32 | 33 | pub struct AlohaDeclaration { 34 | monitor_running: Arc, 35 | } 36 | impl Drop for AlohaDeclaration { 37 | fn drop(&mut self) { 38 | self.monitor_running 39 | .store(false, std::sync::atomic::Ordering::Relaxed); 40 | } 41 | } 42 | impl AlohaDeclaration { 43 | pub fn new(session: Session, key: OwnedKeyExpr, beacon_period: Duration) -> Self { 44 | let monitor_running = Arc::new(AtomicBool::new(true)); 45 | spawn_runtime(Self::aloha_monitor_task( 46 | beacon_period, 47 | monitor_running.clone(), 48 | key, 49 | session, 50 | )); 51 | Self { monitor_running } 52 | } 53 | 54 | //PRIVATE: 55 | async fn aloha_monitor_task( 56 | beacon_period: Duration, 57 | monitor_running: Arc, 58 | key: OwnedKeyExpr, 59 | session: Session, 60 | ) { 61 | let beacon_task_flag = Arc::new(AtomicBool::new(false)); 62 | 63 | let remote_beacons = Arc::new(AtomicUsize::new(0)); 64 | let rb = remote_beacons.clone(); 65 | let _beacon_listener = session 66 | .declare_subscriber(key.clone()) 67 | .allowed_origin(Locality::Remote) 68 | .callback(move |_| { 69 | rb.fetch_add(1, std::sync::atomic::Ordering::SeqCst); 70 | }) 71 | .await 72 | .unwrap(); 73 | 74 | let mut sending_beacons = true; 75 | Self::start_beacon_task( 76 | beacon_period, 77 | key.clone(), 78 | session.clone(), 79 | beacon_task_flag.clone(), 80 | ); 81 | 82 | while monitor_running.load(std::sync::atomic::Ordering::Relaxed) { 83 | match remote_beacons.fetch_and(0, std::sync::atomic::Ordering::SeqCst) { 84 | 0 => { 85 | if !sending_beacons { 86 | // start publisher in ALOHA style... 87 | let period_ns = beacon_period.as_nanos(); 88 | let aloha_wait: u128 = rand::random::() % period_ns; 89 | tokio::time::sleep(Duration::from_nanos(aloha_wait.try_into().unwrap())) 90 | .await; 91 | if remote_beacons.load(std::sync::atomic::Ordering::SeqCst) == 0 { 92 | Self::start_beacon_task( 93 | beacon_period, 94 | key.clone(), 95 | session.clone(), 96 | beacon_task_flag.clone(), 97 | ); 98 | sending_beacons = true; 99 | } 100 | } 101 | } 102 | _ => { 103 | if sending_beacons && rand::random::() { 104 | Self::stop_beacon_task(beacon_task_flag.clone()); 105 | sending_beacons = false; 106 | } 107 | } 108 | } 109 | tokio::time::sleep(beacon_period).await; 110 | } 111 | Self::stop_beacon_task(beacon_task_flag.clone()); 112 | } 113 | 114 | fn start_beacon_task( 115 | beacon_period: Duration, 116 | key: OwnedKeyExpr, 117 | session: Session, 118 | running: Arc, 119 | ) { 120 | running.store(true, std::sync::atomic::Ordering::SeqCst); 121 | spawn_runtime(Self::aloha_publishing_task( 122 | beacon_period, 123 | key, 124 | session, 125 | running, 126 | )); 127 | } 128 | 129 | fn stop_beacon_task(running: Arc) { 130 | running.store(false, std::sync::atomic::Ordering::Relaxed); 131 | } 132 | 133 | async fn aloha_publishing_task( 134 | beacon_period: Duration, 135 | key: OwnedKeyExpr, 136 | session: Session, 137 | running: Arc, 138 | ) { 139 | let publisher = session 140 | .declare_publisher(key) 141 | .reliability(Reliability::BestEffort) 142 | .allowed_destination(Locality::Remote) 143 | .congestion_control(CongestionControl::Drop) 144 | .priority(Priority::Background) 145 | .await 146 | .unwrap(); 147 | 148 | while running.load(std::sync::atomic::Ordering::Relaxed) { 149 | let _res = publisher.put(ZBuf::default()).await; 150 | tokio::time::sleep(beacon_period).await; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | name: Release 15 | 16 | on: 17 | workflow_dispatch: 18 | inputs: 19 | live-run: 20 | type: boolean 21 | description: Live-run 22 | required: false 23 | default: false 24 | version: 25 | type: string 26 | description: Release number 27 | required: false 28 | zenoh-version: 29 | type: string 30 | description: Zenoh Release number 31 | required: false 32 | branch: 33 | type: string 34 | description: Release branch 35 | required: false 36 | 37 | jobs: 38 | tag: 39 | name: Branch, Bump & tag crates 40 | uses: eclipse-zenoh/ci/.github/workflows/branch-bump-tag-crates.yml@main 41 | with: 42 | repo: ${{ github.repository }} 43 | live-run: ${{ inputs.live-run || false }} 44 | version: ${{ inputs.version }} 45 | branch: ${{ inputs.branch }} 46 | bump-deps-version: | 47 | ${{ inputs.version }} 48 | ${{ inputs.zenoh-version }} 49 | bump-deps-pattern: | 50 | ^zenoh-plugin-ros1$ 51 | ${{ inputs.zenoh-version && 'zenoh.*' || '' }} 52 | bump-deps-branch: ${{ inputs.zenoh-version && format('release/{0}', inputs.zenoh-version) || '' }} 53 | secrets: inherit 54 | 55 | build-debian: 56 | name: Build Debian packages 57 | needs: tag 58 | uses: eclipse-zenoh/ci/.github/workflows/build-crates-debian.yml@main 59 | with: 60 | repo: ${{ github.repository }} 61 | version: ${{ needs.tag.outputs.version }} 62 | branch: ${{ needs.tag.outputs.branch }} 63 | secrets: inherit 64 | 65 | build-standalone: 66 | name: Build executables and libraries 67 | needs: tag 68 | uses: eclipse-zenoh/ci/.github/workflows/build-crates-standalone.yml@main 69 | with: 70 | repo: ${{ github.repository }} 71 | version: ${{ needs.tag.outputs.version }} 72 | branch: ${{ needs.tag.outputs.branch }} 73 | # NOTE: It is possible to build in Windows, but we don't target it for now 74 | exclude-builds: '[{ build: { os: "windows-2019" } }]' 75 | artifact-patterns: | 76 | ^zenoh-bridge-ros1(\.exe)?$ 77 | ^libzenoh_plugin_ros1\.(dylib|so)$ 78 | ^zenoh_plugin_ros1\.dll$ 79 | secrets: inherit 80 | 81 | debian: 82 | name: Publish Debian packages 83 | needs: [tag, build-debian] 84 | uses: eclipse-zenoh/ci/.github/workflows/release-crates-debian.yml@main 85 | with: 86 | no-build: true 87 | live-run: ${{ inputs.live-run || false }} 88 | version: ${{ needs.tag.outputs.version }} 89 | repo: ${{ github.repository }} 90 | branch: ${{ needs.tag.outputs.branch }} 91 | installation-test: false 92 | secrets: inherit 93 | 94 | homebrew: 95 | name: Publish Homebrew formulae 96 | needs: [tag, build-standalone] 97 | uses: eclipse-zenoh/ci/.github/workflows/release-crates-homebrew.yml@main 98 | with: 99 | no-build: true 100 | repo: ${{ github.repository }} 101 | live-run: ${{ inputs.live-run || false }} 102 | version: ${{ needs.tag.outputs.version }} 103 | branch: ${{ needs.tag.outputs.branch }} 104 | artifact-patterns: | 105 | ^zenoh-bridge-ros1$ 106 | ^libzenoh_plugin_ros1\.dylib$ 107 | formulae: | 108 | zenoh-bridge-ros1 109 | zenoh-plugin-ros1 110 | secrets: inherit 111 | 112 | eclipse: 113 | name: Publish artifacts to Eclipse downloads 114 | needs: [tag, build-standalone] 115 | uses: eclipse-zenoh/ci/.github/workflows/release-crates-eclipse.yml@main 116 | with: 117 | no-build: true 118 | live-run: ${{ inputs.live-run || false }} 119 | version: ${{ needs.tag.outputs.version }} 120 | repo: ${{ github.repository }} 121 | branch: ${{ needs.tag.outputs.branch }} 122 | artifact-patterns: | 123 | ^zenoh-bridge-ros1(\.exe)?$ 124 | ^libzenoh_plugin_ros1\.(dylib|so)$ 125 | ^zenoh_plugin_ros1\.dll$ 126 | name: zenoh-plugin-ros1 127 | secrets: inherit 128 | 129 | github: 130 | name: Publish artifacts to GitHub Releases 131 | needs: [tag, build-standalone] 132 | uses: eclipse-zenoh/ci/.github/workflows/release-crates-github.yml@main 133 | with: 134 | no-build: true 135 | live-run: ${{ inputs.live-run || false }} 136 | version: ${{ needs.tag.outputs.version }} 137 | repo: ${{ github.repository }} 138 | branch: ${{ needs.tag.outputs.branch }} 139 | artifact-patterns: | 140 | ^zenoh-bridge-ros1(\.exe)?$ 141 | ^libzenoh_plugin_ros1\.(dylib|so)$ 142 | ^zenoh_plugin_ros1\.dll$ 143 | secrets: inherit 144 | 145 | dockerhub: 146 | name: Publish container image to DockerHub 147 | needs: [tag, build-standalone] 148 | uses: eclipse-zenoh/ci/.github/workflows/release-crates-dockerhub.yml@main 149 | with: 150 | no-build: true 151 | live-run: ${{ inputs.live-run || false }} 152 | version: ${{ needs.tag.outputs.version }} 153 | branch: ${{ needs.tag.outputs.branch }} 154 | repo: ${{ github.repository }} 155 | image: "eclipse/zenoh-bridge-ros1" 156 | binary: zenoh-bridge-ros1 157 | files: | 158 | zenoh-bridge-ros1 159 | libzenoh_plugin_ros1.so 160 | platforms: | 161 | linux/arm64 162 | linux/amd64 163 | licenses: EPL-2.0 OR Apache-2.0 164 | secrets: inherit 165 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/lib.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | #![recursion_limit = "1024"] 15 | 16 | use std::{ 17 | future::Future, 18 | sync::atomic::{AtomicUsize, Ordering}, 19 | time::Duration, 20 | }; 21 | 22 | use ros_to_zenoh_bridge::{ 23 | environment::Environment, ros1_master_ctrl::Ros1MasterCtrl, Ros1ToZenohBridge, 24 | }; 25 | use tokio::task::JoinHandle; 26 | use zenoh::{ 27 | internal::{ 28 | plugins::{RunningPlugin, RunningPluginTrait, ZenohPlugin}, 29 | runtime::Runtime, 30 | }, 31 | Result as ZResult, 32 | }; 33 | use zenoh_plugin_trait::{plugin_long_version, plugin_version, Plugin, PluginControl}; 34 | 35 | use crate::ros_to_zenoh_bridge::environment; 36 | 37 | pub mod ros_to_zenoh_bridge; 38 | 39 | lazy_static::lazy_static! { 40 | static ref WORK_THREAD_NUM: AtomicUsize = AtomicUsize::new(environment::DEFAULT_WORK_THREAD_NUM); 41 | static ref MAX_BLOCK_THREAD_NUM: AtomicUsize = AtomicUsize::new(environment::DEFAULT_MAX_BLOCK_THREAD_NUM); 42 | // The global runtime is used in the dynamic plugins, which we can't get the current runtime 43 | static ref TOKIO_RUNTIME: tokio::runtime::Runtime = tokio::runtime::Builder::new_multi_thread() 44 | .worker_threads(WORK_THREAD_NUM.load(Ordering::SeqCst)) 45 | .max_blocking_threads(MAX_BLOCK_THREAD_NUM.load(Ordering::SeqCst)) 46 | .enable_all() 47 | .build() 48 | .expect("Unable to create runtime"); 49 | } 50 | #[inline(always)] 51 | pub(crate) fn spawn_blocking_runtime(func: F) -> JoinHandle 52 | where 53 | F: FnOnce() -> R + Send + 'static, 54 | R: Send + 'static, 55 | { 56 | // Check whether able to get the current runtime 57 | match tokio::runtime::Handle::try_current() { 58 | Ok(rt) => { 59 | // Able to get the current runtime (standalone binary), use the current runtime 60 | rt.spawn_blocking(func) 61 | } 62 | Err(_) => { 63 | // Unable to get the current runtime (dynamic plugins), reuse the global runtime 64 | TOKIO_RUNTIME.spawn_blocking(func) 65 | } 66 | } 67 | } 68 | #[inline(always)] 69 | pub(crate) fn spawn_runtime(task: F) -> JoinHandle 70 | where 71 | F: Future + Send + 'static, 72 | F::Output: Send + 'static, 73 | { 74 | // Check whether able to get the current runtime 75 | match tokio::runtime::Handle::try_current() { 76 | Ok(rt) => { 77 | // Able to get the current runtime (standalone binary), use the current runtime 78 | rt.spawn(task) 79 | } 80 | Err(_) => { 81 | // Unable to get the current runtime (dynamic plugins), reuse the global runtime 82 | TOKIO_RUNTIME.spawn(task) 83 | } 84 | } 85 | } 86 | #[inline(always)] 87 | pub(crate) fn blockon_runtime(task: F) -> F::Output { 88 | // Check whether able to get the current runtime 89 | match tokio::runtime::Handle::try_current() { 90 | Ok(rt) => { 91 | // Able to get the current runtime (standalone binary), use the current runtime 92 | tokio::task::block_in_place(|| rt.block_on(task)) 93 | } 94 | Err(_) => { 95 | // Unable to get the current runtime (dynamic plugins), reuse the global runtime 96 | tokio::task::block_in_place(|| TOKIO_RUNTIME.block_on(task)) 97 | } 98 | } 99 | } 100 | 101 | // The struct implementing the ZenohPlugin and ZenohPlugin traits 102 | pub struct Ros1Plugin {} 103 | 104 | // declaration of the plugin's VTable for zenohd to find the plugin's functions to be called 105 | #[cfg(feature = "dynamic_plugin")] 106 | zenoh_plugin_trait::declare_plugin!(Ros1Plugin); 107 | 108 | impl ZenohPlugin for Ros1Plugin {} 109 | impl Plugin for Ros1Plugin { 110 | type StartArgs = Runtime; 111 | type Instance = RunningPlugin; 112 | 113 | // A mandatory const to define, in case of the plugin is built as a standalone executable 114 | const DEFAULT_NAME: &'static str = "ros1"; 115 | const PLUGIN_VERSION: &'static str = plugin_version!(); 116 | const PLUGIN_LONG_VERSION: &'static str = plugin_long_version!(); 117 | 118 | // The first operation called by zenohd on the plugin 119 | fn start(name: &str, runtime: &Self::StartArgs) -> ZResult { 120 | // Try to initiate login. 121 | // Required in case of dynamic lib, otherwise no logs. 122 | // But cannot be done twice in case of static link. 123 | zenoh::try_init_log_from_env(); 124 | tracing::debug!("ROS1 plugin {}", Ros1Plugin::PLUGIN_LONG_VERSION); 125 | 126 | let config = runtime.config().lock(); 127 | let self_cfg = config 128 | .plugin(name) 129 | .ok_or("No plugin in the config!")? 130 | .as_object() 131 | .ok_or("Unable to get cfg objet!")?; 132 | tracing::info!("ROS1 config: {:?}", self_cfg); 133 | 134 | // run through the bridge's config options and fill them from plugins config 135 | let plugin_configuration_entries = Environment::env(); 136 | for entry in plugin_configuration_entries.iter() { 137 | if let Some(v) = self_cfg.get(&entry.name.to_lowercase()) { 138 | let str = v.to_string(); 139 | entry.set(str.trim_matches('"').to_string()); 140 | } 141 | } 142 | // Setup the thread numbers 143 | WORK_THREAD_NUM.store(Environment::work_thread_num().get(), Ordering::SeqCst); 144 | MAX_BLOCK_THREAD_NUM.store(Environment::max_block_thread_num().get(), Ordering::SeqCst); 145 | 146 | drop(config); 147 | 148 | // return a RunningPlugin to zenohd 149 | Ok(Box::new(Ros1PluginInstance::new(runtime)?)) 150 | } 151 | } 152 | 153 | // The RunningPlugin struct implementing the RunningPluginTrait trait 154 | struct Ros1PluginInstance { 155 | _bridge: Ros1ToZenohBridge, 156 | } 157 | impl PluginControl for Ros1PluginInstance {} 158 | impl RunningPluginTrait for Ros1PluginInstance {} 159 | impl Drop for Ros1PluginInstance { 160 | fn drop(&mut self) { 161 | if Environment::with_rosmaster().get() { 162 | blockon_runtime(Ros1MasterCtrl::without_ros1_master()); 163 | } 164 | } 165 | } 166 | impl Ros1PluginInstance { 167 | fn new(runtime: &Runtime) -> ZResult { 168 | let bridge: ZResult = blockon_runtime(async { 169 | if Environment::with_rosmaster().get() { 170 | Ros1MasterCtrl::with_ros1_master().await?; 171 | tokio::time::sleep(Duration::from_secs(1)).await; 172 | } 173 | 174 | // create a zenoh Session that shares the same Runtime as zenohd 175 | let session = zenoh::session::init(runtime.clone()).await?; 176 | let bridge = ros_to_zenoh_bridge::Ros1ToZenohBridge::new_with_external_session(session); 177 | Ok(bridge) 178 | }); 179 | 180 | Ok(Self { _bridge: bridge? }) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | [![Discussion](https://img.shields.io/badge/discussion-on%20github-blue)](https://github.com/eclipse-zenoh/roadmap/discussions) 7 | [![Discord](https://img.shields.io/badge/chat-on%20discord-blue)](https://discord.gg/2GJ958VuHs) 8 | [![License](https://img.shields.io/badge/License-EPL%202.0-blue)](https://choosealicense.com/licenses/epl-2.0/) 9 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 10 | 11 | # Eclipse Zenoh 12 | 13 | The Eclipse Zenoh: Zero Overhead Pub/sub, Store/Query and Compute. 14 | 15 | Zenoh (pronounce _/zeno/_) unifies data in motion, data at rest and computations. It carefully blends traditional pub/sub with geo-distributed storages, queries and computations, while retaining a level of time and space efficiency that is well beyond any of the mainstream stacks. 16 | 17 | Check the website [zenoh.io](http://zenoh.io) and the [roadmap](https://github.com/eclipse-zenoh/roadmap) for more detailed information. 18 | 19 | ------------------------------- 20 | 21 | # ROS1 to Zenoh Bridge plugin 22 | 23 | :point_right: **Install latest release:** see [below](#how-to-install-it) 24 | 25 | :point_right: **Docker image:** see [below](#docker-image) 26 | 27 | :point_right: **Build "main" branch:** see [below](#how-to-build-it) 28 | 29 | ## Background 30 | 31 | ROS1 is a well-known mature platform for building robotic systems. Despite the fact that next generation of ROS - ROS2 is released long time ago, many developers still prefer using ROS1. In order to integrate ROS1 systems to Zenoh infrastructure, [as it was done for DDS/ROS2](https://github.com/eclipse-zenoh/zenoh-plugin-dds), ROS1 to Zenoh Bridge was designed. 32 | 33 | ## How to install it 34 | 35 | To install the latest release of either the ROS1 plugin for the Zenoh router, either the `zenoh-bridge-ros1` standalone executable, you can do as follows: 36 | 37 | ### Manual installation (all platforms) 38 | 39 | All release packages can be downloaded from: 40 | 41 | - [https://download.eclipse.org/zenoh/zenoh-plugin-ros1/latest/](https://download.eclipse.org/zenoh/zenoh-plugin-ros1/latest/) 42 | 43 | Each subdirectory has the name of the Rust target. See the platforms each target corresponds to on [https://doc.rust-lang.org/stable/rustc/platform-support.html](https://doc.rust-lang.org/stable/rustc/platform-support.html) 44 | 45 | Choose your platform and download: 46 | 47 | - the `zenoh-plugin-ros1--.zip` file for the plugin. 48 | Then unzip it in the same directory than `zenohd` or to any directory where it can find the plugin library (e.g. /usr/lib) 49 | - the `zenoh-bridge-ros1--.zip` file for the standalone executable. 50 | Then unzip it where you want, and run the extracted `zenoh-bridge-ros1` binary. 51 | 52 | ### Linux Debian 53 | 54 | Add Eclipse Zenoh private repository to the sources list: 55 | 56 | ```bash 57 | echo "deb [trusted=yes] https://download.eclipse.org/zenoh/debian-repo/ /" | sudo tee -a /etc/apt/sources.list > /dev/null 58 | sudo apt update 59 | ``` 60 | 61 | Then either: 62 | 63 | - install the plugin with: `sudo apt install zenoh-plugin-ros1`. 64 | - install the standalone executable with: `sudo apt install zenoh-bridge-ros1`. 65 | 66 | ## How to build it 67 | 68 | > :warning: **WARNING** :warning: : As Rust doesn't have a stable ABI, the plugins should be 69 | built with the exact same Rust version than `zenohd`, and using for `zenoh` dependency the same version (or commit number) than 'zenohd'. 70 | Otherwise, incompatibilities in memory mapping of shared types between `zenohd` and the library can lead to a `"SIGSEV"` crash. 71 | > 72 | 73 | In order to build the ROS1 to Zenoh Bridge, you need first to install the following dependencies: 74 | 75 | - [Rust](https://www.rust-lang.org/tools/install). If you already have the Rust toolchain installed, make sure it is up-to-date with: 76 | 77 | ```bash 78 | rustup update 79 | ``` 80 | 81 | - On Linux, make sure the `llvm` and `clang` development packages are installed: 82 | - on Debians do: `sudo apt install llvm-dev libclang-dev` 83 | - on CentOS or RHEL do: `sudo yum install llvm-devel clang-devel` 84 | - on Alpine do: `apk install llvm11-dev clang-dev` 85 | 86 | Once these dependencies are in place, you may clone the repository on your machine: 87 | 88 | ```bash 89 | git clone https://github.com/eclipse-zenoh/zenoh-plugin-ros1.git 90 | cd zenoh-plugin-ros1 91 | cargo build --release 92 | ``` 93 | 94 | The standalone executable binary `zenoh-bridge-ros1` and a plugin shared library (`*.so` on Linux, `*.dylib` on Mac OS, `*.dll` on Windows) to be dynamically 95 | loaded by the zenoh router `zenohd` will be generated in the `target/release` subdirectory. 96 | 97 | ## Docker image 98 | 99 | The **`zenoh-bridge-ros1`** standalone executable is also available as a [Docker images](https://hub.docker.com/r/eclipse/zenoh-bridge-ros1/tags?page=1&ordering=last_updated) for both amd64 and arm64. To get it, do: 100 | 101 | - `docker pull eclipse/zenoh-bridge-ros1:latest` for the latest release 102 | - `docker pull eclipse/zenoh-bridge-ros1:main` for the main branch version (nightly build) 103 | 104 | Usage: **`docker run --init --net host eclipse/zenoh-bridge-ros1`** 105 | It supports the same command line arguments than the `zenoh-bridge-ros1` (see below or check with `-h` argument). 106 | 107 | ## A quick test with built-in examples 108 | 109 | If you want to run examples or tests, you need to install ROS1: 110 | 111 | ```bash 112 | sudo apt install -y ros-base 113 | ``` 114 | 115 | There is a set of example utilities illustrating bridge in operation. 116 | Here is a description on how to configure the following schema: 117 | 118 | ```raw 119 | _____________________________ ________________________________ 120 | | | | | 121 | | rosmaster_1 | | rosmaster_2 | 122 | | | | | 123 | | ros1_publisher -> zenoh-bridge-ros1 -> zenoh -> zenoh-bridge-ros1 -> ros1_subscriber | 124 | |___________________________| |______________________________| 125 | ``` 126 | 127 | ```bash 128 | # build the bridge from source 129 | cargo build -p zenoh-bridge-ros1 130 | cd target/debug/ 131 | # terminal 1: 132 | ./zenoh-bridge-ros1 --with_rosmaster true --ros_master_uri http://localhost:10000 133 | # terminal 2: 134 | ./zenoh-bridge-ros1 --with_rosmaster true --ros_master_uri http://localhost:10001 135 | # terminal 3: 136 | ROS_MASTER_URI=http://localhost:10000 rostopic pub /topic std_msgs/String -r 1 test_message 137 | # terminal 4: 138 | ROS_MASTER_URI=http://localhost:10001 rostopic echo /topic 139 | ``` 140 | 141 | Once completed, you will see the following exchange between ROS1 publisher and subscriber: 142 | 143 | 144 | ## Implementation 145 | 146 | Currently, ROS1 to Zenoh Bridge is based on [rosrust library fork](https://github.com/ZettaScaleLabs/rosrust). Some limitations are applied due to rosrust's implementation details, and we are re-engineering rosrust to overcome this 147 | 148 | ## Limitations 149 | 150 | - all topic names are bridged as-is 151 | - there is a performance impact coming from rosrust 152 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/environment.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::{collections::HashMap, convert::From, marker::PhantomData, str::FromStr, time::Duration}; 16 | 17 | use duration_string::DurationString; 18 | use rosrust::api::resolve::*; 19 | use tracing::error; 20 | 21 | use super::bridging_mode::BridgingMode; 22 | 23 | pub(crate) const DEFAULT_WORK_THREAD_NUM: usize = 2; 24 | pub(crate) const DEFAULT_MAX_BLOCK_THREAD_NUM: usize = 50; 25 | 26 | #[derive(Clone)] 27 | pub struct Entry<'a, Tvar> 28 | where 29 | Tvar: ToString + FromStr + Clone, 30 | { 31 | pub name: &'a str, 32 | pub default: Tvar, 33 | marker: std::marker::PhantomData, 34 | } 35 | impl<'a, Tvar> Entry<'a, Tvar> 36 | where 37 | Tvar: ToString + FromStr + Clone, 38 | { 39 | fn new(name: &'a str, default: Tvar) -> Entry<'a, Tvar> { 40 | Entry { 41 | name, 42 | default, 43 | marker: PhantomData, 44 | } 45 | } 46 | 47 | pub fn get(&self) -> Tvar { 48 | if let Ok(val) = std::env::var(self.name) { 49 | match Tvar::from_str(&val) { 50 | Ok(val) => return val, 51 | Err(_) => error!("Error parsing settings entry {}", self.name), 52 | } 53 | } 54 | self.default.clone() 55 | } 56 | 57 | pub fn set(&self, value: Tvar) { 58 | std::env::set_var(self.name, value.to_string()); 59 | } 60 | } 61 | 62 | impl<'a> From> for Entry<'a, String> { 63 | fn from(item: Entry<'a, BridgingMode>) -> Entry<'a, String> { 64 | Entry::new(item.name, item.default.to_string()) 65 | } 66 | } 67 | 68 | impl<'a> From> for Entry<'a, String> { 69 | fn from(item: Entry<'a, DurationString>) -> Entry<'a, String> { 70 | Entry::new(item.name, item.default.to_string()) 71 | } 72 | } 73 | 74 | impl<'a> From> for Entry<'a, String> { 75 | fn from(item: Entry<'a, bool>) -> Entry<'a, String> { 76 | Entry::new(item.name, item.default.to_string()) 77 | } 78 | } 79 | 80 | impl<'a> From> for Entry<'a, String> { 81 | fn from(item: Entry<'a, CustomBridgingModes>) -> Entry<'a, String> { 82 | Entry::new(item.name, item.default.to_string()) 83 | } 84 | } 85 | 86 | impl<'a> From> for Entry<'a, String> { 87 | fn from(item: Entry<'a, usize>) -> Entry<'a, String> { 88 | Entry::new(item.name, item.default.to_string()) 89 | } 90 | } 91 | 92 | #[derive(Clone, Default)] 93 | pub struct CustomBridgingModes { 94 | pub modes: HashMap, 95 | } 96 | 97 | impl ToString for CustomBridgingModes { 98 | fn to_string(&self) -> String { 99 | serde_json::to_string(&self.modes).unwrap() 100 | } 101 | } 102 | 103 | impl FromStr for CustomBridgingModes { 104 | type Err = serde_json::Error; 105 | 106 | fn from_str(s: &str) -> Result { 107 | let modes: HashMap = serde_json::from_str(s)?; 108 | Ok(Self { modes }) 109 | } 110 | } 111 | 112 | pub struct Environment; 113 | impl Environment { 114 | pub fn ros_master_uri() -> Entry<'static, String> { 115 | return Entry::new("ROS_MASTER_URI", master()); 116 | } 117 | 118 | pub fn ros_hostname() -> Entry<'static, String> { 119 | return Entry::new("ROS_HOSTNAME", hostname()); 120 | } 121 | 122 | pub fn ros_name() -> Entry<'static, String> { 123 | return Entry::new("ROS_NAME", name("ros1_to_zenoh_bridge")); 124 | } 125 | 126 | pub fn ros_namespace() -> Entry<'static, String> { 127 | return Entry::new("ROS_NAMESPACE", namespace()); 128 | } 129 | 130 | pub fn bridge_namespace() -> Entry<'static, String> { 131 | return Entry::new("BRIDGE_NAMESPACE", "*".to_string()); 132 | } 133 | 134 | pub fn with_rosmaster() -> Entry<'static, bool> { 135 | return Entry::new("WITH_ROSMASTER", false); 136 | } 137 | 138 | pub fn subscriber_bridging_mode() -> Entry<'static, BridgingMode> { 139 | return Entry::new("SUBSCRIBER_BRIDGING_MODE", BridgingMode::Auto); 140 | } 141 | pub fn subscriber_topic_custom_bridging_mode() -> Entry<'static, CustomBridgingModes> { 142 | return Entry::new( 143 | "SUBSCRIBER_TOPIC_CUSTOM_BRIDGING_MODE", 144 | CustomBridgingModes::default(), 145 | ); 146 | } 147 | 148 | pub fn publisher_bridging_mode() -> Entry<'static, BridgingMode> { 149 | return Entry::new("PUBLISHER_BRIDGING_MODE", BridgingMode::Auto); 150 | } 151 | pub fn publisher_topic_custom_bridging_mode() -> Entry<'static, CustomBridgingModes> { 152 | return Entry::new( 153 | "PUBLISHER_TOPIC_CUSTOM_BRIDGING_MODE", 154 | CustomBridgingModes::default(), 155 | ); 156 | } 157 | 158 | pub fn service_bridging_mode() -> Entry<'static, BridgingMode> { 159 | return Entry::new("SERVICE_BRIDGING_MODE", BridgingMode::Auto); 160 | } 161 | pub fn service_topic_custom_bridging_mode() -> Entry<'static, CustomBridgingModes> { 162 | return Entry::new( 163 | "SERVICE_TOPIC_CUSTOM_BRIDGING_MODE", 164 | CustomBridgingModes::default(), 165 | ); 166 | } 167 | 168 | pub fn client_bridging_mode() -> Entry<'static, BridgingMode> { 169 | return Entry::new("CLIENT_BRIDGING_MODE", BridgingMode::Disabled); 170 | } 171 | pub fn client_topic_custom_bridging_mode() -> Entry<'static, CustomBridgingModes> { 172 | return Entry::new( 173 | "CLIENT_TOPIC_CUSTOM_BRIDGING_MODE", 174 | CustomBridgingModes::default(), 175 | ); 176 | } 177 | 178 | pub fn master_polling_interval() -> Entry<'static, DurationString> { 179 | return Entry::new( 180 | "ROS_MASTER_POLLING_INTERVAL", 181 | DurationString::from(Duration::from_millis(100)), 182 | ); 183 | } 184 | 185 | pub fn work_thread_num() -> Entry<'static, usize> { 186 | return Entry::new("WORK_THREAD_NUM", DEFAULT_WORK_THREAD_NUM); 187 | } 188 | 189 | pub fn max_block_thread_num() -> Entry<'static, usize> { 190 | return Entry::new("MAX_BLOCK_THREAD_NUM", DEFAULT_MAX_BLOCK_THREAD_NUM); 191 | } 192 | 193 | pub fn env() -> Vec> { 194 | [ 195 | Self::ros_master_uri(), 196 | Self::ros_hostname(), 197 | Self::ros_name(), 198 | Self::ros_namespace(), 199 | Self::bridge_namespace(), 200 | Self::subscriber_bridging_mode().into(), 201 | Self::publisher_bridging_mode().into(), 202 | Self::service_bridging_mode().into(), 203 | Self::client_bridging_mode().into(), 204 | Self::subscriber_topic_custom_bridging_mode().into(), 205 | Self::publisher_topic_custom_bridging_mode().into(), 206 | Self::service_topic_custom_bridging_mode().into(), 207 | Self::client_topic_custom_bridging_mode().into(), 208 | Self::master_polling_interval().into(), 209 | Self::with_rosmaster().into(), 210 | Self::work_thread_num().into(), 211 | Self::max_block_thread_num().into(), 212 | ] 213 | .to_vec() 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/ros1_to_zenoh_bridge_impl.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::{ 16 | sync::{ 17 | atomic::{AtomicBool, Ordering::Relaxed}, 18 | Arc, 19 | }, 20 | time::Duration, 21 | }; 22 | 23 | use tokio::sync::Mutex; 24 | use tracing::{debug, error}; 25 | use zenoh::{self, internal::zasynclock, Result as ZResult}; 26 | 27 | use super::{ 28 | discovery::{RemoteResources, RemoteResourcesBuilder}, 29 | resource_cache::Ros1ResourceCache, 30 | ros1_client::Ros1Client, 31 | }; 32 | use crate::{ 33 | ros_to_zenoh_bridge::{ 34 | bridges_storage::BridgesStorage, discovery::LocalResources, environment::Environment, 35 | ros1_client, topic_mapping, zenoh_client, 36 | }, 37 | spawn_blocking_runtime, 38 | }; 39 | 40 | #[derive(PartialEq, Clone, Copy)] 41 | pub enum RosStatus { 42 | Unknown, 43 | Ok, 44 | Error, 45 | } 46 | 47 | #[derive(Default, PartialEq, Eq, Clone, Copy)] 48 | pub struct BridgeStatus { 49 | pub ros_publishers: (usize, usize), 50 | pub ros_subscribers: (usize, usize), 51 | pub ros_services: (usize, usize), 52 | pub ros_clients: (usize, usize), 53 | } 54 | 55 | pub async fn work_cycle( 56 | ros_master_uri: &str, 57 | session: zenoh::Session, 58 | flag: Arc, 59 | ros_status_callback: RosStatusCallback, 60 | statistics_callback: BridgeStatisticsCallback, 61 | ) -> ZResult<()> 62 | where 63 | RosStatusCallback: Fn(RosStatus), 64 | BridgeStatisticsCallback: Fn(BridgeStatus), 65 | { 66 | let bridge_ros_node_name = Environment::ros_name().get(); 67 | 68 | let ros1_client = Arc::new(ros1_client::Ros1Client::new( 69 | &bridge_ros_node_name, 70 | ros_master_uri, 71 | )?); 72 | 73 | let aux_ros_node_name = format!("{}_service_cache_node", bridge_ros_node_name); 74 | 75 | let ros1_resource_cache = Ros1ResourceCache::new( 76 | &aux_ros_node_name, 77 | ros1_client.ros.name().to_owned(), 78 | ros_master_uri, 79 | )?; 80 | 81 | let zenoh_client = Arc::new(zenoh_client::ZenohClient::new(session.clone())); 82 | 83 | let local_resources = Arc::new(LocalResources::new( 84 | "*".to_string(), 85 | Environment::bridge_namespace().get(), 86 | session.clone(), 87 | )); 88 | 89 | let bridges = Arc::new(Mutex::new(BridgesStorage::new( 90 | ros1_client.clone(), 91 | zenoh_client, 92 | local_resources, 93 | ))); 94 | 95 | let _remote_resources_discovery = 96 | make_remote_resources_discovery(session.clone(), bridges.clone()).await; 97 | 98 | let mut bridge = RosToZenohBridge::new(ros_status_callback, statistics_callback); 99 | bridge 100 | .run(ros1_client, ros1_resource_cache, bridges, flag) 101 | .await; 102 | Ok(()) 103 | } 104 | 105 | async fn make_remote_resources_discovery<'a>( 106 | session: zenoh::Session, 107 | bridges: Arc>, 108 | ) -> RemoteResources { 109 | let bridges2 = bridges.clone(); 110 | 111 | let builder = RemoteResourcesBuilder::new( 112 | "*".to_string(), 113 | Environment::bridge_namespace().get(), 114 | session, 115 | ); 116 | builder 117 | .on_discovered(move |b_type, topic| { 118 | let bridges = bridges.clone(); 119 | Box::new(Box::pin(async move { 120 | zasynclock!(bridges) 121 | .complementary_for(b_type) 122 | .complementary_entity_discovered(topic) 123 | .await; 124 | })) 125 | }) 126 | .on_lost(move |b_type, topic| { 127 | let bridges = bridges2.clone(); 128 | Box::new(Box::pin(async move { 129 | zasynclock!(bridges) 130 | .complementary_for(b_type) 131 | .complementary_entity_lost(topic) 132 | .await; 133 | })) 134 | }) 135 | .build() 136 | .await 137 | } 138 | 139 | struct RosToZenohBridge 140 | where 141 | RosStatusCallback: Fn(RosStatus), 142 | BridgeStatisticsCallback: Fn(BridgeStatus), 143 | { 144 | ros_status: RosStatus, 145 | ros_status_callback: RosStatusCallback, 146 | 147 | statistics_callback: BridgeStatisticsCallback, 148 | } 149 | 150 | impl 151 | RosToZenohBridge 152 | where 153 | RosStatusCallback: Fn(RosStatus), 154 | BridgeStatisticsCallback: Fn(BridgeStatus), 155 | { 156 | // PUBLIC 157 | pub fn new( 158 | ros_status_callback: RosStatusCallback, 159 | statistics_callback: BridgeStatisticsCallback, 160 | ) -> Self { 161 | RosToZenohBridge { 162 | ros_status: RosStatus::Unknown, 163 | ros_status_callback, 164 | 165 | statistics_callback, 166 | } 167 | } 168 | 169 | pub async fn run( 170 | &mut self, 171 | ros1_client: Arc, 172 | mut ros1_resource_cache: Ros1ResourceCache, 173 | bridges: Arc>, 174 | flag: Arc, 175 | ) { 176 | let poll_interval: Duration = Environment::master_polling_interval().get().into(); 177 | 178 | while flag.load(Relaxed) { 179 | let cl = ros1_client.clone(); 180 | let (ros1_state, returned_cache) = spawn_blocking_runtime(move || { 181 | ( 182 | topic_mapping::Ros1TopicMapping::topic_mapping( 183 | cl.as_ref(), 184 | &mut ros1_resource_cache, 185 | ), 186 | ros1_resource_cache, 187 | ) 188 | }) 189 | .await 190 | .expect("Unable to complete the task"); 191 | ros1_resource_cache = returned_cache; 192 | 193 | debug!("ros state: {:#?}", ros1_state); 194 | 195 | match ros1_state { 196 | Ok(mut ros1_state_val) => { 197 | self.transit_ros_status(RosStatus::Ok); 198 | 199 | let smth_changed; 200 | { 201 | let mut locked = zasynclock!(bridges); 202 | smth_changed = locked.receive_ros1_state(&mut ros1_state_val).await; 203 | self.report_bridge_statistics(&locked); 204 | } 205 | 206 | tokio::time::sleep({ 207 | if smth_changed { 208 | poll_interval / 2 209 | } else { 210 | poll_interval 211 | } 212 | }) 213 | .await; 214 | } 215 | Err(e) => { 216 | error!("Error reading ROS state: {}", e); 217 | 218 | self.transit_ros_status(RosStatus::Error); 219 | { 220 | let mut locked = zasynclock!(bridges); 221 | Self::cleanup(&mut locked); 222 | self.report_bridge_statistics(&locked); 223 | } 224 | tokio::time::sleep(poll_interval).await; 225 | } 226 | } 227 | } 228 | } 229 | 230 | // PRIVATE 231 | fn transit_ros_status(&mut self, new_ros_status: RosStatus) { 232 | if self.ros_status != new_ros_status { 233 | self.ros_status = new_ros_status; 234 | (self.ros_status_callback)(self.ros_status); 235 | } 236 | } 237 | 238 | fn report_bridge_statistics(&self, locked: &BridgesStorage) { 239 | (self.statistics_callback)(locked.status()); 240 | } 241 | 242 | fn cleanup(locked: &mut BridgesStorage) { 243 | locked.clear(); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/aloha_subscription.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::{ 16 | collections::{hash_map::Entry::*, HashMap}, 17 | sync::{ 18 | atomic::{AtomicBool, Ordering::Relaxed}, 19 | Arc, 20 | }, 21 | time::Duration, 22 | }; 23 | 24 | use futures::{join, Future, FutureExt}; 25 | use tokio::sync::Mutex; 26 | use tracing::error; 27 | use zenoh::{ 28 | handlers::FifoChannelHandler, key_expr::OwnedKeyExpr, sample::Sample, Result as ZResult, 29 | Session, 30 | }; 31 | 32 | use crate::spawn_runtime; 33 | 34 | struct AlohaResource { 35 | activity: AtomicBool, 36 | } 37 | impl AlohaResource { 38 | fn new() -> Self { 39 | Self { 40 | activity: AtomicBool::new(true), 41 | } 42 | } 43 | 44 | pub fn update(&mut self) { 45 | self.activity.store(true, Relaxed); 46 | } 47 | 48 | pub fn reset(&mut self) { 49 | self.activity.store(false, Relaxed); 50 | } 51 | 52 | pub fn is_active(&self) -> bool { 53 | self.activity.load(Relaxed) 54 | } 55 | } 56 | 57 | pub type TCallback = dyn Fn(zenoh::key_expr::KeyExpr) -> Box + Unpin + Send> 58 | + Send 59 | + Sync 60 | + 'static; 61 | 62 | pub struct AlohaSubscription { 63 | task_running: Arc, 64 | } 65 | 66 | impl Drop for AlohaSubscription { 67 | fn drop(&mut self) { 68 | self.task_running.store(false, Relaxed); 69 | } 70 | } 71 | impl AlohaSubscription { 72 | pub async fn new( 73 | session: Session, 74 | key: OwnedKeyExpr, 75 | beacon_period: Duration, 76 | on_resource_declared: F, 77 | on_resource_undeclared: F, 78 | ) -> ZResult 79 | where 80 | F: Fn(zenoh::key_expr::KeyExpr) -> Box + Unpin + Send> 81 | + Send 82 | + Sync 83 | + 'static, 84 | { 85 | let task_running = Arc::new(AtomicBool::new(true)); 86 | 87 | spawn_runtime(AlohaSubscription::task( 88 | task_running.clone(), 89 | key, 90 | beacon_period, 91 | session, 92 | on_resource_declared, 93 | on_resource_undeclared, 94 | )); 95 | 96 | Ok(Self { task_running }) 97 | } 98 | 99 | //PRIVATE: 100 | async fn task( 101 | task_running: Arc, 102 | key: OwnedKeyExpr, 103 | beacon_period: Duration, 104 | session: Session, 105 | on_resource_declared: F, 106 | on_resource_undeclared: F, 107 | ) -> ZResult<()> 108 | where 109 | F: Fn(zenoh::key_expr::KeyExpr) -> Box + Unpin + Send> 110 | + Send 111 | + Sync 112 | + 'static, 113 | { 114 | let accumulating_resources = Mutex::new(HashMap::::new()); 115 | let subscriber = session.declare_subscriber(key).await?; 116 | 117 | let listen = Self::listening_task( 118 | task_running.clone(), 119 | &accumulating_resources, 120 | &subscriber, 121 | &on_resource_declared, 122 | ) 123 | .fuse(); 124 | 125 | let listen_timeout = Self::accumulating_task( 126 | task_running, 127 | beacon_period * 3, 128 | &accumulating_resources, 129 | on_resource_undeclared, 130 | ) 131 | .fuse(); 132 | 133 | join!(listen, listen_timeout); 134 | 135 | Ok(()) 136 | } 137 | 138 | async fn listening_task<'a, F>( 139 | task_running: Arc, 140 | accumulating_resources: &Mutex>, 141 | subscriber: &'a zenoh::pubsub::Subscriber>, 142 | on_resource_declared: &F, 143 | ) where 144 | F: Fn(zenoh::key_expr::KeyExpr) -> Box + Unpin + Send> 145 | + Send 146 | + Sync 147 | + 'static, 148 | { 149 | while task_running.load(Relaxed) { 150 | match subscriber.recv_async().await { 151 | Ok(val) => match accumulating_resources 152 | .lock() 153 | .await 154 | .entry(val.key_expr().as_keyexpr().into()) 155 | { 156 | Occupied(mut val) => { 157 | val.get_mut().update(); 158 | } 159 | Vacant(entry) => { 160 | on_resource_declared(entry.key().into()).await; 161 | entry.insert(AlohaResource::new()); 162 | } 163 | }, 164 | Err(e) => { 165 | error!("Listening error: {}", e); 166 | } 167 | } 168 | } 169 | } 170 | 171 | async fn accumulating_task<'a, F>( 172 | task_running: Arc, 173 | accumulate_period: Duration, 174 | accumulating_resources: &Mutex>, 175 | on_resource_undeclared: F, 176 | ) where 177 | F: Fn(zenoh::key_expr::KeyExpr) -> Box + Unpin + Send> 178 | + Send 179 | + Sync 180 | + 'static, 181 | { 182 | while task_running.load(Relaxed) { 183 | accumulating_resources 184 | .lock() 185 | .await 186 | .iter_mut() 187 | .for_each(|val| { 188 | val.1.reset(); 189 | }); 190 | 191 | tokio::time::sleep(accumulate_period).await; 192 | 193 | for (key, val) in accumulating_resources.lock().await.iter() { 194 | if !val.is_active() { 195 | on_resource_undeclared(key.into()).await; 196 | } 197 | } 198 | 199 | accumulating_resources 200 | .lock() 201 | .await 202 | .retain(|_key, val| val.is_active()); 203 | } 204 | } 205 | } 206 | 207 | pub struct AlohaSubscriptionBuilder { 208 | session: Session, 209 | key: OwnedKeyExpr, 210 | beacon_period: Duration, 211 | 212 | on_resource_declared: Option>, 213 | on_resource_undeclared: Option>, 214 | } 215 | 216 | impl AlohaSubscriptionBuilder { 217 | pub fn new(session: Session, key: OwnedKeyExpr, beacon_period: Duration) -> Self { 218 | Self { 219 | session, 220 | key, 221 | beacon_period, 222 | on_resource_declared: None, 223 | on_resource_undeclared: None, 224 | } 225 | } 226 | 227 | pub fn on_resource_declared(mut self, on_resource_declared: F) -> Self 228 | where 229 | F: Fn(zenoh::key_expr::KeyExpr) -> Box + Unpin + Send> 230 | + Send 231 | + Sync 232 | + 'static, 233 | { 234 | self.on_resource_declared = Some(Box::new(on_resource_declared)); 235 | self 236 | } 237 | 238 | pub fn on_resource_undeclared(mut self, on_resource_undeclared: F) -> Self 239 | where 240 | F: Fn(zenoh::key_expr::KeyExpr) -> Box + Unpin + Send> 241 | + Send 242 | + Sync 243 | + 'static, 244 | { 245 | self.on_resource_undeclared = Some(Box::new(on_resource_undeclared)); 246 | self 247 | } 248 | 249 | pub async fn build(self) -> ZResult { 250 | AlohaSubscription::new( 251 | self.session, 252 | self.key, 253 | self.beacon_period, 254 | self.on_resource_declared 255 | .unwrap_or(Box::new(|_dummy| Box::new(Box::pin(async {})))), 256 | self.on_resource_undeclared 257 | .unwrap_or(Box::new(|_dummy| Box::new(Box::pin(async {})))), 258 | ) 259 | .await 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /DEFAULT_CONFIG.json5: -------------------------------------------------------------------------------- 1 | //// 2 | //// This file presents the default configuration used by `zenoh-plugin-ros1` plugin. 3 | //// The "ros1" JSON5 object below can be used as such in the "plugins" part of a config file for the zenoh router (zenohd). 4 | //// 5 | { 6 | plugins: { 7 | //// 8 | //// ROS1 bridge related configuration 9 | //// All settings are optional and are unset by default - uncomment the ones you want to set 10 | //// 11 | ros1: { 12 | //// 13 | //// ros_master_uri: A URI of the ROS1 Master to connect to, the defailt is http://localhost:11311/ 14 | //// 15 | // ros_master_uri: "http://localhost:11311/", 16 | 17 | //// 18 | //// ros_hostname: A hostname to send to ROS1 Master, the default is system's hostname 19 | //// 20 | // ros_hostname: "hostname", 21 | 22 | //// 23 | //// ros_name: A bridge node's name for ROS1, the default is "ros1_to_zenoh_bridge" 24 | //// 25 | // ros_name: "ros1_to_zenoh_bridge", 26 | 27 | //// 28 | //// bridge_namespace: A bridge's namespace in terms of zenoh keys, the default is "*", the wildcard namespace. 29 | //// The generated zenoh keys will be in the form of {data_type}/{md5}/{bridge_namespace}/{topic}. 30 | //// 31 | // bridge_namespace: "*", 32 | 33 | //// 34 | //// with_rosmaster: An option wether the bridge should run it's own rosmaster process, the default is "false" 35 | //// 36 | // with_rosmaster: "false", 37 | 38 | //// 39 | //// subscriber_bridging_mode: Global subscriber's topic bridging mode. Accepted values: 40 | //// - "auto"(default) - bridge topics once they are declared locally or discovered remotely 41 | //// - "lazy_auto" - bridge topics once they are both declared locally and discovered remotely 42 | //// - "disabled" - never bridge topics. This setting will also suppress the topic discovery."# 43 | // subscriber_bridging_mode: "auto", 44 | 45 | //// 46 | //// publisher_bridging_mode: Global publisher's topic bridging mode. Accepted values: 47 | //// - "auto"(default) - bridge topics once they are declared locally or discovered remotely 48 | //// - "lazy_auto" - bridge topics once they are both declared locally and discovered remotely 49 | //// - "disabled" - never bridge topics. This setting will also suppress the topic discovery."# 50 | // publisher_bridging_mode: "auto", 51 | 52 | //// 53 | //// service_bridging_mode: Global service's topic bridging mode. Accepted values: 54 | //// - "auto"(default) - bridge topics once they are declared locally or discovered remotely 55 | //// - "lazy_auto" - bridge topics once they are both declared locally and discovered remotely 56 | //// - "disabled" - never bridge topics. This setting will also suppress the topic discovery."# 57 | // service_bridging_mode: "auto", 58 | 59 | //// 60 | //// client_bridging_mode: Mode of client's topic bridging. Accepted values: 61 | //// - "auto" - bridge topics once they are discovered remotely 62 | //// - "disabled"(default) - never bridge topics. This setting will also suppress the topic discovery. 63 | //// NOTE: there are some pecularities on how ROS1 handles clients: 64 | //// - ROS1 doesn't provide any client discovery mechanism 65 | //// - ROS1 doesn't allow multiple services on the same topic 66 | //// Due to this, client's bridging works differently compared to pub\sub bridging: 67 | //// - lazy bridging mode is not available as there is no way to discover local ROS1 clients 68 | //// - client bridging is disabled by default, as it may break the local ROS1 system if it intends to have client and service interacting on the same topic 69 | //// In order to use client bridging, you have two options: 70 | //// - globally select auto bridging mode (with caution!) with this option 71 | //// - bridge specific topics using 'client_topic_custom_bridging_mode' option (with a little bit less caution!)"# 72 | // client_bridging_mode: "disabled", 73 | 74 | //// 75 | //// subscriber_topic_custom_bridging_mode: A JSON Map describing custom bridging modes for particular topics. 76 | //// Custom bridging mode overrides the global one. 77 | //// Format: {"topic1":"mode", "topic2":"mode"} 78 | //// Example: {"/my/topic1":"lazy_auto","/my/topic2":"auto"} 79 | //// where 80 | //// - topic: ROS1 topic name 81 | //// - mode (auto/lazy_auto/disabled) as described above 82 | //// The default is empty 83 | // subscriber_topic_custom_bridging_mode: "" 84 | 85 | //// 86 | //// publisher_topic_custom_bridging_mode: A JSON Map describing custom bridging modes for particular topics. 87 | //// Custom bridging mode overrides the global one. 88 | //// Format: {"topic1":"mode", "topic2":"mode"} 89 | //// Example: {"/my/topic1":"lazy_auto","/my/topic2":"auto"} 90 | //// where 91 | //// - topic: ROS1 topic name 92 | //// - mode (auto/lazy_auto/disabled) as described above 93 | //// The default is empty 94 | // publisher_topic_custom_bridging_mode: "" 95 | 96 | //// 97 | //// service_topic_custom_bridging_mode: A JSON Map describing custom bridging modes for particular topics. 98 | //// Custom bridging mode overrides the global one. 99 | //// Format: {"topic1":"mode", "topic2":"mode"} 100 | //// Example: {"/my/topic1":"lazy_auto","/my/topic2":"auto"} 101 | //// where 102 | //// - topic: ROS1 topic name 103 | //// - mode (auto/lazy_auto/disabled) as described above 104 | //// The default is empty 105 | // service_topic_custom_bridging_mode: "" 106 | 107 | //// 108 | //// client_topic_custom_bridging_mode: A JSON Map describing custom bridging modes for particular topics. 109 | //// Custom bridging mode overrides the global one. 110 | //// Format: {"topic1":"mode", "topic2":"mode"} 111 | //// Example: {"/my/topic1":"auto","/my/topic2":"auto"} 112 | //// where 113 | //// - topic: ROS1 topic name 114 | //// - mode (auto/disabled) as described above 115 | //// The default is empty 116 | // client_topic_custom_bridging_mode: "" 117 | 118 | //// 119 | //// ros_master_polling_interval: An interval how to poll the ROS1 master for status 120 | //// Bridge polls ROS1 master to get information on local topics, as this is the only way to keep 121 | //// this info updated. This is the interval of this polling. The default is "100ms" 122 | //// 123 | //// Takes a string such as 100ms, 2s, 5m 124 | //// The string format is [0-9]+(ns|us|ms|[smhdwy]) 125 | //// 126 | // ros_master_polling_interval: "100ms", 127 | 128 | //// 129 | //// This plugin uses Tokio (https://tokio.rs/) for asynchronous programming. 130 | //// When running as a plugin within a Zenoh router, the plugin creates its own Runtime managing 2 pools of threads: 131 | //// - worker threads for non-blocking tasks. Those threads are spawn at Runtime creation. 132 | //// - blocking threads for blocking tasks (e.g. I/O). Those threads are spawn when needed. 133 | //// For more details see https://github.com/tokio-rs/tokio/discussions/3858#discussioncomment-869878 134 | //// When running as a standalone bridge the Zenoh Session's Runtime is used and can be configured via the 135 | //// `ZENOH_RUNTIME` environment variable. See https://docs.rs/zenoh-runtime/latest/zenoh_runtime/enum.ZRuntime.html 136 | //// 137 | 138 | //// work_thread_num: The number of worker thread in the asynchronous runtime will use. (default: 2) 139 | //// Only for a plugin, no effect on a bridge. 140 | // work_thread_num: 2, 141 | 142 | //// max_block_thread_num: The number of blocking thread in the asynchronous runtime will use. (default: 50) 143 | //// Only for a plugin, no effect on a bridge. 144 | // max_block_thread_num: 50, 145 | }, 146 | 147 | //// 148 | //// REST API configuration (active only if this part is defined) 149 | //// 150 | // rest: { 151 | // //// 152 | // //// The HTTP port number (for all network interfaces). 153 | // //// You can bind on a specific interface setting a ":" string. 154 | // //// 155 | // http_port: 8000, 156 | // }, 157 | }, 158 | 159 | //// 160 | //// zenoh related configuration (see zenoh documentation for more details) 161 | //// 162 | 163 | //// 164 | //// id: The identifier (as hex-string) that zenoh-bridge-ros1 must use. If not set, a random UUIDv4 will be used. 165 | //// WARNING: this id must be unique in your zenoh network. 166 | // id: "A00001", 167 | 168 | //// 169 | //// mode: The bridge's mode (peer or client) 170 | //// 171 | //mode: "client", 172 | 173 | //// 174 | //// Which endpoints to connect to. E.g. tcp/localhost:7447. 175 | //// By configuring the endpoints, it is possible to tell zenoh which router/peer to connect to at startup. 176 | //// 177 | connect: { 178 | endpoints: [ 179 | // "/:" 180 | ] 181 | }, 182 | 183 | //// 184 | //// Which endpoints to listen on. E.g. tcp/localhost:7447. 185 | //// By configuring the endpoints, it is possible to tell zenoh which are the endpoints that other routers, 186 | //// peers, or client can use to establish a zenoh session. 187 | //// 188 | listen: { 189 | endpoints: [ 190 | // "/:" 191 | ] 192 | }, 193 | } 194 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/bridges_storage.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::{ 16 | collections::{hash_map::Entry, HashMap, HashSet}, 17 | sync::Arc, 18 | }; 19 | 20 | use super::{ 21 | bridge_type::BridgeType, 22 | bridging_mode::{bridging_mode, BridgingMode}, 23 | discovery::LocalResources, 24 | ros1_client, 25 | ros1_to_zenoh_bridge_impl::BridgeStatus, 26 | topic_bridge::TopicBridge, 27 | topic_descriptor::TopicDescriptor, 28 | topic_mapping::Ros1TopicMapping, 29 | zenoh_client, 30 | }; 31 | 32 | struct Bridges { 33 | publisher_bridges: HashMap, 34 | subscriber_bridges: HashMap, 35 | service_bridges: HashMap, 36 | client_bridges: HashMap, 37 | } 38 | impl Bridges { 39 | fn new() -> Self { 40 | Self { 41 | publisher_bridges: HashMap::new(), 42 | subscriber_bridges: HashMap::new(), 43 | service_bridges: HashMap::new(), 44 | client_bridges: HashMap::new(), 45 | } 46 | } 47 | 48 | fn container_mut(&mut self, b_type: BridgeType) -> &mut HashMap { 49 | match b_type { 50 | BridgeType::Publisher => &mut self.publisher_bridges, 51 | BridgeType::Subscriber => &mut self.subscriber_bridges, 52 | BridgeType::Service => &mut self.service_bridges, 53 | BridgeType::Client => &mut self.client_bridges, 54 | } 55 | } 56 | 57 | fn status(&self) -> BridgeStatus { 58 | let fill = |status: &mut (usize, usize), 59 | bridges: &HashMap| { 60 | for (_topic, bridge) in bridges.iter() { 61 | status.0 += 1; 62 | if bridge.is_bridging() { 63 | status.1 += 1; 64 | } 65 | } 66 | }; 67 | 68 | let mut result = BridgeStatus::default(); 69 | fill(&mut result.ros_clients, &self.client_bridges); 70 | fill(&mut result.ros_publishers, &self.publisher_bridges); 71 | fill(&mut result.ros_services, &self.service_bridges); 72 | fill(&mut result.ros_subscribers, &self.subscriber_bridges); 73 | result 74 | } 75 | 76 | fn clear(&mut self) { 77 | self.publisher_bridges.clear(); 78 | self.subscriber_bridges.clear(); 79 | self.service_bridges.clear(); 80 | self.client_bridges.clear(); 81 | } 82 | } 83 | 84 | struct Access<'a> { 85 | container: &'a mut HashMap, 86 | b_type: BridgeType, 87 | ros1_client: Arc, 88 | zenoh_client: Arc, 89 | declaration_interface: Arc, 90 | } 91 | 92 | impl<'a> Access<'a> { 93 | fn new( 94 | b_type: BridgeType, 95 | container: &'a mut HashMap, 96 | ros1_client: Arc, 97 | zenoh_client: Arc, 98 | declaration_interface: Arc, 99 | ) -> Self { 100 | Self { 101 | container, 102 | b_type, 103 | ros1_client, 104 | zenoh_client, 105 | declaration_interface, 106 | } 107 | } 108 | } 109 | 110 | pub struct ComplementaryElementAccessor<'a> { 111 | access: Access<'a>, 112 | } 113 | 114 | impl<'a> ComplementaryElementAccessor<'a> { 115 | fn new( 116 | b_type: BridgeType, 117 | container: &'a mut HashMap, 118 | ros1_client: Arc, 119 | zenoh_client: Arc, 120 | declaration_interface: Arc, 121 | ) -> Self { 122 | Self { 123 | access: Access::new( 124 | b_type, 125 | container, 126 | ros1_client, 127 | zenoh_client, 128 | declaration_interface, 129 | ), 130 | } 131 | } 132 | 133 | pub async fn complementary_entity_lost(&mut self, topic: TopicDescriptor) { 134 | match self.access.container.entry(topic) { 135 | Entry::Occupied(mut val) => { 136 | let bridge = val.get_mut(); 137 | if bridge.set_has_complementary_in_zenoh(false).await && !bridge.is_actual() { 138 | val.remove_entry(); 139 | } 140 | } 141 | Entry::Vacant(_) => {} 142 | } 143 | } 144 | 145 | pub async fn complementary_entity_discovered(&mut self, topic: TopicDescriptor) { 146 | let b_mode = bridging_mode(self.access.b_type, topic.name.as_str()); 147 | if b_mode != BridgingMode::Disabled { 148 | match self.access.container.entry(topic) { 149 | Entry::Occupied(mut val) => { 150 | val.get_mut().set_has_complementary_in_zenoh(true).await; 151 | } 152 | Entry::Vacant(val) => { 153 | let key = val.key().clone(); 154 | let inserted = val.insert(TopicBridge::new( 155 | key, 156 | self.access.b_type, 157 | self.access.declaration_interface.clone(), 158 | self.access.ros1_client.clone(), 159 | self.access.zenoh_client.clone(), 160 | b_mode, 161 | )); 162 | inserted.set_has_complementary_in_zenoh(true).await; 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | pub struct ElementAccessor<'a> { 170 | access: Access<'a>, 171 | } 172 | 173 | impl<'a> ElementAccessor<'a> { 174 | fn new( 175 | b_type: BridgeType, 176 | container: &'a mut HashMap, 177 | ros1_client: Arc, 178 | zenoh_client: Arc, 179 | declaration_interface: Arc, 180 | ) -> Self { 181 | Self { 182 | access: Access::new( 183 | b_type, 184 | container, 185 | ros1_client, 186 | zenoh_client, 187 | declaration_interface, 188 | ), 189 | } 190 | } 191 | 192 | async fn receive_ros1_state( 193 | &mut self, 194 | part_of_ros_state: &mut HashSet, 195 | ) -> bool { 196 | let mut smth_changed = false; 197 | // Run through bridges and actualize their state based on ROS1 state, removing corresponding entries from ROS1 state. 198 | // As a result of this cycle, part_of_ros_state will contain only new topics that doesn't have corresponding bridge. 199 | for (topic, bridge) in self.access.container.iter_mut() { 200 | smth_changed |= bridge 201 | .set_present_in_ros1(part_of_ros_state.take(topic).is_some()) 202 | .await; 203 | } 204 | 205 | // erase all non-actual bridges 206 | { 207 | let size_before_retain = self.access.container.len(); 208 | self.access 209 | .container 210 | .retain(|_topic, bridge| bridge.is_actual()); 211 | smth_changed |= size_before_retain != self.access.container.len(); 212 | } 213 | 214 | // run through the topics and create corresponding bridges 215 | for topic in part_of_ros_state.iter() { 216 | let b_mode = bridging_mode(self.access.b_type, &topic.name); 217 | if b_mode != BridgingMode::Disabled { 218 | match self.access.container.entry(topic.clone()) { 219 | Entry::Occupied(_val) => { 220 | debug_assert!(false); // that shouldn't happen 221 | } 222 | Entry::Vacant(val) => { 223 | let inserted = val.insert(TopicBridge::new( 224 | topic.clone(), 225 | self.access.b_type, 226 | self.access.declaration_interface.clone(), 227 | self.access.ros1_client.clone(), 228 | self.access.zenoh_client.clone(), 229 | b_mode, 230 | )); 231 | inserted.set_present_in_ros1(true).await; 232 | smth_changed = true; 233 | } 234 | } 235 | } 236 | } 237 | smth_changed 238 | } 239 | } 240 | 241 | pub struct BridgesStorage { 242 | bridges: Bridges, 243 | 244 | ros1_client: Arc, 245 | zenoh_client: Arc, 246 | declaration_interface: Arc, 247 | } 248 | impl BridgesStorage { 249 | pub fn new( 250 | ros1_client: Arc, 251 | zenoh_client: Arc, 252 | declaration_interface: Arc, 253 | ) -> Self { 254 | Self { 255 | bridges: Bridges::new(), 256 | 257 | ros1_client, 258 | zenoh_client, 259 | declaration_interface, 260 | } 261 | } 262 | 263 | pub fn complementary_for(&mut self, b_type: BridgeType) -> ComplementaryElementAccessor<'_> { 264 | let b_type = match b_type { 265 | BridgeType::Publisher => BridgeType::Subscriber, 266 | BridgeType::Subscriber => BridgeType::Publisher, 267 | BridgeType::Service => BridgeType::Client, 268 | BridgeType::Client => BridgeType::Service, 269 | }; 270 | ComplementaryElementAccessor::new( 271 | b_type, 272 | self.bridges.container_mut(b_type), 273 | self.ros1_client.clone(), 274 | self.zenoh_client.clone(), 275 | self.declaration_interface.clone(), 276 | ) 277 | } 278 | 279 | pub fn for_type(&mut self, b_type: BridgeType) -> ElementAccessor<'_> { 280 | ElementAccessor::new( 281 | b_type, 282 | self.bridges.container_mut(b_type), 283 | self.ros1_client.clone(), 284 | self.zenoh_client.clone(), 285 | self.declaration_interface.clone(), 286 | ) 287 | } 288 | 289 | pub async fn receive_ros1_state(&mut self, ros1_state: &mut Ros1TopicMapping) -> bool { 290 | let mut smth_changed = self 291 | .for_type(BridgeType::Publisher) 292 | .receive_ros1_state(&mut ros1_state.published) 293 | .await; 294 | smth_changed |= self 295 | .for_type(BridgeType::Service) 296 | .receive_ros1_state(&mut ros1_state.serviced) 297 | .await; 298 | smth_changed |= self 299 | .for_type(BridgeType::Subscriber) 300 | .receive_ros1_state(&mut ros1_state.subscribed) 301 | .await; 302 | smth_changed 303 | } 304 | 305 | pub fn status(&self) -> BridgeStatus { 306 | self.bridges.status() 307 | } 308 | 309 | pub fn clear(&mut self) { 310 | self.bridges.clear() 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/discovery.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::{str, sync::Arc, time::Duration}; 16 | 17 | use futures::Future; 18 | use tracing::error; 19 | use zenoh::{ 20 | internal::bail, 21 | key_expr::{ 22 | format::{kedefine, keformat}, 23 | KeyExpr, 24 | }, 25 | }; 26 | 27 | use super::{ 28 | aloha_declaration::AlohaDeclaration, 29 | aloha_subscription::{AlohaSubscription, AlohaSubscriptionBuilder}, 30 | bridge_type::BridgeType, 31 | topic_descriptor::TopicDescriptor, 32 | topic_utilities::{make_topic, make_topic_key}, 33 | }; 34 | use crate::ZResult; 35 | 36 | kedefine!( 37 | pub discovery_format: "ros1_discovery_info/${discovery_namespace:*}/${resource_class:*}/${data_type:*}/${md5:*}/${bridge_namespace:*}/${topic:**}", 38 | ); 39 | // example: 40 | // ros1_discovery_info/discovery_namespace/publishers|subscribers|services|clients/data_type/md5/bridge_namespace/some/ros/topic 41 | // where 42 | // discovery_namespace - namespace to isolate different discovery pools. Would be * by default ( == global namespace) 43 | // bridge_namespace - namespace to prefix bridge's resources. Would be * by default ( == global namespace) 44 | // publishers|subscribers|services|clients - one of 45 | 46 | const ROS1_DISCOVERY_INFO_PUBLISHERS_CLASS: &str = "pub"; 47 | const ROS1_DISCOVERY_INFO_SUBSCRIBERS_CLASS: &str = "sub"; 48 | const ROS1_DISCOVERY_INFO_SERVICES_CLASS: &str = "srv"; 49 | const ROS1_DISCOVERY_INFO_CLIENTS_CLASS: &str = "cl"; 50 | 51 | pub struct RemoteResources { 52 | _subscriber: Option, 53 | } 54 | impl RemoteResources { 55 | async fn new( 56 | session: zenoh::Session, 57 | discovery_namespace: String, 58 | bridge_namespace: String, 59 | on_discovered: F, 60 | on_lost: F, 61 | ) -> Self 62 | where 63 | F: Fn(BridgeType, TopicDescriptor) -> Box + Unpin + Send> 64 | + Send 65 | + Sync 66 | + 'static, 67 | { 68 | // make proper discovery keyexpr 69 | let mut formatter = discovery_format::formatter(); 70 | let discovery_keyexpr = keformat!( 71 | formatter, 72 | discovery_namespace = discovery_namespace, 73 | resource_class = "*", 74 | data_type = "*", 75 | md5 = "*", 76 | bridge_namespace = bridge_namespace, 77 | topic = "*/**" 78 | ) 79 | .unwrap(); 80 | 81 | let _on_discovered = Arc::new(on_discovered); 82 | let _on_lost = Arc::new(on_lost); 83 | 84 | let subscription = AlohaSubscriptionBuilder::new( 85 | session, 86 | discovery_keyexpr.clone(), 87 | Duration::from_secs(1), 88 | ) 89 | .on_resource_declared(move |key| { 90 | Box::new(Box::pin(Self::process( 91 | key.into_owned(), 92 | _on_discovered.clone(), 93 | ))) 94 | }) 95 | .on_resource_undeclared(move |key| { 96 | Box::new(Box::pin(Self::process(key.into_owned(), _on_lost.clone()))) 97 | }) 98 | .build() 99 | .await; 100 | 101 | let subscriber = match subscription { 102 | Ok(s) => Some(s), 103 | Err(e) => { 104 | error!("ROS1 Discovery: error creating querying subscriber: {}", e); 105 | None 106 | } 107 | }; 108 | 109 | Self { 110 | _subscriber: subscriber, 111 | } 112 | } 113 | 114 | // PRIVATE: 115 | async fn process(data: KeyExpr<'_>, callback: Arc) 116 | where 117 | F: Fn(BridgeType, TopicDescriptor) -> Box + Unpin + Send> 118 | + Send 119 | + Sync 120 | + 'static, 121 | { 122 | match Self::parse_format(&data, &callback).await { 123 | Ok(_) => {} 124 | Err(e) => { 125 | error!( 126 | "ROS1 Discovery: entry {}: processing error: {}", 127 | data.as_str(), 128 | e 129 | ); 130 | debug_assert!(false); 131 | } 132 | } 133 | } 134 | 135 | async fn parse_format(data: &KeyExpr<'_>, callback: &Arc) -> ZResult<()> 136 | where 137 | F: Fn(BridgeType, TopicDescriptor) -> Box + Unpin + Send>, 138 | { 139 | let discovery = discovery_format::parse(data).map_err(|err| err.to_string())?; 140 | Self::handle_format(discovery, callback).await 141 | } 142 | 143 | async fn handle_format( 144 | discovery: discovery_format::Parsed<'_>, 145 | callback: &Arc, 146 | ) -> ZResult<()> 147 | where 148 | F: Fn(BridgeType, TopicDescriptor) -> Box + Unpin + Send>, 149 | { 150 | //let discovery_namespace = discovery.discovery_namespace().ok_or("No discovery_namespace present!")?; 151 | let datatype_bytes = hex::decode(discovery.data_type().as_str())?; 152 | let datatype = std::str::from_utf8(&datatype_bytes)?; 153 | 154 | let md5 = discovery.md5().to_string(); 155 | 156 | let resource_class = discovery.resource_class().to_string(); 157 | let topic = discovery.topic().ok_or("No topic present!")?; 158 | 159 | let ros1_topic = make_topic(datatype, &md5, topic); 160 | 161 | let b_type = match resource_class.as_str() { 162 | ROS1_DISCOVERY_INFO_PUBLISHERS_CLASS => BridgeType::Publisher, 163 | ROS1_DISCOVERY_INFO_SUBSCRIBERS_CLASS => BridgeType::Subscriber, 164 | ROS1_DISCOVERY_INFO_SERVICES_CLASS => BridgeType::Service, 165 | ROS1_DISCOVERY_INFO_CLIENTS_CLASS => BridgeType::Client, 166 | _ => { 167 | bail!("unexpected resource class!"); 168 | } 169 | }; 170 | 171 | callback(b_type, ros1_topic).await; 172 | Ok(()) 173 | } 174 | } 175 | 176 | pub struct LocalResource { 177 | _declaration: AlohaDeclaration, 178 | } 179 | impl LocalResource { 180 | async fn new( 181 | discovery_namespace: &str, 182 | bridge_namespace: &str, 183 | resource_class: &str, 184 | topic: &TopicDescriptor, 185 | session: zenoh::Session, 186 | ) -> ZResult { 187 | // make proper discovery keyexpr 188 | let mut formatter = discovery_format::formatter(); 189 | let discovery_keyexpr = keformat!( 190 | formatter, 191 | discovery_namespace = discovery_namespace, 192 | resource_class = resource_class, 193 | data_type = hex::encode(topic.datatype.as_bytes()), 194 | md5 = topic.md5.clone(), 195 | bridge_namespace = bridge_namespace, 196 | topic = make_topic_key(topic) 197 | )?; 198 | 199 | let _declaration = 200 | AlohaDeclaration::new(session, discovery_keyexpr, Duration::from_secs(1)); 201 | 202 | Ok(Self { _declaration }) 203 | } 204 | } 205 | 206 | pub struct LocalResources { 207 | session: zenoh::Session, 208 | discovery_namespace: String, 209 | bridge_namespace: String, 210 | } 211 | impl LocalResources { 212 | pub fn new( 213 | discovery_namespace: String, 214 | bridge_namespace: String, 215 | session: zenoh::Session, 216 | ) -> LocalResources { 217 | Self { 218 | session, 219 | discovery_namespace, 220 | bridge_namespace, 221 | } 222 | } 223 | 224 | pub async fn declare_with_type( 225 | &self, 226 | topic: &TopicDescriptor, 227 | b_type: BridgeType, 228 | ) -> ZResult { 229 | match b_type { 230 | BridgeType::Publisher => self.declare_publisher(topic).await, 231 | BridgeType::Subscriber => self.declare_subscriber(topic).await, 232 | BridgeType::Service => self.declare_service(topic).await, 233 | BridgeType::Client => self.declare_client(topic).await, 234 | } 235 | } 236 | 237 | pub async fn declare_publisher(&self, topic: &TopicDescriptor) -> ZResult { 238 | self.declare(topic, ROS1_DISCOVERY_INFO_PUBLISHERS_CLASS) 239 | .await 240 | } 241 | 242 | pub async fn declare_subscriber(&self, topic: &TopicDescriptor) -> ZResult { 243 | self.declare(topic, ROS1_DISCOVERY_INFO_SUBSCRIBERS_CLASS) 244 | .await 245 | } 246 | 247 | pub async fn declare_service(&self, topic: &TopicDescriptor) -> ZResult { 248 | self.declare(topic, ROS1_DISCOVERY_INFO_SERVICES_CLASS) 249 | .await 250 | } 251 | 252 | pub async fn declare_client(&self, topic: &TopicDescriptor) -> ZResult { 253 | self.declare(topic, ROS1_DISCOVERY_INFO_CLIENTS_CLASS).await 254 | } 255 | 256 | //PRIVATE: 257 | pub async fn declare( 258 | &self, 259 | topic: &TopicDescriptor, 260 | resource_class: &str, 261 | ) -> ZResult { 262 | LocalResource::new( 263 | &self.discovery_namespace, 264 | &self.bridge_namespace, 265 | resource_class, 266 | topic, 267 | self.session.clone(), 268 | ) 269 | .await 270 | } 271 | } 272 | 273 | pub type TCallback = dyn Fn(BridgeType, TopicDescriptor) -> Box + Unpin + Send> 274 | + Send 275 | + Sync 276 | + 'static; 277 | 278 | pub struct RemoteResourcesBuilder { 279 | discovery_namespace: String, 280 | bridge_namespace: String, 281 | session: zenoh::Session, 282 | 283 | on_discovered: Option>, 284 | on_lost: Option>, 285 | } 286 | 287 | impl RemoteResourcesBuilder { 288 | pub fn new( 289 | discovery_namespace: String, 290 | bridge_namespace: String, 291 | session: zenoh::Session, 292 | ) -> Self { 293 | Self { 294 | discovery_namespace, 295 | bridge_namespace, 296 | session, 297 | on_discovered: None, 298 | on_lost: None, 299 | } 300 | } 301 | 302 | pub fn on_discovered(mut self, on_discovered: F) -> Self 303 | where 304 | F: Fn(BridgeType, TopicDescriptor) -> Box + Unpin + Send> 305 | + Send 306 | + Sync 307 | + 'static, 308 | { 309 | self.on_discovered = Some(Box::new(on_discovered)); 310 | self 311 | } 312 | pub fn on_lost(mut self, on_lost: F) -> Self 313 | where 314 | F: Fn(BridgeType, TopicDescriptor) -> Box + Unpin + Send> 315 | + Send 316 | + Sync 317 | + 'static, 318 | { 319 | self.on_lost = Some(Box::new(on_lost)); 320 | self 321 | } 322 | 323 | pub async fn build(self) -> RemoteResources { 324 | RemoteResources::new( 325 | self.session, 326 | self.discovery_namespace, 327 | self.bridge_namespace, 328 | self.on_discovered 329 | .unwrap_or(Box::new(|_, _| Box::new(Box::pin(async {})))), 330 | self.on_lost 331 | .unwrap_or(Box::new(|_, _| Box::new(Box::pin(async {})))), 332 | ) 333 | .await 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/tests/aloha_declaration_test.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::{ 16 | collections::HashSet, 17 | str::FromStr, 18 | sync::{atomic::AtomicUsize, Arc}, 19 | time::Duration, 20 | }; 21 | 22 | use test_case::test_case; 23 | use tokio::sync::Mutex; 24 | use zenoh::{key_expr::OwnedKeyExpr, session::OpenBuilder, Result as ZResult, Session, Wait}; 25 | use zenoh_plugin_ros1::ros_to_zenoh_bridge::{ 26 | aloha_declaration, aloha_subscription, test_helpers::IsolatedConfig, 27 | }; 28 | 29 | const TIMEOUT: Duration = Duration::from_secs(30); 30 | 31 | fn session_builder(cfg: &IsolatedConfig) -> OpenBuilder { 32 | zenoh::open(cfg.peer()) 33 | } 34 | 35 | fn declaration_builder( 36 | session: Session, 37 | beacon_period: Duration, 38 | ) -> aloha_declaration::AlohaDeclaration { 39 | aloha_declaration::AlohaDeclaration::new( 40 | session, 41 | zenoh::key_expr::OwnedKeyExpr::from_str("key").unwrap(), 42 | beacon_period, 43 | ) 44 | } 45 | 46 | fn subscription_builder( 47 | session: Session, 48 | beacon_period: Duration, 49 | ) -> aloha_subscription::AlohaSubscriptionBuilder { 50 | aloha_subscription::AlohaSubscriptionBuilder::new( 51 | session, 52 | zenoh::key_expr::OwnedKeyExpr::from_str("key").unwrap(), 53 | beacon_period, 54 | ) 55 | } 56 | 57 | fn make_session(cfg: &IsolatedConfig) -> Session { 58 | session_builder(cfg).wait().unwrap() 59 | } 60 | 61 | async fn make_subscription( 62 | session: Session, 63 | beacon_period: Duration, 64 | ) -> aloha_subscription::AlohaSubscription { 65 | subscription_builder(session, beacon_period) 66 | .build() 67 | .await 68 | .expect("Failed to make subscription") 69 | } 70 | 71 | #[tokio::test(flavor = "multi_thread")] 72 | async fn aloha_instantination_one_instance() { 73 | let session = make_session(&IsolatedConfig::default()); 74 | let _declaration = declaration_builder(session.clone(), Duration::from_secs(1)); 75 | let _subscription = make_subscription(session, Duration::from_secs(1)).await; 76 | } 77 | 78 | #[tokio::test(flavor = "multi_thread")] 79 | async fn aloha_instantination_many_instances() { 80 | let cfg = IsolatedConfig::default(); 81 | let mut sessions = Vec::new(); 82 | let mut declarations = Vec::new(); 83 | let mut subscriptions = Vec::new(); 84 | for _ in 0..10 { 85 | let session = make_session(&cfg); 86 | sessions.push(session.clone()); 87 | declarations.push(declaration_builder(session.clone(), Duration::from_secs(1))); 88 | } 89 | 90 | for session in sessions.iter() { 91 | subscriptions.push(make_subscription(session.clone(), Duration::from_secs(1)).await); 92 | } 93 | } 94 | 95 | pub struct PPCMeasurement { 96 | _subscriber: zenoh::pubsub::Subscriber<()>, 97 | ppc: Arc, 98 | measurement_period: Duration, 99 | } 100 | impl PPCMeasurement { 101 | pub async fn new( 102 | session: &Session, 103 | key: String, 104 | measurement_period: Duration, 105 | ) -> ZResult { 106 | let ppc = Arc::new(AtomicUsize::new(0)); 107 | let p = ppc.clone(); 108 | let subscriber = session 109 | .declare_subscriber(key) 110 | .callback(move |_val| { 111 | p.fetch_add(1, std::sync::atomic::Ordering::SeqCst); 112 | }) 113 | .await?; 114 | 115 | Ok(Self { 116 | _subscriber: subscriber, 117 | ppc, 118 | measurement_period, 119 | }) 120 | } 121 | 122 | pub async fn measure_ppc(&self) -> usize { 123 | self.ppc.store(0, std::sync::atomic::Ordering::SeqCst); 124 | tokio::time::sleep(self.measurement_period).await; 125 | self.ppc.load(std::sync::atomic::Ordering::SeqCst) 126 | } 127 | } 128 | 129 | struct DeclarationCollector { 130 | resources: Arc>>, 131 | 132 | to_be_declared: Arc>>, 133 | to_be_undeclared: Arc>>, 134 | } 135 | impl DeclarationCollector { 136 | fn new() -> Self { 137 | Self { 138 | resources: Arc::new(Mutex::new(HashSet::new())), 139 | to_be_declared: Arc::new(Mutex::new(HashSet::new())), 140 | to_be_undeclared: Arc::new(Mutex::new(HashSet::new())), 141 | } 142 | } 143 | 144 | pub fn use_builder( 145 | &self, 146 | mut builder: aloha_subscription::AlohaSubscriptionBuilder, 147 | ) -> aloha_subscription::AlohaSubscriptionBuilder { 148 | let r = self.resources.clone(); 149 | let r2 = r.clone(); 150 | 151 | let declared = self.to_be_declared.clone(); 152 | let undeclared = self.to_be_undeclared.clone(); 153 | 154 | builder = builder 155 | .on_resource_declared(move |k| { 156 | let declared = declared.clone(); 157 | let r = r.clone(); 158 | let k_owned = OwnedKeyExpr::from(k); 159 | Box::new(Box::pin(async move { 160 | assert!(declared.lock().await.remove::(&k_owned)); 161 | assert!(r.lock().await.insert(k_owned)); 162 | })) 163 | }) 164 | .on_resource_undeclared(move |k| { 165 | let undeclared = undeclared.clone(); 166 | let r2 = r2.clone(); 167 | let k_owned = OwnedKeyExpr::from(k); 168 | Box::new(Box::pin(async move { 169 | assert!(undeclared.lock().await.remove(&k_owned)); 170 | assert!(r2.lock().await.remove(&k_owned)); 171 | })) 172 | }); 173 | 174 | builder 175 | } 176 | 177 | pub async fn arm( 178 | &mut self, 179 | declared: HashSet, 180 | undeclared: HashSet, 181 | ) { 182 | *self.to_be_declared.lock().await = declared; 183 | *self.to_be_undeclared.lock().await = undeclared; 184 | } 185 | 186 | pub async fn wait(&self, expected: HashSet) { 187 | while !self.to_be_declared.lock().await.is_empty() 188 | || !self.to_be_undeclared.lock().await.is_empty() 189 | || expected != *self.resources.lock().await 190 | { 191 | tokio::time::sleep(core::time::Duration::from_millis(1)).await; 192 | } 193 | } 194 | } 195 | 196 | #[derive(Default)] 197 | struct State { 198 | pub declarators_count: usize, 199 | } 200 | impl State { 201 | pub fn declarators(mut self, declarators_count: usize) -> Self { 202 | self.declarators_count = declarators_count; 203 | self 204 | } 205 | } 206 | 207 | async fn test_state_transition( 208 | cfg: &IsolatedConfig, 209 | beacon_period: Duration, 210 | declaring_sessions: &mut Vec, 211 | declarations: &mut Vec, 212 | collector: &mut DeclarationCollector, 213 | ppc_measurer: &PPCMeasurement, 214 | state: &State, 215 | ) { 216 | let ke = zenoh::key_expr::OwnedKeyExpr::from_str("key").unwrap(); 217 | let mut result: HashSet = HashSet::new(); 218 | let mut undeclared: HashSet = HashSet::new(); 219 | let mut declared: HashSet = HashSet::new(); 220 | 221 | match (declarations.len(), state.declarators_count) { 222 | (0, 0) => {} 223 | (0, _) => { 224 | result.insert(ke.clone()); 225 | declared.insert(ke.clone()); 226 | } 227 | (_, 0) => { 228 | undeclared.insert(ke.clone()); 229 | } 230 | (_, _) => { 231 | result.insert(ke.clone()); 232 | } 233 | } 234 | 235 | collector.arm(declared, undeclared).await; 236 | 237 | while declarations.len() > state.declarators_count { 238 | declarations.pop(); 239 | } 240 | 241 | while declarations.len() < state.declarators_count { 242 | if declaring_sessions.len() <= declarations.len() { 243 | declaring_sessions.push(session_builder(cfg).await.unwrap()); 244 | } 245 | declarations.push(declaration_builder( 246 | declaring_sessions[declarations.len()].clone(), 247 | beacon_period, 248 | )); 249 | } 250 | 251 | collector.wait(result).await; 252 | tokio::time::sleep(beacon_period).await; 253 | while ppc_measurer.measure_ppc().await != { 254 | let mut res = 1; 255 | if state.declarators_count == 0 { 256 | res = 0; 257 | } 258 | res 259 | } {} 260 | } 261 | 262 | async fn run_aloha(beacon_period: Duration, scenario: Vec) { 263 | let cfg = IsolatedConfig::default(); 264 | let mut declaring_sessions: Vec = Vec::new(); 265 | let mut declarations: Vec = Vec::new(); 266 | 267 | let mut collector = DeclarationCollector::new(); 268 | let subscription_session = session_builder(&cfg).await.unwrap(); 269 | let _subscriber = collector 270 | .use_builder(subscription_builder( 271 | subscription_session.clone(), 272 | beacon_period, 273 | )) 274 | .build() 275 | .await 276 | .unwrap(); 277 | let ppc_measurer = PPCMeasurement::new(&subscription_session, "key".to_string(), beacon_period) 278 | .await 279 | .unwrap(); 280 | for scene in scenario { 281 | println!("Transiting State: {}", scene.declarators_count); 282 | tokio::time::timeout( 283 | TIMEOUT, 284 | test_state_transition( 285 | &cfg, 286 | beacon_period, 287 | &mut declaring_sessions, 288 | &mut declarations, 289 | &mut collector, 290 | &ppc_measurer, 291 | &scene, 292 | ), 293 | ) 294 | .await 295 | .expect("Timeout waiting state transition!"); 296 | } 297 | } 298 | 299 | #[test_case([State::default().declarators(1)].into_iter().collect(); "one")] 300 | #[test_case([State::default().declarators(10)].into_iter().collect(); "many")] 301 | #[test_case([State::default().declarators(10), 302 | State::default().declarators(1), 303 | State::default().declarators(10)].into_iter().collect(); "many one many")] 304 | #[test_case([State::default().declarators(1), 305 | State::default().declarators(0), 306 | State::default().declarators(1)].into_iter().collect(); "one zero one")] 307 | #[test_case([State::default().declarators(10), 308 | State::default().declarators(0), 309 | State::default().declarators(10)].into_iter().collect(); "many zero many")] 310 | #[test_case([State::default().declarators(1), 311 | State::default().declarators(10), 312 | State::default().declarators(1), 313 | State::default().declarators(10), 314 | State::default().declarators(1), 315 | State::default().declarators(10), 316 | State::default().declarators(0), 317 | State::default().declarators(1), 318 | State::default().declarators(10), 319 | State::default().declarators(1), 320 | State::default().declarators(0), 321 | State::default().declarators(10), 322 | State::default().declarators(1)].into_iter().collect(); "many scenarios")] 323 | #[tokio::test(flavor = "multi_thread")] 324 | async fn aloha_declare(vec_state: Vec) { 325 | run_aloha(Duration::from_millis(100), vec_state).await; 326 | } 327 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/resource_cache.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::{ 16 | collections::{hash_map::Entry, HashMap}, 17 | net::{TcpStream, ToSocketAddrs}, 18 | sync::{Arc, Mutex}, 19 | time::Duration, 20 | }; 21 | 22 | use rosrust::{Publisher, RawMessage, RawMessageDescription, RosMsg}; 23 | use zenoh::{ 24 | internal::{bail, zerror}, 25 | Result as ZResult, 26 | }; 27 | 28 | use super::{ros1_client::Ros1Client, topic_descriptor::TopicDescriptor}; 29 | 30 | pub type TopicName = String; 31 | pub type NodeName = String; 32 | pub type DataType = String; 33 | pub type Md5 = String; 34 | 35 | /** 36 | * Ros1ResourceCache - caching resolver for ROS1 resources 37 | */ 38 | pub struct Ros1ResourceCache { 39 | bridge_ros_node_name: String, 40 | aux_node: Ros1Client, 41 | service_cache: HashMap>, 42 | publisher_cache: HashMap>, 43 | subscriber_cache: HashMap, 44 | } 45 | 46 | impl Ros1ResourceCache { 47 | pub fn new( 48 | aux_node_name: &str, 49 | bridge_ros_node_name: String, 50 | ros_master_uri: &str, 51 | ) -> ZResult { 52 | let aux_node = Ros1Client::new(aux_node_name, ros_master_uri)?; 53 | Ok(Ros1ResourceCache { 54 | bridge_ros_node_name, 55 | aux_node, 56 | service_cache: HashMap::default(), 57 | publisher_cache: HashMap::default(), 58 | subscriber_cache: HashMap::default(), 59 | }) 60 | } 61 | 62 | pub fn resolve_subscriber_parameters( 63 | &mut self, 64 | topic: TopicName, 65 | node: NodeName, 66 | ) -> ZResult<(DataType, Md5)> { 67 | if node == self.bridge_ros_node_name { 68 | bail!("Ignoring our own aux node's resources!"); 69 | } 70 | 71 | match self.subscriber_cache.entry(topic.clone()) { 72 | Entry::Occupied(occupied) => occupied.get().query(&node), 73 | Entry::Vacant(vacant) => { 74 | let resolver = TopicSubscribersDiscovery::new(&self.aux_node, &topic)?; 75 | vacant.insert(resolver).query(&node) 76 | } 77 | } 78 | } 79 | 80 | pub fn resolve_publisher_parameters( 81 | &mut self, 82 | topic: TopicName, 83 | node: NodeName, 84 | ) -> ZResult<(DataType, Md5)> { 85 | if node == self.aux_node.ros.name() || node == self.bridge_ros_node_name { 86 | bail!("Ignoring our own aux node's resources!"); 87 | } 88 | 89 | match self.publisher_cache.entry(topic.clone()) { 90 | Entry::Occupied(mut occupied) => { 91 | match occupied.get_mut().entry(node.clone()) { 92 | Entry::Occupied(occupied) => { 93 | // return already resolved data 94 | Ok(occupied.get().clone()) 95 | } 96 | Entry::Vacant(vacant) => { 97 | // actively resolve data through network 98 | let data = Self::resolve_publisher(&self.aux_node, topic, &node)?; 99 | 100 | // save in cache and return data 101 | Ok(vacant.insert(data).clone()) 102 | } 103 | } 104 | } 105 | Entry::Vacant(vacant) => { 106 | // actively resolve datatype through network 107 | let data = Self::resolve_publisher(&self.aux_node, topic, &node)?; 108 | 109 | // save in cache and return datatype 110 | let mut node_map = HashMap::new(); 111 | node_map.insert(node, data.clone()); 112 | vacant.insert(node_map); 113 | Ok(data) 114 | } 115 | } 116 | } 117 | 118 | pub fn resolve_service_parameters( 119 | &mut self, 120 | service: TopicName, 121 | node: NodeName, 122 | ) -> ZResult<(DataType, Md5)> { 123 | if node == self.bridge_ros_node_name { 124 | bail!("Ignoring our own aux node's resources!"); 125 | } 126 | 127 | match self.service_cache.entry(service.clone()) { 128 | Entry::Occupied(mut occupied) => { 129 | match occupied.get_mut().entry(node) { 130 | Entry::Occupied(occupied) => { 131 | // return already resolved data 132 | Ok(occupied.get().clone()) 133 | } 134 | Entry::Vacant(vacant) => { 135 | // actively resolve datatype through network 136 | let data = Self::resolve_service(&self.aux_node, service)?; 137 | 138 | // save in cache and return datatype 139 | Ok(vacant.insert(data).clone()) 140 | } 141 | } 142 | } 143 | Entry::Vacant(vacant) => { 144 | // actively resolve datatype through network 145 | let data = Self::resolve_service(&self.aux_node, service)?; 146 | 147 | // save in cache and return datatype 148 | let mut node_map = HashMap::new(); 149 | node_map.insert(node, data.clone()); 150 | vacant.insert(node_map); 151 | Ok(data) 152 | } 153 | } 154 | } 155 | 156 | fn resolve_service(aux_node: &Ros1Client, service: TopicName) -> ZResult<(DataType, Md5)> { 157 | let resolving_client = aux_node 158 | .client(&TopicDescriptor { 159 | name: service, 160 | datatype: String::from("*"), 161 | md5: String::from("*"), 162 | }) 163 | .map_err(|e| zerror!("{e}"))?; 164 | let mut probe = resolving_client 165 | .probe(Duration::from_secs(1)) 166 | .map_err(|e| zerror!("{e}"))?; 167 | let datatype = probe 168 | .remove("type") 169 | .ok_or("no type field in service's responce!")?; 170 | let md5 = probe 171 | .remove("md5sum") 172 | .ok_or("no md5sum field in service's responce!")?; 173 | Ok((datatype, md5)) 174 | } 175 | 176 | fn resolve_publisher( 177 | aux_node: &Ros1Client, 178 | topic: TopicName, 179 | node: &NodeName, 180 | ) -> ZResult<(DataType, Md5)> { 181 | let publisher_uri = aux_node.ros.master.lookup_node(node)?; 182 | let (protocol, hostname, port) = Self::request_topic(&publisher_uri, "probe", &topic)?; 183 | if protocol != "TCPROS" { 184 | bail!("Publisher responded with a non-TCPROS protocol: {protocol}"); 185 | } 186 | 187 | if let Some(address) = (hostname.as_str(), port as u16).to_socket_addrs()?.next() { 188 | let mut stream = TcpStream::connect(address)?; 189 | let mut headers = Self::exchange_headers::<_>(&mut stream, topic.clone())?; 190 | 191 | let datatype = headers 192 | .remove("type") 193 | .ok_or("no type field in publisher's responce!")?; 194 | let md5 = headers 195 | .remove("md5sum") 196 | .ok_or("no md5sum field in publisher's responce!")?; 197 | 198 | return Ok((datatype, md5)); 199 | } 200 | 201 | bail!("No endpoints found for topic {topic} node name {node}") 202 | } 203 | 204 | fn request_topic( 205 | publisher_uri: &str, 206 | caller_id: &str, 207 | topic: &str, 208 | ) -> ZResult<(String, String, i32)> { 209 | let (_code, _message, protocols): (i32, String, (String, String, i32)) = 210 | xml_rpc::Client::new() 211 | .map_err(|e| zerror!("Foreign XmlRpc: {e}"))? 212 | .call( 213 | &publisher_uri 214 | .parse() 215 | .map_err(|e| zerror!("Bad uri: {publisher_uri} error: {e}"))?, 216 | "requestTopic", 217 | (caller_id, topic, [["TCPROS"]]), 218 | ) 219 | .map_err(|e| zerror!("Error making XmlRpc request: {e}"))? 220 | .map_err(|e| zerror!("Error making XmlRpc request: {}", e.message))?; 221 | Ok(protocols) 222 | } 223 | 224 | fn write_request(mut stream: &mut U, topic: TopicName) -> ZResult<()> { 225 | let mut fields = HashMap::::new(); 226 | fields.insert(String::from("message_definition"), String::from("*")); 227 | fields.insert(String::from("callerid"), String::from("probe")); 228 | fields.insert(String::from("topic"), topic); 229 | fields.insert(String::from("md5sum"), String::from("*")); 230 | fields.insert(String::from("type"), String::from("*")); 231 | Self::encode(&mut stream, &fields)?; 232 | Ok(()) 233 | } 234 | 235 | fn read_response(mut stream: &mut U) -> ZResult> { 236 | let fields = Self::decode(&mut stream)?; 237 | Ok(fields) 238 | } 239 | 240 | fn exchange_headers(stream: &mut U, topic: TopicName) -> ZResult> 241 | where 242 | U: std::io::Write + std::io::Read, 243 | { 244 | Self::write_request::(stream, topic)?; 245 | Self::read_response::(stream) 246 | } 247 | 248 | fn decode(data: &mut R) -> ZResult> { 249 | rosrust::RosMsg::decode(data).map_err(|e| e.into()) 250 | } 251 | 252 | fn encode(writer: &mut W, data: &HashMap) -> ZResult<()> { 253 | data.encode(writer).map_err(|e| e.into()) 254 | } 255 | } 256 | 257 | struct TopicSubscribersDiscovery { 258 | _discovering_publisher: Publisher, 259 | discovered_subscribers: Arc>>, 260 | } 261 | 262 | impl TopicSubscribersDiscovery { 263 | fn new(aux_node: &Ros1Client, topic: &TopicName) -> ZResult { 264 | let discovered_subscribers = Arc::new(Mutex::new(HashMap::default())); 265 | 266 | let c_discovered_subscribers = discovered_subscribers.clone(); 267 | let on_handshake = move |fields: &HashMap| -> bool { 268 | let _ = Self::process_handshake(fields, c_discovered_subscribers.clone()); 269 | false 270 | }; 271 | 272 | let description = RawMessageDescription { 273 | msg_definition: "*".to_string(), 274 | md5sum: "*".to_string(), 275 | msg_type: "*".to_string(), 276 | }; 277 | 278 | let discovering_publisher = aux_node 279 | .ros 280 | .publish_common(topic, 0, Some(description), Some(Box::new(on_handshake))) 281 | .map_err(|e| e.to_string())?; 282 | 283 | Ok(Self { 284 | _discovering_publisher: discovering_publisher, 285 | discovered_subscribers, 286 | }) 287 | } 288 | 289 | fn query(&self, node: &NodeName) -> ZResult<(DataType, Md5)> { 290 | let guard = self 291 | .discovered_subscribers 292 | .lock() 293 | .map_err(|e| zerror!("{e}"))?; 294 | guard 295 | .get(node) 296 | .ok_or_else(|| zerror!("No such node: {}", node).into()) 297 | .map(|v| v.clone()) 298 | } 299 | 300 | fn process_handshake( 301 | fields: &HashMap, 302 | discovered_subscribers: Arc>>, 303 | ) -> ZResult<()> { 304 | let datatype = fields 305 | .get("type") 306 | .ok_or("no type field in subscriber's responce!")?; 307 | let md5 = fields 308 | .get("md5sum") 309 | .ok_or("no md5sum field in subscriber's responce!")?; 310 | let node_name = fields 311 | .get("callerid") 312 | .ok_or("no callerid field in subscriber's responce!")?; 313 | 314 | if let Ok(mut guard) = discovered_subscribers.lock() { 315 | match guard.entry(node_name.to_owned()) { 316 | Entry::Occupied(mut occupied) => { 317 | occupied.insert((datatype.to_owned(), md5.to_owned())); 318 | } 319 | Entry::Vacant(vacant) => { 320 | vacant.insert((datatype.to_owned(), md5.to_owned())); 321 | } 322 | } 323 | } 324 | 325 | Ok(()) 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/src/ros_to_zenoh_bridge/abstract_bridge.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::sync::Arc; 16 | 17 | use rosrust::RawMessageDescription; 18 | use tracing::{debug, error, info}; 19 | use zenoh::{internal::bail, key_expr::keyexpr, Result as ZResult, Wait}; 20 | 21 | use super::{ 22 | bridge_type::BridgeType, ros1_client, topic_descriptor::TopicDescriptor, 23 | topic_utilities::make_zenoh_key, zenoh_client, 24 | }; 25 | use crate::{blockon_runtime, spawn_blocking_runtime, spawn_runtime}; 26 | 27 | pub struct AbstractBridge { 28 | _impl: BridgeIml, 29 | } 30 | 31 | impl AbstractBridge { 32 | pub async fn new( 33 | b_type: BridgeType, 34 | topic: &TopicDescriptor, 35 | ros1_client: &ros1_client::Ros1Client, 36 | zenoh_client: &Arc, 37 | ) -> ZResult { 38 | let _impl = { 39 | match b_type { 40 | BridgeType::Publisher => { 41 | BridgeIml::Pub(Ros1ToZenoh::new(topic, ros1_client, zenoh_client).await?) 42 | } 43 | BridgeType::Subscriber => { 44 | BridgeIml::Sub(ZenohToRos1::new(topic, ros1_client, zenoh_client).await?) 45 | } 46 | BridgeType::Service => BridgeIml::Service( 47 | Ros1ToZenohService::new(topic, ros1_client, zenoh_client).await?, 48 | ), 49 | BridgeType::Client => BridgeIml::Client( 50 | Ros1ToZenohClient::new(topic, ros1_client, zenoh_client.clone()).await?, 51 | ), 52 | } 53 | }; 54 | Ok(Self { _impl }) 55 | } 56 | } 57 | 58 | enum BridgeIml { 59 | Client(Ros1ToZenohClient), 60 | Service(Ros1ToZenohService), 61 | Pub(Ros1ToZenoh), 62 | Sub(ZenohToRos1), 63 | } 64 | 65 | struct Ros1ToZenohClient { 66 | _service: rosrust::Service, 67 | } 68 | impl Ros1ToZenohClient { 69 | async fn new( 70 | topic: &TopicDescriptor, 71 | ros1_client: &ros1_client::Ros1Client, 72 | zenoh_client: Arc, 73 | ) -> ZResult { 74 | info!("Creating ROS1 -> Zenoh Client bridge for {:?}", topic); 75 | 76 | let zenoh_key = make_zenoh_key(topic); 77 | match ros1_client.service::( 78 | topic, 79 | move |q| -> rosrust::ServiceResult { 80 | return Self::on_query(&zenoh_key, q, zenoh_client.as_ref()); 81 | }, 82 | ) { 83 | Ok(service) => Ok(Ros1ToZenohClient { _service: service }), 84 | Err(e) => { 85 | bail!("Ros error: {}", e) 86 | } 87 | } 88 | } 89 | 90 | //PRIVATE: 91 | fn on_query( 92 | key: &keyexpr, 93 | query: rosrust::RawMessage, 94 | zenoh_client: &zenoh_client::ZenohClient, 95 | ) -> rosrust::ServiceResult { 96 | return blockon_runtime(Self::do_zenoh_query(key, query, zenoh_client)); 97 | } 98 | 99 | async fn do_zenoh_query( 100 | key: &keyexpr, 101 | query: rosrust::RawMessage, 102 | zenoh_client: &zenoh_client::ZenohClient, 103 | ) -> rosrust::ServiceResult { 104 | match zenoh_client.make_query_sync(key, query.0).await { 105 | Ok(reply) => match reply.recv_async().await { 106 | Ok(r) => match r.result() { 107 | Ok(sample) => { 108 | let data = sample.payload().to_bytes().into_owned(); 109 | debug!("Zenoh -> ROS1: sending {} bytes!", data.len()); 110 | Ok(rosrust::RawMessage(data)) 111 | } 112 | Err(e) => { 113 | let error = format!("{:?}", e); 114 | error!( 115 | "ROS1 -> Zenoh Client: received Zenoh Query with error: {:?}", 116 | error 117 | ); 118 | Err(error) 119 | } 120 | }, 121 | Err(e) => { 122 | error!( 123 | "ROS1 -> Zenoh Client: error while receiving reply to Zenoh Query: {}", 124 | e 125 | ); 126 | let error = e.to_string(); 127 | Err(error) 128 | } 129 | }, 130 | Err(e) => { 131 | error!( 132 | "ROS1 -> Zenoh Client: error while creating Zenoh Query: {}", 133 | e 134 | ); 135 | let error = e.to_string(); 136 | Err(error) 137 | } 138 | } 139 | } 140 | } 141 | 142 | struct Ros1ToZenohService { 143 | _queryable: zenoh::query::Queryable<()>, 144 | } 145 | impl Ros1ToZenohService { 146 | async fn new<'b>( 147 | topic: &TopicDescriptor, 148 | ros1_client: &ros1_client::Ros1Client, 149 | zenoh_client: &'b zenoh_client::ZenohClient, 150 | ) -> ZResult { 151 | info!( 152 | "Creating ROS1 -> Zenoh Service bridge for topic {}, datatype {}", 153 | topic.name, topic.datatype 154 | ); 155 | 156 | match ros1_client.client(topic) { 157 | Ok(client) => { 158 | let client_in_arc = Arc::new(client); 159 | let topic_in_arc = Arc::new(topic.clone()); 160 | let queryable = zenoh_client 161 | .make_queryable(make_zenoh_key(topic), move |query| { 162 | spawn_runtime(Self::on_query( 163 | client_in_arc.clone(), 164 | query, 165 | topic_in_arc.clone(), 166 | )); 167 | }) 168 | .await?; 169 | Ok(Ros1ToZenohService { 170 | _queryable: queryable, 171 | }) 172 | } 173 | Err(e) => { 174 | bail!("Ros error: {}", e.to_string()) 175 | } 176 | } 177 | } 178 | 179 | //PRIVATE: 180 | async fn on_query( 181 | ros1_client: Arc>, 182 | query: zenoh::query::Query, 183 | topic: Arc, 184 | ) { 185 | match query.payload() { 186 | Some(val) => { 187 | let payload = val.to_bytes().into_owned(); 188 | debug!( 189 | "ROS1 -> Zenoh Service: got query of {} bytes!", 190 | payload.len() 191 | ); 192 | Self::process_query(ros1_client, query, payload, topic).await; 193 | } 194 | None => { 195 | error!("ROS1 -> Zenoh Service: got query without value!"); 196 | } 197 | } 198 | } 199 | 200 | async fn process_query( 201 | ros1_client: Arc>, 202 | query: zenoh::query::Query, 203 | payload: Vec, 204 | topic: Arc, 205 | ) { 206 | // rosrust is synchronous, so we will use spawn_blocking. If there will be an async mode some day for the rosrust, 207 | // than reply_to_query can be refactored to async very easily 208 | let res = spawn_blocking_runtime(move || { 209 | let description = RawMessageDescription { 210 | msg_definition: String::from("*"), 211 | md5sum: topic.md5.clone(), 212 | msg_type: topic.datatype.clone(), 213 | }; 214 | ros1_client.req_with_description(&rosrust::RawMessage(payload), description) 215 | }) 216 | .await 217 | .expect("Unable to compete the task"); 218 | match Self::reply_to_query(res, &query).await { 219 | Ok(_) => {} 220 | Err(e) => { 221 | error!( 222 | "ROS1 -> Zenoh Service: error replying to query on {}: {}", 223 | query.key_expr(), 224 | e 225 | ); 226 | } 227 | } 228 | } 229 | 230 | async fn reply_to_query( 231 | res: rosrust::error::tcpros::Result>, 232 | query: &zenoh::query::Query, 233 | ) -> ZResult<()> { 234 | match res { 235 | Ok(reply) => match reply { 236 | Ok(reply_message) => { 237 | debug!( 238 | "ROS1 -> Zenoh Service: got reply of {} bytes!", 239 | reply_message.0.len() 240 | ); 241 | query 242 | .reply(query.key_expr().clone(), reply_message.0) 243 | .await?; 244 | } 245 | Err(e) => { 246 | error!( 247 | "ROS1 -> Zenoh Service: got reply from ROS1 Service with error: {}", 248 | e 249 | ); 250 | query.reply(query.key_expr().clone(), e).await?; 251 | } 252 | }, 253 | Err(e) => { 254 | error!( 255 | "ROS1 -> Zenoh Service: error while sending request to ROS1 Service: {}", 256 | e 257 | ); 258 | let error = e.to_string(); 259 | query.reply(query.key_expr().clone(), error).await?; 260 | } 261 | } 262 | Ok(()) 263 | } 264 | } 265 | 266 | struct Ros1ToZenoh { 267 | _subscriber: rosrust::Subscriber, 268 | } 269 | impl Ros1ToZenoh { 270 | async fn new<'b>( 271 | topic: &TopicDescriptor, 272 | ros1_client: &ros1_client::Ros1Client, 273 | zenoh_client: &'b zenoh_client::ZenohClient, 274 | ) -> ZResult { 275 | info!( 276 | "Creating ROS1 -> Zenoh bridge for topic {}, datatype {}", 277 | topic.name, topic.datatype 278 | ); 279 | 280 | let publisher = zenoh_client.publish(make_zenoh_key(topic)).await?; 281 | match ros1_client.subscribe(topic, move |msg: rosrust::RawMessage| { 282 | debug!("ROS1 -> Zenoh: sending {} bytes!", msg.0.len()); 283 | match publisher.put(msg.0).wait() { 284 | Ok(_) => {} 285 | Err(e) => { 286 | error!("ROS1 -> Zenoh: error publishing: {}", e); 287 | } 288 | } 289 | }) { 290 | Ok(subscriber) => Ok(Ros1ToZenoh { 291 | _subscriber: subscriber, 292 | }), 293 | Err(e) => { 294 | bail!("Ros error: {}", e.to_string()) 295 | } 296 | } 297 | } 298 | } 299 | 300 | struct ZenohToRos1 { 301 | _subscriber: zenoh::pubsub::Subscriber<()>, 302 | } 303 | impl ZenohToRos1 { 304 | async fn new( 305 | topic: &TopicDescriptor, 306 | ros1_client: &ros1_client::Ros1Client, 307 | zenoh_client: &Arc, 308 | ) -> ZResult { 309 | info!( 310 | "Creating Zenoh -> ROS1 bridge for topic {}, datatype {}", 311 | topic.name, topic.datatype 312 | ); 313 | 314 | match ros1_client.publish(topic) { 315 | Ok(publisher) => { 316 | let publisher_in_arc = Arc::new(publisher); 317 | let subscriber = zenoh_client 318 | .subscribe(make_zenoh_key(topic), move |sample| { 319 | let publisher_in_arc_cloned = publisher_in_arc.clone(); 320 | spawn_blocking_runtime(move || { 321 | let data = sample.payload().to_bytes(); 322 | debug!("Zenoh -> ROS1: sending {} bytes!", data.len()); 323 | match publisher_in_arc_cloned 324 | .send(rosrust::RawMessage(data.into_owned())) 325 | { 326 | Ok(_) => {} 327 | Err(e) => { 328 | error!("Zenoh -> ROS1: error publishing: {}", e); 329 | } 330 | } 331 | }); 332 | }) 333 | .await?; 334 | Ok(ZenohToRos1 { 335 | _subscriber: subscriber, 336 | }) 337 | } 338 | Err(e) => { 339 | bail!("Ros error: {}", e.to_string()) 340 | } 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /zenoh-plugin-ros1/tests/discovery_test.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::{ 16 | sync::{Arc, Mutex}, 17 | time::Duration, 18 | }; 19 | 20 | use multiset::HashMultiSet; 21 | use test_case::test_case; 22 | use zenoh::{key_expr::keyexpr, session::OpenBuilder, Session, Wait}; 23 | use zenoh_plugin_ros1::ros_to_zenoh_bridge::{ 24 | discovery::{self, LocalResources, RemoteResources}, 25 | test_helpers::{BridgeChecker, IsolatedConfig}, 26 | topic_descriptor::TopicDescriptor, 27 | }; 28 | 29 | const TIMEOUT: Duration = Duration::from_secs(60); 30 | 31 | fn session_builder(cfg: &IsolatedConfig) -> OpenBuilder { 32 | zenoh::open(cfg.peer()) 33 | } 34 | 35 | fn remote_resources_builder(session: Session) -> discovery::RemoteResourcesBuilder { 36 | discovery::RemoteResourcesBuilder::new("*".to_string(), "*".to_string(), session) 37 | } 38 | 39 | fn make_session(cfg: &IsolatedConfig) -> Session { 40 | session_builder(cfg).wait().unwrap() 41 | } 42 | 43 | fn make_local_resources(session: Session) -> LocalResources { 44 | LocalResources::new("*".to_owned(), "*".to_owned(), session) 45 | } 46 | async fn make_remote_resources(session: Session) -> RemoteResources { 47 | remote_resources_builder(session).build().await 48 | } 49 | 50 | #[tokio::test(flavor = "multi_thread")] 51 | async fn discovery_instantination_one_instance() { 52 | let session = make_session(&IsolatedConfig::default()); 53 | let _remote = make_remote_resources(session.clone()).await; 54 | let _local = make_local_resources(session); 55 | } 56 | 57 | #[tokio::test(flavor = "multi_thread")] 58 | async fn discovery_instantination_many_instances() { 59 | let cfg = IsolatedConfig::default(); 60 | let mut sessions = Vec::new(); 61 | for _i in 0..10 { 62 | sessions.push(make_session(&cfg)); 63 | } 64 | 65 | let mut discoveries = Vec::new(); 66 | for session in sessions.iter() { 67 | let remote = make_remote_resources(session.clone()).await; 68 | let local = make_local_resources(session.clone()); 69 | discoveries.push((remote, local)); 70 | } 71 | } 72 | 73 | struct DiscoveryCollector { 74 | publishers: Arc>>, 75 | subscribers: Arc>>, 76 | services: Arc>>, 77 | clients: Arc>>, 78 | } 79 | impl DiscoveryCollector { 80 | fn new() -> Self { 81 | Self { 82 | publishers: Arc::new(Mutex::new(HashMultiSet::new())), 83 | subscribers: Arc::new(Mutex::new(HashMultiSet::new())), 84 | services: Arc::new(Mutex::new(HashMultiSet::new())), 85 | clients: Arc::new(Mutex::new(HashMultiSet::new())), 86 | } 87 | } 88 | 89 | pub fn use_builder( 90 | &self, 91 | builder: discovery::RemoteResourcesBuilder, 92 | ) -> discovery::RemoteResourcesBuilder { 93 | let p = self.publishers.clone(); 94 | let s = self.subscribers.clone(); 95 | let srv = self.services.clone(); 96 | let cli = self.clients.clone(); 97 | 98 | let p1 = self.publishers.clone(); 99 | let s1 = self.subscribers.clone(); 100 | let srv1 = self.services.clone(); 101 | let cli1 = self.clients.clone(); 102 | 103 | builder 104 | .on_discovered(move |b_type, topic| { 105 | let container = match b_type { 106 | zenoh_plugin_ros1::ros_to_zenoh_bridge::bridge_type::BridgeType::Publisher => { 107 | &p 108 | } 109 | zenoh_plugin_ros1::ros_to_zenoh_bridge::bridge_type::BridgeType::Subscriber => { 110 | &s 111 | } 112 | zenoh_plugin_ros1::ros_to_zenoh_bridge::bridge_type::BridgeType::Service => { 113 | &srv 114 | } 115 | zenoh_plugin_ros1::ros_to_zenoh_bridge::bridge_type::BridgeType::Client => &cli, 116 | }; 117 | container.lock().unwrap().insert(topic); 118 | Box::new(Box::pin(async {})) 119 | }) 120 | .on_lost(move |b_type, topic| { 121 | let container = match b_type { 122 | zenoh_plugin_ros1::ros_to_zenoh_bridge::bridge_type::BridgeType::Publisher => { 123 | &p1 124 | } 125 | zenoh_plugin_ros1::ros_to_zenoh_bridge::bridge_type::BridgeType::Subscriber => { 126 | &s1 127 | } 128 | zenoh_plugin_ros1::ros_to_zenoh_bridge::bridge_type::BridgeType::Service => { 129 | &srv1 130 | } 131 | zenoh_plugin_ros1::ros_to_zenoh_bridge::bridge_type::BridgeType::Client => { 132 | &cli1 133 | } 134 | }; 135 | container.lock().unwrap().remove(&topic); 136 | Box::new(Box::pin(async {})) 137 | }) 138 | } 139 | 140 | pub async fn wait_publishers(&self, expected: HashMultiSet) { 141 | Self::wait(&self.publishers, expected).await; 142 | } 143 | 144 | pub async fn wait_subscribers(&self, expected: HashMultiSet) { 145 | Self::wait(&self.subscribers, expected).await; 146 | } 147 | 148 | pub async fn wait_services(&self, expected: HashMultiSet) { 149 | Self::wait(&self.services, expected).await; 150 | } 151 | 152 | pub async fn wait_clients(&self, expected: HashMultiSet) { 153 | Self::wait(&self.clients, expected).await; 154 | } 155 | 156 | // PRIVATE: 157 | async fn wait( 158 | container: &Arc>>, 159 | expected: HashMultiSet, 160 | ) { 161 | while expected != *container.lock().unwrap() { 162 | tokio::time::sleep(core::time::Duration::from_millis(10)).await; 163 | } 164 | } 165 | } 166 | 167 | async fn generate_topics( 168 | count: usize, 169 | duplication: usize, 170 | stage: usize, 171 | ) -> HashMultiSet { 172 | let mut result = HashMultiSet::new(); 173 | for number in 0..count { 174 | for _dup in 0..duplication { 175 | unsafe { 176 | let topic = BridgeChecker::make_topic(keyexpr::from_str_unchecked( 177 | format!("name_{}_{}", number, stage).as_str(), 178 | )); 179 | result.insert(topic); 180 | } 181 | } 182 | } 183 | result 184 | } 185 | 186 | #[derive(Default)] 187 | struct State { 188 | publishers_count: usize, 189 | publishers_duplication: usize, 190 | subscribers_count: usize, 191 | subscribers_duplication: usize, 192 | services_count: usize, 193 | services_duplication: usize, 194 | clients_count: usize, 195 | clients_duplication: usize, 196 | stage: usize, 197 | } 198 | impl State { 199 | pub fn publishers(mut self, publishers_count: usize, publishers_duplication: usize) -> Self { 200 | self.publishers_count = publishers_count; 201 | self.publishers_duplication = publishers_duplication; 202 | self 203 | } 204 | pub fn subscribers(mut self, subscribers_count: usize, subscribers_duplication: usize) -> Self { 205 | self.subscribers_count = subscribers_count; 206 | self.subscribers_duplication = subscribers_duplication; 207 | self 208 | } 209 | pub fn services(mut self, services_count: usize, services_duplication: usize) -> Self { 210 | self.services_count = services_count; 211 | self.services_duplication = services_duplication; 212 | self 213 | } 214 | pub fn clients(mut self, clients_count: usize, clients_duplication: usize) -> Self { 215 | self.clients_count = clients_count; 216 | self.clients_duplication = clients_duplication; 217 | self 218 | } 219 | 220 | async fn summarize( 221 | &self, 222 | ) -> ( 223 | HashMultiSet, 224 | HashMultiSet, 225 | HashMultiSet, 226 | HashMultiSet, 227 | ) { 228 | ( 229 | generate_topics( 230 | self.publishers_count, 231 | self.publishers_duplication, 232 | self.stage, 233 | ) 234 | .await, 235 | generate_topics( 236 | self.subscribers_count, 237 | self.subscribers_duplication, 238 | self.stage, 239 | ) 240 | .await, 241 | generate_topics(self.services_count, self.services_duplication, self.stage).await, 242 | generate_topics(self.clients_count, self.clients_duplication, self.stage).await, 243 | ) 244 | } 245 | } 246 | 247 | async fn test_state_transition( 248 | local_resources: &LocalResources, 249 | rcv: &DiscoveryCollector, 250 | state: &State, 251 | ) { 252 | let (publishers, subscribers, services, clients) = state.summarize().await; 253 | 254 | let mut _pub_entities = Vec::new(); 255 | for publisher in publishers.iter() { 256 | _pub_entities.push(local_resources.declare_publisher(publisher).await); 257 | } 258 | 259 | let mut _sub_entities = Vec::new(); 260 | for subscriber in subscribers.iter() { 261 | _sub_entities.push(local_resources.declare_subscriber(subscriber).await); 262 | } 263 | 264 | let mut _srv_entities = Vec::new(); 265 | for service in services.iter() { 266 | _srv_entities.push(local_resources.declare_service(service).await); 267 | } 268 | 269 | let mut _cl_entities = Vec::new(); 270 | for client in clients.iter() { 271 | _cl_entities.push(local_resources.declare_client(client).await); 272 | } 273 | 274 | rcv.wait_publishers(publishers).await; 275 | rcv.wait_subscribers(subscribers).await; 276 | rcv.wait_services(services).await; 277 | rcv.wait_clients(clients).await; 278 | } 279 | 280 | async fn run_discovery(scenario: Vec) { 281 | let cfg = IsolatedConfig::default(); 282 | 283 | let src_session = session_builder(&cfg).await.unwrap(); 284 | let local_resources = make_local_resources(src_session.clone()); 285 | 286 | let rcv = DiscoveryCollector::new(); 287 | let rcv_session = session_builder(&cfg).await.unwrap(); 288 | let _rcv_discovery = rcv 289 | .use_builder(remote_resources_builder(rcv_session)) 290 | .build() 291 | .await; 292 | 293 | for scene in scenario { 294 | tokio::time::timeout( 295 | TIMEOUT, 296 | test_state_transition(&local_resources, &rcv, &scene), 297 | ) 298 | .await 299 | .expect("Timeout waiting state transition!"); 300 | } 301 | } 302 | 303 | #[test_case([State::default().publishers(1, 1)].into_iter().collect(); "single_publisher")] 304 | #[test_case([State::default().subscribers(1, 1)].into_iter().collect(); "single_subscriber")] 305 | #[test_case([State::default().services(1, 1)].into_iter().collect(); "single_service")] 306 | #[test_case([State::default().clients(1, 1)].into_iter().collect(); "single_client")] 307 | #[test_case([State::default().publishers(1, 1), 308 | State::default().subscribers(1, 1), 309 | State::default().services(1, 1), 310 | State::default().clients(1, 1),].into_iter().collect(); "single_transition")] 311 | #[test_case([State::default().publishers(1, 1), 312 | State::default(), 313 | State::default().subscribers(1, 1), 314 | State::default(), 315 | State::default().services(1, 1), 316 | State::default(), 317 | State::default().clients(1, 1),].into_iter().collect(); "single_transition_with_zero_state")] 318 | #[test_case([State::default().publishers(100, 1)].into_iter().collect(); "multiple_publisher")] 319 | #[test_case([State::default().subscribers(100, 1)].into_iter().collect(); "multiple_subscriber")] 320 | #[test_case([State::default().services(100, 1)].into_iter().collect(); "multiple_service")] 321 | #[test_case([State::default().clients(100, 1)].into_iter().collect(); "multiple_client")] 322 | #[test_case([State::default().publishers(100, 1), 323 | State::default().subscribers(100, 1), 324 | State::default().services(100, 1), 325 | State::default().clients(100, 1),].into_iter().collect(); "multiple_transition")] 326 | #[test_case([State::default().publishers(100, 1), 327 | State::default(), 328 | State::default().subscribers(100, 1), 329 | State::default(), 330 | State::default().services(100, 1), 331 | State::default(), 332 | State::default().clients(100, 1),].into_iter().collect(); "multiple_transition_with_zero_state")] 333 | #[tokio::test(flavor = "multi_thread")] 334 | async fn discover(vec_state: Vec) { 335 | run_discovery(vec_state).await; 336 | } 337 | --------------------------------------------------------------------------------