Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ DISTRO ?= lyrical
ros2-interop:
@printf "$(GREEN)ROS 2 interop on $(DISTRO)...$(NC)\n"
cargo build --package aimdb-zenoh-connector --features "std" --examples
cargo build --package aimdb-cli
./aimdb-zenoh-connector/tests/interop/ros2_interop.sh $(DISTRO)

## Drift guards
Expand Down
11 changes: 11 additions & 0 deletions aimdb-zenoh-connector/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **ROS 2 examples and walkthrough (#421).** `ros2_gateway` keeps
`geometry_msgs/Twist` from `/cmd_vel` as a record and republishes it as
JSON on plain Zenoh, on one session, with AimX for `aimdb watch`.
`turtlesim_mcp` drives turtlesim with `GoTo`/`Stop` goals written over AimX,
for example by Claude through `aimdb-mcp`; a `transform_join` controller
turns goal and pose into `cmd_vel`. `zenoh_sub` prints a key expression's
samples. `docs/ros2-walkthrough.md` runs both, and `make ros2-interop`
checks them against Jazzy and Lyrical.

## [0.1.0] - 2026-11-03

### Added
Expand Down
14 changes: 14 additions & 0 deletions aimdb-zenoh-connector/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
# `tests/ros2_outbound.rs`: derived ROS types and a non-CDR codec.
aimdb-data-contracts = { path = "../aimdb-data-contracts", features = ["ros2", "linkable-json"] }
serde = { version = "1", features = ["derive"] }
# `examples/ros2_gateway.rs`: AimX over a Unix socket, for `aimdb watch`.
aimdb-uds-connector = { path = "../aimdb-uds-connector" }
# The `register` bound, as a compile error.
trybuild = "1.0"

Expand All @@ -73,3 +75,15 @@ required-features = ["std"]
[[example]]
name = "ros2_listener"
required-features = ["std"]

[[example]]
name = "ros2_gateway"
required-features = ["std"]

[[example]]
name = "zenoh_sub"
required-features = ["std"]

[[example]]
name = "turtlesim_mcp"
required-features = ["std"]
13 changes: 11 additions & 2 deletions aimdb-zenoh-connector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,16 @@ cargo test -p aimdb-zenoh-connector --features std # includes an in-process ro
make ros2-interop DISTRO=lyrical # against real ROS 2, in Docker
```

The examples `ros2_talker` and `ros2_listener` are a publisher and a
subscription for trying the connector against a ROS 2 host.
## Examples

| Example | What it shows |
|---|---|
| `ros2_talker`, `ros2_listener` | A ROS publisher and a ROS subscription |
| [`ros2_gateway`](examples/ros2_gateway.rs) | `geometry_msgs/Twist` from `/cmd_vel` kept as a record, republished as JSON on plain Zenoh on the same session, watched with `aimdb watch` |
| [`turtlesim_mcp`](examples/turtlesim_mcp.rs) | turtlesim driven by goals written over AimX, for example by Claude through `aimdb-mcp`; an AimDB controller turns goal and pose into `cmd_vel` |
| [`zenoh_sub`](examples/zenoh_sub.rs) | Prints the samples on a key expression, a stand-in for `z_sub` |

[The ROS 2 walkthrough](https://github.com/aimdb-dev/aimdb/blob/main/docs/ros2-walkthrough.md)
runs the gateway and the turtle step by step; `make ros2-interop` checks it.

Design: [053 — Zenoh connector](https://github.com/aimdb-dev/aimdb/blob/main/docs/design/053-zenoh-connector.md).
104 changes: 104 additions & 0 deletions aimdb-zenoh-connector/examples/ros2_gateway.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! A ROS 2 → Zenoh telemetry gateway: every `geometry_msgs/msg/Twist` on
//! `/cmd_vel` becomes a typed AimDB record, is republished as JSON on the
//! plain Zenoh key `telemetry/cmd_vel`, and can be watched over AimX.
//!
//! `ros2://` and `zenoh://` share one session to the router at
//! `ZENOH_ENDPOINT` (default `tcp/127.0.0.1:7447`). The ROS domain comes from
//! `ROS_DOMAIN_ID` (default 0). AimX listens on `AIMX_SOCKET` (default
//! `/tmp/aimdb-ros2-gateway.sock`), read-only.
//!
//! ```text
//! cargo run -p aimdb-zenoh-connector --features std --example ros2_gateway
//! ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
//! "{linear: {x: 0.5}, angular: {z: 0.3}}"
//! aimdb watch robot.cmd_vel
//! cargo run -p aimdb-zenoh-connector --features std --example zenoh_sub -- 'telemetry/**'
//! ```
//!
//! Walkthrough: `docs/ros2-walkthrough.md`.

use std::sync::Arc;

use aimdb_core::buffer::BufferCfg;
use aimdb_core::remote::{AimxConfig, SecurityPolicy};
use aimdb_core::AimDbBuilder;
use aimdb_data_contracts::link_codecs::Json;
use aimdb_data_contracts::{LinkCodecRegistrarExt, LinkableRegistrarExt, RosMessage};
use aimdb_tokio_adapter::{TokioAdapter, TokioRecordRegistrarExt};
use aimdb_uds_connector::UdsServer;
use aimdb_zenoh_connector::{Ros2Node, ZenohConnector};
use serde::{Deserialize, Serialize};

/// `geometry_msgs/msg/Vector3`: `float64 x, y, z`.
#[derive(Clone, Debug, Serialize, Deserialize, RosMessage)]
#[ros(
type = "geometry_msgs/msg/Vector3",
hash = "RIHS01_cc12fe83e4c02719f1ce8070bfd14aecd40f75a96696a67a2a1f37f7dbb0765d"
)]
struct Vector3 {
x: f64,
y: f64,
z: f64,
}

/// `geometry_msgs/msg/Twist`, with the hash Jazzy and Lyrical publish
/// (`ros2 topic info -v /cmd_vel`). The fields are in `.msg` order: CDR is
/// positional, and `Vector3` is encoded inline.
#[derive(Clone, Debug, Serialize, Deserialize, RosMessage)]
#[ros(
type = "geometry_msgs/msg/Twist",
hash = "RIHS01_9c45bf16fe0983d80e3cfe750d6835843d265a9a6c46bd2e609fcddde6fb8d2a"
)]
struct Twist {
linear: Vector3,
angular: Vector3,
}

#[tokio::main]
async fn main() {
let endpoint = std::env::var("ZENOH_ENDPOINT").unwrap_or_else(|_| "tcp/127.0.0.1:7447".into());
let socket =
std::env::var("AIMX_SOCKET").unwrap_or_else(|_| "/tmp/aimdb-ros2-gateway.sock".into());

// One Zenoh session: the ROS view is derived from the plain connector.
let zenoh = ZenohConnector::new(endpoint.as_str());
let ros2 = zenoh
.ros2(Ros2Node::new("telemetry_gateway").namespace("/aimdb"))
.register::<Twist>();

// AimX over a Unix socket, for `aimdb watch`. Read-only: ROS is the only
// writer of the record.
let _ = std::fs::remove_file(&socket);
let aimx = UdsServer::from_config(
AimxConfig::uds_default()
.socket_path(socket.as_str())
.security_policy(SecurityPolicy::read_only()),
);

let mut builder = AimDbBuilder::new()
.runtime(Arc::new(TokioAdapter::new().unwrap()))
.with_connector(zenoh)
.with_connector(ros2)
.with_connector(aimx);
// SingleLatest: the record is the robot's current command, which
// `aimdb record get` reads and a late `aimdb watch` receives first.
builder.configure::<Twist>("robot.cmd_vel", |reg| {
reg.buffer(BufferCfg::SingleLatest)
.with_remote_access()
.linked_from("ros2://cmd_vel") // CDR, as a ROS subscription
.linked_to_with("zenoh://telemetry/cmd_vel", Json); // JSON, plain Zenoh
});
let (db, runner) = builder.build().await.expect("build");
let mut reader = db.subscribe::<Twist>("robot.cmd_vel").expect("reader");
tokio::spawn(runner.run());

println!("ros2_gateway: /cmd_vel -> robot.cmd_vel -> telemetry/cmd_vel");
println!("ros2_gateway: router {endpoint}, AimX unix://{socket}");
while let Ok(twist) = reader.recv().await {
let Twist { linear, angular } = &twist.value;
println!(
"cmd_vel: linear ({}, {}, {}) angular ({}, {}, {})",
linear.x, linear.y, linear.z, angular.x, angular.y, angular.z
);
}
}
Loading
Loading