Skip to content

Building Your Own Operator

Most robots never need a custom operator — operate.adamohq.com provides gamepad teleop, VR, recording, replay, and fleet management. Skip this page until you’ve already shipped a robot using the platform.

You build your own operator when you outgrow the hosted UI:

  • A custom XR app that wants direct hand and head tracking on your own hardware.
  • A mobile app that drives a delivery robot.
  • A leader/follower rig (GELLO, Aloha) where one robot’s joint state drives another.
  • A browser app for an end customer that should never see operate.adamohq.com.

The pattern is symmetric: an operator is just another adamo.Robot (Python) or Session (Rust/C) participant. It publishes; the robot subscribes. Same SDK, same wire format, same priority knobs.

A control loop has two sides:

Both sides connect to the same Adamo network. Each side picks a name, opens an SDK session, and the topic key expressions follow the convention {name}/{track} — the SDK adds the org scope for you.

Use REAL_TIME priority (250) for control commands so they drain ahead of video under congestion. Use drop-on-congestion + best-effort reliability so a stuck operator never blocks the robot.

This is the sender side. Pair it with the robot-side receiver on the Receiving Control page.

import json
import adamo
op = adamo.Robot(api_key="ak_...", name="xr-operator")
left = op.publish("control/xr/hand/left", priority=250, express=True)
right = op.publish("control/xr/hand/right", priority=250, express=True)
head = op.publish("control/xr/head", priority=250, express=True)
# your OpenXR / WebXR / Quest loop
for frame in xr_stream():
left.put(json.dumps(frame.left))
right.put(json.dumps(frame.right))
head.put(json.dumps(frame.head))

What’s happening:

  • op = adamo.Robot(name="xr-operator") — open a participant called xr-operator. No video tracks; this side just publishes.
  • op.publish(track, priority=250, express=True) — declare a publisher under {xr-operator}/{track} with REAL_TIME priority, drop-on-congestion, best-effort reliability, and express sends. The robot subscribes to xr-operator/control/xr/hand/{side} to receive these.
  • The XR loop runs at the headset’s frame rate (72 / 90 / 120 Hz). Each frame becomes one put per channel — small JSON payloads, no batching.

You can substitute any input source — Quest controllers, Leap Motion, Apple Vision Pro, an Aloha leader rig — as long as it produces frames you can serialise.

For operators that run in a browser — your own teleop UI, a customer-facing dashboard — use the TypeScript SDK. Same primitives plus React components for video, gamepad, and presence:

import { AdamoProvider, Stream, GamepadController } from "@adamo/react";
<AdamoProvider url="wss://router.adamohq.com:443" org="my-org">
<Stream robot="my-arm" track="main" />
<GamepadController topic="adamo/my-org/my-arm/control/joy" />
</AdamoProvider>

The full surface — useRobots, useTracks, useSubscription, usePublisher, XR stereo player — is in the TypeScript SDK reference.

If your operator should attach to whichever robot is online, use Robot Discovery to enumerate or watch the live set:

robots = [k.removesuffix("/alive") for k in op.session.live_tokens()]
print("online:", robots)

Then pick one and start publishing to its control topics.