v2.4.1 — STABLE
⚡ Open-Source Robotics SDK

Remote Control Your Robot
From Anywhere on Earth

TeleOp SDK makes it trivially easy to add internet-scale teleoperation to any Arduino-based robot — with sub-50ms latency, built-in security, and a clean, expressive API.

<50ms
Round-trip latency
AES-256
Command encryption
10Hz
Telemetry stream
Distance, via Internet
System Architecture
WebSocket + MQTT over TLS
🎮
Operator
Browser / Python Client
HTTPS/WSS
TLS 1.3
☁️
TeleOp Relay
Anycast edge nodes
MQTT/TLS
+AES-256
📡
Robot Gateway
ESP32 / Raspberry Pi
USB Serial
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
P C1 S2 BAT3 DROP ZONE SCORE: 0
← → move robot  |  ↑ move forward  |  SPACE pick/drop  |  or use buttons below
MOVEMENT
LIVE TELEMETRY
POS X120
POS Y190
HEADING
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