robopark 2.8.35 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -63
- package/bin/robopark.js +7 -17
- package/conversation/elevenlabs_agent.py +1985 -0
- package/conversation/requirements.txt +3 -0
- package/conversation/supervisor_store.py +189 -0
- package/dist/kernel/config-schema.js +37 -0
- package/dist/kernel/types.js +7 -0
- package/dist/robopark/access.js +99 -0
- package/dist/robopark/add-robot.js +188 -0
- package/dist/robopark/agent-ctl.js +305 -0
- package/dist/robopark/auto-start.js +289 -0
- package/dist/robopark/conversation.js +505 -0
- package/dist/robopark/deployment-commands.js +47 -0
- package/dist/robopark/discovery.js +180 -0
- package/dist/robopark/doctor.js +175 -0
- package/dist/robopark/enroll.js +68 -0
- package/dist/robopark/llm-set.js +87 -0
- package/dist/robopark/motor-control.js +195 -0
- package/dist/robopark/preview-agent-launcher.js +77 -0
- package/dist/robopark/probe.js +138 -0
- package/dist/robopark/profile.js +69 -0
- package/dist/robopark/python-env.js +162 -0
- package/dist/robopark/robot-runtime.js +489 -0
- package/dist/robopark/scan.js +97 -0
- package/dist/robopark/screen-control.js +55 -0
- package/dist/robopark/secrets.js +41 -0
- package/dist/robopark/serve.js +285 -0
- package/dist/robopark/server-add.js +114 -0
- package/dist/robopark/setup-livekit.js +300 -0
- package/dist/robopark/setup.js +286 -0
- package/dist/robopark/standalone.js +466 -0
- package/dist/robopark/stop-all.js +141 -0
- package/dist/robopark/verify.js +192 -0
- package/dist/robopark/vision-agent-launcher.js +98 -0
- package/dist/robopark/vision-control.js +81 -0
- package/dist/robopark-cli.js +799 -0
- package/package.json +21 -5
- package/pi-client/_install_steps.sh +29 -29
- package/pi-client/client.py +61 -2
- package/pi-client/install.sh +40 -40
- package/pi-client/join_convo.sh +54 -54
- package/pi-client/livekit_bridge.py +16 -7
- package/pi-client/motor_bridge.py +6 -3
- package/scheduler/fleet_config.json +75 -0
- package/scheduler/main.py +4505 -135
- package/scheduler/media_lock.py +57 -0
- package/scheduler/preview_agent.py +1465 -87
- package/scheduler/production_config.json +139 -0
- package/scheduler/robot_supervisor.py +1705 -0
- package/scheduler/scripts/install-robot-supervisor-linux.sh +33 -0
- package/scheduler/scripts/install-robot-supervisor-windows.ps1 +49 -0
- package/scheduler/scripts/robopark-supervisor.service +20 -0
- package/scheduler/scripts/start-scheduler-local.ps1 +50 -0
- package/scheduler/supervisor.example.json +26 -0
- package/scheduler/vision_motion_trigger.py +101 -0
- package/screen/screen_runtime.py +75 -0
- package/vision/app_pi_clean.py +253 -16
- package/vision/audio_server_pi.py +19 -0
- package/vision/install.sh +34 -34
- package/vision/motor_server.py +224 -61
- package/vision/requirements_camera.txt +6 -0
- package/vision/requirements_motor.txt +4 -0
- package/vision/requirements_pi_unified.txt +1 -0
- package/vision/requirements_vision_agent.txt +19 -0
- package/vision/run.sh +244 -244
- package/vision/services/services.sh +12 -12
- package/scheduler/__pycache__/main.cpython-312.pyc +0 -0
- package/scheduler/__pycache__/preview_agent.cpython-312.pyc +0 -0
package/package.json
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robopark",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "RoboPark
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Standalone packaged RoboPark control center and supervised robot runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"robopark": "./bin/robopark.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/robopark.js",
|
|
11
|
+
"dist",
|
|
11
12
|
"scheduler",
|
|
12
13
|
"pi-client",
|
|
14
|
+
"conversation",
|
|
13
15
|
"vision",
|
|
16
|
+
"screen",
|
|
14
17
|
"README.md"
|
|
15
18
|
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
21
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
22
|
+
"test": "node --test \"test/**/*.test.mjs\"",
|
|
23
|
+
"prepack": "npm run build && npm test"
|
|
24
|
+
},
|
|
16
25
|
"engines": {
|
|
17
26
|
"node": ">=20"
|
|
18
27
|
},
|
|
19
28
|
"dependencies": {
|
|
20
|
-
"
|
|
29
|
+
"chalk": "^5.3.0",
|
|
30
|
+
"commander": "^12.1.0",
|
|
31
|
+
"conf": "^12.0.0",
|
|
32
|
+
"execa": "^9.3.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.14.0",
|
|
36
|
+
"typescript": "^5.4.5"
|
|
21
37
|
},
|
|
22
38
|
"keywords": [
|
|
23
39
|
"robopark",
|
|
@@ -26,8 +42,8 @@
|
|
|
26
42
|
"livekit",
|
|
27
43
|
"raspberry-pi",
|
|
28
44
|
"cli",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
45
|
+
"supervisor",
|
|
46
|
+
"camera"
|
|
31
47
|
],
|
|
32
48
|
"author": "Ra Kai'Un",
|
|
33
49
|
"license": "MIT",
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -e
|
|
3
|
-
echo "=== reset perms (cleanup from previous attempt) ==="
|
|
4
|
-
sudo rm -rf /opt/robopark-pi-client/.venv 2>/dev/null || true
|
|
5
|
-
sudo chown -R robopark:robopark /opt/robopark-pi-client
|
|
6
|
-
|
|
7
|
-
echo "=== apt deps (no signing strict) ==="
|
|
8
|
-
sudo apt-get -o Acquire::AllowInsecureRepositories=true update 2>&1 | tail -3
|
|
9
|
-
sudo apt-get install -y --no-install-recommends python3-venv python3-pip libatlas3-base libasound2t64 portaudio19-dev libjpeg-dev libpng-dev ffmpeg 2>&1 | tail -3
|
|
10
|
-
echo "apt-done"
|
|
11
|
-
|
|
12
|
-
echo "=== create venv as robopark ==="
|
|
13
|
-
sudo -u robopark python3 -m venv /opt/robopark-pi-client/.venv
|
|
14
|
-
sudo -u robopark /opt/robopark-pi-client/.venv/bin/pip install --quiet --upgrade pip
|
|
15
|
-
sudo -u robopark /opt/robopark-pi-client/.venv/bin/pip install --quiet httpx
|
|
16
|
-
echo "httpx-done"
|
|
17
|
-
|
|
18
|
-
echo "=== media stack (optional, may take a while) ==="
|
|
19
|
-
sudo -u robopark /opt/robopark-pi-client/.venv/bin/pip install --quiet livekit livekit-api numpy 2>&1 | tail -3
|
|
20
|
-
echo "lk-done"
|
|
21
|
-
sudo -u robopark /opt/robopark-pi-client/.venv/bin/pip install --quiet opencv-python-headless 2>&1 | tail -3
|
|
22
|
-
echo "cv-done"
|
|
23
|
-
sudo -u robopark /opt/robopark-pi-client/.venv/bin/pip install --quiet sounddevice 2>&1 | tail -3
|
|
24
|
-
echo "sd-done"
|
|
25
|
-
|
|
26
|
-
echo "=== final state ==="
|
|
27
|
-
ls -la /opt/robopark-pi-client
|
|
28
|
-
ls /opt/robopark-pi-client/.venv/bin | head -5
|
|
29
|
-
echo "all-done"
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
echo "=== reset perms (cleanup from previous attempt) ==="
|
|
4
|
+
sudo rm -rf /opt/robopark-pi-client/.venv 2>/dev/null || true
|
|
5
|
+
sudo chown -R robopark:robopark /opt/robopark-pi-client
|
|
6
|
+
|
|
7
|
+
echo "=== apt deps (no signing strict) ==="
|
|
8
|
+
sudo apt-get -o Acquire::AllowInsecureRepositories=true update 2>&1 | tail -3
|
|
9
|
+
sudo apt-get install -y --no-install-recommends python3-venv python3-pip libatlas3-base libasound2t64 portaudio19-dev libjpeg-dev libpng-dev ffmpeg 2>&1 | tail -3
|
|
10
|
+
echo "apt-done"
|
|
11
|
+
|
|
12
|
+
echo "=== create venv as robopark ==="
|
|
13
|
+
sudo -u robopark python3 -m venv /opt/robopark-pi-client/.venv
|
|
14
|
+
sudo -u robopark /opt/robopark-pi-client/.venv/bin/pip install --quiet --upgrade pip
|
|
15
|
+
sudo -u robopark /opt/robopark-pi-client/.venv/bin/pip install --quiet httpx
|
|
16
|
+
echo "httpx-done"
|
|
17
|
+
|
|
18
|
+
echo "=== media stack (optional, may take a while) ==="
|
|
19
|
+
sudo -u robopark /opt/robopark-pi-client/.venv/bin/pip install --quiet livekit livekit-api numpy 2>&1 | tail -3
|
|
20
|
+
echo "lk-done"
|
|
21
|
+
sudo -u robopark /opt/robopark-pi-client/.venv/bin/pip install --quiet opencv-python-headless 2>&1 | tail -3
|
|
22
|
+
echo "cv-done"
|
|
23
|
+
sudo -u robopark /opt/robopark-pi-client/.venv/bin/pip install --quiet sounddevice 2>&1 | tail -3
|
|
24
|
+
echo "sd-done"
|
|
25
|
+
|
|
26
|
+
echo "=== final state ==="
|
|
27
|
+
ls -la /opt/robopark-pi-client
|
|
28
|
+
ls /opt/robopark-pi-client/.venv/bin | head -5
|
|
29
|
+
echo "all-done"
|
package/pi-client/client.py
CHANGED
|
@@ -22,10 +22,12 @@
|
|
|
22
22
|
|
|
23
23
|
import argparse
|
|
24
24
|
import asyncio
|
|
25
|
+
import glob
|
|
25
26
|
import json
|
|
26
27
|
import logging
|
|
27
28
|
import os
|
|
28
29
|
import signal
|
|
30
|
+
import subprocess
|
|
29
31
|
import sys
|
|
30
32
|
import time
|
|
31
33
|
from pathlib import Path
|
|
@@ -42,6 +44,44 @@ logging.basicConfig(
|
|
|
42
44
|
log = logging.getLogger("robopark-pi")
|
|
43
45
|
|
|
44
46
|
|
|
47
|
+
def get_device_inventory() -> dict:
|
|
48
|
+
"""Report real Linux media devices so the remote dashboard can select them."""
|
|
49
|
+
inventory = {
|
|
50
|
+
"video": [{"id": "auto", "name": "Auto detect"}, {"id": "none", "name": "Disable camera"}],
|
|
51
|
+
"audio_input": [{"id": "default", "name": "System default input"}],
|
|
52
|
+
"audio_output": [{"id": "default", "name": "System default output"}],
|
|
53
|
+
"platform": sys.platform,
|
|
54
|
+
}
|
|
55
|
+
for path in sorted(glob.glob("/dev/video*")):
|
|
56
|
+
inventory["video"].append({"id": path, "name": path, "backend": "v4l2"})
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
import pyaudio
|
|
60
|
+
pa = pyaudio.PyAudio()
|
|
61
|
+
for index in range(pa.get_device_count()):
|
|
62
|
+
info = pa.get_device_info_by_index(index)
|
|
63
|
+
name = str(info.get("name", f"Audio device {index}"))
|
|
64
|
+
item = {"id": str(index), "name": name, "host_api": str(info.get("hostApi", ""))}
|
|
65
|
+
if info.get("maxInputChannels", 0) > 0:
|
|
66
|
+
inventory["audio_input"].append(item.copy())
|
|
67
|
+
if info.get("maxOutputChannels", 0) > 0:
|
|
68
|
+
inventory["audio_output"].append(item.copy())
|
|
69
|
+
pa.terminate()
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
# Keep the device visible even when PortAudio is not installed; ALSA
|
|
72
|
+
# names are still useful for an operator diagnosing a Pi remotely.
|
|
73
|
+
log.debug("PyAudio inventory unavailable: %s", exc)
|
|
74
|
+
for command, key, kind in (("arecord", "audio_input", "input"), ("aplay", "audio_output", "output")):
|
|
75
|
+
try:
|
|
76
|
+
output = subprocess.run([command, "-L"], capture_output=True, text=True, timeout=3, check=False).stdout
|
|
77
|
+
for name in (line.strip() for line in output.splitlines()):
|
|
78
|
+
if name and not name.startswith("(") and name not in {x["id"] for x in inventory[key]}:
|
|
79
|
+
inventory[key].append({"id": name, "name": name, "backend": "alsa", "kind": kind})
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
return inventory
|
|
83
|
+
|
|
84
|
+
|
|
45
85
|
class PiClient:
|
|
46
86
|
def __init__(self, args: argparse.Namespace):
|
|
47
87
|
self.scheduler_url = args.scheduler_url.rstrip("/")
|
|
@@ -57,6 +97,9 @@ class PiClient:
|
|
|
57
97
|
self.config_poll_interval = args.config_poll_interval
|
|
58
98
|
self.join_room = args.join_room
|
|
59
99
|
self.room_name = args.room_name
|
|
100
|
+
self.video_device = args.video_device
|
|
101
|
+
self.audio_device = args.audio_device
|
|
102
|
+
self.audio_output_device = args.audio_output_device
|
|
60
103
|
|
|
61
104
|
self.device_id: Optional[str] = None
|
|
62
105
|
self.device_token: Optional[str] = None
|
|
@@ -130,7 +173,11 @@ class PiClient:
|
|
|
130
173
|
self._save_state()
|
|
131
174
|
|
|
132
175
|
async def heartbeat(self):
|
|
133
|
-
body = {
|
|
176
|
+
body = {
|
|
177
|
+
"status": "online",
|
|
178
|
+
"ip": self.tailscale_ip or self.lan_ip,
|
|
179
|
+
"device_inventory": get_device_inventory(),
|
|
180
|
+
}
|
|
134
181
|
async with httpx.AsyncClient(timeout=10.0) as c:
|
|
135
182
|
r = await c.post(
|
|
136
183
|
f"{self.scheduler_url}/api/devices/{self.device_id}/heartbeat",
|
|
@@ -187,6 +234,9 @@ class PiClient:
|
|
|
187
234
|
scheduler_url=self.scheduler_url,
|
|
188
235
|
device_id=self.device_id,
|
|
189
236
|
device_token=self.device_token,
|
|
237
|
+
video_device=self.video_device,
|
|
238
|
+
audio_device=self.audio_device,
|
|
239
|
+
audio_output_device=self.audio_output_device,
|
|
190
240
|
)
|
|
191
241
|
|
|
192
242
|
# ---- main loop ------------------------------------------------------
|
|
@@ -236,6 +286,12 @@ class PiClient:
|
|
|
236
286
|
self.production_mode = bool(cfg.get("production_mode", False))
|
|
237
287
|
if cfg.get("character_id"):
|
|
238
288
|
self.character_id = cfg["character_id"]
|
|
289
|
+
if cfg.get("video_device") is not None:
|
|
290
|
+
self.video_device = cfg["video_device"]
|
|
291
|
+
if cfg.get("audio_device") is not None:
|
|
292
|
+
self.audio_device = cfg["audio_device"]
|
|
293
|
+
if cfg.get("audio_output_device") is not None:
|
|
294
|
+
self.audio_output_device = cfg["audio_output_device"]
|
|
239
295
|
except Exception as e:
|
|
240
296
|
log.debug(f"Config poll error: {e}")
|
|
241
297
|
|
|
@@ -269,6 +325,9 @@ def parse_args() -> argparse.Namespace:
|
|
|
269
325
|
p.add_argument("--character-id", default=None, help="Override character assignment")
|
|
270
326
|
p.add_argument("--room-name", default="robopark-pi-standby",
|
|
271
327
|
help="LiveKit room to join")
|
|
328
|
+
p.add_argument("--video-device", default="auto", help="Camera path/index, picamera, or auto")
|
|
329
|
+
p.add_argument("--audio-device", default="default", help="Microphone name/index or default")
|
|
330
|
+
p.add_argument("--audio-output-device", default="default", help="Speaker name/index or default")
|
|
272
331
|
p.add_argument("--heartbeat-interval", type=float, default=10.0)
|
|
273
332
|
p.add_argument("--config-poll-interval", type=float, default=30.0)
|
|
274
333
|
p.add_argument("--join-room", action="store_true",
|
|
@@ -302,4 +361,4 @@ def main():
|
|
|
302
361
|
|
|
303
362
|
|
|
304
363
|
if __name__ == "__main__":
|
|
305
|
-
main()
|
|
364
|
+
main()
|
package/pi-client/install.sh
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Install RoboPark Pi Client on a Raspberry Pi
|
|
3
|
-
# Usage:
|
|
4
|
-
# sudo ./install.sh --scheduler-url http://100.64.1.5:8080 \
|
|
5
|
-
# --enrollment-token <TOKEN>
|
|
6
|
-
set -euo pipefail
|
|
7
|
-
|
|
8
|
-
SCHEDULER_URL=""
|
|
9
|
-
ENROLLMENT_TOKEN=""
|
|
10
|
-
|
|
11
|
-
while [[ $# -gt 0 ]]; do
|
|
12
|
-
case "$1" in
|
|
13
|
-
--scheduler-url) SCHEDULER_URL="$2"; shift 2 ;;
|
|
14
|
-
--enrollment-token) ENROLLMENT_TOKEN="$2"; shift 2 ;;
|
|
15
|
-
*) echo "Unknown arg: $1"; exit 1 ;;
|
|
16
|
-
esac
|
|
17
|
-
done
|
|
18
|
-
|
|
19
|
-
if [[ -z "$SCHEDULER_URL" || -z "$ENROLLMENT_TOKEN" ]]; then
|
|
20
|
-
echo "Usage: $0 --scheduler-url URL --enrollment-token TOKEN"
|
|
21
|
-
exit 1
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
id "robopark" 2>/dev/null || useradd -r -s /bin/false robopark
|
|
25
|
-
install -d -m 755 -o robopark -g robopark /opt/robopark-pi-client
|
|
26
|
-
install -d -m 700 -o robopark -g robopark /etc/robopark
|
|
27
|
-
cp -r ./* /opt/robopark-pi-client/
|
|
28
|
-
chown -R robopark:robopark /opt/robopark-pi-client
|
|
29
|
-
|
|
30
|
-
sudo -u robopark python3 -m venv /opt/robopark-pi-client/.venv
|
|
31
|
-
/opt/robopark-pi-client/.venv/bin/pip install --upgrade pip
|
|
32
|
-
/opt/robopark-pi-client/.venv/bin/pip install -r /opt/robopark-pi-client/requirements.txt
|
|
33
|
-
# On Pi you typically want the full media stack:
|
|
34
|
-
/opt/robopark-pi-client/.venv/bin/pip install livekit livekit-api sounddevice numpy opencv-python av
|
|
35
|
-
|
|
36
|
-
cp /opt/robopark-pi-client/robopark-pi-client.service /etc/systemd/system/
|
|
37
|
-
systemctl daemon-reload
|
|
38
|
-
systemctl enable --now robopark-pi-client.service
|
|
39
|
-
|
|
40
|
-
echo "First-boot enrollment will run as part of the service start."
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install RoboPark Pi Client on a Raspberry Pi
|
|
3
|
+
# Usage:
|
|
4
|
+
# sudo ./install.sh --scheduler-url http://100.64.1.5:8080 \
|
|
5
|
+
# --enrollment-token <TOKEN>
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
SCHEDULER_URL=""
|
|
9
|
+
ENROLLMENT_TOKEN=""
|
|
10
|
+
|
|
11
|
+
while [[ $# -gt 0 ]]; do
|
|
12
|
+
case "$1" in
|
|
13
|
+
--scheduler-url) SCHEDULER_URL="$2"; shift 2 ;;
|
|
14
|
+
--enrollment-token) ENROLLMENT_TOKEN="$2"; shift 2 ;;
|
|
15
|
+
*) echo "Unknown arg: $1"; exit 1 ;;
|
|
16
|
+
esac
|
|
17
|
+
done
|
|
18
|
+
|
|
19
|
+
if [[ -z "$SCHEDULER_URL" || -z "$ENROLLMENT_TOKEN" ]]; then
|
|
20
|
+
echo "Usage: $0 --scheduler-url URL --enrollment-token TOKEN"
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
id "robopark" 2>/dev/null || useradd -r -s /bin/false robopark
|
|
25
|
+
install -d -m 755 -o robopark -g robopark /opt/robopark-pi-client
|
|
26
|
+
install -d -m 700 -o robopark -g robopark /etc/robopark
|
|
27
|
+
cp -r ./* /opt/robopark-pi-client/
|
|
28
|
+
chown -R robopark:robopark /opt/robopark-pi-client
|
|
29
|
+
|
|
30
|
+
sudo -u robopark python3 -m venv /opt/robopark-pi-client/.venv
|
|
31
|
+
/opt/robopark-pi-client/.venv/bin/pip install --upgrade pip
|
|
32
|
+
/opt/robopark-pi-client/.venv/bin/pip install -r /opt/robopark-pi-client/requirements.txt
|
|
33
|
+
# On Pi you typically want the full media stack:
|
|
34
|
+
/opt/robopark-pi-client/.venv/bin/pip install livekit livekit-api sounddevice numpy opencv-python av
|
|
35
|
+
|
|
36
|
+
cp /opt/robopark-pi-client/robopark-pi-client.service /etc/systemd/system/
|
|
37
|
+
systemctl daemon-reload
|
|
38
|
+
systemctl enable --now robopark-pi-client.service
|
|
39
|
+
|
|
40
|
+
echo "First-boot enrollment will run as part of the service start."
|
|
41
41
|
echo "Tail logs with: journalctl -u robopark-pi-client -f"
|
package/pi-client/join_convo.sh
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Join a LiveKit conversation from the Pi.
|
|
3
|
-
#
|
|
4
|
-
# Calls the local Pi UI's /api/join endpoint, which itself asks the scheduler
|
|
5
|
-
# for a short-lived LiveKit access token. The script prints the meet URL
|
|
6
|
-
# AND (if --open) opens it in the default browser on the Pi.
|
|
7
|
-
#
|
|
8
|
-
# Usage:
|
|
9
|
-
# ./join_convo.sh
|
|
10
|
-
# ./join_convo.sh --room robopark-pi-test --identity alice --open
|
|
11
|
-
# PI_UI=http://192.168.1.159:8081 ./join_convo.sh --open
|
|
12
|
-
|
|
13
|
-
set -euo pipefail
|
|
14
|
-
|
|
15
|
-
PI_UI="${PI_UI:-http://127.0.0.1:8081}"
|
|
16
|
-
ROOM="${ROOM:-robopark-pi-test}"
|
|
17
|
-
IDENTITY="${IDENTITY:-}"
|
|
18
|
-
OPEN=0
|
|
19
|
-
|
|
20
|
-
while [[ $# -gt 0 ]]; do
|
|
21
|
-
case "$1" in
|
|
22
|
-
--room) ROOM="$2"; shift 2 ;;
|
|
23
|
-
--identity) IDENTITY="$2"; shift 2 ;;
|
|
24
|
-
--open) OPEN=1; shift ;;
|
|
25
|
-
--pi-ui) PI_UI="$2"; shift 2 ;;
|
|
26
|
-
-h|--help)
|
|
27
|
-
sed -n '2,12p' "$0"; exit 0 ;;
|
|
28
|
-
*) echo "unknown arg: $1" >&2; exit 1 ;;
|
|
29
|
-
esac
|
|
30
|
-
done
|
|
31
|
-
|
|
32
|
-
BODY=$(jq -nc --arg room "$ROOM" --arg identity "$IDENTITY" \
|
|
33
|
-
'{room:$room, identity:$identity}')
|
|
34
|
-
|
|
35
|
-
echo "Requesting token from $PI_UI (room=$ROOM identity=$IDENTITY)..."
|
|
36
|
-
RESP=$(curl -fsS -X POST "$PI_UI/api/join" \
|
|
37
|
-
-H 'Content-Type: application/json' -d "$BODY")
|
|
38
|
-
|
|
39
|
-
URL=$(echo "$RESP" | jq -r '
|
|
40
|
-
"https://meet.livekit.io/custom?livekitUrl=" + (.url | @uri) +
|
|
41
|
-
"&token=" + (.token | @uri)
|
|
42
|
-
')
|
|
43
|
-
|
|
44
|
-
EXPIRES=$(echo "$RESP" | jq -r '.expires_at')
|
|
45
|
-
echo "Token expires: $EXPIRES"
|
|
46
|
-
echo "Meet URL:"
|
|
47
|
-
echo " $URL"
|
|
48
|
-
|
|
49
|
-
if [[ $OPEN -eq 1 ]]; then
|
|
50
|
-
if command -v xdg-open >/dev/null 2>&1; then xdg-open "$URL" >/dev/null 2>&1 || true
|
|
51
|
-
elif command -v sensible-browser >/dev/null 2>&1; then sensible-browser "$URL" >/dev/null 2>&1 || true
|
|
52
|
-
elif command -v open >/dev/null 2>&1; then open "$URL" >/dev/null 2>&1 || true
|
|
53
|
-
else echo "no browser opener found; copy the URL above" >&2; exit 2; fi
|
|
54
|
-
echo "(opened in browser)"
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Join a LiveKit conversation from the Pi.
|
|
3
|
+
#
|
|
4
|
+
# Calls the local Pi UI's /api/join endpoint, which itself asks the scheduler
|
|
5
|
+
# for a short-lived LiveKit access token. The script prints the meet URL
|
|
6
|
+
# AND (if --open) opens it in the default browser on the Pi.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ./join_convo.sh
|
|
10
|
+
# ./join_convo.sh --room robopark-pi-test --identity alice --open
|
|
11
|
+
# PI_UI=http://192.168.1.159:8081 ./join_convo.sh --open
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
PI_UI="${PI_UI:-http://127.0.0.1:8081}"
|
|
16
|
+
ROOM="${ROOM:-robopark-pi-test}"
|
|
17
|
+
IDENTITY="${IDENTITY:-}"
|
|
18
|
+
OPEN=0
|
|
19
|
+
|
|
20
|
+
while [[ $# -gt 0 ]]; do
|
|
21
|
+
case "$1" in
|
|
22
|
+
--room) ROOM="$2"; shift 2 ;;
|
|
23
|
+
--identity) IDENTITY="$2"; shift 2 ;;
|
|
24
|
+
--open) OPEN=1; shift ;;
|
|
25
|
+
--pi-ui) PI_UI="$2"; shift 2 ;;
|
|
26
|
+
-h|--help)
|
|
27
|
+
sed -n '2,12p' "$0"; exit 0 ;;
|
|
28
|
+
*) echo "unknown arg: $1" >&2; exit 1 ;;
|
|
29
|
+
esac
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
BODY=$(jq -nc --arg room "$ROOM" --arg identity "$IDENTITY" \
|
|
33
|
+
'{room:$room, identity:$identity}')
|
|
34
|
+
|
|
35
|
+
echo "Requesting token from $PI_UI (room=$ROOM identity=$IDENTITY)..."
|
|
36
|
+
RESP=$(curl -fsS -X POST "$PI_UI/api/join" \
|
|
37
|
+
-H 'Content-Type: application/json' -d "$BODY")
|
|
38
|
+
|
|
39
|
+
URL=$(echo "$RESP" | jq -r '
|
|
40
|
+
"https://meet.livekit.io/custom?livekitUrl=" + (.url | @uri) +
|
|
41
|
+
"&token=" + (.token | @uri)
|
|
42
|
+
')
|
|
43
|
+
|
|
44
|
+
EXPIRES=$(echo "$RESP" | jq -r '.expires_at')
|
|
45
|
+
echo "Token expires: $EXPIRES"
|
|
46
|
+
echo "Meet URL:"
|
|
47
|
+
echo " $URL"
|
|
48
|
+
|
|
49
|
+
if [[ $OPEN -eq 1 ]]; then
|
|
50
|
+
if command -v xdg-open >/dev/null 2>&1; then xdg-open "$URL" >/dev/null 2>&1 || true
|
|
51
|
+
elif command -v sensible-browser >/dev/null 2>&1; then sensible-browser "$URL" >/dev/null 2>&1 || true
|
|
52
|
+
elif command -v open >/dev/null 2>&1; then open "$URL" >/dev/null 2>&1 || true
|
|
53
|
+
else echo "no browser opener found; copy the URL above" >&2; exit 2; fi
|
|
54
|
+
echo "(opened in browser)"
|
|
55
55
|
fi
|
|
@@ -78,6 +78,9 @@ async def run_livekit(
|
|
|
78
78
|
scheduler_url: str = "",
|
|
79
79
|
device_id: str = "",
|
|
80
80
|
device_token: str = "",
|
|
81
|
+
video_device: str = "auto",
|
|
82
|
+
audio_device: str = "default",
|
|
83
|
+
audio_output_device: str = "default",
|
|
81
84
|
):
|
|
82
85
|
"""Join a LiveKit room, publish mic + cam, subscribe to data channel.
|
|
83
86
|
|
|
@@ -140,7 +143,8 @@ async def run_livekit(
|
|
|
140
143
|
try:
|
|
141
144
|
stream = rtc.AudioStream(track=track)
|
|
142
145
|
# Open output stream at 48kHz mono (Pi speaker)
|
|
143
|
-
|
|
146
|
+
output = None if str(audio_output_device).lower() in ("", "default", "auto") else audio_output_device
|
|
147
|
+
out = sd.OutputStream(samplerate=OUT_RATE, channels=1, dtype="int16", blocksize=0, device=output)
|
|
144
148
|
out.start()
|
|
145
149
|
_active_streams[sid] = out
|
|
146
150
|
|
|
@@ -203,10 +207,13 @@ async def run_livekit(
|
|
|
203
207
|
except Exception as e:
|
|
204
208
|
log.warning(f"sounddevice unavailable, skipping mic: {e}")
|
|
205
209
|
return
|
|
206
|
-
|
|
207
|
-
MIC_DEVICE =
|
|
210
|
+
selected_mic = str(audio_device or "default")
|
|
211
|
+
MIC_DEVICE = None if selected_mic.lower() in ("", "default", "auto") else selected_mic
|
|
208
212
|
try:
|
|
209
|
-
|
|
213
|
+
if MIC_DEVICE is None:
|
|
214
|
+
log.info("mic: opening system default input")
|
|
215
|
+
else:
|
|
216
|
+
log.info(f"mic: opening device {MIC_DEVICE}")
|
|
210
217
|
except Exception:
|
|
211
218
|
log.warning(f"mic: device {MIC_DEVICE} not found, trying default")
|
|
212
219
|
MIC_DEVICE = None
|
|
@@ -241,9 +248,11 @@ async def run_livekit(
|
|
|
241
248
|
except Exception as e:
|
|
242
249
|
log.warning(f"opencv unavailable, skipping camera: {e}")
|
|
243
250
|
return
|
|
244
|
-
|
|
251
|
+
selected_camera = str(video_device or "auto")
|
|
252
|
+
camera_index = 0 if selected_camera.lower() in ("", "auto", "default", "first") else selected_camera
|
|
253
|
+
cap = cv2.VideoCapture(camera_index, cv2.CAP_V4L2)
|
|
245
254
|
if not cap.isOpened():
|
|
246
|
-
log.warning("camera
|
|
255
|
+
log.warning(f"camera {selected_camera} not available")
|
|
247
256
|
return
|
|
248
257
|
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
|
|
249
258
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
|
|
@@ -286,4 +295,4 @@ async def run_livekit(
|
|
|
286
295
|
except Exception: pass
|
|
287
296
|
try: await room.disconnect()
|
|
288
297
|
except Exception: pass
|
|
289
|
-
log.info("left room, will loop to reconnect")
|
|
298
|
+
log.info("left room, will loop to reconnect")
|
|
@@ -11,6 +11,7 @@ Command wire format (JSON on the data channel):
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
14
|
+
import os
|
|
14
15
|
import json
|
|
15
16
|
import logging
|
|
16
17
|
from typing import Awaitable, Callable, Optional
|
|
@@ -24,6 +25,8 @@ class MotorBridge:
|
|
|
24
25
|
def __init__(self, motor_server_url: Optional[str], dry_run: bool = False):
|
|
25
26
|
self.motor_server_url = (motor_server_url or "").rstrip("/")
|
|
26
27
|
self.dry_run = dry_run
|
|
28
|
+
token = os.getenv("ROBOPARK_MOTOR_TOKEN", "").strip() or os.getenv("ROBOPARK_MESH_TOKEN", "").strip()
|
|
29
|
+
self.headers = {"X-RoboPark-Motor-Token": token} if token else {}
|
|
27
30
|
self._lock = asyncio.Lock() # the local motor server allows one motor at a time
|
|
28
31
|
|
|
29
32
|
async def handle_command(self, raw: str | bytes) -> str:
|
|
@@ -60,7 +63,7 @@ class MotorBridge:
|
|
|
60
63
|
async with httpx.AsyncClient(timeout=seconds + 5.0) as c:
|
|
61
64
|
r = await c.post(
|
|
62
65
|
f"{self.motor_server_url}/trigger-motor",
|
|
63
|
-
json={"motor_name": name, "seconds": seconds},
|
|
66
|
+
json={"motor_name": name, "seconds": seconds}, headers=self.headers,
|
|
64
67
|
)
|
|
65
68
|
if r.status_code == 200:
|
|
66
69
|
return f"moved {name} for {seconds}s"
|
|
@@ -75,8 +78,8 @@ class MotorBridge:
|
|
|
75
78
|
return "dry-run: stop"
|
|
76
79
|
try:
|
|
77
80
|
async with httpx.AsyncClient(timeout=5.0) as c:
|
|
78
|
-
r = await c.post(f"{self.motor_server_url}/stop-motors", json={})
|
|
81
|
+
r = await c.post(f"{self.motor_server_url}/stop-motors", json={}, headers=self.headers)
|
|
79
82
|
return f"stop {r.status_code}"
|
|
80
83
|
except Exception as e:
|
|
81
84
|
log.error(f"stop_all failed: {e}")
|
|
82
|
-
return f"error: {e}"
|
|
85
|
+
return f"error: {e}"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"revision": 1,
|
|
4
|
+
"tailnet": "tail2086c6.ts.net",
|
|
5
|
+
"gateway": {
|
|
6
|
+
"name": "robo1",
|
|
7
|
+
"dns": "robo1.tail2086c6.ts.net",
|
|
8
|
+
"tailscale_ip": "100.84.147.24",
|
|
9
|
+
"scheduler_url": "http://robo1.tail2086c6.ts.net:8080",
|
|
10
|
+
"dashboard_url": "http://robo1.tail2086c6.ts.net:8080",
|
|
11
|
+
"robovoice_url": "https://robo1.tail2086c6.ts.net:3443",
|
|
12
|
+
"livekit_url": "wss://robo1.tail2086c6.ts.net:3443",
|
|
13
|
+
"management_url": "http://robo1.tail2086c6.ts.net:47913"
|
|
14
|
+
},
|
|
15
|
+
"voice_video_devices": [
|
|
16
|
+
{
|
|
17
|
+
"name": "jaguar-voice",
|
|
18
|
+
"dns": "jaguar-voice.tail2086c6.ts.net",
|
|
19
|
+
"tailscale_ip": "100.107.146.50",
|
|
20
|
+
"character": "jaguar"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "magnus-voice",
|
|
24
|
+
"dns": "magnus-voice.tail2086c6.ts.net",
|
|
25
|
+
"tailscale_ip": "100.92.64.10",
|
|
26
|
+
"character": "magnus"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "paladin-voice",
|
|
30
|
+
"dns": "paladin-voice.tail2086c6.ts.net",
|
|
31
|
+
"tailscale_ip": "100.116.232.45",
|
|
32
|
+
"character": "palladin"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "ronin-voice",
|
|
36
|
+
"dns": "ronin-voice.tail2086c6.ts.net",
|
|
37
|
+
"tailscale_ip": "100.64.213.41",
|
|
38
|
+
"character": "ronin"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "titan-voice",
|
|
42
|
+
"dns": "titan-voice.tail2086c6.ts.net",
|
|
43
|
+
"tailscale_ip": "100.126.121.92",
|
|
44
|
+
"character": "titan"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "vixen-voice",
|
|
48
|
+
"dns": "vixen-voice.tail2086c6.ts.net",
|
|
49
|
+
"tailscale_ip": "100.112.220.46",
|
|
50
|
+
"character": "vixen"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"motor_camera_devices": [
|
|
54
|
+
{
|
|
55
|
+
"name": "bmw",
|
|
56
|
+
"dns": "bmw.tail2086c6.ts.net",
|
|
57
|
+
"tailscale_ip": "100.84.123.35",
|
|
58
|
+
"character": "vixen"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "titan",
|
|
62
|
+
"dns": "titan.tail2086c6.ts.net",
|
|
63
|
+
"tailscale_ip": "100.78.77.96",
|
|
64
|
+
"character": "titan"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"unassigned_tailnet_devices": [
|
|
68
|
+
{
|
|
69
|
+
"name": "voicevoice",
|
|
70
|
+
"dns": "voicevoice.tail2086c6.ts.net",
|
|
71
|
+
"tailscale_ip": "100.79.79.58",
|
|
72
|
+
"reason": "Existing tagged voice device has no verified character binding."
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|