# AI Teammate Robot Bridge

[![PyPI](https://img.shields.io/pypi/v/ai-teammate-ros2-bridge)](https://pypi.org/project/ai-teammate-ros2-bridge/)
[![Python](https://img.shields.io/pypi/pyversions/ai-teammate-ros2-bridge)](https://pypi.org/project/ai-teammate-ros2-bridge/)

Connects ROS2 robots to the [AI Teammate](https://ai-teammate.net) 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](https://ai-teammate.net) 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

## Quick Install (Recommended)

```bash
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](https://ai-teammate.net/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](https://ai-teammate.net/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:

```bash
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:
```bash
curl -sL "..." | bash -s -- --proxy=http://proxy:8080
```

### Step 3. Verify Installation

```bash
# 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](https://ai-teammate.net/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:

```bash
# 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

```bash
pip3 install ai-teammate-ros2-bridge
```

### Configuration

```bash
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_USER=robot_bridge
MQTT_PASS=mqtt_r0b0t_2026
EOF
```

### Run

```bash
# 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 |

## 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).

```bash
# 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`.
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 |

## 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](https://mobiolabs.net)
