File size: 8,651 Bytes
8b7ec50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# Show Me β€” Developer Guide

## Quick Start

```bash
cd show-me

# Install JS dependencies
npm install

# Start the simulated robot (needs Python venv with reachy-mini)
.venv/bin/python launch_sim_webrtc.py

# In another terminal, start the dev server
npm run dev
```

Open `http://localhost:5174` β€” click Connect to see SMPTE test bars from the sim.

To connect to a real robot instead, change `ROBOT_HOST` in `src/App.svelte` to `"reachy-mini.local"`.

## Project Structure

```
show-me/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ main.js                          # Mounts Svelte app
β”‚   β”œβ”€β”€ App.svelte                       # Root layout, creates robot store
β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”œβ”€β”€ robot/
β”‚   β”‚   β”‚   β”œβ”€β”€ connection.js            # ReachyMini class (pure JS, no Svelte)
β”‚   β”‚   β”‚   └── commands.js              # High-level command helpers
β”‚   β”‚   └── stores/
β”‚   β”‚       └── robot.svelte.js          # Svelte 5 runes reactive adapter
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ VideoFeed.svelte             # <video> element bound to stream
β”‚   β”‚   └── Controls.svelte              # Connect, Wake Up, Sleep, State viewer
β”‚   └── styles/
β”‚       └── global.css                   # Dark theme, full-viewport reset
β”œβ”€β”€ vendor/
β”‚   └── gstwebrtc-api-3.0.0.tgz         # GStreamer WebRTC JS lib (built from source)
β”œβ”€β”€ patches/
β”‚   └── fix_webrtc_asyncio.py            # Fix for robot daemon async commands
β”œβ”€β”€ docs/                                # Documentation
β”œβ”€β”€ launch_sim_webrtc.py                 # Desktop simulation launcher
β”œβ”€β”€ index.html                           # Entry point
β”œβ”€β”€ package.json
β”œβ”€β”€ vite.config.js
└── svelte.config.js
```

### Architecture Layers

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Svelte Components (VideoFeed, Controls, App)       β”‚  UI β€” reads from store
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  robot.svelte.js β€” Svelte 5 reactive adapter        β”‚  Glue β€” $state() runes
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  connection.js + commands.js β€” pure JS              β”‚  Robot β€” framework-agnostic
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  gstwebrtc-api v3 β€” WebRTC signaling + media        β”‚  Transport
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

The robot layer (`src/lib/robot/`) has **zero** Svelte imports. It extends `EventTarget` and emits standard events. This is intentional β€” it's designed for future extraction into an `npm` package (`reachy-mini-js`).

The store layer (`src/lib/stores/`) is a thin adapter that wires robot events to Svelte 5 `$state()` runes.

## Key Technical Details

### gstwebrtc-api v3

Built from [gst-plugins-rs](https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs) `net/webrtc/gstwebrtc-api/`. Stored as `vendor/gstwebrtc-api-3.0.0.tgz`.

**v3 API patterns (different from v2):**

```javascript
// Discovery β€” callback-based, NOT addEventListener
api.registerPeerListener({
  producerAdded(producer) { /* producer.id, producer.meta */ },
  producerRemoved(producer) { },
});

// Connection listener
api.registerConnectionListener({
  connected(clientId) { },
  disconnected() { },
});

// Consumer session
const session = api.createConsumerSession(producerId);
session.addEventListener("streamsChanged", () => { /* session.streams */ });
session.addEventListener("stateChanged", () => { /* session.state: 0-3 */ });
session.connect();

// Session states: 0=idle, 1=connecting, 2=streaming, 3=closed
```

**Data channel caveat:** The library only handles channels named `"control"` internally. The robot creates a channel named `"data"` β€” we intercept it via the underlying `RTCPeerConnection`:

```javascript
session.addEventListener("rtcPeerConnectionChanged", () => {
  const pc = session.rtcPeerConnection;
  pc.addEventListener("datachannel", (event) => {
    if (event.channel.label === "data") { /* use it */ }
  });
});
```

### Data Channel Protocol

Commands are JSON objects with a single top-level key. Responses include `"status"`, `"error"`, or `"command"` keys.

| Command | Example | Async? |
|---|---|---|
| `get_state` | `{ "get_state": true }` | No |
| `set_target` | `{ "set_target": [[4x4 matrix]] }` | No |
| `set_antennas` | `{ "set_antennas": [right, left] }` | No |
| `set_body_yaw` | `{ "set_body_yaw": 0.5 }` | No |
| `goto_target` | `{ "goto_target": { head, antennas, duration, body_yaw } }` | Yes |
| `wake_up` | `{ "wake_up": true }` | Yes |
| `goto_sleep` | `{ "goto_sleep": true }` | Yes |
| `play_sound` | `{ "play_sound": "file.wav" }` | No |
| `set_motor_mode` | `{ "set_motor_mode": "enabled" }` | No |
| `get_motor_mode` | `{ "get_motor_mode": true }` | No |

Async commands respond only when the motion/animation completes (up to 30s).

All angles are in **radians**. Head poses are **4x4 homogeneous matrices** (row-major).

**State poll vs command response disambiguation:** State poll responses only have a `"state"` key. Command responses have `"status"`, `"error"`, or `"command"` keys. The message handler in `connection.js` uses this to avoid state polls accidentally resolving command promises (FIFO queue).

### Robot Daemon Bug: GStreamer Thread vs asyncio

The daemon's data channel callback fires on a GStreamer thread, not the asyncio event loop. Synchronous commands work fine, but async ones (`wake_up`, `goto_sleep`, `goto_target`) call `asyncio.create_task()` which fails with "no running event loop".

**Fix:** `patches/fix_webrtc_asyncio.py` β€” creates a dedicated `asyncio.new_event_loop()` on a background thread and dispatches all data channel messages there via `call_soon_threadsafe()`.

This patch is applied automatically by `launch_sim_webrtc.py` for local simulation. For a real robot, copy the patch and apply it before the daemon starts (see instructions in the patch file).

The bug exists in reachy-mini v1.3.1. The command handler is in `reachy_mini/daemon/backend/abstract.py` β†’ `_process_webrtc_command()` (line ~874).

### Simulation

`launch_sim_webrtc.py` monkey-patches the daemon to replace hardware-specific GStreamer elements:

| Original (RPi) | Patched (desktop) | Purpose |
|---|---|---|
| `libcamerasrc` | `videotestsrc` (SMPTE bars) | Camera |
| `v4l2h264enc` | handled by `webrtcsink` | Encoding |
| `alsasrc` | `audiotestsrc` (silence) | Microphone |
| `alsasink` | skipped | Speaker |

Requires: Python 3.10+, GStreamer with Rust plugins (`brew install gstreamer` on macOS), and `reachy-mini[gstreamer]` package.

The sim exposes:
- `http://localhost:8000` β€” REST API + dashboard
- `ws://localhost:8443` β€” WebRTC signaling

## Build

```bash
npm run build    # β†’ dist/ (static output, ~135KB JS + 2KB CSS)
npm run preview  # preview the built app
```

## What's Done (V0)

- Svelte 5 + Vite 7 scaffold
- gstwebrtc-api v3 integration (built from source)
- WebRTC connection with producer discovery
- Video streaming display (full-viewport, mobile-first)
- Data channel for robot commands
- State polling (500ms) with reactive updates
- Basic controls: Connect, Disconnect, Wake Up, Sleep
- Robot state JSON viewer
- Desktop simulation support
- Daemon asyncio bug fix

## What's Next (V1+)

Per the brainstorm (`docs/brainstorm.md`), the roadmap includes:

- **AI models** β€” STT (Whisper via transformers.js), LLM (SmolLM2), TTS (Web Speech API / Kokoro), Vision (YOLO)
- **Tools system** β€” LLM-callable tools (`src/lib/tools/`) for object detection, web search, robot control
- **"Show Me" interaction** β€” the core UX where the robot directs what's on screen
- **Canvas overlay** β€” AR annotations on the video feed (`<canvas>` over `<video>`)
- **Web content display** β€” embedded videos, images, cards
- **Expressive behavior** β€” head tracking, gestures, conversational feedback
- **HF Spaces deployment** β€” static SDK, auto-build on push
- **`reachy-mini-js` npm package** β€” extract `src/lib/robot/` into standalone package