Skip to content

feat: ros2 connector #379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jan 21, 2025
Merged

feat: ros2 connector #379

merged 18 commits into from
Jan 21, 2025

Conversation

maciejmajek
Copy link
Member

@maciejmajek maciejmajek commented Jan 20, 2025

Purpose

New RAI sdk is based on Connector architecture which closely resembles ROS 2 features/api. This PR implements framework standard ROS2 connectors.

Proposed Changes

ROS2 ARI (Agent-Robot-Interface) connector based on out generic ROS 2 api.

Issues

  • Links to relevant issues

Testing

  • How was it tested, what were the results?

Summary by CodeRabbit

  • New Features

    • Enhanced ROS2 communication framework with improved message handling.
    • Added flexible message initialization with metadata support.
    • Introduced new ROS2 connector for advanced messaging capabilities.
  • Bug Fixes

    • Improved error handling in message and action processing.
    • Refined topic and message type verification mechanisms.
  • Refactor

    • Simplified message class structure by removing unnecessary classes.
    • Centralized test helper utilities for ROS2 communication.
    • Updated message and action handling methods.
  • Tests

    • Added comprehensive test suite for ROS2 communication components.
    • Implemented test cases for message sending, receiving, and action management.
    • Updated existing tests to enhance maintainability and address threading model changes.

@maciejmajek maciejmajek requested a review from rachwalk January 20, 2025 22:04
Copy link
Contributor

coderabbitai bot commented Jan 21, 2025

@maciejmajek i'll perform a comprehensive review of the ros2 connector implementation in the rai sdk. i'll analyze the code changes, architecture, and implementation details thoroughly.

✅ Actions performed

Full review triggered.

@maciejmajek
Copy link
Member Author

@coderabbitai full review

Copy link
Contributor

coderabbitai bot commented Jan 21, 2025

Walkthrough

This pull request introduces significant modifications to the ROS2 communication framework, focusing on the refactoring of message handling and the enhancement of API capabilities. Key changes include the removal of the ROS2RRIMessage class, updates to the ARIMessage class for improved initialization, and the introduction of the ROS2ARIConnector class for ROS2 communication. Additionally, the changes enhance the testing infrastructure with new helper classes and comprehensive test cases for various functionalities, aiming to streamline the communication process within ROS2-based systems.

Changes

File Change Summary
src/rai/rai/communication/ari_connector.py - Removed ROS2RRIMessage class
- Updated ARIMessage constructor to accept flexible payload and metadata
src/rai/rai/communication/base_connector.py - Removed Protocol inheritance
- Added metadata attribute to BaseMessage
- Updated constructor to support optional metadata
src/rai/rai/communication/ros2/api.py - Added _verify_receive_args method in ROS2TopicAPI
- Introduced ThreadPoolExecutor for feedback handling in ROS2ActionAPI
- Enhanced method signatures for more robust message and action handling
src/rai/rai/communication/ros2/connectors.py - Added ROS2ARIMessage class
- Implemented ROS2ARIConnector with comprehensive communication methods
- Added methods for message sending, receiving, service calls, and action management
tests/communication/ros2/helpers.py - Added helper classes for ROS2 testing
- Implemented utility functions for multi-threaded spinning and executor management
tests/communication/ros2/test_connectors.py - Added comprehensive test suite for ROS2ARIConnector
- Implemented tests for message sending, receiving, service calls, and action management

Sequence Diagram

sequenceDiagram
    participant Connector as ROS2ARIConnector
    participant TopicAPI as ROS2TopicAPI
    participant ActionAPI as ROS2ActionAPI
    
    Connector->>TopicAPI: send_message()
    TopicAPI-->>Connector: Verify message metadata
    
    Connector->>TopicAPI: receive_message()
    TopicAPI-->>Connector: Return ROS2ARIMessage
    
    Connector->>ActionAPI: start_action()
    ActionAPI->>ActionAPI: Manage concurrent feedback
    ActionAPI-->>Connector: Return action handle
    
    Connector->>ActionAPI: terminate_action()
    ActionAPI-->>Connector: Terminate goal
Loading

Possibly related PRs

Suggested reviewers

  • rachwalk
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (11)
src/rai/rai/communication/ros2/connectors.py (1)

48-51: Consider implementing automatic message type detection for enhanced usability.

The send_message method currently requires the msg_type in the message metadata. The TODO comment suggests allowing msg_type to be None and adding automatic topic type detection. Implementing this feature would improve the usability by removing the necessity for the user to specify the message type when it can be inferred.

src/rai/rai/communication/ros2/api.py (5)

Line range hint 162-169: Clarify documentation for publish method parameters.

The publish method's docstring mentions that msg_type is required, but with the introduction of *, the auto_qos_matching and qos_profile parameters are now keyword-only arguments. Update the docstring to reflect these changes and adjust the parameter descriptions accordingly.

Apply this diff to update the docstring:

     def publish(
         self,
         topic: str,
         msg_content: Dict[str, Any],
         msg_type: str,
+        *,
         auto_qos_matching: bool = True,
         qos_profile: Optional[QoSProfile] = None,
     ) -> None:
         """Publish a message to a ROS2 topic.

         Args:
             topic: Name of the topic to publish to
             msg_content: Dictionary containing the message content
             msg_type: ROS2 message type as string (e.g. 'std_msgs/msg/String')
+            auto_qos_matching: Whether to automatically match QoS with subscribers
+            qos_profile: Optional custom QoS profile to use

         Raises:
             ValueError: If neither auto_qos_matching is True nor qos_profile is provided
         """

186-193: Ensure proper argument validation in _verify_receive_args.

The _verify_receive_args method correctly checks for incompatible arguments. However, consider refining the error messages to provide clearer guidance to the user.

Apply this diff to improve error messages:

     def _verify_receive_args(
         self, topic: str, auto_topic_type: bool, msg_type: Optional[str]
     ) -> None:
         if auto_topic_type and msg_type is not None:
-            raise ValueError("Cannot provide both auto_topic_type and msg_type")
+            raise ValueError("Provide either 'msg_type' or set 'auto_topic_type' to True, not both.")
         if not auto_topic_type and msg_type is None:
-            raise ValueError("msg_type must be provided if auto_topic_type is False")
+            raise ValueError("When 'auto_topic_type' is False, 'msg_type' must be provided.")

221-231: Redundant msg_type check in receive method.

After calling _verify_receive_args, there's an extra check for msg_type being None, which is redundant since it's already handled. Consider removing the redundant check to simplify the code.

Apply this diff to remove the redundant check:

         self._verify_receive_args(topic, auto_topic_type, msg_type)
         topic_endpoints = self._verify_publisher_exists(topic)

         # TODO: Verify publishers topic type consistency
         if auto_topic_type:
             msg_type = topic_endpoints[0].topic_type
-        else:
-            if msg_type is None:
-                raise ValueError(
-                    "msg_type must be provided if auto_topic_type is False"
-                )

297-306: Update _verify_publisher_exists documentation to reflect return type change.

The _verify_publisher_exists method now returns a List[TopicEndpointInfo], but the docstring still suggests it only raises an exception. Update the documentation to include information about the return value.

Apply this diff to update the docstring:

     def _verify_publisher_exists(self, topic: str) -> List[TopicEndpointInfo]:
         """Verify that at least one publisher exists for the given topic.

+        Returns:
+            A list of TopicEndpointInfo objects for the publishers.

         Raises:
             ValueError: If no publisher exists for the topic
         """

450-451: Handle exceptions in done_callback to prevent unhandled errors.

When adding the done_callback, it's important to handle any exceptions that might occur within the callback to prevent them from propagating and causing unhandled errors.

Apply this diff to wrap the done_callback:

         get_result_future = cast(Future, goal_handle.get_result_async())
-        get_result_future.add_done_callback(done_callback)
+        def wrapped_done_callback(future):
+            try:
+                done_callback(future)
+            except Exception as e:
+                self._logger.error(f"Exception in done_callback: {e}")
+
+        get_result_future.add_done_callback(wrapped_done_callback)
tests/communication/ros2/helpers.py (3)

56-56: Consider renaming the class to avoid trailing underscore.

The class name ActionServer_ uses an unconventional naming pattern. Consider a more descriptive name like TestActionServer or MockActionServer.


69-81: Consider increasing the sleep duration in execute callback.

The sleep duration of 0.01s might be too short and could lead to excessive CPU usage in tests. Consider increasing it to 0.1s unless there's a specific requirement for rapid feedback.

-            time.sleep(0.01)
+            time.sleep(0.1)

110-124: Consider using a single executor for multiple nodes.

Creating separate executors for each node might be inefficient. Consider using a single MultiThreadedExecutor for all nodes unless there's a specific requirement for isolation.

def multi_threaded_spinner(
    nodes: List[Node],
) -> Tuple[List[MultiThreadedExecutor], List[threading.Thread]]:
-    executors: List[MultiThreadedExecutor] = []
+    executor = MultiThreadedExecutor()
     executor_threads: List[threading.Thread] = []
     for node in nodes:
-        executor = MultiThreadedExecutor()
         executor.add_node(node)
-        executors.append(executor)
-    for executor in executors:
-        executor_thread = threading.Thread(target=executor.spin)
-        executor_thread.daemon = True
-        executor_thread.start()
-        executor_threads.append(executor_thread)
+    executor_thread = threading.Thread(target=executor.spin)
+    executor_thread.daemon = True
+    executor_thread.start()
+    executor_threads.append(executor_thread)
-    return executors, executor_threads
+    return [executor], executor_threads
tests/communication/ros2/test_api.py (1)

378-380: Consider extracting common test setup code.

Multiple test functions have similar setup code. Consider creating a fixture to reduce duplication:

@pytest.fixture
def setup_action_test(request):
    action_name = f"{request.node.originalname}_action"
    node_name = f"{request.node.originalname}_node"
    action_server = ActionServer(action_name)
    node = Node(node_name)
    executors, threads = multi_threaded_spinner([action_server])
    yield action_server, node, executors, threads
    shutdown_executors_and_threads(executors, threads)

Also applies to: 406-408

src/rai/rai/communication/base_connector.py (1)

22-22: Consider restricting metadata value types.

The current type annotation Dict[str, Any] allows any value type in metadata, which could lead to type-related issues. Consider restricting value types to JSON-serializable types or defining a more specific type union.

-    metadata: Dict[str, Any]
+    metadata: Dict[str, Union[str, int, float, bool, None, List[Any], Dict[str, Any]]]
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1595c52 and b80575a.

📒 Files selected for processing (8)
  • src/rai/rai/communication/ari_connector.py (2 hunks)
  • src/rai/rai/communication/base_connector.py (1 hunks)
  • src/rai/rai/communication/ros2/api.py (9 hunks)
  • src/rai/rai/communication/ros2/connectors.py (1 hunks)
  • tests/communication/ros2/__init__.py (1 hunks)
  • tests/communication/ros2/helpers.py (1 hunks)
  • tests/communication/ros2/test_api.py (19 hunks)
  • tests/communication/ros2/test_connectors.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
tests/communication/ros2/test_api.py

36-36: Found useless expression. Either assign it to a variable or remove it.

(B018)

tests/communication/ros2/test_connectors.py

34-34: Found useless expression. Either assign it to a variable or remove it.

(B018)

🔇 Additional comments (12)
src/rai/rai/communication/ros2/connectors.py (2)

25-28: Well-defined ROS2ARIMessage class extending ARIMessage.

The ROS2ARIMessage class correctly extends ARIMessage, and the constructor properly initializes the payload and metadata.


31-42: Proper initialization and resource management in ROS2ARIConnector.

The __init__ method sets up the ROS2 node and APIs correctly. The use of a MultiThreadedExecutor and starting it on a separate thread is appropriate for handling incoming messages asynchronously. The shutdown mechanism is also in place via the shutdown method.

src/rai/rai/communication/ari_connector.py (2)

27-34: Ensure consistency with updated ARIMessage constructor.

The addition of the __init__ method in ARIMessage allows for payload and metadata initialization. This change enhances flexibility in message creation.


15-15: Confirm removal of ROS2RRIMessage and adjust dependent code accordingly.

The ROS2RRIMessage class has been removed. Ensure that all references to this class in the codebase have been updated to use ROS2ARIMessage or another appropriate class to prevent import errors or broken functionality.

Run the following script to search for any remaining references:

✅ Verification successful

ROS2RRIMessage removal verified successfully

The codebase shows no remaining references to ROS2RRIMessage and demonstrates consistent usage of ROS2ARIMessage across both implementation and test files. All dependent code appears to be properly updated.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find references to the removed ROS2RRIMessage class.

# Search the codebase for 'ROS2RRIMessage'
rg 'ROS2RRIMessage'

Length of output: 19


Script:

#!/bin/bash
# Search with various patterns to ensure thorough verification
echo "=== Searching for any RRI or ARI message patterns ==="
rg -i "ros2\s*(rri|ari).*message"

echo -e "\n=== Checking specifically for ROS2ARIMessage ==="
rg "ROS2ARIMessage"

echo -e "\n=== Looking for any message type definitions ==="
ast-grep --pattern 'class $_Message'

Length of output: 3824

tests/communication/ros2/helpers.py (3)

31-41: LGTM! Clean and focused test service implementation.

The service server implementation is well-structured and appropriate for testing purposes.


44-53: LGTM! Well-structured message receiver for testing.

The implementation properly stores received messages for verification in tests.


98-107: LGTM! Clean publisher implementation.

The implementation uses appropriate timer frequency for testing purposes.

tests/communication/ros2/__init__.py (1)

15-29: LGTM! Clean module initialization.

The implementation properly defines the public API and follows Python best practices.

src/rai/rai/communication/base_connector.py (4)

16-16: LGTM! Import changes are well-structured.

The import statement properly includes all necessary types from the typing module to support the new metadata feature.


Line range hint 41-41: LGTM! BaseConnector remains compatible.

The generic typing with T = TypeVar("T", bound=BaseMessage) properly handles the BaseMessage changes, maintaining type safety in the connector interface.


Line range hint 1-73: Verify consistent metadata handling across connectors.

Since BaseMessage now includes metadata support, ensure that all implementing connectors properly handle metadata propagation.

✅ Verification successful

Metadata handling is properly implemented across connectors

The verification shows that metadata is consistently handled:

  • Base implementation provides proper metadata support
  • ROS2 connector correctly initializes and extends metadata with domain-specific information
  • Test coverage validates the metadata handling for messages, services, and actions
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Find all connector implementations
rg -t py "class.*\(.*BaseConnector\)" -C 5

# Search for metadata usage in message handling
rg -t py "metadata.*=.*" -C 2

Length of output: 5274


20-20: Verify impact of Protocol removal on type checking.

The removal of Protocol base class might affect static type checking in code that relies on structural typing. Please ensure this change doesn't break existing type checks.

✅ Verification successful

Protocol removal is safe for type checking

The removal of Protocol from BaseMessage is safe as type checking in the codebase primarily relies on langchain_core.messages.BaseMessage for annotations. Our BaseMessage class is only used as a concrete base class for message implementations (ARIMessage, HRIMessage, MultimodalMessage), not for structural typing.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for usage of BaseMessage in type annotations
rg -t py "BaseMessage" -C 2

Length of output: 13385

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🔭 Outside diff range comments (1)
src/rai/rai/communication/ros2/api.py (1)

Line range hint 197-231: Improve error handling and avoid redundant checks in receive method.

The method _verify_receive_args already handles the validation of auto_topic_type and msg_type. The additional check at lines 228-231 is redundant.

Apply this diff to remove the redundant check:

         else:
-            if msg_type is None:
-                raise ValueError(
-                    "msg_type must be provided if auto_topic_type is False"
-                )
♻️ Duplicate comments (5)
src/rai/rai/communication/ros2/connectors.py (3)

82-85: ⚠️ Potential issue

Add validation for 'msg_type' in message metadata to prevent KeyError.

In the service_call method, message.metadata["msg_type"] is accessed without checking if 'msg_type' exists. This could raise a KeyError if 'msg_type' is missing from message.metadata. It's important to validate the presence of 'msg_type' before using it.

Apply this diff to add the validation:

 def service_call(
     self, message: ROS2ARIMessage, target: str, timeout_sec: float = 1.0
 ):
+    if "msg_type" not in message.metadata:
+        raise ValueError("'msg_type' is required in message metadata for service calls")
     msg = self._service_api.call_service(
         service_name=target,
         service_type=message.metadata["msg_type"],

92-104: ⚠️ Potential issue

Ensure action_data is not None to prevent AttributeError.

The start_action method accepts action_data as Optional[ROS2ARIMessage], but if action_data is None, accessing action_data.metadata and action_data.payload will raise an AttributeError. It's advisable to check if action_data is None before proceeding or change the parameter type to ROS2ARIMessage.

Apply this diff to ensure action_data is not None:

 def start_action(
     self,
-    action_data: Optional[ROS2ARIMessage],
+    action_data: ROS2ARIMessage,
     target: str,
     on_feedback: Callable[[Any], None] = lambda _: None,
     on_done: Callable[[Any], None] = lambda _: None,
     timeout_sec: float = 1.0,
 ) -> str:
-    if not isinstance(action_data, ROS2ARIMessage):
+    if action_data is None or not isinstance(action_data, ROS2ARIMessage):
         raise ValueError("Action data must be a ROS2ARIMessage instance")

103-104: ⚠️ Potential issue

Validate presence of 'msg_type' in action_data metadata.

In start_action, action_data.metadata["msg_type"] is accessed without checking if 'msg_type' exists in the metadata. This could result in a KeyError if 'msg_type' is missing. Please add a check to ensure 'msg_type' is present.

Apply this diff to add the validation:

     if action_data is None or not isinstance(action_data, ROS2ARIMessage):
         raise ValueError("Action data must be a ROS2ARIMessage instance")
+    if "msg_type" not in action_data.metadata:
+        raise ValueError("'msg_type' is required in action_data metadata")
src/rai/rai/communication/ros2/api.py (2)

363-398: ⚠️ Potential issue

Ensure proper shutdown of ThreadPoolExecutor.

The _callback_executor is introduced for handling feedback callbacks. It is crucial to ensure it is properly shut down to avoid resource leaks.

This issue is similar to a past review comment.

Apply this diff to add a shutdown method and ensure it's called appropriately:

 class ROS2ActionAPI:
     def __init__(self, node: rclpy.node.Node) -> None:
         self.node = node
         self._logger = node.get_logger()
         self.actions: Dict[str, ROS2ActionData] = {}
         self._callback_executor = ThreadPoolExecutor(max_workers=10)

+    def shutdown(self) -> None:
+        """Shut down the ThreadPoolExecutor to release resources."""
+        if self._callback_executor:
+            self._callback_executor.shutdown(wait=False)

     def _generate_handle(self):
         return str(uuid.uuid4())

Ensure that shutdown is called when the application is terminating or when the ROS2ActionAPI instance is no longer needed.


483-486: 🛠️ Refactor suggestion

Avoid using __del__ for resource cleanup; use explicit shutdown instead.

Using __del__ is not reliable for resource cleanup because object destruction timing is nondeterministic in Python.

This issue is similar to a past review comment.

Apply this diff to remove the __del__ method:

     def get_result(self, handle: str) -> Any:
         if not self.is_goal_done(handle):
             raise ValueError(f"Goal {handle} is not done")
         if self.actions[handle]["result_future"] is None:
             raise ValueError(f"No result available for goal {handle}")
         return self.actions[handle]["result_future"].result()

-    def __del__(self) -> None:
-        """Cleanup thread pool when object is destroyed."""
-        if hasattr(self, "_callback_executor"):
-            self._callback_executor.shutdown(wait=False)

Ensure that the shutdown method added earlier is called appropriately to manage resources.

🧹 Nitpick comments (14)
src/rai/rai/communication/ari_connector.py (2)

27-34: Remove unnecessary *args and **kwargs from ARIMessage.__init__.

The __init__ method includes *args and **kwargs, but they are not used in the method or passed to super().__init__(). Including them can lead to unintended behavior if unexpected arguments are passed. It's recommended to remove them for clarity and to prevent potential issues.

Apply this diff to remove *args and **kwargs:

 def __init__(
     self,
     payload: Any,
     metadata: Optional[Dict[str, Any]] = None,
-    *args: Any,
-    **kwargs: Any,
 ):
-    super().__init__(payload, metadata, *args, **kwargs)
+    super().__init__(payload, metadata)

Line range hint 35-42: Correct the docstring in ARIConnector to remove unrelated text.

The docstring for ARIConnector contains an unrelated sentence: "Base class for Robot-Robot Interaction (RRI) connectors." This appears to be a copy-paste error and may confuse readers about the purpose of the class. Please remove or correct this line to maintain clarity.

Apply this diff to correct the docstring:

     Usage:
         Inherit from this class to implement specific ARI connectors, such as
-        ROS2-based or custom protocol-based connectors.   Base class for Robot-Robot Interaction (RRI) connectors.
+        ROS2-based or custom protocol-based connectors.
src/rai/rai/communication/ros2/connectors.py (1)

48-51: Offer assistance for implementing auto topic type detection.

The TODO comment indicates an intention to allow msg_type to be None and add auto topic type detection in the send_message method. If you need help implementing this feature, I can assist with high confidence.

Would you like me to propose an implementation for auto-detecting the message type when msg_type is not provided?

src/rai/rai/communication/ros2/api.py (3)

186-193: Add documentation for _verify_receive_args method.

The newly added _verify_receive_args method lacks a docstring. Providing a clear docstring enhances code readability and maintainability.

Apply this diff to add a docstring:

 def _verify_receive_args(
     self, topic: str, auto_topic_type: bool, msg_type: Optional[str]
 ) -> None:
+    """
+    Verify the arguments provided to the receive method.
+
+    Args:
+        topic: Name of the topic.
+        auto_topic_type: Flag to determine if topic type should be auto-detected.
+        msg_type: The message type, if not auto-detected.
+
+    Raises:
+        ValueError: If both auto_topic_type is True and msg_type is provided,
+                    or if auto_topic_type is False and msg_type is None.
+    """
     if auto_topic_type and msg_type is not None:
         raise ValueError("Cannot provide both auto_topic_type and msg_type")
     if not auto_topic_type and msg_type is None:
         raise ValueError("msg_type must be provided if auto_topic_type is False")

224-226: Enhance topic type consistency verification.

The TODO comment at line 224 suggests verifying publishers' topic type consistency. Implementing this check can prevent runtime errors due to mismatched message types.

Would you like assistance in implementing the topic type consistency verification?


303-306: Optimize publisher existence verification.

The _verify_publisher_exists method returns topic_endpoints but does not utilize it beyond checking for emptiness. If only the existence check is needed, returning the list might be unnecessary.

If the topic_endpoints are not used elsewhere, consider modifying the method to return bool or None.

Alternatively, if topic_endpoints are needed, ensure they are utilized appropriately.

tests/communication/ros2/helpers.py (4)

104-108: Avoid hardcoded messages in publish_message method.

The message content is hardcoded as "Hello, ROS2!". For flexibility, consider parameterizing the message.

Apply this diff to parameterize the message:

 class MessagePublisher(Node):
     def __init__(self, topic: str, message: str = "Hello, ROS2!"):
         super().__init__("test_message_publisher")
         self.publisher = self.create_publisher(String, topic, 10)
         self.timer = self.create_timer(0.1, self.publish_message)
+        self.message_content = message

     def publish_message(self) -> None:
         msg = String()
-        msg.data = "Hello, ROS2!"
+        msg.data = self.message_content
         self.publisher.publish(msg)

110-124: Ensure proper handling of daemon threads in multi_threaded_spinner.

Setting threads as daemons can cause abrupt termination without cleanup. Consider not setting daemon = True and ensuring proper shutdown.

Apply this diff to remove the daemon setting:

         for executor in executors:
             executor_thread = threading.Thread(target=executor.spin)
-            executor_thread.daemon = True
             executor_thread.start()
             executor_threads.append(executor_thread)

Ensure that all threads are properly joined during shutdown.


126-138: Reduce sleep duration in shutdown_executors_and_threads.

The sleep duration of 0.5 seconds may be unnecessary and could slow down tests.

Consider reducing or removing the sleep:

     def shutdown_executors_and_threads(
         executors: List[MultiThreadedExecutor], threads: List[threading.Thread]
     ) -> None:
         # First shutdown executors
         for executor in executors:
             executor.shutdown()
         # Small delay to allow executors to finish pending operations
-        time.sleep(0.5)
+        time.sleep(0.1)
         # Then join threads with a timeout
         for thread in threads:
             thread.join(timeout=2.0)

140-144: Ensure proper cleanup in ros_setup fixture.

The ros_setup fixture does not handle exceptions that might occur between yield and rclpy.shutdown(). This could leave the ROS client library in an inconsistent state.

Wrap the yield in a try-finally block to ensure rclpy.shutdown() is always called:

 @pytest.fixture(scope="function")
 def ros_setup() -> Generator[None, None, None]:
     rclpy.init()
+    try:
         yield
+    finally:
         rclpy.shutdown()
tests/communication/ros2/test_connectors.py (3)

90-93: Add timeout handling for service calls.

The service call could potentially block indefinitely. Consider:

  1. Adding a timeout parameter to prevent hanging
  2. Implementing retry logic with exponential backoff
  3. Adding assertions for service availability before making the call

159-159: Improve error handling test case.

Using 1 / 0 for error testing is not ideal. Consider:

  1. Using a custom exception that better represents real-world scenarios
  2. Testing multiple error conditions
  3. Verifying error logging and recovery behavior
-            on_feedback=lambda feedback: 1 / 0,
+            on_feedback=lambda feedback: raise RuntimeError("Simulated callback error"),

167-172: Design decision needed for callback lifecycle management.

The skipped test highlights an important architectural decision regarding callback handling during shutdown. Consider:

  1. Implementing a callback timeout mechanism
  2. Adding explicit callback cancellation during shutdown
  3. Using asyncio or threading events instead of sleep

Would you like me to propose a design for proper callback lifecycle management?

tests/communication/ros2/__init__.py (1)

23-29: Update __all__ to include all imported items.

The ServiceServer class is imported via helpers but not included in __all__. Consider adding all imported items to maintain a complete public interface.

 __all__ = [
     "shutdown_executors_and_threads",
     "multi_threaded_spinner",
     "MessagePublisher",
     "ActionServer",
     "MessageReceiver",
+    "ServiceServer",
 ]
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1595c52 and b80575a.

📒 Files selected for processing (8)
  • src/rai/rai/communication/ari_connector.py (2 hunks)
  • src/rai/rai/communication/base_connector.py (1 hunks)
  • src/rai/rai/communication/ros2/api.py (9 hunks)
  • src/rai/rai/communication/ros2/connectors.py (1 hunks)
  • tests/communication/ros2/__init__.py (1 hunks)
  • tests/communication/ros2/helpers.py (1 hunks)
  • tests/communication/ros2/test_api.py (19 hunks)
  • tests/communication/ros2/test_connectors.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
tests/communication/ros2/test_api.py

36-36: Found useless expression. Either assign it to a variable or remove it.

(B018)

tests/communication/ros2/test_connectors.py

34-34: Found useless expression. Either assign it to a variable or remove it.

(B018)

🔇 Additional comments (5)
tests/communication/ros2/test_api.py (1)

36-36: Fix the useless expression warning.

The line ros_setup # type: ignore results in a useless expression warning from the static analysis tool. Assigning the fixture to _ explicitly uses it and prevents the warning.

Apply this diff to fix the warning:

-ros_setup  # type: ignore prevents pytest from complaining about unused fixture
+_ = ros_setup  # Explicitly use the fixture to prevent pytest warnings
🧰 Tools
🪛 Ruff (0.8.2)

36-36: Found useless expression. Either assign it to a variable or remove it.

(B018)

🧰 Tools
🪛 Ruff (0.8.2)

36-36: Found useless expression. Either assign it to a variable or remove it.

(B018)

src/rai/rai/communication/base_connector.py (1)

20-35: Add input validation and documentation for metadata in BaseMessage.

The metadata attribute lacks input validation, and there's no documentation explaining its intended use.

This is similar to a past review comment.

Apply this diff to add validation and a docstring:

 class BaseMessage:
     payload: Any
     metadata: Dict[str, Any]

+    """
+    Base class for messages with payload and optional metadata.
+
+    Attributes:
+        payload: The main data of the message.
+        metadata: Additional information associated with the message.
+    """

     def __init__(
         self,
         payload: Any,
         metadata: Optional[Dict[str, Any]] = None,
         *args: Any,
         **kwargs: Any,
     ):
         self.payload = payload
+        if metadata is None:
+            self.metadata = {}
+        else:
+            if not all(isinstance(k, str) for k in metadata.keys()):
+                raise ValueError("Metadata keys must be strings")
+            self.metadata = metadata
tests/communication/ros2/helpers.py (1)

69-88: Handle potential infinite loops in handle_test_action method.

The handle_test_action method uses a fixed range, but if the range is altered or conditions change, there could be a risk of an infinite loop.

Ensure that the loop in handle_test_action has a definite termination condition.

tests/communication/ros2/test_connectors.py (2)

34-34: Fix the useless expression warning.

The line ros_setup # type: ignore is flagged by static analysis. Consider using the fixture properly:

-ros_setup  # type: ignore prevents pytest from complaining about unused fixture
+_ = ros_setup  # Explicitly use the fixture to prevent pytest warnings
🧰 Tools
🪛 Ruff (0.8.2)

34-34: Found useless expression. Either assign it to a variable or remove it.

(B018)


50-50: Consider implementing a proper discovery mechanism.

The test relies on time.sleep() for synchronization, which can be flaky. Consider implementing a proper discovery mechanism with retries and timeouts.

Copy link
Contributor

@rachwalk rachwalk left a comment

Choose a reason for hiding this comment

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

I have a few questions regarding design choices, would appreciate if you address them

@maciejmajek maciejmajek requested a review from rachwalk January 21, 2025 12:54
Copy link
Contributor

@rachwalk rachwalk left a comment

Choose a reason for hiding this comment

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

LGTM

@rachwalk rachwalk merged commit c6c49b9 into development Jan 21, 2025
5 checks passed
@rachwalk rachwalk deleted the feat/ros2-connectors branch January 21, 2025 12:58
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