Skip to content

Dev_Actions

Mehmet Emre Çakal edited this page Jan 20, 2025 · 1 revision

4.4 Actions in ROS#

Actions in ROS# provide a mechanism to handle long-running tasks that can be preempted. This is particularly useful for tasks that may take an indeterminate amount of time to complete and need to provide feedback or be canceled. The implementation involves several components that work together to facilitate communication between action clients and servers. This documentation will cover the key components, their interactions, and provide examples of how to implement specific actions.

This page will guide you through the under the hood implementation of actions in ROS# and explain fibonacci action client/server examples. Note that this page only considers ROS2 action implementation.

Table of Contents

4.4.1 Roadmap - General Picture 4.4.2 Communication: ROS Bridge Interface 4.4.3 Communicators: Consumers and Providers 4.4.4 RosSocket: Accessing Communicatiors 4.4.5 Abstract Classes

4.4.1 Roadmap - General Picture

Actions in ROS# are structured in a modular way, distributed over different classes: Abstract class, RosSocket, Communication, Communicator, and finally the actual client/server implementation on a derived class.

Client Diagram:

Server Diagram:

Before diving to the details, in ROS#, every abstact action implementation is designed to be generic. Please mind the notation:

Class Template Parameters

Parameter Description
TAction The ROS 2 action type (e.g., Fibonacci, NavigateToPose).
TActionGoal The action goal message type interface. (Communication.cs)
TActionResult The action result message type interface. (Communication.cs)
TActionFeedback The action feedback message type interface. (Communication.cs)
TGoal The user defined goal message type (used within TActionGoal).
TResult The user defined result message type (used within TActionResult).
TFeedback The user defined feedback message type (used within TActionFeedback).

4.4.2 Communication: ROS Bridge Interface

Like any other message in ROS#, actions must be serialized in a way that Ros Bridge can understand. Each message type has its own structure defined by the ROS Design and implemented by the Ros Bridge Protocol. Including actions, ROS# implements these message types in Communication.cs: Incoming and outgoing messages are deserialized and serialized respectively according to derived `Communication' classes.

The SendActionGoal class represents a goal request message sent by the client and received by the server.

internal class SendActionGoal<T> : Communication where T : Message 
{
    public string action { get; set; } // required, the name of the action to send a goal to
    public string action_type { get; set; } // required, the action message type
    public T args { get; set; } // optional, list of json objects representing the arguments to the service
    public bool feedback { get; set; } // optional, if true, sends feedback messages over rosbridge. Defaults to false.
    public int fragment_size { get; set; } // optional, maximum size that the result and feedback messages can take before they are fragmented
    public string compression {  get; set; } // optional, an optional string to specify the compression scheme to be used on messages. Valid values are "none" and "png"

    internal SendActionGoal(string id, string action, string action_type, T args, bool feedback = false, int fragment_size = int.MaxValue, string compression = "none") : base(id)
    {
        this.op = "send_action_goal";
        this.id = id;
        this.action = action;
        this.action_type = action_type;
        this.args = args;
        this.feedback = feedback;
        this.fragment_size = fragment_size;
        this.compression = compression;
    }
}

4.4.3 Communicators: Consumers and Providers

By ROS design, both action clients and services have specific functionality. A client not only sends or cancels a goal, but (not necessarily) it is also expected to consume (receive) the server's feedback and result response. Similarly, an action server should also be able to listen to new goal and cancel requests in addition to publishing feedback and result. These server and client behaviors are implemented in Communicatior.cs: Provider and Consumer respectively.

4.4.3.1 Consumer (Client Side)

Below is the generic ActionConsumer class located in Communicators.cs. Basically it has two functions: Consuming feedback and result messages sent by the server, and preparing send/cancel target messages. Whenever an action client wants to send or cancel an action, an ActionConsumer instance is created.

    internal ActionConsumer(string id,
                                  string action,
                                  ActionResultResponseHandler<TActionResult> actionResultResponseHandler = null,
                                  ActionFeedbackResponseHandler<TActionFeedback> actionFeedbackResponseHandler = null,
                                  ActionCancelResponseHandler<TActionResult> actionCancelResponseHandler = null)
    {
        Id = id;
        Action = action;
        ActionType = GetRosName<TActionResult>().Substring(0, GetRosName<TActionResult>().LastIndexOf("ActionResult"));
        ActionResultResponseHandler = actionResultResponseHandler;
        ActionFeedbackResponseHandler = actionFeedbackResponseHandler;
        ActionCancelResponseHandler = actionCancelResponseHandler;
    }
    ...

The ConsumeResultResponse, ConsumeFeedbackResponse and ConsumeCancelResponse functions deserialize the incoming message from the server (via RosBridge) according to the message interface definitions in the Communication.cs file. It then triggers the user-defined callback functions (Consume...Response) with the deserialized messages on different threads, which are generic delegate functions passed to the ActionConsumer constructor when sending or canceling an action.


    ...
    internal override void ConsumeResultResponse(string incomingJsonResultResponse, ISerializer serializer)
    {
        try
        {
            ActionResultResponseHandler.Invoke(JsonSerializer.Deserialize<TActionResult>(incomingJsonResultResponse));
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error in ConsumeResultResponse: " + ex.Message);
        }
    }
    internal override void ConsumeFeedbackResponse(string incomingJsonFeedbackResponse, ISerializer serializer)
    {
        try
        {
            ActionFeedbackResponseHandler.Invoke(JsonSerializer.Deserialize<TActionFeedback>(incomingJsonFeedbackResponse));
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error in ConsumeFeedbackResponse: " + ex.Message);
        }
    }
    internal override void ConsumeCancelResponse(string incomingJsonFeedbackResponse, ISerializer serializer) 
    {
    ...

Furthermore, SendActionGoalRequest and CancelActionGoalRequest are the functions responsible for preparing the send/cancel request messages to be sent. As discussed before, they return an abstract Communication type that represents the message structure defined by the Ros Bridge protocol. The SendActionGoalRequest function takes the actual goal message (TActionGoal type), which is automatically generated by the RosSharp.MessageGeneration library. On the other hand, CancelActionGoalRequest does not take a custom cancel action message type, but only takes the id and name of the action to be canceled, as it is much simpler.


    ...
    internal override Communication SendActionGoalRequest<TActionGoal, TGoal>(TActionGoal actionGoal)
    {
        return new SendActionGoal<TGoal>(
            id: Id,
            action: actionGoal.action,
            action_type: ActionType,
            args: actionGoal.args,
            feedback: actionGoal.feedback,
            fragment_size: actionGoal.fragment_size,
            compression: actionGoal.compression
        );
    }

    internal override Communication CancelActionGoalRequest(string frameId, string actionName)
    { ...

4.4.3.2 Provider (Server Side)

The ActionProvider class in Communicators.cs is responsible for handling the server-side logic of ROS actions. It manages the advertisement of actions, listens for incoming goal and cancel requests, and sends feedback and result responses to the clients.

Unlike how services are handled in ROS#, responding is divided into listening and responding for actions. By the ROS design, these functions are blocking and expeced to return quickly. However, action responses are often like that and they require some time to perform the action itself. Thus, Listen...Action functions transfer the clients' request to the abstract server and returns quickly.

Below is the generic ActionProvider class. It has several key functions: advertising actions, listening for goal and cancel requests, and sending feedback and result responses. Whenever an action server is initialized, an ActionProvider instance is created (from RosSocket).

internal abstract class ActionProvider : Communicator
{
    internal abstract string Action { get; }

    internal abstract Communication RespondResult<TActionResult, TResult>(TActionResult ActionResult) 
        where TActionResult : ActionResult<TResult>
        where TResult : Message;
    internal abstract Communication RespondFeedback<TActionFeedback, TFeedback>(TActionFeedback ActionFeedback)
        where TActionFeedback : ActionFeedback<TFeedback>
        where TFeedback : Message;

    internal abstract void ListenSendGoalAction(string message, ISerializer serializer);
    internal abstract void ListenCancelGoalAction(string frameId, string actionName, ISerializer serializer);

    internal ActionUnadvertisement UnadvertiseAction()
    {
        return new ActionUnadvertisement(Action);
    }
}

The ActionProvider class has several abstract methods that must be implemented by derived server class. These methods handle the core functionality of the action server:

  • RespondResult: Sends the result of an action goal to the client.
  • RespondFeedback: Sends periodic feedback about the progress of an action goal to the client.
  • ListenSendGoalAction: Listens for incoming goal requests from clients.
  • ListenCancelGoalAction: Listens for incoming cancel requests from clients.

The ActionProvider<TActionGoal> class is a concrete implementation of the ActionProvider class. It handles specific action goals and manages goal reception and feedback/result responses.

internal class ActionProvider<TActionGoal> : ActionProvider 
    where TActionGoal : Message
{
    internal override string Action { get; }

    internal SendActionGoalHandler<TActionGoal> SendActionGoalHandler;
    internal CancelActionGoalHandler CancelActionGoalHandler;

    internal ActionProvider(string action, 
        SendActionGoalHandler<TActionGoal> sendActionGoalHandler,
        CancelActionGoalHandler cancelActionGoalHandler,
        out ActionAdvertisement actionAdvertisement)
    {
        Action = action;
        SendActionGoalHandler = sendActionGoalHandler;
        CancelActionGoalHandler = cancelActionGoalHandler;

        string actionGoalROSName = GetRosName<TActionGoal>();
        actionAdvertisement = new ActionAdvertisement(action, actionGoalROSName.Substring(0, actionGoalROSName.LastIndexOf("ActionGoal"))); 
    }

In the constructor of ActionProvider<TActionGoal>, the action name and handlers for sending goals and canceling goals are initialized.

    internal override void ListenSendGoalAction(string message, ISerializer serializer)
    {
        try
        {
            SendActionGoalHandler.Invoke(serializer.Deserialize<TActionGoal>(message));
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error in ListenSendGoalAction: " + ex.Message);
        }
    }

    internal override void ListenCancelGoalAction(string frameId, string actionName, ISerializer serializer)
    {
        try
        {
            CancelActionGoalHandler.Invoke(frameId, actionName);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error in ListenCancelGoalAction: " + ex.Message);
        }
    }

The ListenSendGoalAction method deserializes incoming goal requests and invokes the SendActionGoalHandler delegate. Similarly, the ListenCancelGoalAction method deserializes incoming cancel requests and invokes the CancelActionGoalHandler delegate.

    internal override Communication RespondFeedback<TActionFeedback, TFeedback>(TActionFeedback actionFeedback)
    {
        return new ActionFeedbackResponse<TFeedback>(
            id: actionFeedback.id,
            action: actionFeedback.action,
            values: actionFeedback.values
        );
    }

    internal override Communication RespondResult<TActionResult, TResult>(TActionResult ActionResult)
    {
        return new ActionResultResponse<TResult>(
            id: ActionResult.id,
            action: ActionResult.action, 
            values: ActionResult.values,
            status: ActionResult.status,
            result: ActionResult.result
        );
    }
}

The `RespondFeedback` method creates an `ActionFeedbackResponse` message to send feedback to the client. The `RespondResult` method creates an `ActionResultResponse` message to send the result of an action goal to the client. Again, these messages are not sent directly from the provider class, they are just prepared here to be sent by the `RosSocket`.

4.4.4 RosSocket: Accessing Communicators

The abstract Comminicator classes are only accessed from RosSocket.cs Whenever a client sends an action goal, the SendActionGoalRequest function is called. It gets a unique id for the specific action goal, then calls the ActionConsumer constructor. Finally, it sends the goal request message from the consumer instance (SendActionGoalRequest). Sending a goal cancellation request works similarly.

public string SendActionGoalRequest<TActionGoal, TGoal, TActionFeedback, TActionResult>(
    TActionGoal actionGoal,
    ActionResultResponseHandler<TActionResult> actionResultResponseHandler,
    ActionFeedbackResponseHandler<TActionFeedback> actionFeedbackResponseHandler)
    where TActionGoal : ActionGoal<TGoal>
    where TGoal : Message
    where TActionResult : Message
    where TActionFeedback : Message
{
    string id = GetUnusedCounterID(ActionConsumers, actionGoal.action);
    ActionConsumers.Add(id, new ActionConsumer<TActionResult, TActionFeedback>(
        id,
        actionGoal.action,
        actionResultResponseHandler: actionResultResponseHandler,
        actionFeedbackResponseHandler: actionFeedbackResponseHandler)
    );

    Send(ActionConsumers[id].SendActionGoalRequest<TActionGoal, TGoal>(actionGoal));
    return id;
}

Although similar, advertising an action has a few more steps compared to the client side. This time, only one consumer instance is created when the action is advertised to the Ros Bridge. Again, a unique id is chosen for the action provider and the ActionProvider' instance is generated. The RespondFeedback' and `RespondResull' functions perform almost the same as the consumer side sending functions.

public string AdvertiseAction<TActionGoal, TActionFeedback, TActionResult>(
    string action,
    SendActionGoalHandler<TActionGoal> sendActionGoalHandler,
    CancelActionGoalHandler cancelActionGoalHandler)
    where TActionGoal : Message
    where TActionFeedback : Message
    where TActionResult : Message
{
    string id = action;
    if (ActionProviders.ContainsKey(id))
        UnadvertiseAction(id);

    ActionAdvertisement actionAdvertisement;
    ActionProviders.Add(id, new ActionProvider<TActionGoal>(
        action,
        sendActionGoalHandler,
        cancelActionGoalHandler,
        out actionAdvertisement));
    Send(actionAdvertisement);

    return id;
}

public void RespondFeedback<TActionFeedback, TFeedback>(string id, TActionFeedback actionFeedback)
    where TActionFeedback : ActionFeedback<TFeedback>
    where TFeedback : Message
{
    Send(ActionProviders[id].RespondFeedback<TActionFeedback, TFeedback>(actionFeedback));
}
...

On the other hand, listen and consumer functions are triggered by the Receive function, again located in RosSocket.cs. Based on the op and id property of the incoming message from the Ros Bridge, the respond/listen function of the matching consumer/provider is triggered.

4.4.5 Abstract Classes

In the final level of abstraction, we finally implement the basic functionaility of clients and servers in ActionClient.cs and ActionServer.cs, respectively.

4.4.5.1 ActionClient.cs

The ActionClient class abstracts away the complexities of managing actions in ROS 2 by handling communication with the ROS 2 action server. Users can inherit from this class to define custom actions by implementing specific methods for handling feedback, status, and results.

Key Features
  • Send Goals: Supports sending goals with additional parameters (e.g., feedback, fragment size, compression).
  • Cancel Goals: Allows canceling goals with or without specifying a frameId.
  • Feedback Handling: Provides callback methods for processing feedback from the action server.
  • Result Handling: Processes results received from the action server.
  • Status Updates: Tracks and updates the current goal status.
Usage Instructions
  1. Inherit from ActionClient: Create a custom class that inherits from ActionClient and specify the action-related types.
  2. Implement Abstract Methods: Define the methods SetActionGoal, OnStatusUpdated, OnFeedbackReceived, and OnResultReceived in the derived class.
  3. Set Goals: Use SetActionGoal to define the action goal with additional parameters such as feedback and compression.
  4. Send Goals: Call SendGoal to send the action goal to the action server.
  5. Cancel Goals: Use CancelGoal to cancel an ongoing goal by providing a frameId or using the last frameId.

Methods Implemented by the User

These methods must be implemented in the derived class to customize the handling of goals, feedback, status, and results.

Method Parameters Description
SetActionGoal TGoal goal, bool feedback, int fragmentSize, string compression Defines the action goal with additional parameters.
GetActionGoal None Retrieves the configured action goal.
OnStatusUpdated None Handles updates to the action goal's status.
OnFeedbackReceived None Processes feedback received from the action server.
OnResultReceived None Processes the result received from the action server.

Under-the-Hood Methods

These methods handle core functionality and are not intended to be overridden by the user.

Method Parameters Description
SendGoal None Sends the current action goal to the action server using RosSocket.SendActionGoalRequest.
CancelGoal string frameId = null Cancels the action goal using RosSocket.CancelActionGoalRequest.
StatusCallback GoalStatusArray Internal callback for processing status updates from the action server. Passed to Communicators.cs.
FeedbackCallback TActionFeedback Internal callback for processing feedback from the action server. Passed to Communicators.cs.
ResultCallback TActionResult Internal callback for processing results from the action server. Passed to Communicators.cs.

4.4.5.2 ActionServer.cs

The ActionServer class serves as a base class for creating custom ROS 2 action servers. It simplifies the handling of action goals, cancellations, feedback, and results while abstracting away the underlying communication with the ROS 2 action client.

For more information about server state machine model, see the wiki page.

Key Features
  • Advertise Actions: Automatically sets up the action topic communication with the ROS 2 ecosystem.
  • Handle Goals: Processes incoming action goals from the client.
  • Manage Cancellations: Handles goal cancellations and transitions to appropriate states.
  • Publish Feedback: Sends real-time feedback about the execution status of goals to the client.
  • Publish Results: Sends the final result of an action goal to the client.
  • State Management: Tracks and updates the internal state of the action server to ensure proper goal execution flow.
Usage Instructions
  1. Inherit from ActionServer: Create a custom class that derives from ActionServer and defines action-specific types.
  2. Implement Abstract Methods: Override key methods such as OnGoalReceived, OnGoalExecuting, OnGoalCanceling, OnGoalSucceeded, OnGoalAborted, and OnGoalCanceled to define custom behaviors.
  3. Initialize the Server: Call Initialize to set up communication with the ROS 2 ecosystem.
  4. Terminate the Server: Call Terminate to cleanly shut down the server when it is no longer needed.

Methods Implemented by the User

These methods must be implemented in the derived class to customize how the server handles different phases of action processing.

Method Parameters Description
OnGoalReceived None Handles the reception of a new goal from the action client.
OnGoalExecuting None Handles the transition of a goal into the execution phase.
OnGoalCanceling None Handles goal cancellation requests from the action client.
OnGoalSucceeded None Handles the successful completion of a goal.
OnGoalAborted None Handles the case where the goal execution fails.
OnGoalCanceled None Handles the cancellation of a goal.

Under-the-Hood Methods

These methods handle core functionality and should not typically be overridden by the user.

Method Parameters Description
Initialize None Sets up the action server by advertising the action topic and initializing the state.
Terminate None Shuts down the action server and unadvertises the action topic.
SetExecuting None Transitions the goal state to EXECUTING and invokes the OnGoalExecuting method.
SetSucceeded TResult result = null Marks the goal as SUCCEEDED and publishes the result.
SetAborted None Marks the goal as ABORTED and logs the failure.
SetCanceled TResult result = null Marks the goal as CANCELED and optionally sets a result.
GoalCallback TActionGoal actionGoal Internal callback for handling incoming goals from the client.
CancelCallback string frameId, string action Internal callback for handling goal cancellation requests.
PublishFeedback None Publishes feedback to the action client.
PublishResult None Publishes the final result of the goal to the action client.
UpdateAndPublishStatus ActionStatus actionStatus Updates the server's internal state and publishes it (only ROS1) .

© Siemens AG, 2017-2025

Clone this wiki locally