The robotics surface lives in its own subpath import. You construct one Robotics client per (tenant, robot), then open scoped publications and subscriptions. The client holds a single MoQT session, connects lazily on the first call, and auto-reconnects — replaying every publication and subscription on reconnect — so your code does nothing on a link flap.

Import

import { Robotics } from "@clutchcall/sdk/robotics";

Construct a client

const r = new Robotics({
  relayHost: "relay.clutchcall.dev",
  robotId:   "spot-12",
  token:     process.env.CLUTCHCALL_CREDENTIALS!,
});

RoboticsOptions

relayHost
string
required
Relay host or full URL — "relay.clutchcall.dev" or "https://relay.clutchcall.dev". Every namespace derives from it.
robotId
string
required
The robot this client speaks for. Used to build robot/<id> (telemetry) and robot/<id>/ctl (commands).
token
string
Bearer token for the relay session, scoped to (tenant, robotId).
serverCertificateHash
string
Base64 SHA-256 of the relay’s cert, for pinned WebTransport in the browser.
webTransport
WebTransportFactory
Inject a custom WebTransport factory (Node polyfill / tests).
onError
(e: Error) => void
Called when the underlying relay session closes with an error.

Methods

publishTelemetry(spec)

Open a robot → cloud track under robot/<id>. Returns a RoboticsPublication.
spec.topic
string
required
Track name under the telemetry namespace, e.g. "odom", "battery_state".
spec.typeName
string
required
Full ROS 2 message type, e.g. "nav_msgs/msg/Odometry". Prefixed on the wire.
spec.qos
QoS
QoS profile (below). Omit for the best-effort default.
const odom = await r.publishTelemetry({
  topic:    "odom",
  typeName: "nav_msgs/msg/Odometry",
  qos:      { reliability: "best_effort", depth: 1 },
});
odom.write(cdrBytes);

publishCommand(spec)

Open a cloud → robot track under robot/<id>/ctl. Same spec shape as publishTelemetry; returns a RoboticsPublication.
const cmd = await r.publishCommand({
  topic:    "cmd_vel",
  typeName: "geometry_msgs/msg/Twist",
  qos:      { reliability: "reliable" },
});
cmd.write(twistCdr);

subscribeTelemetry(spec, onMessage)

Receive robot → cloud frames from robot/<id>. The callback gets the raw CDR payload and the wire type name — use the type name to guard / route.
spec.topic
string
required
Track name to subscribe under the telemetry namespace.
onMessage
(cdr: Uint8Array, typeName: string) => void
required
Invoked per delivered message. cdr is the opaque payload; typeName is the prefix decoded off the wire.
await r.subscribeTelemetry({ topic: "odom" }, (cdr, typeName) => {
  if (typeName !== "nav_msgs/msg/Odometry") return; // schema guard
  handleOdom(cdr);
});

subscribeCommand(spec, onMessage)

Receive cloud → robot frames from robot/<id>/ctl. Same shape as subscribeTelemetry; this is the call the robot (or the on-robot bridge) makes to pick up teleop commands.
await r.subscribeCommand({ topic: "cmd_vel" }, (cdr, typeName) => {
  drive(cdr);
});

close()

Tear down the underlying MoQT session and all publications / subscriptions. Idempotent.
r.close();

Handles

RoboticsPublication

Returned by publishTelemetry / publishCommand.
write(cdr: Uint8Array, priority?: number)
method
Push one typed message. cdr is the raw CDR (or whatever matches your typeName); the SDK prepends the type-name prefix and opens a fresh MoQT group per message. priority (TypeScript) overrides the QoS default for this single frame — 0 highest, 255 lowest. Returns immediately; the MoQT layer handles ordering and reliability.
close()
method
Stop publishing this track.

RoboticsSubscription (TypeScript)

subscribeTelemetry / subscribeCommand resolve once the subscription is attached. In the TypeScript SDK the resolved handle exposes close() to stop receiving; in Python / Go the returned subscription object exposes the same.
Keep the subscription handle alive for as long as you want frames. In garbage-collected languages it owns the delivery callback — if it is collected, the engine may call into freed memory. Drop the publication handle (or call close) to stop publishing.

QoS profile

interface QoS {
  reliability?: "reliable" | "best_effort"; // default best_effort
  durability?:  "volatile" | "transient_local"; // default volatile
  depth?:       number; // keep-last group window the relay holds (default 10)
}
FieldMaps to
reliabilitysubgroup stream (reliable) vs QUIC datagram (best_effort)
durabilitytransient_local adds late-join retain
depthper-track recent-group window for late subscribers
The default profile matches ROS 2’s SensorDataQoS (best-effort, volatile, keep-last). See Details → QoS → lanes for the full capability mapping.

Events & lifecycle

  • Connection state — the client surfaces relay session lifecycle (Connecting · Connected · Reconnecting · Closed · Failed). In TypeScript pass onState via the underlying options; in other SDKs the connect callback reports state codes. Auto-reconnect with capped backoff re-attaches every track on its own.
  • Late join — a subscriber that joins after a publisher immediately receives the relay’s retained group window (sized by depth, latched by transient_local).
  • ErrorsonError (TypeScript) fires on relay close with a reason string; individual publish/subscribe calls reject if the session can’t be established.

Wire frame (for non-SDK peers)

If you bridge a non-SDK device, emit the same envelope so SDK subscribers demux:
[u16 BE type_name_len] [type_name UTF-8] [CDR payload]
The CDR payload is opaque; the prefix is the only contract. Every SDK encodes and decodes this identically.
  • Details — wire model, lanes, transports.
  • Cookbook — short task snippets.
  • Recipes — full worked examples.