Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- roslibrust_transforms gained `latest_common_time()` for looking up the newest time a transform can be served, and `remove_frame()` for removing a frame from the local buffer (e.g. to allow re-parenting it).

### Fixed

- @JesseGuillory-CM removed several panics and poor error handling from rosbridge client.

### Changed

- Upgraded roslibrust_transforms to transforms v2.1. Static transforms are now represented by `Stamp::Static` instead of a zero timestamp, transforms are built with `Transform::new` / `Transform::static_between` instead of struct literals, and `add_transform()` publishes static transforms to /tf_static automatically, replacing `update_static_transform()`. Invalid or conflicting transforms received over the wire are now dropped with a warning instead of silently corrupting the buffer.

## 0.21.0 - May 19th, 2026

### Added
Expand Down
22 changes: 17 additions & 5 deletions Cargo.lock

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

3 changes: 1 addition & 2 deletions roslibrust_transforms/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ keywords = ["ROS", "robotics", "tf2", "transforms", "coordinates"]
roslibrust_common = { path = "../roslibrust_common", version = "0.21" }
# roslibrust is needed because the macro-generated code references ::roslibrust::
roslibrust = { path = "../roslibrust", version = "0.21", features = ["macro", "codegen"] }
transforms = "1.2"
transforms = "2.1"
tokio = { workspace = true }
log = { workspace = true }
thiserror = "2.0"
serde = { workspace = true }
tokio-util = "0.7"
chrono = "0.4"

[dev-dependencies]
env_logger = "0.11"
Expand Down
57 changes: 28 additions & 29 deletions roslibrust_transforms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ roslibrust_transforms = "0.1"
### Basic Example

```rust
use roslibrust_transforms::{TransformManager, Ros1TFMessage};
use roslibrust_transforms::{TransformManager, Ros1TFMessage, Timestamp};

async fn example(ros: impl roslibrust_common::TopicProvider + Clone + Send + Sync + 'static) {
// Create a TransformManager (subscribes to /tf and /tf_static automatically)
let manager = TransformManager::<Ros1TFMessage, _>::new(&ros).await.unwrap();
let manager = TransformManager::<Ros1TFMessage, _>::new(&ros, std::time::Duration::from_secs(10)).await.unwrap();

// Look up a transform
let transform = manager.lookup_latest_transform("base_link", "camera_link").await.unwrap();
let transform = manager.get_transform("base_link", "camera_link", Timestamp::now()).await.unwrap();

println!("Translation: {:?}", transform.translation);
println!("Rotation: {:?}", transform.rotation);
println!("Translation: {:?}", transform.translation());
println!("Rotation: {:?}", transform.rotation());
}
```

Expand All @@ -41,38 +41,37 @@ The only difference is the message type parameter:

```rust
// ROS1
let manager = TransformManager::<Ros1TFMessage, _>::new(&ros).await?;
let manager = TransformManager::<Ros1TFMessage, _>::new(&ros, std::time::Duration::from_secs(10)).await?;

// ROS2
let manager = TransformManager::<Ros2TFMessage, _>::new(&ros).await?;
let manager = TransformManager::<Ros2TFMessage, _>::new(&ros, std::time::Duration::from_secs(10)).await?;
```

### Publishing Transforms

```rust
use roslibrust_transforms::{TransformManager, Ros1TFMessage, Transform, Timestamp};
use roslibrust_transforms::{TransformManager, Ros1TFMessage, Stamp, Timestamp, Transform, Quaternion, Vector3};

async fn broadcast_example(ros: impl roslibrust_common::TopicProvider + Clone + Send + Sync + 'static) {
let manager = TransformManager::<Ros1TFMessage, _>::new(&ros).await.unwrap();

// Create and publish a dynamic transform
let transform = Transform {
parent_frame: "world".to_string(),
child_frame: "robot".to_string(),
translation: Default::default(),
rotation: Default::default(),
timestamp: Timestamp::now(),
};
manager.update_transform(transform).await.unwrap();

// Or publish a static transform
let static_tf = Transform {
parent_frame: "robot".to_string(),
child_frame: "sensor".to_string(),
translation: Default::default(),
rotation: Default::default(),
timestamp: Timestamp::now(), // Will be set to zero internally
};
manager.update_static_transform(static_tf).await.unwrap();
let manager = TransformManager::<Ros1TFMessage, _>::new(&ros, std::time::Duration::from_secs(10)).await.unwrap();

// Create and publish a dynamic transform, published on /tf
let transform = Transform::new(
"world",
"robot",
Vector3::new(1.0, 0.0, 0.0),
Quaternion::identity(),
Stamp::At(Timestamp::now()),
).unwrap();
manager.add_transform(transform).await.unwrap();

// Static transforms are valid for all time, and are published on /tf_static
let static_tf = Transform::static_between(
"robot",
"sensor",
Vector3::new(0.1, 0.0, 0.5),
Quaternion::identity(),
).unwrap();
manager.add_transform(static_tf).await.unwrap();
}
```
41 changes: 25 additions & 16 deletions roslibrust_transforms/examples/ros1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

use std::time::Duration;

use roslibrust_transforms::{Ros1TFMessage, Timestamp, TransformManager};
use roslibrust_transforms::{Ros1TFMessage, Stamp, Timestamp, TransformManager};

use log::*;

Expand Down Expand Up @@ -57,30 +57,39 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Try to look up a transform from "world" to "base_link"
match manager.get_transform("world", "base_link", Timestamp::now()).await {
Ok(transform) => {
let translation = transform.translation();
info!(
"Transform world -> base_link: translation=({:.3}, {:.3}, {:.3})",
transform.translation.x,
transform.translation.y,
transform.translation.z
translation.x,
translation.y,
translation.z
);
}
Err(e) => {
warn!("Could not look up transform: {}", e);
}
}

// Also try looking up with a specific timestamp (for static transforms, use zero)
match manager.get_transform("world", "base_link", Timestamp::zero()).await {
Ok(transform) => {
info!(
"Static transform world -> base_link: translation=({:.3}, {:.3}, {:.3})",
transform.translation.x,
transform.translation.y,
transform.translation.z
);
}
Err(_) => {
// Static transform not available yet
// Also look up the newest transform the local buffer can serve
if let Ok(latest) = manager.latest_common_time("world", "base_link").await {
let time = match latest {
Stamp::At(time) => time,
// Frames connected by static transforms only can be looked up at any time
Stamp::Static => Timestamp::now(),
};
match manager.get_transform("world", "base_link", time).await {
Ok(transform) => {
let translation = transform.translation();
info!(
"Latest transform world -> base_link: translation=({:.3}, {:.3}, {:.3})",
translation.x,
translation.y,
translation.z
);
}
Err(e) => {
warn!("Could not look up latest transform: {}", e);
}
}
}
}
Expand Down
41 changes: 25 additions & 16 deletions roslibrust_transforms/examples/ros2_rosbridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
use std::time::Duration;

use roslibrust_rosbridge::ClientHandle;
use roslibrust_transforms::{Ros2TFMessage, Timestamp, TransformManager};
use roslibrust_transforms::{Ros2TFMessage, Stamp, Timestamp, TransformManager};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
Expand Down Expand Up @@ -55,30 +55,39 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Try to look up a transform from "world" to "base_link"
match manager.get_transform("world", "base_link", Timestamp::now()).await {
Ok(transform) => {
let translation = transform.translation();
log::info!(
"Transform world -> base_link: translation=({:.3}, {:.3}, {:.3})",
transform.translation.x,
transform.translation.y,
transform.translation.z
translation.x,
translation.y,
translation.z
);
}
Err(e) => {
log::warn!("Could not look up transform: {}", e);
}
}

// Also try looking up with a specific timestamp (for static transforms, use zero)
match manager.get_transform("world", "base_link", Timestamp::zero()).await {
Ok(transform) => {
log::info!(
"Static transform world -> base_link: translation=({:.3}, {:.3}, {:.3})",
transform.translation.x,
transform.translation.y,
transform.translation.z
);
}
Err(_) => {
// Static transform not available yet
// Also look up the newest transform the local buffer can serve
if let Ok(latest) = manager.latest_common_time("world", "base_link").await {
let time = match latest {
Stamp::At(time) => time,
// Frames connected by static transforms only can be looked up at any time
Stamp::Static => Timestamp::now(),
};
match manager.get_transform("world", "base_link", time).await {
Ok(transform) => {
let translation = transform.translation();
log::info!(
"Latest transform world -> base_link: translation=({:.3}, {:.3}, {:.3})",
translation.x,
translation.y,
translation.z
);
}
Err(e) => {
log::warn!("Could not look up latest transform: {}", e);
}
}
}
}
Expand Down
28 changes: 14 additions & 14 deletions roslibrust_transforms/examples/shared_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use log::*;
// Example uses the mock backend for simplicity in running / testing, but any backend can be used
use roslibrust::mock::MockRos;
use roslibrust_transforms::{
Quaternion, Ros1TFMessage, Timestamp, Transform, TransformManager, Vector3,
Quaternion, Ros1TFMessage, Stamp, Timestamp, Transform, TransformManager, Vector3,
};

#[tokio::main]
Expand All @@ -34,16 +34,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("Publishing transform...");
// Because of internal mutability, add_transform() doesn't require `&mut self`
// So you don't need to use Mutex<T> or RefCell<T> to share the manager
tf_mgr2
.add_transform(Transform {
parent: "world".to_string(),
child: "robot".to_string(),
translation: Vector3::new(x, 0.0, 0.0),
rotation: Quaternion::identity(),
timestamp: Timestamp::now(),
})
.await
.unwrap();
let transform = Transform::new(
"world",
"robot",
Vector3::new(x, 0.0, 0.0),
Quaternion::identity(),
Stamp::At(Timestamp::now()),
)
.unwrap();
tf_mgr2.add_transform(transform).await.unwrap();
x += 1.0;
}
});
Expand All @@ -65,10 +64,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
xform = tf_mgr.wait_for_transform("world", "robot", lookup, None) => {
match xform {
Ok(xform) => {
let translation = xform.translation();
info!("Transform: translation=({:.3}, {:.3}, {:.3})",
xform.translation.x,
xform.translation.y,
xform.translation.z
translation.x,
translation.y,
translation.z
);
}
Err(e) => {
Expand Down
Loading
Loading