Skip to content

Add ROS2 Action client and server support#325

Closed
comoc wants to merge 1 commit into
Unity-Technologies:mainfrom
comoc:feature/action-support
Closed

Add ROS2 Action client and server support#325
comoc wants to merge 1 commit into
Unity-Technologies:mainfrom
comoc:feature/action-support

Conversation

@comoc
Copy link
Copy Markdown

@comoc comoc commented Apr 10, 2026

Proposed change(s)

Add C# Action client and server classes that bridge ROS2 Actions through the TCP connection. Works with a patched ROS-TCP-Endpoint (comoc/ROS-TCP-Endpoint PR #178) that adds the corresponding Python-side syscommands.

Changes

New classes:

  • ROSActionClient<TGoal, TResult, TFeedback> — async SendGoal, feedback events, GetResult, Cancel
  • ROSActionServer<TGoal, TResult, TFeedback> — GoalReceived event, PublishFeedback, SetResult
  • ActionGoalHandle / ActionServerGoalHandle for per-goal state management

ROSConnection integration:

  • CreateActionClient() / CreateActionServer() factory methods
  • AllocateServiceRequest() internal helper for thread-safe srv_id allocation
  • QueueRawMessage() for sending data frames without publisher registration
  • TryRouteToActionServer() for routing incoming goals to action servers

SysCommand additions:

  • 6 new constants: __action_client, __action_send_goal, __action_get_result, __action_cancel_goal, __action_server, __action_publish_feedback
  • 3 new param structs

Protocol details:

  • Action client sends __action_send_goal{srv_id} + CDR Goal body, receives UUID-prepended SendGoal_Response
  • GetResult response: skips int8 status + 3-byte padding before deserializing Result
  • Action server receives goals via existing __request/__response pair (16-byte UUID + CDR Goal), sends feedback via __action_publish_feedback, result via __response
  • Feedback routing: SubscribeByMessageName to handle type name inconsistency between __subscribe and __topic_list

Useful links

Types of change(s)

  • Bug fix
  • New feature
  • Code refactor
  • Documentation update
  • Other

Testing and Verification

Tested end-to-end with action_tutorials_interfaces/Fibonacci action:

  1. Action client (Unity -> ROS2 fibonacci_action_server): send_goal accepted, 9 feedback messages received with partial_sequence growing correctly, final result [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

  2. Action server (ros2 action send_goal -> Unity): goal received, 4 feedback messages published at 1-second intervals, result [0, 1, 1, 2, 3, 5] returned with status SUCCEEDED

  3. Reconnect: after endpoint restart, feedback subscription re-registers correctly

Test Configuration:

  • Unity Version: Unity 6000.0
  • Unity machine OS: Windows 11
  • ROS machine OS + version: Ubuntu 22.04, ROS2 Humble
  • ROS-Unity communication: TCP (WSL2)
  • Endpoint: comoc/ROS-TCP-Endpoint main-ros2 branch

Checklist

  • Ensured this PR is up-to-date with the main branch
  • Created this PR to target the dev branch (Note: dev branch has not been updated since 2022)
  • Followed the style guidelines as described in the Contribution Guidelines
  • Added tests that prove my fix is effective or that my feature works
  • Updated the Changelog
  • Updated the documentation as appropriate

Notes

This PR requires the companion endpoint changes in PR #178. The implementation is backwards-compatible — existing topic/service functionality is completely unaffected. The new Action classes are opt-in; projects that don't use Actions will see no changes in behavior.

The Fibonacci message classes included are hand-written for testing. A future enhancement would add .action file support to the MessageGeneration tool.

Adds C# Action client and server classes that work with the patched
ROS-TCP-Endpoint (comoc/ROS-TCP-Endpoint, main-ros2 branch) to
bridge ROS2 Actions through the TCP connection.

SysCommand.cs:
- 6 new command constants (__action_client, __action_send_goal,
  __action_get_result, __action_cancel_goal, __action_server,
  __action_publish_feedback)
- 3 new param structs (SysCommand_ActionRegistration,
  SysCommand_ActionGoalOp, SysCommand_ActionFeedback)

ROSActionClient.cs:
- ROSActionClient<TGoal, TResult, TFeedback> with async SendGoal()
  returning ActionGoalHandle
- ActionGoalHandle with async GetResult(), Cancel(), and
  FeedbackReceived event
- RegisterFeedbackSubscription<TFeedbackMessage>() for wiring up
  goal-specific feedback routing via SubscribeByMessageName
- GetResultRequestProxy and CancelGoalRequestProxy internal messages
- Handles the endpoint's UUID-prepend convention on SendGoal response
- Skips int8 status + alignment padding in GetResult response

ROSActionServer.cs:
- ROSActionServer<TGoal, TResult, TFeedback> with GoalReceived event
- ActionServerGoalHandle with PublishFeedback() and SetResult()
- Goal routing via __request/__response reusing existing service
  infrastructure

ROSConnection.cs:
- AllocateServiceRequest() internal helper for thread-safe srv_id
  allocation without exposing private fields
- QueueRawMessage() internal helper for sending data frames without
  triggering Publish()'s publisher-registration check
- CreateActionClient<>() and CreateActionServer<>() factory methods
- TryRouteToActionServer() for __request dispatch to action servers
- Action server routing integrated into ReceiveSysCommand's
  m_SpecialIncomingMessageHandler
- FindObjectOfType -> FindAnyObjectByType (deprecation fix)

Hand-written Fibonacci message classes for testing:
- FibonacciGoal, FibonacciResult, FibonacciFeedback,
  FibonacciFeedbackMessage (with dual MessageRegistry registration
  for short and /action/ type name variants)

Verified end-to-end:
- Action client: Unity -> ROS2 fibonacci_action_server, all 9
  feedback messages + result [0,1,1,2,3,5,8,13,21,34,55]
- Action server: ros2 action send_goal -> Unity, 4 feedback messages
  + result [0,1,1,2,3,5] with SUCCEEDED status
- Reconnect after endpoint restart works correctly
Copilot AI review requested due to automatic review settings April 10, 2026 03:45
@cla-assistant-unity
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds ROS2 Action client/server support to the ROS TCP Connector by introducing new C# action abstractions and integrating them into ROSConnection’s existing syscommand + request/response plumbing (intended to work with the patched ROS-TCP-Endpoint changes).

Changes:

  • Add new Action syscommands + JSON param structs to support action registration, goal ops, and feedback publishing.
  • Add ROSActionClient / ROSActionServer (and goal-handle helpers) plus ROSConnection factory + routing helpers.
  • Add handwritten Fibonacci action message types used for end-to-end validation.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
com.unity.robotics.ros-tcp-connector/Runtime/TcpConnector/SysCommand.cs Adds action-related syscommand constants and param structs.
com.unity.robotics.ros-tcp-connector/Runtime/TcpConnector/ROSConnection.cs Adds action factories, raw-frame enqueue helper, and __request routing to action servers; updates instance lookup API.
com.unity.robotics.ros-tcp-connector/Runtime/TcpConnector/ROSActionServer.cs New Unity-side action server + per-goal handle for feedback/result.
com.unity.robotics.ros-tcp-connector/Runtime/TcpConnector/ROSActionServer.cs.meta Unity asset metadata for new file.
com.unity.robotics.ros-tcp-connector/Runtime/TcpConnector/ROSActionClient.cs New Unity-side action client + per-goal handle for feedback/result/cancel.
com.unity.robotics.ros-tcp-connector/Runtime/TcpConnector/ROSActionClient.cs.meta Unity asset metadata for new file.
com.unity.robotics.ros-tcp-connector/Runtime/Messages/ActionTutorialsInterfaces.meta Adds new message package folder metadata.
com.unity.robotics.ros-tcp-connector/Runtime/Messages/ActionTutorialsInterfaces/action.meta Adds new action/ folder metadata.
com.unity.robotics.ros-tcp-connector/Runtime/Messages/ActionTutorialsInterfaces/action/FibonacciGoal.cs Adds Fibonacci action Goal message type.
com.unity.robotics.ros-tcp-connector/Runtime/Messages/ActionTutorialsInterfaces/action/FibonacciGoal.cs.meta Unity asset metadata for new file.
com.unity.robotics.ros-tcp-connector/Runtime/Messages/ActionTutorialsInterfaces/action/FibonacciResult.cs Adds Fibonacci action Result message type.
com.unity.robotics.ros-tcp-connector/Runtime/Messages/ActionTutorialsInterfaces/action/FibonacciResult.cs.meta Unity asset metadata for new file.
com.unity.robotics.ros-tcp-connector/Runtime/Messages/ActionTutorialsInterfaces/action/FibonacciFeedback.cs Adds Fibonacci action Feedback message type.
com.unity.robotics.ros-tcp-connector/Runtime/Messages/ActionTutorialsInterfaces/action/FibonacciFeedback.cs.meta Unity asset metadata for new file.
com.unity.robotics.ros-tcp-connector/Runtime/Messages/ActionTutorialsInterfaces/action/FibonacciFeedbackMessage.cs Adds Fibonacci FeedbackMessage wrapper (goal_id + feedback) and registers alt typename.
com.unity.robotics.ros-tcp-connector/Runtime/Messages/ActionTutorialsInterfaces/action/FibonacciFeedbackMessage.cs.meta Unity asset metadata for new file.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 548 to 552
{
// Prefer to use the ROSConnection in the scene, if any
_instance = FindObjectOfType<ROSConnection>();
_instance = FindAnyObjectByType<ROSConnection>();
if (_instance != null)
return _instance;
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

FindAnyObjectByType<T>() is not available in Unity 2020.2 (the package’s minimum Unity version in package.json). This will cause compile errors for supported Unity versions; use FindObjectOfType<ROSConnection>() or wrap the newer API behind version-dependent #if guards with an older fallback.

Copilot uses AI. Check for mistakes.
Comment on lines +412 to +421
internal bool TryRouteToActionServer(int srvId, string destination, byte[] data)
{
if (m_ActionServers.TryGetValue(destination, out var serverObj))
{
// serverObj is ROSActionServer<,,> but we stored it as object.
// Use reflection to call OnGoalRequest.
var method = serverObj.GetType().GetMethod("OnGoalRequest",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
method?.Invoke(serverObj, new object[] { srvId, data });
return true;
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

TryRouteToActionServer returns true as soon as the destination matches, even if reflection fails to find OnGoalRequest (or if invocation throws). That can silently drop incoming __request messages. Consider caching a typed delegate/interface instead of reflection, and only returning true after a successful dispatch (otherwise log and fall back to normal service routing).

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +90
void EnsureRegistered()
{
if (m_Registered)
return;
m_Registered = true;

m_Connection.QueueSysCommand(
SysCommand.k_SysCommand_ActionClient,
new SysCommand_ActionRegistration
{
action_name = m_ActionName,
action_type = m_ActionType
});
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Action client registration is only sent once per ROSActionClient instance (m_Registered), but ROSConnection re-registers topics/services on reconnect. If the endpoint restarts, this client may never re-send __action_client, potentially breaking subsequent action calls/feedback. Consider resetting m_Registered on connection loss or integrating with ROSConnection’s reconnect path to re-register action clients.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +26
readonly Dictionary<string, ActionGoalHandle<TResult, TFeedback>> m_GoalHandles =
new Dictionary<string, ActionGoalHandle<TResult, TFeedback>>();

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

m_GoalHandles entries are added for accepted goals but never removed (not after GetResult() completes and not after Cancel() succeeds). Over time this can grow unbounded and keep routing feedback to stale handles; consider removing the entry when a goal reaches a terminal state (result received / cancel acknowledged).

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +68
public void EnsureRegistered()
{
if (m_Registered)
return;
m_Registered = true;

m_Connection.QueueSysCommand(
SysCommand.k_SysCommand_ActionServer,
new SysCommand_ActionRegistration
{
action_name = m_ActionName,
action_type = m_ActionType
});
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Action server registration is only sent once (m_Registered), but ROSConnection will reconnect and re-register topics/services after an endpoint restart. If the endpoint loses state, Unity’s action server may never re-send __action_server, so ROS clients won’t be able to send goals until restart. Consider resetting m_Registered on connection loss or hooking into ROSConnection’s reconnect path to re-register action servers.

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +170
public void SetResult(TResult result)
{
// Send __response{srv_id} + CDR Result body.
// QueueSysCommand is public on ROSConnection.
m_Connection.QueueSysCommand(
SysCommand.k_SysCommand_ServiceResponse,
new SysCommand_Service { srv_id = m_SrvId });
m_Connection.QueueRawMessage(m_ActionName, result);
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

ROSActionServer tracks active goals (m_ActiveGoals) and even has RemoveGoal, but ActionServerGoalHandle.SetResult() never calls back to remove the goal. This leaves completed goals in the dictionary indefinitely; either remove the tracking entirely or ensure the goal is removed when a terminal result is set (or on cancel).

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +197
public async Task<TResult> GetResult()
{
var (srvId, pauser) = m_Connection.AllocateServiceRequest();

m_Connection.QueueSysCommand(
SysCommand.k_SysCommand_ActionGetResult,
new SysCommand_ActionGoalOp { action_name = m_ActionName, srv_id = srvId });

var request = new GetResultRequestProxy { goal_id = GoalId };
m_Connection.QueueRawMessage(m_ActionName, request);

byte[] responseBytes = (byte[])await pauser.PauseUntilResumed();

if (responseBytes == null || responseBytes.Length == 0)
return default;

// GetResult_Response CDR layout:
// [4 bytes CDR header (00 01 00 00)]
// [1 byte int8 status]
// [3 bytes alignment padding to 4-byte boundary]
// [Result CDR body (WITHOUT its own CDR header)]
//
// We need to strip the CDR header + status + padding, then
// prepend a fresh CDR header so the deserializer sees a
// well-formed CDR stream for TResult.
const int headerSize = 4; // CDR encapsulation header
const int statusSize = 1; // int8 status
const int padSize = 3; // alignment to 4-byte boundary
int skipBytes = headerSize + statusSize + padSize;

if (responseBytes.Length <= skipBytes)
return default;

// Build a new byte array: CDR header + Result body
byte[] resultCdr = new byte[4 + (responseBytes.Length - skipBytes)];
resultCdr[0] = 0x00; resultCdr[1] = 0x01; // CDR_LE
resultCdr[2] = 0x00; resultCdr[3] = 0x00;
Array.Copy(responseBytes, skipBytes, resultCdr, 4,
responseBytes.Length - skipBytes);

var deserializer = new MessageDeserializer();
return deserializer.DeserializeMessage<TResult>(resultCdr);
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

This PR introduces new action-specific behaviors (goal request/response framing, GetResult byte-skipping, feedback subscription routing, and __request dispatch to action servers) but there are no corresponding unit/integration tests. Since the package already has Runtime tests, please add coverage for at least the protocol parsing/packing logic and routing behavior (e.g., verifying GetResult payload handling and that __request is routed correctly when an action server is registered).

Copilot uses AI. Check for mistakes.
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