Rcl.NET.Templates
1.0.1
See the version list below for details.
dotnet new install Rcl.NET.Templates::1.0.1
rclnet
rclnet is a fast and easy-to-use .NET wrapper over ROS 2 client library, allowing .NET applications to interact with other ROS applications.
Supported Platforms
Supported .NET Versions:
- .NET 7
Supported ROS 2 Distributions:
- Humble Hawksbill
- Foxy Fitzroy
Getting Started
rclnet provides project templates to help you getting started quickly. You can install the templates using the following command:
dotnet new install Rcl.NET.Templates
Then, to create a node application:
mkdir MyNode
cd MyNode
dotnet new ros2-node
To create a message only library:
mkdir MyMessageLibrary
cd MyMessageLibrary
dotnet new ros2-msg
If you prefer creating the projects by yourself, or want to use rclnet in existing projects, refer the Installing and Generating Messages section.
Installing
Stable releases of rclnet are hosted on NuGet. You can install them using the following command:
dotnet add package Rcl.NET
For message only projects, you can install only Rosidl.Runtime
without Rcl.NET
:
dotnet add package Rosidl.Runtime
To generate message classes, you'll also need to install ros2cs
utility:
dotnet tool install -g ros2cs
Generating Messages
rclnet does not ship with message definitions. In order to communicate with other ROS 2 nodes, you need to generate messages first.
Message definitions are .NET classes / structs, you can either include messages in a console app which runs as an ROS 2 node, or compile separately in another library.
Projects containing messages will have to meet the following requirements:
Rcl.NET
orRosidl.Runtime
NuGet package is installed.AllowUnsafeBlocks
is set totrue
. This can be done by adding the following lines to the.csproj
file:<PropertyGroup> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup>
- Runtime marshalling for the assembly is disabled. You can the following line to somewhere in the source code of the project:
[assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]
To generate messages, you also need to add a ros2cs.spec
file to somewhere in the project (usually the project root).
A ros2cs.spec
file contains configurations such as output directory and where to find packages,
see here for detailed explanations.
Assuming you've already installed the ros2cs
utility, simply run the following command to generate messages:
ros2cs /path/to/ros2cs.spec
Features
- Completely asynchronous and
async
/await
friendly. - Flexible asynchronous scheduling control to fit rclnet into existing applications.
- Unified message generation for POCOs and blittable structures.
- Intuitive ROS graph querying and monitoring APIs.
- Easy-to-use POCO-based APIs.
- Fast and zero managed heap allocation APIs operating directly on native message buffers.
- Single package with runtime support for different ROS 2 distros.
- Builtin support for querying topic messages and ROS graph events with Reactive Extensions.
Supported ROS Features
Feature | Status | Additional Information |
---|---|---|
Topics | ✅ | N/A |
Services | ✅ | N/A |
Actions | ✅ | Managed implementation. |
Clocks | ✅ | Supports external time source by setting use_sim_time to true .<br/>CancellationTokenSource s can also be configured to cancel with timeout measured by external clock. |
Timers | ✅ | N/A |
Guard Conditions | ✅ | N/A |
Events | ✅ | Event handlers can be registered via SubscriptionOptions or PublisherOptions when creating the subscirption or publisher. |
ROS Graph | ✅ | Managed implementation. |
Logging | ✅ | Supports logging to stdout, /rosout and log files. Configurable with --ros-args . |
Parameter Service | ⚠️ | Supports loading parameters from command-line arguments and parameter files.<br/>Locally declared parameters are exposed via Parameter API.<br/>Parameter client is not implemented. |
Lifecycle | ❌ | N/A |
Network Flow Endpoints | ❌ | Available since galactic. |
Content Filtered Topics | ❌ | Available since humble. |
✅Supported ⚠️Partial support ❌Not supported ⏳In development
Asynchronous Execution Model
Unlike rclcpp and rclpy, rclnet doesn't have the concept of executors. Each RclContext
runs its
own event loop for waiting on signals and dispatching callbacks, which is essentialy a single-threaded
executor.
Although rclnet does not provide multi-threaded executors, it doesn't mean that you can't process messages or handle service requests using multiple threads. All communication primitives in rclnet provide both synchronous and asynchronous APIs for different needs and scenarios. Synchronous APIs are simpler and faster if the work need to be done is simple enough, e.g. neither CPU-intensive nor needs to issue blocking calls. Asynchronous APIs, in contrast, are for scenarios where you need to perform asynchronous calls or offload blocking operations into background threads.
Take subscriptions for example, you can receive messages synchronously using IRclSubscription<T>.Subscribe
,
or asynchronously using IRclSubscription<T>.ReadAllAsync
. To process messages asynchronously, one
would typically write:
await foreach (var msg in sub.ReadAllAsync())
{
// Perform asynchronous operation.
await SomeAsyncOperation(msg);
// Perform synchronous operation and wait for its completion without blocking the event loop.
await Task.Run(() => SomeOffloadedSyncOperation(msg));
}
RclContext
always gives control to user code on its event loop if no SynchronizationContext
or TaskScheduler
is captured.
Take the above code for example:
await foreach (var msg in sub.ReadAllAsync())
{
// On event loop.
await SomeAsyncOperation(msg);
// On thread pool thread.
await Task.Run(() => SomeOffloadedSyncOperation(msg));
// On thread pool thread.
}
You can use RclContext.SynchronizationContext
, RclContext.Yield()
, Task.Yield()
and
ConfigureAwait(false)
to perform fine-grained control over the asynchronous execution flow:
using var context = new RclContext(useSynchronizationContext: true);
...
await foreach (var msg in sub.ReadAllAsync())
{
// On event loop.
await SomeAsyncOperation(msg);
// On event loop.
await Task.Run(() => SomeOffloadedSyncOperation(msg));
// On event loop.
await Task.Yield();
// On event loop.
...
// The execution of current async method will stay on the event
// loop unless we break out of the SynchronizationContext using
// ConfigureAwait(false).
await AnotherAsyncOperation(msg).ConfigureAwait(false);
// On thread pool thread.
// We can still transition back to the event loop with context.Yield().
await context.Yield();
// On event loop.
}
Without enabling SynchronizationContext
on the event loop (which is the default), asynchronous continuations are not enforced
to resume on the event loop. The execution flow will leave the event loop as soon as an async point other
than RclContext.Yield
(or other asynchronous APIs in rclnet, see the following section) is encountered.
using var context = new RclContext();
...
await foreach (var msg in sub.ReadAllAsync())
{
// On event loop.
await SomeAsyncOperation(msg);
// On thread pool thread.
await Task.Run(() => SomeOffloadedSyncOperation(msg));
// On thread pool thread.
await Task.Yield();
// On thread pool thread.
...
await context.Yield();
// On event loop.
}
Controlling Execution of Continuations
Timers and guard conditions created by RclContext
implements IRclWaitObject
interface,
which allow the caller to asynchronously wait for the signal.
IRclWaitObject
interface exposes the following two overloads of WaitOneAsync
:
ValueTask WaitOneAsync(bool runContinuationAsynchronously, CancellationToken cancellationToken = default);
ValueTask WaitOneAsync(CancellationToken cancellationToken = default);
The latter overload simply calls another one with runContinuationAsynchronously
set to false
.
WaitOneAsync
allows the caller to explicitly control the execution of the continuation via runContinuationAsynchronously
parameter. Assuming there's no captured SynchronizationContext
or TaskScheduler
, when runContinuationAsynchronously
is set to true
, the continuation will be scheduled to execute in thread pool. And if runContinuationAsynchronously
is set to false
, the continuation is guaranteed to execute on the event loop.
However, when a SynchronizationContext
or TaskScheduler
is captured, the continuation of the call to WaitOneAsync
will always execute in the captured context, regardless of the value of runContinuationAsynchronously
.
Since context capture can be suppressed by calling ConfigureAwait
with continueOnCapturedContext
set to false
,
execution of the continuation can be precisely controlled using runContinuationAsynchronously
in conjunction with
continueOnCapturedContext
. See the following table:
runContinuationAsynchronously |
continueOnCapturedContext |
Continuation Execution |
---|---|---|
true |
true |
Captured SynchronizationContext or TaskScheduler if any, thread pool otherwise |
true |
false |
Thread pool |
false |
true |
Captured SynchronizationContext or TaskScheduler if any, event loop otherwise |
false |
false |
Event loop |
Awaiting all other asynchronous APIs in rclnet, including IAsyncEnumerable<T>
returning methods, behave exactly the same as IRclWaitObject.WaitOneAsync
,
except that these methods do not have a runContinuationAsynchronously
parameter, they always execute with runContinuationAsynchronously
set to false
.
A special case is RclContext.Yield
, awaiting RclContext.Yield
never captures scheduling context, and will always resume execution
on the event loop of the RclContext
.
Building and Running Examples
Install dependencies
The following instruction assumes that you've already installed ROS 2 foxy or humble in your system.
You'll need .NET 7.0 SDK to build and run the examples, see instructions here.
Make sure you have all dependencies installed by running:
rosdep install -i --from-paths examples
Run with dotnet run
Now you can run example projects using dotnet run
, e.g.
dotnet run examples/turtle_rotate
Run with ros2 run
Or you can build and install examples as colcon packages:
colcon build --executor sequential --merge-install --paths examples/*
source install/setup.bash
To run an example node, use ros2 run
, e.g.
ros2 run graph_monitor graph_monitor
Showcase
Subscribing
using var ctx = new RclContext(args);
using var node = ctx.CreateNode("hellow_world");
using var sub = node.CreateSubscription<Twist>("/cmd_vel");
await foreach (Twist msg in sub.ReadAllAsync())
{
...
}
Publishing
using var pub = node.CreatePublisher<Vector3>("/vec");
pub.Publish(new Vector3(x: 1, y: 2, z: 3));
Handling Service Calls
using var server = node.CreateService<
EmptyService,
EmptyServiceRequest,
EmptyServiceResponse>("/vec",
(request, state) =>
{
return new EmptyServiceResponse();
});
await Task.Delay(-1);
Calling Services
using var client = node.CreateClient<
EmptyService,
EmptyServiceRequest,
EmptyServiceResponse>("/vec");
await client.InvokeAsync(new EmptyServiceRequest());
Monitoring ROS Graph Changes
node.Graph
.OfType<NodeAppearedEvent>()
.Subscribe(x =>
{
Console.WriteLine($"Node {x.Node.Name} is online.");
});
await node.Graph.WaitForServiceServerAsync("/my/service");
Calling Action Servers
using var client = node.CreateActionClient<
SpinAction,
SpinActionGoal,
SpinActionResult,
SpinActionFeedback>("/spin");
using var goal = await client.SendGoalAsync(
new SpinActionGoal(targetYaw: Math.PI));
await foreach (var feedback in goal.ReadFeedbacksAsync())
{
Console.WriteLine(feedback.AngularDistanceTraveled);
}
var result = await goal.GetResultAsync();
Zero (Managed Heap) Allocation APIs
using var sub = node.CreateNativeSubscription<Twist>("/cmd_vel");
await foreach (RosMessageBuffer msg in sub.ReadAllAsync())
{
using (msg) ProcessMessage(msg);
static void ProcessMessage(RosMessageBuffer buffer)
{
ref var twist = ref buffer.AsRef<Twist.Priv>();
...
}
}
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.