Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions strands_robots/device_connect/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,7 @@ print(invoke_device('reachy-mini-1', 'look', {'pitch': -15, 'yaw': 30}))
print(invoke_device('reachy-mini-1', 'nod'))
"
```

For the full Raspberry Pi setup, including the working NATS sidecar pattern,
required `TENANT` / `DEVICE_ID` settings, and the "reuse the existing local
Reachy daemon" flow, see `reachy-guide.md` in this directory.
329 changes: 225 additions & 104 deletions strands_robots/device_connect/reachy-guide.md
Original file line number Diff line number Diff line change
@@ -1,165 +1,286 @@
# Reachy Mini — Cloud Zenoh Mesh Guide
# Reachy Mini — Device Connect NATS Guide

Connect a Reachy Mini Lite (USB) to the AWS-hosted Zenoh mesh via Device Connect so any agent on the mesh can discover and control it.
Connect a Reachy Mini on a Raspberry Pi to a remote Device Connect NATS broker.

## Prerequisites
This guide covers the working setup we validated on the Pi at `10.118.156.11`:

- Reachy Mini Lite plugged in via USB
- Python 3.12+, `uv` installed
- the existing Reachy browser-control demo continues to own the local robot daemon
- a separate Device Connect sidecar runs alongside it
- the sidecar bridges Reachy RPCs into Device Connect over NATS

## 1. Start the Reachy Daemon
## Where this code belongs

The daemon bridges USB serial to a local REST/WebSocket API on port 9002.
The Pi-side Device Connect bridge belongs in the `robots` repo, not in
`conference-room`.

```bash
# From the repo root (uses uv inline script, auto-installs reachy-mini)
uv run --python 3.13 --with reachy-mini start_reachy_daemon.py```
- `conference-room` owns the app-specific Reachy browser demo:
`apps/reachy_mini_browser_control/...`
- `robots` owns the reusable Strands + Device Connect Reachy integration:
`strands_robots/device_connect/reachy_mini_driver.py`
`strands_robots/device_connect/reachy_transport.py`
`strands_robots/device_connect/run_reachy_nats.py`

Expected output:
If you need to document how the local Pi demo starts Reachy, keep that in the
`conference-room` README. If you need to document how Reachy registers into
Device Connect, keep that here.

## Two valid deployment patterns

### Option A: Reuse an existing local Reachy daemon

Use this when the Pi is already running the browser-control demo or another
local Reachy app that exposes the daemon on `127.0.0.1:8000`.

This is the safest option if you do not want to disturb an existing demo.

### Option B: Start a dedicated Reachy daemon from this repo

Use this when no local Reachy daemon is already running.

That path is what Sourav described originally:

```bash
source .venv/bin/activate
python strands_robots/device_connect/start_reachy_daemon.py
```
Starting reachy-mini-daemon on /dev/cu.usbmodemXXXX (API port 9002)
```

Leave this running in a separate terminal.
It starts a local daemon on port `9002` and is fine for a standalone setup.

## Recommended setup on a Pi with an existing Reachy demo

## 2. Clone and Set Up the Strands Robots SDK
### 1. Prepare the `robots` repo

```bash
git clone --branch feat/device-connect-integration-draft \
https://github.com/atsyplikhin/robots.git
cd robots
./strands_robots/device_connect/setup.sh
source .venv/bin/activate
uv pip install websockets # required dependency not yet in setup
uv pip install websockets
```

## 3. Connect to the Cloud Zenoh Mesh
### 2. Verify the local Reachy daemon

Set environment variables (every terminal that talks to the mesh):
If the Pi already runs `reachy_mini_browser_control`, verify that the local
daemon is healthy:

```bash
export ZENOH_CONNECT=tcp/zenoh-nlb-2cb0b84309701828.elb.us-east-1.amazonaws.com:7447
export ZENOH_MODE=client
curl http://127.0.0.1:8000/api/daemon/status
```

Expected output includes:

```json
{
"state": "running",
"wireless_version": false
}
```

If nothing is listening on port `8000`, use **Option B** above to start a
dedicated daemon on `9002` instead.

### 3. Start the Device Connect sidecar

If you already have a daemon on `127.0.0.1:8000`, run the sidecar against that
daemon:

```bash
cd /path/to/robots
source .venv/bin/activate

export MESSAGING_BACKEND=nats
export NATS_URL='nats://nats-nlb-52ebd3849ae4cb02.elb.us-east-1.amazonaws.com:4222'
export MESSAGING_URLS="$NATS_URL"
export DEVICE_CONNECT_ALLOW_INSECURE=true

python strands_robots/device_connect/run_reachy_nats.py
```

Start the Device Connect runtime (bridges the local daemon to the cloud mesh):
### Configuration requirements

The exact required settings depend on which broker you are trying to join.

#### Public broker

This configuration is enough for the public broker:

```bash
export MESSAGING_BACKEND=nats
export NATS_URL='nats://nats-nlb-52ebd3849ae4cb02.elb.us-east-1.amazonaws.com:4222'
export MESSAGING_URLS="$NATS_URL"
export DEVICE_CONNECT_ALLOW_INSECURE=true
```

#### Private broker / Device Connect dashboard

For the private broker that backs the Device Connect dashboard, the working
configuration must include these parameters:

```bash
export MESSAGING_BACKEND=nats
export NATS_URL='nats://137.184.86.16:4222'
export MESSAGING_URLS="$NATS_URL"
export TENANT='souravpati'
export DEVICE_ID='reachy-mini-1'
export NATS_CREDENTIALS_FILE='/path/to/souravpati-reachy-mini-1.creds.json'
export DEVICE_CONNECT_ALLOW_INSECURE=true
export REACHY_HOST='127.0.0.1'
export REACHY_PORT='8000'
export REACHY_TRANSPORT_MODE='websocket'
```

The critical piece is `TENANT='souravpati'`. Without that, the same device ID
and credentials connect to the private broker but fail to register on the
expected Device Connect subjects.

By default, the runner uses:

- `TENANT=default`
- `DEVICE_ID=reachy-mini-1`
- `REACHY_HOST=127.0.0.1`
- `REACHY_PORT=8000`
- `REACHY_TRANSPORT_MODE=websocket`

You can override them if needed:

```bash
export TENANT='souravpati'
export DEVICE_ID='reachy-mini-1'
export REACHY_HOST='127.0.0.1'
export REACHY_PORT='8000'
export REACHY_TRANSPORT_MODE='websocket'
export NATS_CREDENTIALS_FILE='/path/to/souravpati-reachy-mini-1.creds.json'
python strands_robots/device_connect/run_reachy_nats.py
```

Expected successful logs:

```text
INFO - Using NATS messaging backend
INFO - Connected to NATS broker: ['nats://nats-nlb-52ebd3849ae4cb02.elb.us-east-1.amazonaws.com:4222']
INFO - Driver connected: reachy_mini
INFO - Device registered: ...
INFO - Subscribed to commands on device-connect.souravpati.reachy-mini-1.cmd
```

### 4. Sourav's original direct-runtime form

If you prefer not to use the helper runner, this is the equivalent direct form.

When reusing an existing daemon on the Pi, change the original `api_port=9002`
example to `api_port=8000`:

```bash
cd /path/to/robots
source .venv/bin/activate
export MESSAGING_BACKEND=nats
export NATS_URL='nats://137.184.86.16:4222'
export MESSAGING_URLS="$NATS_URL"
export TENANT='souravpati'
export DEVICE_ID='reachy-mini-1'
export NATS_CREDENTIALS_FILE='/path/to/souravpati-reachy-mini-1.creds.json'
export DEVICE_CONNECT_ALLOW_INSECURE=true

python -c "
import asyncio, os
from strands_robots.device_connect import ReachyMiniDriver
from device_connect_sdk import DeviceRuntime

driver = ReachyMiniDriver(host='localhost', api_port=9002)
driver = ReachyMiniDriver(
host='localhost',
api_port=8000,
transport_mode='websocket',
)
runtime = DeviceRuntime(
driver=driver,
device_id='reachy-mini-1',
messaging_urls=[os.environ['ZENOH_CONNECT']],
device_id=os.environ['DEVICE_ID'],
tenant=os.environ['TENANT'],
messaging_urls=[os.environ['NATS_URL']],
messaging_backend='nats',
nats_credentials_file=os.environ['NATS_CREDENTIALS_FILE'],
allow_insecure=True,
)
asyncio.run(runtime.run())
"
```

Expected output:

```
INFO - Using ZENOH messaging backend
INFO - Connected to ZENOH broker: ['tcp/zenoh-nlb-...amazonaws.com:7447']
INFO - Driver connected: reachy_mini
INFO - Device registered: registration_id=...
INFO - Subscribed to commands on device-connect.default.reachy-mini-1.cmd
```

Leave this running. The robot is now on the mesh as `reachy-mini-1`.
Use `api_port=9002` only if you started the dedicated daemon from
`start_reachy_daemon.py`.

## 4. Invoke Commands from Any Mesh Client
## Verify from a client

From another terminal (with the same env vars and venv activated):
From another terminal:

```bash
source robots/.venv/bin/activate
export ZENOH_CONNECT=tcp/zenoh-nlb-2cb0b84309701828.elb.us-east-1.amazonaws.com:7447
export ZENOH_MODE=client
cd /path/to/robots
source .venv/bin/activate
export MESSAGING_BACKEND=nats
export NATS_URL='nats://137.184.86.16:4222'
export MESSAGING_URLS="$NATS_URL"
export TENANT='souravpati'
export DEVICE_CONNECT_ALLOW_INSECURE=true
```

### Move antennas

```python
from device_connect_agent_tools import connect, invoke_device
connect()
r = invoke_device('reachy-mini-1', 'antennas', {'left': 30, 'right': -30})
print('RESULT:', r)
# {'success': True, 'result': {'status': 'success', 'left': 30, 'right': -30}}
```

### Look (head pose)

```python
invoke_device('reachy-mini-1', 'look', {'pitch': -15, 'yaw': 15, 'roll': 0})
# pitch: up/down (negative = look up), yaw: left/right, roll: tilt
```
Discover devices:

### Expressions
```bash
python -c "
from device_connect_agent_tools import connect, discover_devices
from device_connect_agent_tools.connection import disconnect

```python
invoke_device('reachy-mini-1', 'nod') # yes gesture
invoke_device('reachy-mini-1', 'shake') # no gesture
invoke_device('reachy-mini-1', 'happy') # antenna wiggle
connect()
try:
print(discover_devices())
finally:
disconnect()
"
```

### Sequence example
Invoke RPCs:

```python
```bash
python -c "
from device_connect_agent_tools import connect, invoke_device
import time
from device_connect_agent_tools.connection import disconnect

connect()
invoke_device('reachy-mini-1', 'look', {'pitch': -10, 'yaw': 15})
time.sleep(1)
invoke_device('reachy-mini-1', 'nod')
time.sleep(2)
invoke_device('reachy-mini-1', 'look', {'pitch': 0, 'yaw': 0, 'roll': 0})
print('Done!')
try:
print(invoke_device('reachy-mini-1', 'look', {'pitch': -15, 'yaw': 15}))
print(invoke_device('reachy-mini-1', 'nod'))
finally:
disconnect()
"
```

## Available RPCs

| RPC | Parameters | Description |
|-----|-----------|-------------|
| `look` | `pitch`, `roll`, `yaw`, `x`, `y`, `z` | Set head pose (degrees / mm) |
| `antennas` | `left`, `right` | Set antenna angles (degrees) |
| `body` | `yaw` | Set body yaw (degrees) |
| `nod` | — | Yes gesture |
| `shake` | — | No gesture |
| `happy` | — | Antenna wiggle |
| `getJoints` | — | Current joint positions |
| `getImu` | — | IMU sensor data |
| `enableMotors` | `motor_ids` (optional) | Torque on |
| `disableMotors` | `motor_ids` (optional) | Torque off |
| `wakeUp` | — | Enable motors + wake animation |
| `sleep` | — | Sleep animation + disable motors |
| `stopMotion` | — | Stop all motion |
| `getDaemonStatus` | — | Daemon status and motor state |
| `playMove` | `move_name`, `library` | Play recorded move (`emotions` or `dance`) |
| `listMoves` | `library` | List available moves |

## Process Summary

You need **three terminals**:

| Terminal | Command | Purpose |
|----------|---------|---------|
| 1 | `python start_reachy_daemon.py` | USB serial daemon (port 9002) |
| 2 | Device Connect runtime script (step 3) | Bridges daemon to cloud Zenoh mesh |
| 3 | `invoke_device(...)` calls (step 4) | Send commands to the robot |
## File inventory

These are the reusable Reachy Device Connect files in this repo:

- `strands_robots/device_connect/reachy_mini_driver.py`
Reachy-specific Device Connect RPC driver
- `strands_robots/device_connect/reachy_transport.py`
REST/WebSocket and Zenoh transport helpers
- `strands_robots/device_connect/run_reachy_nats.py`
Small sidecar runner for NATS registration
- `strands_robots/device_connect/start_reachy_daemon.py`
Helper for standalone daemon startup when you are not reusing an existing Pi
daemon

## Troubleshooting

- **`No USB serial device found`** — Check that Reachy is plugged in (`ls /dev/cu.usbmodem*`)
- **`ModuleNotFoundError: websockets`** — Run `uv pip install websockets`
- **`KeyboardInterrupt` on import** — The `cv2` import can hang; make sure the daemon terminal is separate from the Device Connect terminal
- **Connection refused on port 9002** — Daemon not running; start it first (step 1)
- `Authorization Violation`
The NATS credentials are valid enough to authenticate, but not valid for the
broker you are pointing at.
- `permissions violation for publish to "device-connect.default.registry"`
The credentials authenticate, but the broker ACLs do not allow Device Connect
registration.
- Connects but registers under the wrong tenant
For the private broker/dashboard path, set `TENANT='souravpati'`.
- `Connection refused` on port `8000`
No local Reachy daemon is running there. Either start the browser-control demo
or run `start_reachy_daemon.py` and point the sidecar at `9002`.
- `No USB serial device found`
Only relevant when starting a dedicated daemon directly from this repo.
- `ModuleNotFoundError: websockets`
Run `uv pip install websockets`.
Loading