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.
Files changed (68) hide show
  1. package/README.md +88 -63
  2. package/bin/robopark.js +7 -17
  3. package/conversation/elevenlabs_agent.py +1985 -0
  4. package/conversation/requirements.txt +3 -0
  5. package/conversation/supervisor_store.py +189 -0
  6. package/dist/kernel/config-schema.js +37 -0
  7. package/dist/kernel/types.js +7 -0
  8. package/dist/robopark/access.js +99 -0
  9. package/dist/robopark/add-robot.js +188 -0
  10. package/dist/robopark/agent-ctl.js +305 -0
  11. package/dist/robopark/auto-start.js +289 -0
  12. package/dist/robopark/conversation.js +505 -0
  13. package/dist/robopark/deployment-commands.js +47 -0
  14. package/dist/robopark/discovery.js +180 -0
  15. package/dist/robopark/doctor.js +175 -0
  16. package/dist/robopark/enroll.js +68 -0
  17. package/dist/robopark/llm-set.js +87 -0
  18. package/dist/robopark/motor-control.js +195 -0
  19. package/dist/robopark/preview-agent-launcher.js +77 -0
  20. package/dist/robopark/probe.js +138 -0
  21. package/dist/robopark/profile.js +69 -0
  22. package/dist/robopark/python-env.js +162 -0
  23. package/dist/robopark/robot-runtime.js +489 -0
  24. package/dist/robopark/scan.js +97 -0
  25. package/dist/robopark/screen-control.js +55 -0
  26. package/dist/robopark/secrets.js +41 -0
  27. package/dist/robopark/serve.js +285 -0
  28. package/dist/robopark/server-add.js +114 -0
  29. package/dist/robopark/setup-livekit.js +300 -0
  30. package/dist/robopark/setup.js +286 -0
  31. package/dist/robopark/standalone.js +466 -0
  32. package/dist/robopark/stop-all.js +141 -0
  33. package/dist/robopark/verify.js +192 -0
  34. package/dist/robopark/vision-agent-launcher.js +98 -0
  35. package/dist/robopark/vision-control.js +81 -0
  36. package/dist/robopark-cli.js +799 -0
  37. package/package.json +21 -5
  38. package/pi-client/_install_steps.sh +29 -29
  39. package/pi-client/client.py +61 -2
  40. package/pi-client/install.sh +40 -40
  41. package/pi-client/join_convo.sh +54 -54
  42. package/pi-client/livekit_bridge.py +16 -7
  43. package/pi-client/motor_bridge.py +6 -3
  44. package/scheduler/fleet_config.json +75 -0
  45. package/scheduler/main.py +4505 -135
  46. package/scheduler/media_lock.py +57 -0
  47. package/scheduler/preview_agent.py +1465 -87
  48. package/scheduler/production_config.json +139 -0
  49. package/scheduler/robot_supervisor.py +1705 -0
  50. package/scheduler/scripts/install-robot-supervisor-linux.sh +33 -0
  51. package/scheduler/scripts/install-robot-supervisor-windows.ps1 +49 -0
  52. package/scheduler/scripts/robopark-supervisor.service +20 -0
  53. package/scheduler/scripts/start-scheduler-local.ps1 +50 -0
  54. package/scheduler/supervisor.example.json +26 -0
  55. package/scheduler/vision_motion_trigger.py +101 -0
  56. package/screen/screen_runtime.py +75 -0
  57. package/vision/app_pi_clean.py +253 -16
  58. package/vision/audio_server_pi.py +19 -0
  59. package/vision/install.sh +34 -34
  60. package/vision/motor_server.py +224 -61
  61. package/vision/requirements_camera.txt +6 -0
  62. package/vision/requirements_motor.txt +4 -0
  63. package/vision/requirements_pi_unified.txt +1 -0
  64. package/vision/requirements_vision_agent.txt +19 -0
  65. package/vision/run.sh +244 -244
  66. package/vision/services/services.sh +12 -12
  67. package/scheduler/__pycache__/main.cpython-312.pyc +0 -0
  68. package/scheduler/__pycache__/preview_agent.cpython-312.pyc +0 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * RoboPark — `robopark verify`.
3
+ *
4
+ * Checks the fleet end-to-end and reports what's working vs. missing:
5
+ * - hub / scheduler reachable
6
+ * - LiveKit configured and server registered
7
+ * - production mode enabled
8
+ * - robots/devices enrolled
9
+ * - preview agent running on robots
10
+ * - preview room can be started
11
+ *
12
+ * Usage:
13
+ * robopark verify
14
+ * robopark verify --scheduler-url http://livekit-1:8080 --robot-id robobmw
15
+ */
16
+ import { existsSync } from 'node:fs';
17
+ import { homedir, hostname } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import chalk from 'chalk';
20
+ import { discoverContext } from './discovery.js';
21
+ function loadDeviceToken(robotId) {
22
+ const paths = [
23
+ join(homedir(), '.robopark', 'device_token'),
24
+ ];
25
+ for (const p of paths) {
26
+ if (existsSync(p))
27
+ return readFile(p);
28
+ }
29
+ return undefined;
30
+ }
31
+ function readFile(p) {
32
+ const { readFileSync } = require('node:fs');
33
+ return readFileSync(p, 'utf8').trim();
34
+ }
35
+ async function fetchJson(url, init) {
36
+ const res = await fetch(url, { ...init, signal: AbortSignal.timeout(5000) });
37
+ if (!res.ok)
38
+ throw new Error(`${res.status} ${res.statusText}`);
39
+ return res.json();
40
+ }
41
+ export async function roboparkVerify(opts) {
42
+ const ctx = await discoverContext();
43
+ const schedulerUrl = (opts.schedulerUrl ?? (ctx.hub ? `http://${ctx.hub.ip}:8080` : 'http://localhost:8080')).replace(/\/$/, '');
44
+ const robotId = opts.robotId ?? hostname().split('.')[0];
45
+ console.log(chalk.bold('\n robopark verify'));
46
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
47
+ console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
48
+ console.log(` robot: ${chalk.cyan(robotId)}`);
49
+ console.log();
50
+ const checks = [];
51
+ // 1. Scheduler reachable
52
+ try {
53
+ const settings = (await fetchJson(`${schedulerUrl}/api/settings`));
54
+ checks.push({ name: 'scheduler reachable', ok: true, message: `production_mode=${settings.production_mode ?? false}` });
55
+ if (!settings.production_mode) {
56
+ checks.push({ name: 'production mode', ok: false, message: 'Production mode is OFF. Toggle it in the dashboard or PUT /api/settings', detail: 'Pi preview/session enrollment is blocked until production mode is ON.' });
57
+ }
58
+ else {
59
+ checks.push({ name: 'production mode', ok: true, message: 'ON' });
60
+ }
61
+ }
62
+ catch (e) {
63
+ checks.push({ name: 'scheduler reachable', ok: false, message: `could not reach scheduler: ${e.message}`, detail: 'Is robopark serve running on the hub? Is the scheduler port open?' });
64
+ }
65
+ // 2. LiveKit configured
66
+ try {
67
+ const lk = (await fetchJson(`${schedulerUrl}/api/livekit/config`));
68
+ if (lk.url && lk.has_secret) {
69
+ checks.push({ name: 'livekit configured', ok: true, message: `url=${lk.url}` });
70
+ }
71
+ else {
72
+ checks.push({ name: 'livekit configured', ok: false, message: 'LiveKit URL or API secret missing', detail: 'Set LiveKit config in the dashboard or via robopark setup-livekit.' });
73
+ }
74
+ }
75
+ catch (e) {
76
+ checks.push({ name: 'livekit configured', ok: false, message: `error: ${e.message}` });
77
+ }
78
+ // 3. LiveKit server registered
79
+ try {
80
+ const servers = (await fetchJson(`${schedulerUrl}/api/servers`));
81
+ const online = servers.filter(s => s.status === 'online');
82
+ if (online.length > 0) {
83
+ checks.push({ name: 'livekit server registered', ok: true, message: `${online.length} online server(s)` });
84
+ }
85
+ else if (servers.length > 0) {
86
+ checks.push({ name: 'livekit server registered', ok: false, message: `${servers.length} server(s) but none online`, detail: 'Check that the LiveKit container/process is running and reachable from the hub.' });
87
+ }
88
+ else {
89
+ checks.push({ name: 'livekit server registered', ok: false, message: 'no servers in scheduler', detail: 'Add a LiveKit server in the dashboard or run robopark setup-livekit.' });
90
+ }
91
+ }
92
+ catch (e) {
93
+ checks.push({ name: 'livekit server registered', ok: false, message: `error: ${e.message}` });
94
+ }
95
+ // 4. Device enrolled
96
+ let deviceToken;
97
+ let deviceId;
98
+ try {
99
+ const devices = (await fetchJson(`${schedulerUrl}/api/devices`));
100
+ const device = devices.find(d => d.name === robotId || d.id === robotId);
101
+ if (device) {
102
+ deviceId = device.id;
103
+ checks.push({ name: 'device enrolled', ok: true, message: `${device.id} (${device.status})` });
104
+ }
105
+ else {
106
+ checks.push({ name: 'device enrolled', ok: false, message: `${robotId} not found in scheduler`, detail: 'Run robopark enroll on the robot with the scheduler enrollment token.' });
107
+ }
108
+ }
109
+ catch (e) {
110
+ checks.push({ name: 'device enrolled', ok: false, message: `error: ${e.message}` });
111
+ }
112
+ // 5. Local device token
113
+ deviceToken = loadDeviceToken(robotId);
114
+ if (deviceToken) {
115
+ checks.push({ name: 'local device token', ok: true, message: `found ~/.robopark/device_token` });
116
+ }
117
+ else {
118
+ checks.push({ name: 'local device token', ok: false, message: 'no ~/.robopark/device_token', detail: 'Run robopark enroll on this machine first.' });
119
+ }
120
+ // 6. Robot row exists
121
+ try {
122
+ const robots = (await fetchJson(`${schedulerUrl}/api/robots`));
123
+ const robot = robots.find(r => r.id === (deviceId || robotId) || r.name === robotId);
124
+ if (robot) {
125
+ checks.push({ name: 'robot registered', ok: true, message: `${robot.id} (${robot.status})` });
126
+ }
127
+ else {
128
+ checks.push({ name: 'robot registered', ok: false, message: `${robotId} not in robots table`, detail: 'The device should auto-create a robot row on enrollment. Re-run enroll or check scheduler logs.' });
129
+ }
130
+ }
131
+ catch (e) {
132
+ checks.push({ name: 'robot registered', ok: false, message: `error: ${e.message}` });
133
+ }
134
+ // 7. Preview agent polling
135
+ if (deviceToken) {
136
+ try {
137
+ const agentRes = (await fetchJson(`${schedulerUrl}/api/robots/${robotId}/preview/agent`, {
138
+ headers: { Authorization: `Bearer ${deviceToken}` },
139
+ }));
140
+ if (agentRes.active) {
141
+ checks.push({ name: 'preview agent polling', ok: true, message: 'preview currently active' });
142
+ }
143
+ else {
144
+ checks.push({ name: 'preview agent polling', ok: true, message: `no active preview (reason=${agentRes.reason || 'none'})`, detail: 'This is normal when the dashboard is not watching. Start a preview from the dashboard.' });
145
+ }
146
+ }
147
+ catch (e) {
148
+ checks.push({ name: 'preview agent polling', ok: false, message: `error: ${e.message}`, detail: 'Is robopark preview-agent running on the robot?' });
149
+ }
150
+ }
151
+ else {
152
+ checks.push({ name: 'preview agent polling', ok: false, message: 'skipped (no device token)' });
153
+ }
154
+ // 8. Try starting a preview from the operator side
155
+ try {
156
+ const preview = (await fetchJson(`${schedulerUrl}/api/robots/${robotId}/preview/start`, { method: 'POST' }));
157
+ if (preview.active) {
158
+ checks.push({ name: 'operator preview start', ok: true, message: `room assigned at ${preview.url}` });
159
+ // stop it so we don't leave a stale preview running
160
+ await fetch(`${schedulerUrl}/api/robots/${robotId}/preview/stop`, { method: 'POST' });
161
+ }
162
+ else {
163
+ checks.push({ name: 'operator preview start', ok: false, message: `could not start: ${preview.reason || 'unknown'}`, detail: 'Enable production mode, add a LiveKit server, and ensure the robot is not in a session.' });
164
+ }
165
+ }
166
+ catch (e) {
167
+ checks.push({ name: 'operator preview start', ok: false, message: `error: ${e.message}` });
168
+ }
169
+ // Print results
170
+ let pass = 0;
171
+ let fail = 0;
172
+ for (const c of checks) {
173
+ const icon = c.ok ? chalk.green('✓') : chalk.red('✗');
174
+ const color = c.ok ? chalk.green : chalk.red;
175
+ console.log(` ${icon} ${color(c.name)} — ${c.message}`);
176
+ if (c.detail) {
177
+ console.log(chalk.dim(` → ${c.detail}`));
178
+ }
179
+ if (c.ok)
180
+ pass++;
181
+ else
182
+ fail++;
183
+ }
184
+ console.log();
185
+ if (fail === 0) {
186
+ console.log(chalk.bold.green(` ${pass}/${checks.length} checks passed — fleet is ready for preview.`));
187
+ }
188
+ else {
189
+ console.log(chalk.bold.yellow(` ${pass}/${checks.length} passed, ${fail} failed — fix the items above.`));
190
+ process.exitCode = 1;
191
+ }
192
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * RoboPark — `robopark vision-agent` launcher.
3
+ *
4
+ * Starts app_pi_clean.py (RoboVisionAI_PI's camera/motion detector) with the
5
+ * right Python interpreter, self-armed and pointed at the local
6
+ * preview-agent's motion-webhook listener by default — no manual curl calls.
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+ import chalk from 'chalk';
10
+ import { findPython, findVisionPath, findVisionAudioPath, prepareRobotPython, VISION_REQUIRED_MODULES } from './python-env.js';
11
+ const DEFAULT_VISION_PORT = 5000;
12
+ const DEFAULT_MOTION_WEBHOOK_PORT = 5057; // must match preview-agent's --vision-webhook-port default
13
+ function isPiArm() {
14
+ return process.platform === 'linux' && (process.arch === 'arm' || process.arch === 'arm64');
15
+ }
16
+ export async function roboparkVisionAgent(opts) {
17
+ const script = await findVisionPath();
18
+ if (!script) {
19
+ console.log(chalk.red(' ✗ could not find app_pi_clean.py'));
20
+ process.exit(1);
21
+ }
22
+ const basePython = findPython();
23
+ // Real Pi: opencv comes from `apt install python3-opencv`, not pip (see
24
+ // requirements_pi_unified.txt) — pip-installing it here would fight the
25
+ // ARM-optimized system build. Everywhere else: plain pip install works.
26
+ const reqFile = isPiArm() ? 'requirements_vision_agent.txt' : 'requirements-demo.txt';
27
+ const requiredModules = isPiArm() ? [...VISION_REQUIRED_MODULES, 'fastapi', 'uvicorn', 'sounddevice', 'soundfile', 'groq', 'lgpio'] : VISION_REQUIRED_MODULES;
28
+ const python = prepareRobotPython(basePython, script, requiredModules, reqFile);
29
+ if (!python)
30
+ process.exit(1);
31
+ const port = opts.port ?? String(DEFAULT_VISION_PORT);
32
+ const webhookUrl = opts.motionWebhookUrl ?? `http://127.0.0.1:${DEFAULT_MOTION_WEBHOOK_PORT}/`;
33
+ const motionActive = opts.motionActive ?? true; // armed by default — "vision trigger as production system"
34
+ const args = [script, '--port', port, '--motion-webhook-url', webhookUrl];
35
+ if (motionActive)
36
+ args.push('--motion-active');
37
+ // The original RoboVision audio selector lives in audio_server_pi.py.
38
+ // Start it beside the camera service on real Pi robots so /devices and
39
+ // /set-device remain the authoritative source for Park dropdowns.
40
+ let audioScript = null;
41
+ if (isPiArm()) {
42
+ audioScript = await findVisionAudioPath();
43
+ }
44
+ console.log(chalk.bold('\n robopark vision-agent'));
45
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
46
+ console.log(` port: ${chalk.cyan(port)}`);
47
+ console.log(` webhook: ${chalk.cyan(webhookUrl)}`);
48
+ console.log(` motion: ${motionActive ? chalk.green('armed') : chalk.dim('off')}`);
49
+ if (audioScript)
50
+ console.log(` audio: ${chalk.green('RoboVision audio server :8000')}`);
51
+ console.log(` motors: ${chalk.dim('managed independently by robopark motor :8001')}`);
52
+ console.log();
53
+ if (opts.foreground) {
54
+ const audio = audioScript
55
+ ? spawn(python, [audioScript], { stdio: 'inherit', env: { ...process.env, ROBOPARK_AUDIO_SERVER: '1' } })
56
+ : null;
57
+ const camera = spawn(python, args, { stdio: 'inherit' });
58
+ let stopping = false;
59
+ const stopChildren = () => {
60
+ if (stopping)
61
+ return;
62
+ stopping = true;
63
+ camera.kill('SIGTERM');
64
+ audio?.kill('SIGTERM');
65
+ };
66
+ process.once('SIGINT', stopChildren);
67
+ process.once('SIGTERM', stopChildren);
68
+ // If either required media service exits, tear down its sibling. The
69
+ // robot runtime then restarts one coherent pair instead of accumulating
70
+ // orphan audio servers and duplicate owners of ports/devices.
71
+ await new Promise((resolve) => {
72
+ let resolved = false;
73
+ const finish = () => {
74
+ if (resolved)
75
+ return;
76
+ resolved = true;
77
+ stopChildren();
78
+ resolve();
79
+ };
80
+ camera.once('close', finish);
81
+ audio?.once('close', finish);
82
+ });
83
+ return;
84
+ }
85
+ if (audioScript) {
86
+ const audio = spawn(python, [audioScript], {
87
+ stdio: 'ignore', detached: true, env: { ...process.env, ROBOPARK_AUDIO_SERVER: '1' },
88
+ });
89
+ audio.unref();
90
+ }
91
+ const proc = spawn(python, args, {
92
+ stdio: 'ignore',
93
+ detached: true,
94
+ env: { ...process.env, ROBOPARK_VISION_AGENT: '1' },
95
+ });
96
+ proc.unref();
97
+ console.log(chalk.green(' ✓ vision agent started in background'));
98
+ }
@@ -0,0 +1,81 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import chalk from 'chalk';
3
+ import { unregisterAutoStart } from './auto-start.js';
4
+ import { resolveContext } from './profile.js';
5
+ import { roboparkSetup } from './setup.js';
6
+ function runtimeUnit(name) {
7
+ return `robopark-robot-runtime-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}.service`;
8
+ }
9
+ export async function roboparkVisionUp(config, name, opts) {
10
+ if (opts.lan && opts.tailscale)
11
+ throw new Error('choose exactly one of --lan or --tailscale');
12
+ const context = await resolveContext({ hubUrl: opts.hubUrl, token: opts.token });
13
+ if (!context.hubUrl) {
14
+ throw new Error('no hub resolved; run `robopark hub-use --hub-url http://HUB_IP:47913 --token TOKEN` once');
15
+ }
16
+ if (!context.token) {
17
+ throw new Error('no mesh token resolved; run `robopark hub-use --hub-url http://HUB_IP:47913 --token TOKEN` once');
18
+ }
19
+ console.log(chalk.bold('\n robopark vision up'));
20
+ console.log(chalk.dim(' ' + '-'.repeat(52)));
21
+ console.log(` robot: ${chalk.cyan(name)}`);
22
+ console.log(` camera: ${chalk.cyan(opts.videoDevice ?? 'auto')}`);
23
+ console.log(` relay: ${chalk.green('always on, independent of voice calls')}`);
24
+ console.log();
25
+ await roboparkSetup(config, {
26
+ role: 'robot',
27
+ name,
28
+ site: context.site,
29
+ hub: context.hubUrl,
30
+ token: context.token,
31
+ port: opts.port ?? '47913',
32
+ character: opts.character,
33
+ videoDevice: opts.videoDevice ?? 'auto',
34
+ // A vision-only registration must never reserve the production microphone.
35
+ audioDevice: 'none',
36
+ lan: opts.tailscale ? false : true,
37
+ tailscale: Boolean(opts.tailscale),
38
+ start: true,
39
+ autoStart: opts.autoStart !== false,
40
+ yes: true,
41
+ });
42
+ console.log(chalk.dim(` dashboard relay: /fed/media/robots/${encodeURIComponent(name)}/video_feed`));
43
+ }
44
+ export function roboparkVisionDown(name) {
45
+ const result = unregisterAutoStart('robot-runtime', name);
46
+ if (!result.ok)
47
+ throw new Error(result.message);
48
+ console.log(chalk.green(` ${result.message}`));
49
+ }
50
+ export function roboparkVisionRestart(name) {
51
+ if (process.platform !== 'linux') {
52
+ throw new Error('vision restart currently requires systemd; rerun `robopark vision up NAME` on this platform');
53
+ }
54
+ const unit = runtimeUnit(name);
55
+ const result = spawnSync('systemctl', ['restart', unit], { encoding: 'utf8' });
56
+ if (result.status !== 0)
57
+ throw new Error(String(result.stderr || result.stdout || `could not restart ${unit}`).trim());
58
+ console.log(chalk.green(` restarted ${unit}`));
59
+ }
60
+ export async function roboparkVisionStatus(name) {
61
+ const unit = runtimeUnit(name);
62
+ const service = process.platform === 'linux'
63
+ ? spawnSync('systemctl', ['is-active', unit], { encoding: 'utf8' }).stdout.trim()
64
+ : 'unknown';
65
+ let camera = 'unreachable';
66
+ try {
67
+ const response = await fetch('http://127.0.0.1:5000/api/camera/status', {
68
+ signal: AbortSignal.timeout(3_000),
69
+ });
70
+ camera = response.ok ? 'ready' : `HTTP ${response.status}`;
71
+ }
72
+ catch {
73
+ // Printed as unreachable below.
74
+ }
75
+ console.log(chalk.bold(`\n ${name} vision`));
76
+ console.log(` service: ${service === 'active' ? chalk.green(service) : chalk.yellow(service)}`);
77
+ console.log(` camera: ${camera === 'ready' ? chalk.green(camera) : chalk.yellow(camera)}`);
78
+ console.log(` local: http://127.0.0.1:5000/video_feed`);
79
+ if (service !== 'active' || camera !== 'ready')
80
+ process.exitCode = 1;
81
+ }