Service Client Demo (Python)

This tutorial shows how to integrate ROS 2 services into a YASMIN state machine using the ServiceState class. Services in ROS 2 provide request-response communication - ideal for computations, queries, or triggering specific actions that complete quickly. Unlike actions, services don't provide feedback during execution and can't be canceled. We'll create a state that calls the AddTwoInts service to demonstrate the complete workflow: preparing a request from blackboard data, calling the service, and processing the response.

Prerequisites

You need a ROS 2 service server running. Start the example AddTwoInts service in a separate terminal:

ros2 run yasmin_demos add_two_ints_server

1. Create the ServiceState

Create a custom state that inherits from ServiceState and implements request creation and response handling. The constructor takes five parameters: the service type (AddTwoInts), the service name ("/add_two_ints"), a callback to create the request, additional custom outcomes (beyond the default SUCCEED and ABORT), and a response handler. The create_request_handler method retrieves integers a and b from the blackboard and populates a service request object. The response_handler is called when the service returns, extracts the sum from the response, stores it in the blackboard, and returns "outcome1" to signal completion. The ServiceState automatically handles service availability checking and error scenarios.

from yasmin_ros import ServiceState
from example_interfaces.srv import AddTwoInts

class AddTwoIntsState(ServiceState):
    """
    A state that calls the AddTwoInts service to add two integers.

    This class is a state in a finite state machine that sends a request
    to the AddTwoInts service, retrieves the response, and updates the
    blackboard with the result.

    Attributes:
        service_type (type): The service type being used (AddTwoInts).
        service_name (str): The name of the service.
        outcomes (list): The list of possible outcomes for this state.
    """

    def __init__(self) -> None:
        """
        Initializes the AddTwoIntsState.

        Calls the parent constructor with the specific service type,
        service name, request handler, outcomes, and response handler.
        """
        super().__init__(
            AddTwoInts,  # srv type
            "/add_two_ints",  # service name
            self.create_request_handler,  # cb to create the request
            ["outcome1"],  # outcomes. Includes (SUCCEED, ABORT)
            self.response_handler,  # cb to process the response
        )

    def create_request_handler(self, blackboard: Blackboard) -> AddTwoInts.Request:
        """
        Creates the service request from the blackboard data.

        Args:
            blackboard (Blackboard): The blackboard containing the input values.

        Returns:
            AddTwoInts.Request: The request object populated with values from the blackboard.
        """
        req = AddTwoInts.Request()
        req.a = blackboard["a"]
        req.b = blackboard["b"]
        return req

    def response_handler(
        self, blackboard: Blackboard, response: AddTwoInts.Response
    ) -> str:
        """
        Processes the response from the AddTwoInts service.

        Updates the blackboard with the sum result from the response.

        Args:
            blackboard (Blackboard): The blackboard to update with the sum.
            response (AddTwoInts.Response): The response from the service call.

        Returns:
            str: The outcome of the operation, which is "outcome1".
        """
        blackboard["sum"] = response.sum
        return "outcome1"

2. Create Helper States

Create callback states to set input values and print the result. The set_ints function initializes the blackboard with two integers (a=10, b=5) that will be used as inputs for the service call. The print_sum function retrieves and logs the computed sum from the blackboard after the service completes. Both functions return SUCCEED to indicate successful execution. These simple callback functions demonstrate how to use CbState for straightforward operations that don't require a full custom state class - perfect for data preparation, logging, or simple conditional checks.

from yasmin import CbState
from yasmin_ros.basic_outcomes import SUCCEED

def set_ints(blackboard: Blackboard) -> str:
    """
    Sets the integer values in the blackboard.

    This function initializes the blackboard with two integer values to be added.

    Args:
        blackboard (Blackboard): The blackboard to update with integer values.

    Returns:
        str: The outcome of the operation, which is SUCCEED.
    """
    blackboard["a"] = 10
    blackboard["b"] = 5
    return SUCCEED

def print_sum(blackboard: Blackboard) -> str:
    """
    Logs the sum value from the blackboard.

    This function retrieves the sum from the blackboard and logs it.

    Args:
        blackboard (Blackboard): The blackboard from which to retrieve the sum.

    Returns:
        str: The outcome of the operation, which is SUCCEED.
    """
    yasmin.YASMIN_LOG_INFO(f"Sum: {blackboard['sum']}")
    return SUCCEED

3. Build the State Machine

Assemble the states into a state machine that orchestrates the complete service call workflow. The state machine initializes ROS 2, creates three states with defined transitions, and sets up the YASMIN viewer for real-time visualization. The SETTING_INTS state prepares the input data and transitions to ADD_TWO_INTS on success. The ADD_TWO_INTS service state can produce three outcomes: "outcome1" if the service returns successfully (transitioning to PRINTING_SUM), SUCCEED if completed without errors, or ABORT if the service fails (both terminating with "outcome4"). Finally, PRINTING_SUM logs the result and transitions to the final outcome. This sequential flow ensures proper data preparation, service execution, and result handling with graceful error management.

def main() -> None:
    yasmin.YASMIN_LOG_INFO("yasmin_service_client_demo")

    # Init ROS 2
    rclpy.init()

    # Set ROS 2 logs
    set_ros_loggers()

    # Create a FSM
    sm = StateMachine(outcomes=["outcome4"])

    # Add states
    sm.add_state(
        "SETTING_INTS",
        CbState([SUCCEED], set_ints),
        transitions={SUCCEED: "ADD_TWO_INTS"},
    )
    sm.add_state(
        "ADD_TWO_INTS",
        AddTwoIntsState(),
        transitions={
            "outcome1": "PRINTING_SUM",
            SUCCEED: "outcome4",
            ABORT: "outcome4",
        },
    )
    sm.add_state(
        "PRINTING_SUM",
        CbState([SUCCEED], print_sum),
        transitions={
            SUCCEED: "outcome4",
        },
    )

    # Publish FSM info
    viewer = YasminViewerPub(sm, "YASMIN_SERVICE_CLIENT_DEMO")

    # Execute FSM
    try:
        outcome = sm()
        yasmin.YASMIN_LOG_INFO(outcome)
    except KeyboardInterrupt:
        if sm.is_running():
            sm.cancel_state()
    finally:
        viewer.cleanup()
        del sm

        # Shutdown ROS 2 if it's running
        if rclpy.ok():
            rclpy.shutdown()


if __name__ == "__main__":
    main()

4. Run the Demo

First, start the service server that provides the integer addition functionality:

ros2 run yasmin_demos add_two_ints_server

Then run the client demo to execute the state machine that calls the service and displays the result:

ros2 run yasmin_demos service_client_demo.py

Expected Output

The state machine will:

  1. Set a = 10 and b = 5
  2. Call the /add_two_ints service
  3. Print the sum: Sum: 15