Skip to main content

AI Teammate Robot Bridge

PyPI Python

Connects ROS2 robots to the AI Teammate platform via WebSocket + MQTT.

Features

  • rclpy native — Direct DDS participation, no rosbridge required
  • Dual telemetry — MQTT (primary, 1Hz) + WebSocket (fallback)
  • Auto-detection — cmd_vel, camera, IMU, LiDAR topics discovered at startup
  • Camera streaming — RealSense / USB camera auto-detect, on-demand JPEG streaming
  • SLAM mapping — Trajectory collection (5Hz, 5cm spacing) + WiFi fingerprint auto-collection
  • WiFi fingerprinting — Save/localize/list fingerprints with mobile AP blocklist
  • Plugin architecture — Optional AMR Skills for 54+ robot commands
  • Auto-reconnect — WebSocket + MQTT reconnection with exponential backoff
  • Simulator mode — Works without ROS2 for development/testing
  • systemd ready — Auto-restart, boot start with linger
curl -sL "https://ai-teammate.net/api/b2b/install/MY-ROBOT-01?key=YOUR_API_KEY" | bash

This single command will:

  1. Detect ROS2 and auto-configure topics
  2. Install bridge from PyPI + AMR skills (pre-built binary)
  3. Configure MQTT telemetry
  4. Register device with cloud gateway
  5. Create systemd service (auto-restart + boot start)

Get your API key from Control Tower > Devices > + button.

Step-by-Step Setup Guide

Prerequisites

  • Robot PC: Ubuntu 22.04 + ROS2 Humble (or Jazzy)
  • Robot hardware bringup running (motors, LiDAR, IMU topics publishing)
  • Internet connection (WSS + MQTT to ai-teammate.net)

Step 1. Get API Key

  1. Open Control Tower
  2. Go to Devices tab > click + button
  3. Enter a Device ID (e.g. my-robot-01) and copy the generated API Key (dk_...)

Step 2. Run Install Script

SSH into the robot and run:

curl -sL "https://ai-teammate.net/api/b2b/install/my-robot-01?key=dk_YOUR_API_KEY" | bash

The script automatically:

  • Detects ROS2 and discovers topics (cmd_vel, odom, imu, lidar, camera)
  • Installs bridge from PyPI + optional AMR skills binary
  • Generates .env and sensors.yaml with auto-detected topic names
  • Configures MQTT telemetry
  • Registers device with cloud gateway
  • Sets up sudoers for shutdown/reboot (NOPASSWD)
  • Creates systemd service with boot-start enabled

For networks with a proxy:

curl -sL "..." | bash -s -- --proxy=http://proxy:8080

Step 3. Verify Installation

# Check service status
systemctl --user status ai-teammate-bridge

# Watch live logs
tail -f ~/ai-teammate-bridge/bridge.log

# Review detected config
cat ~/ai-teammate-bridge/.env
cat ~/ai-teammate-bridge/sensors.yaml

Step 4. Verify in Control Tower

  1. Open Control Tower > Devices tab
  2. Robot should show as connected with green indicator
  3. Click the robot > Health section should show IMU, odom, lidar as OK
  4. SLAM tab should display sensor data

Troubleshooting

Symptom Cause Fix
sensor: no data (never received) Topic name mismatch Run ros2 topic list to find actual names, update ~/ai-teammate-bridge/.env, then systemctl --user restart ai-teammate-bridge
SHUTDOWN/REBOOT not working Missing sudoers echo "$USER ALL=(ALL) NOPASSWD: /usr/sbin/shutdown, /usr/sbin/reboot" | sudo tee /etc/sudoers.d/ai-teammate-bridge && sudo chmod 440 /etc/sudoers.d/ai-teammate-bridge
Bridge not starting after reboot Linger not enabled loginctl enable-linger $USER
WS connection failed Firewall/proxy Test with curl https://ai-teammate.net/api/health, use --proxy flag if needed
ros2: command not found in service ROS2 env not sourced Ensure /etc/env.sh exists and sources ROS2 setup, or add source /opt/ros/humble/setup.bash to service ExecStart

Manual Topic Override

If auto-detection picks the wrong topic:

# Check what's actually publishing
ros2 topic list | grep -E 'odom|imu|scan|cmd_vel'

# Edit .env
vi ~/ai-teammate-bridge/.env
# Example: ODOM_TOPIC=/base_controller/odom

# Restart
systemctl --user restart ai-teammate-bridge

Common non-standard topic names:

Sensor Standard Variants
Odometry /odom /base_controller/odom, /odometry/filtered, /wheel/odometry
IMU /imu/data /imu/data_raw, /camera/imu, /front_lidar/imu
LiDAR /scan /scan_filtered, /front_lidar/scan
cmd_vel /cmd_vel /base_controller/cmd_vel_unstamped, /cmd_vel_mux/input/teleop

Manual Install

pip3 install ai-teammate-ros2-bridge

Configuration

mkdir -p ~/ai-teammate-bridge && cd ~/ai-teammate-bridge

ai-teammate-bridge init
# → Creates .env with interactive prompts

# Or manually:
cat > .env << EOF
DEVICE_ID=my-robot-01
GATEWAY_URL=wss://ai-teammate.net/gw
API_KEY=dk_YOUR_API_KEY
CONNECTION_MODE=rclpy
STATUS_REPORT_INTERVAL=1.0
CMD_VEL_TOPIC=/cmd_vel
IMU_TOPIC=/imu/data
ODOM_TOPIC=/odom
SCAN_TOPIC=/scan
MQTT_BROKER=ai-teammate.net
MQTT_PORT=1883
# MQTT credentials: provisioned by the one-line installer
# (or ask ops). Never commit real credentials to docs.
MQTT_USER=<ask-ops>
MQTT_PASS=<ask-ops>
EOF

Run

# Source ROS2 environment first
source /opt/ros/humble/setup.bash

ai-teammate-bridge

Environment Variables

Variable Default Description
DEVICE_ID required Unique device identifier
GATEWAY_URL required Device Gateway WebSocket URL
API_KEY required Device API key (dk_xxx)
CONNECTION_MODE rclpy rclpy for ROS2, simulator for testing
STATUS_REPORT_INTERVAL 1.0 Sensor update interval (seconds)
CMD_VEL_TOPIC /cmd_vel ROS2 velocity command topic
IMU_TOPIC /imu/data IMU subscription topic
ODOM_TOPIC /odom Odometry subscription topic
SCAN_TOPIC /scan LiDAR scan topic
MQTT_BROKER (empty=disabled) MQTT broker hostname
MQTT_PORT 1883 MQTT broker port
MQTT_USER robot_bridge MQTT username
MQTT_PASS (empty) MQTT password
MQTT_TRANSPORT tcp tcp or websockets
MQTT_TLS (off) 1 to wrap the connection in TLS
MQTT_WS_PATH /mqtt WebSocket path (only for websockets)
COORD_ENABLED (off) 1/true/yes/on to obey fleet coordination hold leases
COORD_DEGRADED_SPEED 0.15 Forward cap (m/s) while no valid lease is held
FLEET_TENANT (empty) Tenant namespace for the coordination topic — not the same as FLEET_ID, see below
FLEET_ID (from gateway) Fleet id for the broadcast e-stop topic — not the same as FLEET_TENANT, see below
MAP_INSTALL_ROOT ~/maps/installed Where MAP_INSTALL keeps installed map versions, the current/previous pointers and the receipt journal. Commands cannot override it
MAP_INSTALL_ALLOWED_HOST_SUFFIXES (empty = any https host) Comma-separated host suffixes the map bundle may be downloaded from (e.g. .amazonaws.com). IP-literal hosts, localhost, non-https and redirects are always refused

Fleet coordination (교차로 hold lease)

Off by default. When COORD_ENABLED is on, the bridge subscribes to fleet/<FLEET_TENANT>/coordination/hold/<DEVICE_ID> and turns the leases it receives into one forward-speed cap, applied both in-process and in the Nav2 cmd_vel relay.

This is traffic management, not safety — the edge hard-stop (safety.py) runs after it and is never weakened by it. Three states: a live clear lease uncaps, a live hold lease stops the robot, and no valid lease means COORD_DEGRADED_SPEED — "if you don't know, go slow". That fallback is deliberately not a full stop, so a dead cloud cannot brick the fleet; values of 0 or below are rejected at startup (logged, replaced by the default).

FLEET_TENANT must match what the coordination service publishes under, or the robot never receives its leases and stays capped at the degraded speed.

FLEET_TENANT and FLEET_ID are different identifiers. They look alike, they both namespace a fleet/... topic, and setting one to the other's value fails in a way that is hard to trace:

topic carries if wrong
FLEET_TENANT fleet/<tenant>/coordination/hold/<DEVICE_ID> traffic (hold/clear leases) robot silently never receives a lease and just crawls at COORD_DEGRADED_SPEEDno error anywhere
FLEET_ID fleet/<id>/broadcast/stop emergency stop robot does not receive broadcast e-stop

They also default differently. FLEET_ID falls back to the gateway-supplied project_id/fleet_id (ws_client._subscribe_fleet_topic), so it usually works without being set — which is exactly why someone reaches for that same value when filling in FLEET_TENANT. FLEET_TENANT has no fallback: unset means the legacy un-namespaced topic, and any non-matching value means silence. Coordination silence is never read as permission (that is the point of the degraded fallback), so the symptom is a fleet that mysteriously crawls, not one that errors.

Release ordering — bridge before skills. Robots update the skills package by extracting a .so tarball straight into site-packages, which bypasses pip entirely, so the bridge floor declared in ai-teammate-ros2-skills' pyproject.toml is not enforced on the robot. A skills build that imports a symbol this bridge lacks fails at import, and the failure disables every skill command, not just the new one. So: publish and roll out the bridge first, confirm robots picked it up, and only then publish skills. The auto-updater enforces the same order per cycle (_defer_skills_update), and skills carries a fallback for the coordination import specifically, but neither removes the need to ship in this order.

MQTT over WebSocket (broker behind a CDN)

Raw TCP MQTT cannot be proxied by a CDN, so a broker that sits behind one is only reachable over wss on 443. Point the bridge at it like this — the robot then needs a single outbound 443, which most site firewalls already allow:

MQTT_BROKER=mqtt.example.com
MQTT_PORT=443
MQTT_TRANSPORT=websockets
MQTT_TLS=1

The default stays plain TCP on 1883, so bridges already in the field keep working until their env is changed.

Architecture

Robot                              Cloud (ai-teammate.net)
┌──────────────────────┐          ┌─────────────────────┐
│  ai-teammate-bridge  │          │  Device Gateway      │
��  (CLI entry point)   │          │  (port 8003)         │
│                      │          │                      │
│  ���────────────────┐  │  WSS    │  /ws/{device_id}     │
│  │  ws_client.py  │──┼────────▶│    commands ◀──────  │──▶ Control Tower
│  │  DeviceBridge   │◀─┼────────│    responses          │
│  └────────────────┘  │          │                      │
│                      │  MQTT   │  Mosquitto (1883)    │
│  ┌────────────────┐  │────────▶│    telemetry          │
│  │  MQTT publisher │  │         │    position           │
│  └────────────────┘  │         │    health              │
│                      │          └─────────────────────┘
│  ┌────────────────┐  │
│  │  ros2_node.py  │  │  ROS2 DDS (no rosbridge)
│  │  ├─ cmd_vel pub│  │
│  │  ├─ IMU sub    │  │
│  │  ├─ odom sub   │  │
│  │  ├─ scan sub   │  │
│  │  ├─ camera sub │  │  on-demand (auto-stop 30s)
│  │  └─ TF listener│  │  map→base_footprint (5Hz)
│  └────────────────┘  │
│                      │
│  ┌────────────────┐  │
│  │  Skills (opt)  │  │  ai-teammate-ros2-skills
│  │  54+ commands  │  │  (pre-built .so binary)
│  └────────────────┘  │
└──────────────────────┘

Communication Channels

Channel Data Direction Frequency
WebSocket Commands, responses Bidirectional On-demand
WebSocket Camera frames Robot → Cloud 2-5 FPS (on-demand)
WebSocket SLAM map + trajectory Robot → Cloud Every 5s during mapping
MQTT Telemetry (IMU, battery) Robot → Cloud 1 Hz
MQTT Position (x, y, theta) Robot → Cloud 1 Hz (retained)
MQTT Health alerts Robot → Cloud On-event (retained)

Package Structure

ai_teammate_ros2_bridge/
├── cli.py            # Entry point: ai-teammate-bridge command
├── config.py         # Shared state (_ros2_cache), env loading, MQTT config
├── ws_client.py      # DeviceBridge class, WS + MQTT event loop, health checks
├── device_bridge.py  # Command handlers (SLAM, camera, nav, fingerprint, etc.)
├── ros2_node.py      # rclpy spin thread, ROS2 subscriptions, TF listener
├── sensors.py        # Sensor data aggregation, battery fallback
├── sensors_config.py # Hardware abstraction (sensors.yaml)
├── lan_server.py     # LAN WebSocket server for local clients
└── utils.py          # Input sanitization helpers

Systemd Service

The install script creates a systemd service automatically. For user-level services, the unit uses After=basic.target (not network-online.target, which is unavailable in user scope).

# System-level (if sudo available)
sudo systemctl status ai-teammate-bridge
sudo systemctl restart ai-teammate-bridge
sudo journalctl -u ai-teammate-bridge -f

# User-level (no sudo, with loginctl enable-linger)
systemctl --user status ai-teammate-bridge
systemctl --user restart ai-teammate-bridge

Command Dispatch

Commands from the cloud are dispatched in two tiers:

  1. Bridge-first commands — Camera commands (CAPTURE_IMAGE, CAMERA_CAPTURE, START_CAMERA_STREAM, STOP_CAMERA_STREAM) always go to device_bridge.py first because they require access to the shared config._ros2_cache. GET_STATUS, UPDATE_BRIDGE, MAP_INSTALL and MAP_INSTALL_STATUS are bridge-first too: the bridge owns that state, and a same-named handler in the skills plugin would silently shadow it.
  2. Skills-first — All other commands try the optional ai-teammate-ros2-skills plugin first. If the plugin returns unknown_command, the bridge falls back to device_bridge.py.

SLAM Commands

Command Description
SLAM_START Launch slam_toolbox, begin trajectory + WiFi fingerprint collection
SLAM_STOP Stop SLAM, return trajectory/fingerprint counts
SLAM_SAVE Save map, auto-link fingerprints to nearby locations, detect mobile APs

During SLAM mapping, the bridge automatically:

  • Collects trajectory points at 5Hz with 5cm minimum spacing (via TF listener)
  • Scans WiFi fingerprints every 5s with 50cm minimum displacement
  • On SLAM_SAVE: links fingerprints to saved locations within 1.5m as "predicted" source
  • Detects mobile APs (same BSSID at distant locations) and adds them to wifi_blocklist.json

WiFi Fingerprint Commands

Command Description
SAVE_FINGERPRINT Save WiFi scan at current pose. source: verified (default) or predicted
LIST_FINGERPRINTS List all saved fingerprints with pose and AP count
LOCALIZE_FINGERPRINT Match current WiFi scan against saved fingerprints (filters blocked BSSIDs)

Location Commands

Command Description
SAVE_LOCATION Save named location. Supports explicit x, y params (map pin) or uses current robot pose

Map Install Commands

Command Description
MAP_INSTALL Install a published map version. Params: installation (the map-api MapInstallationResponse, verbatim) and raw_artifact (the manifest's raw_artifact, verbatim). Returns immediately with state: accepted and runs in the background
MAP_INSTALL_STATUS The active installed map, the last attempt (phase, outcome, error_code, receipt) and the number of receipts still waiting to be delivered

MAP_INSTALL downloads the bundle from the signed https URL, verifies its SHA-256 against the installation target, unpacks it into ~/maps/installed/versions/<sha256>/, and atomically switches the current pointer. It refuses to run while Nav2 is active. Every attempt that starts ends with exactly one receipt, posted to the gateway with the device API key (the gateway signs it for map-api); undelivered receipts are journaled and retried across restarts. NAV2_START without a map parameter picks whichever map action happened last — the installed map (time of the current pointer) or the newest .yaml saved under ~/maps (e.g. by SLAM_SAVE); ties go to the installed map — and reports what it picked in map / map_source.

Re-issuing MAP_INSTALL for the installation that is already running answers state: running with the same attempt_id (the phone's retry does not start a second attempt); a different installation while one is running is refused with install_busy. A request is also refused before any attempt starts — so without a receipt — when the bundle is not mobio-ros-map-bundle-v1 (unsupported_format), when byte_length is above the 256 MiB cap (bundle_too_large), when the manifest checksum differs from the installation target (target_mismatch), when the signed URL is not an acceptable https address (url_rejected) or has already expired (url_expired), when the installation targets another robot (wrong_device), or while Nav2 is running (nav2_active).

The install only switches the pointer — relocalization is not measured, so receipts always carry relocalization_result: "not_attempted".

The bridge installs no SIGTERM handler, so a restart during an install — including one triggered by UPDATE_BRIDGE or the auto-updater — kills the attempt where it stands. The next boot closes it from the journal as interrupted (or as installed, if the current pointer had already been switched) and sends that receipt. Nothing is left half-installed, but the map is not installed either: re-issue the command.

Camera (On-Demand)

Camera subscriptions are created on-demand and auto-stop after 30s of inactivity to save CPU (~10% reduction). Camera health is excluded from the all_ok flag and alert system — it only reports status when actively streaming.

Capture flow: start_camera() is always called first, then a 10s wait for a fresh frame before capturing.

Supported Platforms

Platform Architecture Python ROS2
Ubuntu 20.04+ x86_64 3.10, 3.12 Humble, Jazzy
Ubuntu 20.04+ aarch64 (Jetson, RPi4+) 3.10, 3.12 Humble, Jazzy
Any Linux x86_64/aarch64 3.10+ Simulator mode (no ROS2)

Requirements

  • Python 3.10+
  • ROS2 Humble or Jazzy (optional — simulator mode works without)
  • Internet access to ai-teammate.net (WSS + MQTT)

Changelog (v2.6.14 — v2.6.24)

v2.6.24

  • feat(radio): Mobile AP detection — same BSSID seen at distant locations during SLAM is auto-blocklisted in wifi_blocklist.json
  • feat(place): SAVE_LOCATION supports explicit x, y coordinates (map pin placement vs robot pose)
  • feat(place): SLAM_SAVE auto-links collected fingerprints to nearby saved locations as "predicted" source

v2.6.23

  • feat(slam): Auto-collect WiFi fingerprints during SLAM mapping (5s interval, 50cm min displacement)

v2.6.22

  • feat(slam): Collect trajectory during mapping via TF listener (5Hz, 5cm spacing), include in map upload metadata

v2.6.21

  • fix(slam): Send initial SLAM map on WS connect regardless of age; flag-based dedup instead of age-only check

v2.6.20

  • fix(stream): camera_stream and follow_state use shared config._ros2_cache instead of local cache

v2.6.19

  • fix(camera): Route camera commands (CAPTURE_IMAGE, CAMERA_CAPTURE, START_CAMERA_STREAM, STOP_CAMERA_STREAM) to bridge first, skip skills — camera needs shared config cache

v2.6.18

  • fix(camera): Always call start_camera() + 10s wait for fresh frame before capture

v2.6.17

  • fix(camera): Use shared config._ros2_cache for CAPTURE_IMAGE instead of local cache

v2.6.16

  • fix(camera): Check frame presence instead of _camera_active flag for capture readiness

v2.6.15

  • fix(camera): Improved capture_image — check start_camera result, 5s wait, better logging

v2.6.14

  • fix(health): Camera (on-demand) excluded from all_ok and health alerts — only reports when active

License

MIT - MobioLabs

Release files for ai-teammate-ros2-bridge 2.6.62

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ai-teammate-ros2-bridge 2.6.62
File Size Uploaded
ai_teammate_ros2_bridge-2.6.62.tar.gz 221.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ai-teammate-ros2-bridge 2.6.62
File Interpreter ABI Platform
ai_teammate_ros2_bridge-2.6.62-py3-none-any.whl Python 3 none any Details

Total release size: 364.1 kB

Release files / ai_teammate_ros2_bridge-2.6.62.tar.gz

Download URL ai_teammate_ros2_bridge-2.6.62.tar.gz
Size 221.5 kB
Tags Source
SHA-256 checksum
How to use checksums
d910111f9aa824278614198df3a7392da5e1c1b5c24fcb80980a789439b1fff3
BLAKE2b-256 checksum
How to use checksums
d0e2647072775503174283fa183c772ca09d8ee654e78c9b3224c46c94066f71
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.11

Release files / ai_teammate_ros2_bridge-2.6.62-py3-none-any.whl

Download URL ai_teammate_ros2_bridge-2.6.62-py3-none-any.whl
Size 142.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f42d90be33dab1210a4b04e0cbbc42f9b704113a7bbf9ea29dc4bbbea11bfd95
BLAKE2b-256 checksum
How to use checksums
bc056823231c68ffd97c8ece4395950a6e22163d0965dc9ed6418d95157b6b01
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.11

Release history Release notifications | RSS feed

This release

2.6.62 This release

2 release files

2.6.46

2 release files

2.6.45

2 release files

2.6.44

2 release files

2.6.43

2 release files

2.6.42

2 release files

2.6.26

2 release files

2.6.23

2 release files

2.6.22

2 release files

2.6.21

2 release files

2.6.20

2 release files

2.6.19

2 release files

2.6.18

2 release files

2.6.17

2 release files

2.6.16

2 release files

2.6.15

2 release files

2.6.14

2 release files

2.6.13

2 release files

2.6.12

2 release files

2.6.11

2 release files

2.6.10

2 release files

2.6.9

2 release files

2.6.8

2 release files

2.6.7

2 release files

2.6.6

2 release files

2.6.5

1 release file

2.6.4

2 release files

2.6.3

2 release files

2.6.2

2 release files

2.6.1

2 release files

2.6.0

2 release files

2.5.2

2 release files

2.5.1

2 release files

2.5.0

2 release files

2.4.3

1 release file

2.4.2

1 release file

2.4.1

1 release file

2.4.0

1 release file

2.3.0

2 release files

2.2.0

2 release files

2.1.9

2 release files

2.1.8

2 release files

2.1.7

2 release files

2.1.6

2 release files

2.1.5

2 release files

2.1.4

2 release files

2.1.3

2 release files

2.1.2

2 release files

2.1.0

2 release files

2.0.9

2 release files

2.0.8

2 release files

2.0.6

2 release files

2.0.5

2 release files

2.0.4

2 release files

2.0.3

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page