Swift-ros2 3.0: native Swift ROS 2 client library, now with generated common_interfaces and its own org

swift-ros2 3.0.0 is out. It is the first stable 3.x release of the native Swift client library for ROS 2 that I introduced here in April.

swift-ros2 lets a Swift app join a ROS 2 graph directly, over Zenoh (round-trip compatible with rmw_zenoh_cpp) or DDS (rmw_cyclonedds_cpp). There is no bridge and no rcl cross-compile.

demo

What’s new in 3.0

Three packages, mirroring upstream ROS 2. The project moved to the swift-ros2 GitHub organization:

Package What it holds
swift-ros2-rosidl 1.0.0 XCDR v1 codec, message / service / action protocols, runtime interfaces, and the swift-ros2-gen generator with its SwiftPM build plugin
swift-ros2 3.0.0 The runtime: ROS2Context, nodes, publishers, subscriptions, services, actions, parameters
swift-ros2-common-interfaces 1.0.0 std_msgs, std_srvs, geometry_msgs, sensor_msgs, nav_msgs, shape_msgs, stereo_msgs, trajectory_msgs, visualization_msgs, diagnostic_msgs and tf2_msgs, generated from the IDL

Wire interop

  • Services and actions are keyed by their service-level type hashes.
  • Over Zenoh a swift-ros2 node announces itself as a real node, with a GID per publisher and client.
  • Wire DDS nodes appear in the ROS graph through ros_discovery_info.
  • Late-joining action clients get the current status.

Verified against live ROS 2 Humble, Jazzy, Kilted and Lyrical hosts over Zenoh and DDS, through a fresh-user walkthrough of the docs, and in Conduit.

Looking back: the real ROS 2 stack since 2.0

swift-ros2 began in April as a pure-Swift implementation of the ROS 2 wire protocols over zenoh-pico and CycloneDDS. That made it portable, but it meant re-implementing type hashes, QoS rules and graph details and keeping them in step with upstream. The 2.x line changed the foundation:

  • 1.3.0 (July) added a native RCL backend: upstream rcl with rmw_zenoh_cpp or rmw_cyclonedds_cpp. It ships as prebuilt xcframeworks on Apple and links a system ROS 2 install on Linux.
  • 1.4.0 turned it on by default on Apple.
  • 2.0.0 (September) removed the wire clients from the public API. ROS2Context picks the backend. The pure-Swift wire path stays as an internal fallback where rcl is not available, such as Android, Windows and Humble hosts.
  • 2.2.0 rebuilt the Apple RCL stack from ROS 2 Lyrical.

On the RCL backend, type hashes, QoS semantics, the node graph and introspection match upstream by construction, because they are upstream. 3.0 builds on that foundation.

A taste of the API

Topics and services

import SwiftROS2
import CommonInterfaces

let context = try await ROS2Context(
    transport: .zenoh(locator: "tcp/192.168.1.100:7447"), distro: .jazzy, domainId: 123)
let node = try await context.createNode(name: "sensor_node", namespace: "/ios")

let pub = try await node.createPublisher(Imu.self, topic: "imu")
try pub.publish(Imu(header: Header.now(frameId: "imu_link"),
                    linearAcceleration: Vector3(x: 0, y: 0, z: 9.81)))

let svc = try await node.createService(TriggerSrv.self, name: "/trigger") { _ in
    TriggerSrv.Response(success: true, message: "ok")
}

Actions (example_interfaces/action/Fibonacci, server with feedback, and a client)

actor FibonacciHandler: ActionServerHandler {
    typealias Action = FibonacciAction

    func handleGoal(_ goal: FibonacciAction.Goal) async -> GoalResponse {
        goal.order > 0 ? .accept : .reject
    }
    func handleCancel(_ handle: ActionGoalHandle<FibonacciAction>) async -> CancelResponse { .accept }

    func execute(_ handle: ActionGoalHandle<FibonacciAction>) async throws -> FibonacciAction.Result {
        var sequence: [Int32] = [0, 1]
        for _ in 2..<10 {
            sequence.append(sequence[sequence.count - 1] + sequence[sequence.count - 2])
            try await handle.publishFeedback(FibonacciAction.Feedback(sequence: sequence))
        }
        return FibonacciAction.Result(sequence: sequence)
    }
}
let server = try await node.createActionServer(
    FibonacciAction.self, name: "/fibonacci", handler: FibonacciHandler())

let client = try await node.createActionClient(FibonacciAction.self, name: "/fibonacci")
try await client.waitForActionServer(timeout: .seconds(5))
let goal = try await client.sendGoal(FibonacciAction.Goal(order: 10))
Task { for await feedback in goal.feedback { print(feedback.sequence) } }
if case .succeeded(let result) = try await goal.result() { print(result.sequence) }

Parameters (visible to ros2 param list / get / set / describe)

_ = try await node.declareParameter(
    "rate", default: Int64(30),
    descriptor: ROS2ParameterDescriptor(
        name: "rate", type: .integer, description: "publish rate in Hz",
        integerRange: Int64(1)...Int64(120)))

_ = await node.setOnSetParametersCallback { proposed in
    for p in proposed where p.name == "rate" {
        if case .integer(let v) = p.value, !(1...120).contains(v) {
            return .failure(reason: "rate must be in [1, 120]")
        }
    }
    return .success()
}

Runnable versions of all of these live in Sources/Examples (swift run action-server zenoh, swift run parameter-demo zenoh, …).

Platforms

iOS / iPadOS 16+, macOS 13+, Mac Catalyst, visionOS, Linux (Ubuntu 22.04 / 24.04, x86_64 and aarch64), Windows 10 / 11, and Android API 28+ (Zenoh only).

Upgrading from 2.x? See MIGRATION.md.

See it live

I’m presenting swift-ros2 and Conduit at ROS By-The-Bay on Thursday, October 22, at Intrinsic HQ in Mountain View, with a live demo of Apple devices talking to ROS 2 nodes on Linux:

Feedback, issues and PRs are very welcome.

1 Like