Skip to content

Conversation

@leshy
Copy link
Contributor

@leshy leshy commented Jan 16, 2026

2026-01-15_00-23

@leshy leshy requested a review from a team January 16, 2026 01:08
@greptile-apps
Copy link

greptile-apps bot commented Jan 16, 2026

Greptile Summary

This PR adds ROS 2 transport support to dimos, enabling seamless interoperability between LCM and ROS messaging systems. The implementation includes automatic message conversion between dimos_lcm and ROS message types, comprehensive benchmarking infrastructure, and integration with the existing blueprint system.

Key Changes:

  • New RawROS class provides direct ROS 2 pub/sub with configurable QoS profiles
  • LCM2ROSMixin enables automatic bidirectional message conversion between LCM and ROS formats
  • ROSTransport integrates ROS messaging into the transport abstraction layer
  • Blueprint system now supports default_transport="ros" configuration
  • Comprehensive benchmarks compare ROS (best-effort and reliable), LCM, SHM, and Memory transports
  • Unit tests verify roundtrip conversion for simple and nested message types

Issues Found:

  • Multiple ROSTransport instances each create separate ROS nodes, causing resource overhead and potential node name conflicts
  • rclpy.init() and rclpy.shutdown() are called without proper multi-instance coordination
  • Busy-waiting in executor spin loop (timeout_sec=0) wastes CPU
  • Instantiating LCM types with type()() in blueprints.py assumes no-arg constructors

Confidence Score: 3/5

  • This PR has solid architecture but critical multi-instance bugs will cause issues in production
  • The ROS transport implementation is architecturally sound with good test coverage, but has critical bugs around resource management (multiple ROS nodes per transport, improper rclpy init/shutdown coordination) that will cause failures when multiple transports are used. The message conversion logic is well-designed and tested. Since this is marked WIP, these issues should be addressed before merging.
  • Pay close attention to dimos/protocol/pubsub/rospubsub.py and dimos/core/transport.py for multi-instance resource management issues

Important Files Changed

Filename Overview
dimos/protocol/pubsub/rospubsub.py New ROS2 transport layer added with automatic LCM↔ROS message conversion. Includes RawROS for direct ROS messaging and DimosROS/LCM2ROSMixin for seamless interop. Potential issue: Multiple ROSTransport instances create separate ROS nodes which may cause resource issues.
dimos/core/transport.py Added ROSTransport class for publishing/subscribing via ROS2 with automatic LCM message conversion. Issue: Each ROSTransport creates its own DimosROS instance, leading to multiple ROS nodes per topic.
dimos/core/blueprints.py Modified to support ROS transport backend selection via GlobalConfig. Added logic to create ROSTransport instances when backend="ros". Clean integration with existing transport system.

Sequence Diagram

sequenceDiagram
    participant User
    participant Blueprint
    participant ROSTransport
    participant DimosROS
    participant RawROS
    participant ROS2

    User->>Blueprint: build() with default_transport="ros"
    Blueprint->>Blueprint: _connect_transports()
    Blueprint->>Blueprint: _get_transport_for(name, type, "ros")
    Blueprint->>Blueprint: _get_ros_type(lcm_type())
    Note over Blueprint: Creates temp instance to get ROS type
    Blueprint->>ROSTransport: new ROSTransport(topic, ros_type)
    
    User->>ROSTransport: broadcast(msg)
    ROSTransport->>ROSTransport: start()
    ROSTransport->>DimosROS: new DimosROS()
    DimosROS->>RawROS: __init__()
    DimosROS->>RawROS: start()
    RawROS->>ROS2: rclpy.init()
    RawROS->>ROS2: Node(node_name)
    RawROS->>ROS2: SingleThreadedExecutor()
    RawROS-->>RawROS: spawn _spin thread
    
    ROSTransport->>DimosROS: publish(topic, lcm_msg)
    DimosROS->>DimosROS: encode(lcm_msg)
    Note over DimosROS: LCM2ROSMixin converts to ROS msg
    DimosROS->>RawROS: publish(topic, ros_msg)
    RawROS->>ROS2: publisher.publish(ros_msg)
    
    User->>ROSTransport: subscribe(callback)
    ROSTransport->>DimosROS: subscribe(topic, callback)
    DimosROS->>RawROS: subscribe(topic, internal_callback)
    RawROS->>ROS2: create_subscription(topic, ros_callback)
    
    ROS2-->>RawROS: message received
    RawROS-->>DimosROS: ros_callback(ros_msg)
    DimosROS->>DimosROS: decode(ros_msg)
    Note over DimosROS: LCM2ROSMixin converts to LCM msg
    DimosROS-->>User: callback(lcm_msg)
    
    User->>ROSTransport: stop()
    ROSTransport->>DimosROS: stop()
    DimosROS->>RawROS: stop()
    RawROS->>ROS2: rclpy.shutdown()
    Note over RawROS: Shuts down ALL ROS nodes in process!
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +129 to +175
class ROSTransport(PubSubTransport[T]):
"""Transport that publishes/subscribes via ROS 2.
Automatically converts between dimos_lcm messages and ROS messages.
Usage:
from geometry_msgs.msg import Vector3 as ROSVector3
ROSTransport("/my_topic", ROSVector3)
"""

_started: bool = False
_ros: DimosROS | None = None

def __init__(self, topic: str, ros_type: type, **kwargs) -> None: # type: ignore[no-untyped-def]
if not ROS_AVAILABLE:
raise ImportError("ROS not available. Install rclpy to use ROSTransport.")
super().__init__(ROSTopic(topic, ros_type))
self._kwargs = kwargs

def _ensure_ros(self) -> DimosROS:
if self._ros is None:
self._ros = DimosROS(**self._kwargs)
return self._ros

def start(self) -> None:
if not self._started:
self._ensure_ros().start()
self._started = True

def stop(self) -> None:
if self._ros is not None:
self._ros.stop()

def __reduce__(self): # type: ignore[no-untyped-def]
return (ROSTransport, (self.topic.topic, self.topic.ros_type))

def broadcast(self, _, msg) -> None: # type: ignore[no-untyped-def]
"""Publish a dimos_lcm message to ROS (auto-converts to ROS message)."""
self.start()
self._ensure_ros().publish(self.topic, msg)

def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: # type: ignore[assignment, override]
"""Subscribe to ROS topic (auto-converts ROS messages to dimos_lcm)."""
self.start()
return self._ensure_ros().subscribe(self.topic, lambda msg, topic: callback(msg)) # type: ignore[return-value]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Each ROSTransport instance creates its own DimosROS node. When multiple topics use ROS transport, this creates multiple ROS nodes which is resource-intensive and can cause node name conflicts.

Consider using a singleton pattern or shared ROS node instance across all ROSTransport instances, similar to how LCM shares a single LCM instance.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +328 to +331
def _spin(self) -> None:
"""Background thread for spinning the ROS executor."""
while self._running and self._executor:
self._executor.spin_once(timeout_sec=0) # Non-blocking for max throughput
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: timeout_sec=0 causes busy-waiting which consumes CPU even when idle. Consider using a small timeout (e.g., 0.01 or 0.1 seconds) to reduce CPU usage while maintaining responsiveness.

Comment on lines +289 to +290
if not rclpy.ok():
rclpy.init()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: rclpy.init() is called without checking if it was already initialized by another instance. If multiple RawROS instances are created, this will cause an error on the second call.

Suggested change
if not rclpy.ok():
rclpy.init()
if not rclpy.ok():
try:
rclpy.init()
except RuntimeError:
# Already initialized by another instance
pass

Comment on lines +321 to +322
if rclpy.ok():
rclpy.shutdown()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: rclpy.shutdown() is called unconditionally, which will shut down rclpy for all ROS nodes in the process. If multiple RawROS instances exist, shutting down one will break the others.

Consider not calling rclpy.shutdown() here, or implementing reference counting to only shut down when the last instance stops.

Comment on lines +144 to +145

ros_type = _get_ros_type(type()) # Get ROS type from an instance
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Creating a temporary instance with type()() to call _get_ros_type assumes the LCM type has a no-argument constructor. This will fail for types that require constructor arguments.

Consider modifying _get_ros_type to accept a type instead of an instance, or handle the case where instantiation fails.

@leshy leshy mentioned this pull request Jan 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants