# Robotics — Cookbook

> Task-oriented snippets: publish telemetry, teleop, QoS lanes, late-join retain, cross-language demux, multi-topic fan-in, graceful shutdown.

Short, copy-pasteable answers to "how do I X" for the robotics modality. Each
assumes a constructed client:

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

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

## Publish a telemetry stream

Open one track per topic and `write` raw CDR. Best-effort by default — perfect
for high-rate pose / odometry where the freshest sample wins.

```ts
const odom = await r.publishTelemetry({
  topic:    "odom",
  typeName: "nav_msgs/msg/Odometry",
});
setInterval(() => odom.write(serializeOdom()), 50); // 20 Hz
```

## Send a teleop command

Commands go cloud → robot on `robot/<id>/ctl`. Use the reliable lane so a stop
or a goal can't be dropped.

```ts
const cmd = await r.publishCommand({
  topic:    "cmd_vel",
  typeName: "geometry_msgs/msg/Twist",
  qos:      { reliability: "reliable" },
});
cmd.write(twistCdr(0.4 /* m/s */, 0.0 /* rad/s */));
```

## Subscribe to a robot's telemetry from the cloud

The callback receives the raw CDR and the wire type name. Guard on the type name
before decoding.

```ts
await r.subscribeTelemetry({ topic: "odom" }, (cdr, typeName) => {
  if (typeName !== "nav_msgs/msg/Odometry") return;
  const { x, y } = decodeOdom(cdr);
  dashboard.plot(x, y);
});
```

## Have the robot receive commands

On the robot (or the on-robot bridge), subscribe the command track and apply
each message to the local actuator stack.

```ts
await r.subscribeCommand({ topic: "cmd_vel" }, (cdr) => applyTwist(cdr));
```

## Pick the right QoS lane

Reliable for things that must arrive (commands, maps, goals); best-effort for
high-rate sensors where latency beats completeness.

```ts
// must-arrive map update → subgroup stream, ordered
await r.publishTelemetry({ topic: "map", typeName: "nav_msgs/msg/OccupancyGrid",
  qos: { reliability: "reliable" } });

// 30 Hz LIDAR scan → QUIC datagram, lossy, lowest latency
await r.publishTelemetry({ topic: "scan", typeName: "sensor_msgs/msg/LaserScan",
  qos: { reliability: "best_effort", depth: 1 } });
```

## Latch a value for late subscribers

`transient_local` makes the relay retain the last group(s), so a dashboard that
connects later immediately gets the current value — like a latched / retained
message.

```ts
const state = await r.publishTelemetry({
  topic:    "robot_state",
  typeName: "std_msgs/msg/String",
  qos:      { durability: "transient_local", reliability: "reliable", depth: 1 },
});
state.write(encode("idle")); // a future subscriber sees "idle" on join
```

## Override priority per message

The QoS reliability sets a default send priority; override it on a single
`write` to push an urgent frame ahead of the queue (`0` highest, `255` lowest).

```ts
const cmd = await r.publishCommand({ topic: "cmd_vel", typeName: "geometry_msgs/msg/Twist" });
cmd.write(normalTwist);            // default priority for the lane
cmd.write(emergencyStop(), 0);     // jump the queue
```

## Demux across languages

The type-name prefix means a Python publisher and a TypeScript subscriber agree
with no schema registry. Publish from Python:

```python
from clutchcall.robotics import Robotics
r = Robotics(relay_host="relay.clutchcall.dev", robot_id="spot-12", token=tok)
pub = r.publish_telemetry(topic="odom", type_name="nav_msgs/msg/Odometry")
pub.write(serialize_message(msg))   # raw CDR
```

…and the TypeScript dashboard above receives `("nav_msgs/msg/Odometry", cdr)`
unchanged.

## Fan multiple topics into one dashboard

One client, many subscriptions — they all ride the single QUIC session.

```ts
for (const t of ["odom", "battery_state", "scan"]) {
  await r.subscribeTelemetry({ topic: t }, (cdr, typeName) => ingest(t, typeName, cdr));
}
```

## Subscribe to many robots from one process

Construct one client per robot; each opens its own session and namespace.

```ts
const fleet = ["spot-12", "spot-13", "spot-14"].map(id =>
  new Robotics({ relayHost: "relay.clutchcall.dev", robotId: id, token }));

await Promise.all(fleet.map(c =>
  c.subscribeTelemetry({ topic: "battery_state" }, (cdr) => trackBattery(cdr))));
```

## Bridge a local ROS 2 topic onto the mesh

Subscribe the local graph, forward raw CDR to a telemetry track. Below in
Python with `rclpy` serialization:

```python
from rclpy.serialization import serialize_message
pub = r.publish_telemetry(topic="odom", type_name="nav_msgs/msg/Odometry")
node.create_subscription(Odometry, "/odom", lambda m: pub.write(serialize_message(m)), 10)
```

## Bridge a command back onto the local graph

The other direction: subscribe the MoQT command track, re-publish onto local
DDS. Guard on type name so schema drift is dropped, not mis-decoded.

```python
from rclpy.serialization import deserialize_message
cmd_pub = node.create_publisher(Twist, "/cmd_vel", 10)
def on_cmd(cdr, type_name):
    if type_name == "geometry_msgs/msg/Twist":
        cmd_pub.publish(deserialize_message(cdr, Twist))
r.subscribe_command(topic="cmd_vel", on_message=on_cmd)
```

## Tune the late-join window

`depth` caps how many recent groups the relay holds for a late subscriber —
mirror your ROS 2 keep-last history.

```ts
await r.publishTelemetry({ topic: "tf", typeName: "tf2_msgs/msg/TFMessage",
  qos: { reliability: "reliable", depth: 50 } });
```

## Shut down cleanly

Close publications and subscriptions, then the client — this tears down the QUIC
session.

```ts
odom.close();
r.close();
```

## Related

- [SDK Methods](/modalities/robotics/sdk-methods) — full signatures.
- [Recipes](/modalities/robotics/recipes) — end-to-end examples.
