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
|
@@ -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
|
+
}
|