Basic Usage

This guide covers the fundamental building blocks of an rclgd application: nodes, communication patterns, and periodic tasks.

Initializing a Node

The RosNode is the entry point for all ROS 2 communication. It should be initialized with a unique name and optional namespace.

extends Node

var ros_node: RosNode

func _ready():
    if not rclgd.ok():
        rclgd.init()

    ros_node = RosNode.new()
    # init(name, namespace)
    ros_node.init("my_godot_node", "/my_robot")

    print("Node Name: ", ros_node.get_name())
    print("Namespace: ", ros_node.get_namespace())

Warning

All rclgd communication objects (RosNode, RosPublisher, RosSubscriber, RosTimer, ...) are RefCounted: they live only as long as something holds a reference to them. Always store the value returned by the create_* factories in a member variable — a discarded subscription or timer is freed immediately and silently stops working.

Topics (Pub/Sub)

Topics allow for continuous data streams between nodes.

Publishing

var pub: RosPublisher

func setup_publisher():
    pub = ros_node.create_publisher("/cmd_vel", "geometry_msgs/msg/Twist")

func send_velocity(linear: float, angular: float):
    var msg = RosMsg.from_type("geometry_msgs/msg/Twist")
    msg.linear.x = linear
    msg.angular.z = angular
    pub.publish(msg)

Subscribing

var sub: RosSubscriber

func setup_subscription():
    sub = ros_node.create_subscription("/odom", "nav_msgs/msg/Odometry", _on_odom)

func _on_odom(msg: RosMsg):
    # Access nested members directly
    var x = msg.pose.pose.position.x
    print("Robot X position: ", x)

Note

Subscription callbacks are always invoked on the Godot main thread, so it is safe to touch the scene tree from them.

Timers

Use RosTimer for periodic logic instead of Godot's built-in timers when synchronization with the ROS executor is required. Timers run on the node's clock: with use_sim_time:=true they follow /clock and pause when the simulation pauses.

var timer: RosTimer

func setup_timer():
    # create_timer(seconds, callback)
    timer = ros_node.create_timer(1.0, _on_timer_timeout)

func _on_timer_timeout():
    print("Periodic task executed at ROS time: ", ros_node.now())

func stop_timer():
    if timer.is_ready():
        timer.cancel()

Services

Services provide a request-response pattern.

# Client (Async)
func call_status_service():
    var client = ros_node.create_client("/get_status", "std_srvs/srv/Trigger")
    if not client.wait_for_service(2.0):
        return

    var request = client.create_request()
    var future = client.async_send_request(request)
    var response = await future.completed
    if response:
        print("Status Success: ", response.success)

# Server: fill the response in-place, it is sent when the callback returns
func setup_service_server():
    var srv = ros_node.create_service("/get_status", "std_srvs/srv/Trigger", _handle_request)

func _handle_request(_req: RosMsg, res: RosMsg):
    res.success = true
    res.message = "System OK"

Warning

Service server callbacks run synchronously on the ROS executor thread (the response must be filled before returning to the client). Keep them self-contained and use call_deferred for side effects on the scene. See RosService for details.

Actions

Actions are made for long-running tasks: they stream feedback while the goal executes and can be canceled mid-flight. Servers receive each accepted goal as a RosServerGoalHandle on the main thread and must finish it with succeed, abort or canceled.

# Server: classic Fibonacci action
func setup_action_server():
    var server = ros_node.create_action_server("fibonacci",
        "example_interfaces/action/Fibonacci", _execute_goal)

func _execute_goal(goal_handle: RosServerGoalHandle):
    var sequence = [0, 1]
    for i in range(2, goal_handle.get_goal().order):
        if goal_handle.is_cancel_requested():
            var canceled_result = goal_handle.create_result()
            canceled_result.sequence = sequence
            goal_handle.canceled(canceled_result)
            return
        sequence.append(sequence[i - 1] + sequence[i - 2])
        var fb = goal_handle.create_feedback()
        fb.sequence = sequence
        goal_handle.publish_feedback(fb)
        # Spread long-running goals over frames instead of blocking
        await get_tree().process_frame
    var result = goal_handle.create_result()
    result.sequence = sequence
    goal_handle.succeed(result)

# Client
func send_fibonacci_goal():
    var client = ros_node.create_action_client("fibonacci",
        "example_interfaces/action/Fibonacci")
    if not client.wait_for_server(2.0):
        return

    var goal = client.create_goal()
    goal.order = 10
    var goal_handle = client.send_goal(goal)
    goal_handle.feedback.connect(func(msg): print("Partial: ", msg.sequence))

    await goal_handle.completed
    if goal_handle.get_status() == RosGoalHandle.STATUS_SUCCEEDED:
        print("Result: ", goal_handle.get_result().sequence)

A running goal can be canceled from the client with goal_handle.cancel(); the final status then arrives through the completed signal as STATUS_CANCELED.