Skip to main content

Hosted Teleop

Operate a DimOS robot remotely from any browser or Quest headset over WebRTC. The robot dials out to a hosted Cloudflare Realtime SFU broker (teleop.dimensionalos.com), so you don’t need to open any inbound ports on the robot’s network — it works behind a home router, on Wi-Fi, or wired LAN.

Quick Start

TELEOP_API_KEY=dtk_live_... \
TELEOP_ROBOT_ID=my-robot \
TELEOP_ROBOT_NAME="Lab Go2" \
dimos run teleop-hosted-go2
The robot registers with the broker. Open teleop.dimensionalos.com, log in, and your robot appears under Available Robots. Click Connect and you’re driving.
ModuleRole
HostedTeleopModuleDials the broker, owns the WebRTC connection, datachannels, video, clock-sync, and live telemetry
HostedTwistTeleopModuleMobile-base subclass: scales operator commands into cmd_vel (m/s, rad/s)
HostedArmTeleopModuleArm-IK subclass: per-hand pose routing to coordinator tasks
Go2Module (in the blueprint)The robot driver receiving cmd_vel

Get an API key

  1. Visit teleop.dimensionalos.com and sign up.
  2. On the dashboard, API Keys → + New Key.
  3. Copy the key (shown once) and pass it as TELEOP_API_KEY when launching the blueprint.
The key is per-robot; one key authenticates one robot. The same user account can manage many keys for different robots.

Available blueprints

BlueprintUse caseSubclass
teleop-hosted-go2Mobile base (Unitree Go2, wheeled robots)HostedTwistTeleopModule
teleop-hosted-xarm7Arm IK (UFactory xArm7)HostedArmTeleopModule
Pair with the recorder to log a session and emit a transport-stats report:
dimos run teleop-hosted-go2 teleop-recorder
This writes recording_teleop_<ts>.db plus a report.md next to it on disconnect. Reports can also be regenerated from an existing .db:
python -m dimos.teleop.utils.report path/to/recording.db

Operator inputs

The browser is modality-agnostic — it just streams whatever the device gives it, and the robot blueprint decides what to do with it.
DeviceInputMaps to
Desktop browserWASD keyboardTwistStampedcmd_vel
PhoneOn-screen WASD keyssame path as keyboard
Quest 3 (Twist robot)Left thumbstick Y → forward/back, X → strafe; Right thumbstick X → yawJoy → derived twist on the robot
Quest 3 (Arm robot)Controller poses + analog triggersPoseStamped → coordinator TeleopIKTask
Shift = 2× speed, Ctrl = ½× speed and strafe on keyboard.

Live metrics HUD

While connected, the operator sees a metrics overlay (corner pill in the browser, in-headset stats panel in VR). Color-coded green/amber/red based on video and command-plane health:
MetricSource
fps, bitrate, loss, jitter buffer, decode time, freezesOperator’s getStats() on the inbound video track
RTTNTP-style min-RTT clock sync over the reliable datachannel
cmd latency, jitter, rateRobot-measured from the inbound twist stream — what actually arrived, sent back over state_reliable_back
The recorder captures these to the session .db and summarizes them in report.md.

Configuration

HostedTeleopConfig (base, applies to both subclasses):
FieldDefaultNotes
broker_urlhttps://teleop.dimensionalos.comOverride via -o / config to point at a self-hosted broker
broker_api_key""Required. Env: TELEOP_API_KEY
robot_id""Required, identifies this robot. Env: TELEOP_ROBOT_ID
robot_name""Display name shown in the dashboard. Env: TELEOP_ROBOT_NAME
control_loop_hz50.0Per-hand publish + button-state cycle
heartbeat_hz1.0HTTP heartbeat to the broker (also drives channel-id sync)
telemetry_hz3.0Robot → operator HUD command-plane stats
stun_urls[stun:stun.cloudflare.com:3478]STUN servers for ICE
turn_urls, turn_username, turn_credential""TURN credentials. Fields exist; not yet auto-provisioned.
HostedTwistTeleopConfig adds:
FieldDefaultNotes
linear_speed0.5Multiplied into cmd_vel.linear (m/s)
angular_speed0.8Multiplied into cmd_vel.angular (rad/s)
HostedArmTeleopConfig adds:
FieldDefaultNotes
task_names{}Maps "left"/"right" → coordinator task name (e.g. "teleop_xarm"), used as frame_id so the coordinator routes to the right IK task

How it connects

robot                          broker (Cloudflare SFU)            operator browser/Quest
─────                          ──────────────────────             ──────────────────────
HostedTeleopModule
  POST /api/v1/sessions  ───►  CF session + datachannels  ◄───    POST /sessions/{id}/join
                                                                     (operator joins)
  cmd_unreliable        ◄────  (operator → robot, lossy)  ◄────    WASD / Joy / poses
  state_reliable        ◄────  (operator → robot, json)   ◄────    ping, video_stats
  state_reliable_back   ────►  (robot → operator, json)    ────►   pong, robot_telemetry
  video track           ────►  CF publishes + pulls        ────►   <video> sink
Datachannels are negotiated (SCTP ids assigned by the broker). The video track is added before the SDP offer; the broker pulls it onto each operator session and renegotiates so the operator’s ontrack fires. For the WebRTC / aiortc / Cloudflare implementation details (MAX_BUNDLE constraints, candidate propagation, the throwaway SCTP id 0 channel, thread model), see dimos/teleop/quest_hosted/README.md.

Known Limitations

  • Single operator per robot session today. Multi-viewer / single-driver+watchers is roadmapped.
  • TURN is not wired yet. ICE relies on STUN only, so direct connectivity must succeed — works on most home/office networks, can fail on symmetric NAT or cellular. TURN field plumbing exists.
  • No auto-reconnect. If the link drops mid-session, the operator must click Connect again. The robot side stays up; reconnection is supported, just manual.
  • Single camera per robot today. Multi-camera support is roadmapped.
  • Operator is in a fixed slot until clean disconnect — a tab-close leaves the slot held until the broker’s grace timeout fires (or the robot restarts).