System Architecture
WebSocket + MQTT over TLS
Operator
Browser / Python Client
⟶
HTTPS/WSS
TLS 1.3
TLS 1.3
⟶
TeleOp Relay
Anycast edge nodes
⟶
MQTT/TLS
+AES-256
+AES-256
⟶
Robot Gateway
ESP32 / Raspberry Pi
⟶
USB Serial
115200 baud
115200 baud
⟶
Arduino
TeleOp Library
↑ Telemetry flows in reverse at 10Hz — sensors, IMU, battery, position
SDK Features
Ultra-Low Latency
Anycast relay network with edge nodes on 6 continents. Commands reach your robot in under 50ms from anywhere on Earth.
End-to-End Encryption
AES-256-GCM for all command payloads. TLS 1.3 transport. JWT-signed robot identity — no command spoofing.
Live Telemetry
Stream sensor data, IMU, GPS, battery, and custom metrics back to the operator at configurable rates up to 50Hz.
Gamepad Support
Plug-and-play support for Xbox, PS5, and generic HID gamepads via the Web Gamepad API. Deadzone, curve, and axis mapping built-in.
Arduino Library
Install via Arduino Library Manager. Works on Uno, Mega, Nano, and any board with a serial interface to an ESP32 gateway.
Multi-Platform SDKs
Official clients for Python, JavaScript/Node.js, ROS2 (C++), and a browser-native web SDK. REST fallback for constrained networks.
Quickstart
Install → Wire → Control
# Library Manager (Arduino IDE 2.x)
$
arduino-cli lib install "TeleOpSDK"
$
pip install teleop-sdk
$
npm install @teleop/sdk
Requires Arduino IDE 2.x or CLI 0.34+. The gateway firmware runs on any ESP32 board. See hardware guide →
Arduino (Robot)
Python (Operator)
JavaScript (Web)
ESP32 Gateway
ROS2 Node
// ── Arduino Robot Controller ──────────────────────────────────────── // Include the TeleOp SDK library (install via Library Manager) #include <TeleOpSDK.h> // Robot credentials — generate at dashboard.teleop.dev const char* ROBOT_ID = "robo-xyz-7a3f"; const char* SECRET = "sk-live-abc123..."; TeleOpSDK teleop(ROBOT_ID, SECRET); // Declare servo and motor pins const int MOTOR_L = 5; const int MOTOR_R = 6; const int SERVO_PAN = 9; void setup() { Serial.begin(115200); // Gateway talks over USB serial teleop.begin(Serial); // Register command handlers — called when operator sends a command teleop.onCommand("drive", [](TeleOpCmd cmd) { float speed = cmd.getFloat("speed"); // -1.0 → +1.0 float turn = cmd.getFloat("turn"); int l = constrain((speed + turn) * 255, -255, 255); int r = constrain((speed - turn) * 255, -255, 255); analogWrite(MOTOR_L, abs(l)); analogWrite(MOTOR_R, abs(r)); }); teleop.onCommand("pan", [](TeleOpCmd cmd) { int angle = cmd.getInt("angle"); // 0-180° analogWrite(SERVO_PAN, map(angle, 0, 180, 500, 2500)); }); teleop.onCommand("estop", [](TeleOpCmd cmd) { analogWrite(MOTOR_L, 0); analogWrite(MOTOR_R, 0); teleop.emit("alert", {"msg": "ESTOP triggered"}); }); } void loop() { // Process incoming commands from gateway (non-blocking) teleop.poll(); // Push telemetry every 100ms (10 Hz) if (teleop.telemetryDue()) { teleop.telemetry() .add("battery_v", analogRead(A0) * 0.0049 * 4.2) .add("temp_c", readTemperature()) .add("imu_yaw", imu.getYaw()) .send(); } }
#!/usr/bin/env python3 # ── Python Operator Client ───────────────────────────────────────── import asyncio from teleop_sdk import TeleOpClient, Gamepad async def main(): # Connect to a remote robot by ID — works over the internet async with TeleOpClient( robot_id="robo-xyz-7a3f", api_key="ak-live-your-key", latency_target_ms=40, # SDK picks the closest relay node reconnect=True, # auto-reconnect on drop ) as bot: # Subscribe to telemetry @bot.on_telemetry async def on_telem(data): print(f"Battery: {data['battery_v']:.2f}V Yaw: {data['imu_yaw']:.1f}°") # Drive loop with Xbox gamepad async with Gamepad() as gp: while True: axes = await gp.read() await bot.command("drive", { "speed": axes.left_y, # left stick Y "turn": axes.right_x, # right stick X }) if axes.button_b: await bot.command("estop") asyncio.run(main())
// ── Browser / Node.js Operator SDK ───────────────────────────────── import { TeleOpClient, GamepadBinding } from '@teleop/sdk'; const bot = new TeleOpClient({ robotId: 'robo-xyz-7a3f', apiKey: 'ak-live-your-key', relay: 'auto', // picks nearest Anycast node reconnect: true, }); // Real-time telemetry callback bot.onTelemetry((data) => { document.getElementById('battery').textContent = data.battery_v.toFixed(2) + 'V'; }); // Keyboard → drive command mapping const keys = {}; document.addEventListener('keydown', e => keys[e.key] = true); document.addEventListener('keyup', e => keys[e.key] = false); setInterval(async () => { const speed = (keys['ArrowUp'] ? 1 : 0) - (keys['ArrowDown'] ? 1 : 0); const turn = (keys['ArrowRight'] ? 1 : 0) - (keys['ArrowLeft'] ? 1 : 0); await bot.command('drive', { speed, turn }); }, 50); // 20 Hz command rate // Gamepad binding helper const gp = new GamepadBinding(bot, { axes: { 1: 'drive.speed', 2: 'drive.turn' }, buttons: { 1: 'estop' }, deadzone: 0.08, }); gp.start(); await bot.connect(); console.log('Connected to', await bot.ping(), 'ms away');
// ── ESP32 Gateway Firmware ────────────────────────────────────────── // Flash this to an ESP32. It bridges Arduino (USB Serial) ↔ TeleOp cloud #include <TeleOpGateway.h> #include <WiFi.h> const char* SSID = "YourWiFi"; const char* PASS = "password"; const char* ROBOT_ID = "robo-xyz-7a3f"; const char* SECRET = "sk-live-abc123..."; // TeleOpGateway handles: WiFi reconnection, MQTT-over-TLS to relay, // AES-256-GCM command decryption, and forwarding to Arduino via Serial2 TeleOpGateway gw(ROBOT_ID, SECRET); void setup() { Serial.begin(115200); // Debug Serial2.begin(115200, SERIAL_8N1, 16, 17); // Arduino link WiFi.begin(SSID, PASS); gw.begin(Serial2); // Pass the serial port to Arduino // Optional: report gateway status in telemetry gw.addStatusProvider([]() { return TeleOpStatus{ .rssi = WiFi.RSSI(), .heap = ESP.getFreeHeap(), .uptime = millis() / 1000UL, }; }); } void loop() { gw.loop(); // Non-blocking — handles WiFi, MQTT, bridging }
// ── ROS2 TeleOp Bridge Node (C++) ───────────────────────────────── #include "teleop_sdk/ros2_bridge.hpp" #include "geometry_msgs/msg/twist.hpp" #include "sensor_msgs/msg/imu.hpp" using namespace teleop_sdk; class TeleOpBridgeNode : public Ros2Bridge { public: TeleOpBridgeNode() : Ros2Bridge("teleop_bridge") { // Declare parameters robot_id_ = declare_parameter<std::string>("robot_id"); api_key_ = declare_parameter<std::string>("api_key"); // Map TeleOp "drive" command → /cmd_vel topic mapCommandToTopic<geometry_msgs::msg::Twist>( "drive", "/cmd_vel", [](TeleOpCmd cmd, geometry_msgs::msg::Twist& msg) { msg.linear.x = cmd.getDouble("speed"); msg.angular.z = cmd.getDouble("turn"); } ); // Map /imu/data topic → telemetry stream mapTopicToTelemetry<sensor_msgs::msg::Imu>( "/imu/data", "imu", [](sensor_msgs::msg::Imu msg, TeleOpTelemetry& telem) { telem.add("yaw", quaternionToYaw(msg.orientation)); telem.add("accel", msg.linear_acceleration.x); } ); connect(robot_id_, api_key_); } };
API Reference
Click to expand
CLASS
TeleOpSDK(robot_id, secret)
Arduino — core robot-side object
robot_idconst char*Your robot's unique ID from the dashboard
secretconst char*Shared secret for HMAC command authentication
.begin(serial)voidInit with the hardware serial port connected to gateway
.poll()voidProcess incoming commands. Call in loop() — non-blocking
.onCommand(name, cb)voidRegister a handler function for a named command
.emit(name, data)voidSend a named event back to all connected operators
.telemetry()TelemetryBuilderReturns builder; chain .add(key, val).send() to publish
.telemetryDue()boolTrue when the telemetry interval has elapsed (default 100ms)
.setRate(hz)voidSet telemetry publish rate (1–50 Hz)
POST
/v1/robots/{id}/command
REST — send a command via HTTP (low-frequency use)
AuthorizationHeaderBearer ak-live-…
commandstringCommand name, e.g. "drive"
paramsobjectKey-value parameters sent to robot handler
priorityint 0-9Commands >5 bypass queue (for ESTOP, etc.)
Response: {"latency_ms": 12, "queued": false, "robot_ack": true}
WS
wss://relay.teleop.dev/v1/control/{id}
WebSocket — real-time bidirectional control channel
Subprotocolstringteleop-v2
Client → Server message frames:
{"type":"cmd"}objectSend command: {type, name, params, seq}
{"type":"ping"}objectLatency probe, receives pong with ts
Server → Client message frames:
{"type":"telem"}objectTelemetry batch from robot
{"type":"event"}objectNamed event emitted by robot
{"type":"status"}objectConnection health: latency, drops, robot online
EVENT
TeleOpCmd — command argument accessor
Arduino — passed to onCommand() handlers
.getFloat(key)floatRead a float parameter
.getInt(key)intRead an integer parameter
.getString(key)StringRead a string parameter
.getBool(key)boolRead a boolean parameter
.has(key)boolCheck if a key exists
.sequint32_tSequence number — detect dropped commands
.timestamp_msuint64_tUnix ms when command was sent by operator
Simulator — Interactive Demo
ROBOT ONLINE — sim
12.4
BATTERY (V)
0.00
SPEED CMD
18
LATENCY (ms)
0.0°
HEADING
◈ TELEOPERATION SIMULATION — use controls below or arrow keys
← → move robot | ↑ move forward | SPACE pick/drop | or use buttons below
MOVEMENT
LIVE TELEMETRY
POS X120
POS Y190
HEADING0°
PAYLOADNONE
OBJECTS3 / 3
SCORE0
COMMAND LOG
[00:00.000] ✓ TeleOpSDK initialized — robot online
[00:00.012] → Connected to relay: us-east-1.relay.teleop.dev
[00:00.018] → RTT: 18ms
[00:00.100] ✓ Simulation arena loaded — 3 objects detected