infinicode 2.8.108 → 2.8.113
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/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +404 -155
- package/dist/kernel/mcp/mcp-server.js +61 -0
- package/dist/robopark/python-env.d.ts +1 -0
- package/dist/robopark/python-env.js +3 -0
- package/dist/robopark/robot-runtime.d.ts +1 -0
- package/dist/robopark/robot-runtime.js +52 -10
- package/dist/robopark/setup.d.ts +1 -0
- package/dist/robopark/setup.js +2 -0
- package/dist/robopark-cli.js +4 -0
- package/docs/ROBOPARK_ROBOVOICE_SIMPLIFICATION_PLAN.html +139 -0
- package/package.json +5 -2
- package/packages/robopark/pi-client/motor_bridge.py +10 -7
- package/packages/robopark/scheduler/main.py +144 -19
- package/packages/robopark/scheduler/robot_supervisor.py +46 -15
- package/packages/robopark/vision/motor_server.py +44 -18
- package/scripts/integrate-robovoice-control-center.mjs +423 -0
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
14
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
16
19
|
const text = (v) => ({
|
|
17
20
|
content: [{ type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v, null, 2) }],
|
|
18
21
|
});
|
|
@@ -177,6 +180,64 @@ export function createMcpServer(deps) {
|
|
|
177
180
|
kernel.notify(`[voice${room ? ':' + room : ''}] ${say}`);
|
|
178
181
|
return text({ ok: true, spoken: say, room });
|
|
179
182
|
});
|
|
183
|
+
// MCP never receives raw GPIO access. It may only request a saved sequence;
|
|
184
|
+
// the scheduler enforces the active room and assigned character allowlist.
|
|
185
|
+
const roboparkScheduler = (process.env.ROBOPARK_SCHEDULER_URL || 'http://127.0.0.1:8080').replace(/\/+$/, '');
|
|
186
|
+
const roboparkAgentToken = () => {
|
|
187
|
+
const configured = process.env.ROBOPARK_AGENT_TOKEN?.trim();
|
|
188
|
+
if (configured)
|
|
189
|
+
return configured;
|
|
190
|
+
const tokenFile = process.env.ROBOPARK_AGENT_TOKEN_FILE
|
|
191
|
+
|| join(process.env.SCHEDULER_DATA_DIR || join(homedir(), '.robopark', 'scheduler'), '.robopark-agent-token');
|
|
192
|
+
try {
|
|
193
|
+
return existsSync(tokenFile) ? readFileSync(tokenFile, 'utf8').trim() : '';
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return '';
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
server.registerTool('robopark_room_capabilities', {
|
|
200
|
+
title: 'RoboPark room capabilities',
|
|
201
|
+
description: 'Read the character and exact saved motor sequences allowed in an active RoboPark room.',
|
|
202
|
+
inputSchema: { room: z.string().min(1).describe('active LiveKit room name') },
|
|
203
|
+
}, async ({ room }) => {
|
|
204
|
+
const response = await fetch(`${roboparkScheduler}/api/devices/by-room/${encodeURIComponent(room)}/voice-config`, { signal: AbortSignal.timeout(5_000) });
|
|
205
|
+
if (!response.ok)
|
|
206
|
+
return text({ ok: false, status: response.status, error: await response.text() });
|
|
207
|
+
const config = await response.json();
|
|
208
|
+
return text({
|
|
209
|
+
ok: true,
|
|
210
|
+
characterId: config.character_id,
|
|
211
|
+
characterName: config.character_name,
|
|
212
|
+
allowedMotorSequences: config.allowed_motor_sequences ?? [],
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
server.registerTool('robopark_run_motor_sequence', {
|
|
216
|
+
title: 'Run an allowed RoboPark motor sequence',
|
|
217
|
+
description: 'Queue one validated saved sequence. Character policy is enforced server-side and raw GPIO is never accepted.',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
room: z.string().min(1).describe('active LiveKit room name'),
|
|
220
|
+
sequenceId: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,31}$/),
|
|
221
|
+
},
|
|
222
|
+
}, async ({ room, sequenceId }) => {
|
|
223
|
+
const agentToken = roboparkAgentToken();
|
|
224
|
+
if (!agentToken)
|
|
225
|
+
return text({ ok: false, error: 'RoboPark agent token is not initialized; start the scheduler first' });
|
|
226
|
+
const response = await fetch(`${roboparkScheduler}/api/devices/by-room/${encodeURIComponent(room)}/motor-sequences/${encodeURIComponent(sequenceId)}/run`, {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: { 'X-RoboPark-Agent-Token': agentToken },
|
|
229
|
+
signal: AbortSignal.timeout(8_000),
|
|
230
|
+
});
|
|
231
|
+
const body = await response.text();
|
|
232
|
+
let parsed = body;
|
|
233
|
+
try {
|
|
234
|
+
parsed = JSON.parse(body);
|
|
235
|
+
}
|
|
236
|
+
catch { /* retain diagnostic text */ }
|
|
237
|
+
return text(response.ok
|
|
238
|
+
? { ok: true, queued: parsed }
|
|
239
|
+
: { ok: false, status: response.status, error: parsed });
|
|
240
|
+
});
|
|
180
241
|
// ── Autonomy (proactive goals) — only if a goal loop is wired ───────────────
|
|
181
242
|
if (deps.autonomy) {
|
|
182
243
|
const auto = deps.autonomy;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Modules app_pi_clean.py imports — kept in sync with requirements_pi_unified.txt. */
|
|
2
2
|
export declare const VISION_REQUIRED_MODULES: string[];
|
|
3
3
|
export declare function findSchedulerPath(): Promise<string | null>;
|
|
4
|
+
export declare function findRobotSupervisorPath(): Promise<string | null>;
|
|
4
5
|
export declare function findVisionPath(): Promise<string | null>;
|
|
5
6
|
export declare function findVisionAudioPath(): Promise<string | null>;
|
|
6
7
|
export declare function findVisionMotorPath(): Promise<string | null>;
|
|
@@ -36,6 +36,9 @@ async function findPackageScript(relativePath) {
|
|
|
36
36
|
export async function findSchedulerPath() {
|
|
37
37
|
return findPackageScript('scheduler/preview_agent.py');
|
|
38
38
|
}
|
|
39
|
+
export async function findRobotSupervisorPath() {
|
|
40
|
+
return findPackageScript('scheduler/robot_supervisor.py');
|
|
41
|
+
}
|
|
39
42
|
export async function findVisionPath() {
|
|
40
43
|
return findPackageScript('vision/app_pi_clean.py');
|
|
41
44
|
}
|
|
@@ -14,8 +14,10 @@ import { dirname, join } from 'node:path';
|
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import chalk from 'chalk';
|
|
16
16
|
import { resolveInfinicodeBin } from './serve.js';
|
|
17
|
+
import { findPython, findRobotSupervisorPath, prepareRobotPython } from './python-env.js';
|
|
17
18
|
const VISION_URL = 'http://127.0.0.1:5000/api/media/inventory';
|
|
18
19
|
const VISION_STATUS_URL = 'http://127.0.0.1:5000/api/camera/status';
|
|
20
|
+
const MOTOR_SERVER_URL = 'http://127.0.0.1:8001';
|
|
19
21
|
const RESTART_DELAY_MS = 5_000;
|
|
20
22
|
const MEDIA_WATCHDOG_INTERVAL_MS = 10_000;
|
|
21
23
|
const SCHEDULER_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
@@ -64,6 +66,8 @@ function startSchedulerHeartbeat(opts, livekitUrl) {
|
|
|
64
66
|
let identity = readSchedulerIdentity();
|
|
65
67
|
let stopped = false;
|
|
66
68
|
let inFlight = false;
|
|
69
|
+
let motorDiscoveryQueued = false;
|
|
70
|
+
let lastMotorDiscoveryAttempt = 0;
|
|
67
71
|
const bootstrap = async () => {
|
|
68
72
|
const response = await fetch(`${opts.schedulerUrl.replace(/\/+$/, '')}/api/devices/bootstrap`, {
|
|
69
73
|
method: 'POST',
|
|
@@ -71,7 +75,13 @@ function startSchedulerHeartbeat(opts, livekitUrl) {
|
|
|
71
75
|
'content-type': 'application/json',
|
|
72
76
|
'x-robopark-mesh-token': opts.token,
|
|
73
77
|
},
|
|
74
|
-
body: JSON.stringify({
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
name: opts.name,
|
|
80
|
+
character_id: opts.character,
|
|
81
|
+
lan_ip: currentLanIp(),
|
|
82
|
+
livekit_url: livekitUrl,
|
|
83
|
+
motor_server_url: opts.vision === false ? undefined : MOTOR_SERVER_URL,
|
|
84
|
+
}),
|
|
75
85
|
signal: AbortSignal.timeout(10_000),
|
|
76
86
|
});
|
|
77
87
|
if (!response.ok)
|
|
@@ -101,7 +111,10 @@ function startSchedulerHeartbeat(opts, livekitUrl) {
|
|
|
101
111
|
'content-type': 'application/json',
|
|
102
112
|
'x-robopark-mesh-token': opts.token,
|
|
103
113
|
},
|
|
104
|
-
body: JSON.stringify({
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
status: 'online', ip: currentLanIp(), livekit_url: livekitUrl,
|
|
116
|
+
motor_server_url: opts.vision === false ? undefined : MOTOR_SERVER_URL,
|
|
117
|
+
}),
|
|
105
118
|
signal: AbortSignal.timeout(10_000),
|
|
106
119
|
});
|
|
107
120
|
if (response.status === 401 || response.status === 404) {
|
|
@@ -113,12 +126,29 @@ function startSchedulerHeartbeat(opts, livekitUrl) {
|
|
|
113
126
|
'content-type': 'application/json',
|
|
114
127
|
'x-robopark-mesh-token': opts.token,
|
|
115
128
|
},
|
|
116
|
-
body: JSON.stringify({
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
status: 'online', ip: currentLanIp(), livekit_url: livekitUrl,
|
|
131
|
+
motor_server_url: opts.vision === false ? undefined : MOTOR_SERVER_URL,
|
|
132
|
+
}),
|
|
117
133
|
signal: AbortSignal.timeout(10_000),
|
|
118
134
|
});
|
|
119
135
|
}
|
|
120
136
|
if (!response.ok)
|
|
121
137
|
throw new Error(`heartbeat HTTP ${response.status}`);
|
|
138
|
+
if (opts.vision !== false && !motorDiscoveryQueued && Date.now() - lastMotorDiscoveryAttempt > 30_000) {
|
|
139
|
+
lastMotorDiscoveryAttempt = Date.now();
|
|
140
|
+
const discovery = await fetch(`${opts.schedulerUrl.replace(/\/+$/, '')}/api/robots/${encodeURIComponent(identity.deviceId)}/motors/discover`, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: {
|
|
143
|
+
authorization: `Bearer ${identity.deviceToken}`,
|
|
144
|
+
'x-robopark-mesh-token': opts.token,
|
|
145
|
+
},
|
|
146
|
+
signal: AbortSignal.timeout(10_000),
|
|
147
|
+
});
|
|
148
|
+
motorDiscoveryQueued = discovery.ok;
|
|
149
|
+
if (discovery.ok)
|
|
150
|
+
console.log(chalk.green(' motor registry discovery queued'));
|
|
151
|
+
}
|
|
122
152
|
}
|
|
123
153
|
catch (error) {
|
|
124
154
|
console.error(chalk.yellow(` scheduler heartbeat watchdog: ${error instanceof Error ? error.message : String(error)}`));
|
|
@@ -206,6 +236,12 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
206
236
|
const infinicode = await resolveInfinicodeBin();
|
|
207
237
|
if (!infinicode)
|
|
208
238
|
throw new Error('could not resolve infinicode CLI');
|
|
239
|
+
const supervisorScript = await findRobotSupervisorPath();
|
|
240
|
+
if (!supervisorScript)
|
|
241
|
+
throw new Error('could not resolve RoboPark robot supervisor');
|
|
242
|
+
const supervisorPython = prepareRobotPython(findPython(), supervisorScript, ['httpx'], 'requirements-robot.txt');
|
|
243
|
+
if (!supervisorPython)
|
|
244
|
+
throw new Error('could not prepare RoboPark robot supervisor Python environment');
|
|
209
245
|
children.push({
|
|
210
246
|
label: 'mesh',
|
|
211
247
|
command: infinicode.node,
|
|
@@ -221,9 +257,17 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
221
257
|
'--motion-webhook-url', 'http://127.0.0.1:5057/'],
|
|
222
258
|
env: {
|
|
223
259
|
ROBOPARK_CAMERA_DEVICE: hardwareVideo,
|
|
260
|
+
ROBOPARK_MESH_TOKEN: opts.token,
|
|
261
|
+
ROBOPARK_MOTOR_TOKEN: opts.token,
|
|
224
262
|
},
|
|
225
263
|
});
|
|
226
264
|
}
|
|
265
|
+
children.push({
|
|
266
|
+
label: 'supervisor',
|
|
267
|
+
command: supervisorPython,
|
|
268
|
+
args: [supervisorScript, '--commands-only'],
|
|
269
|
+
env: { ROBOPARK_MESH_TOKEN: opts.token },
|
|
270
|
+
});
|
|
227
271
|
const previewArgs = [cli, 'preview-agent', '--foreground',
|
|
228
272
|
'--scheduler-url', opts.schedulerUrl, '--robot-id', opts.name,
|
|
229
273
|
'--video-device', hardwareVideo,
|
|
@@ -375,10 +419,8 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
375
419
|
await Promise.all([...children.filter(entry => entry.label !== 'mesh'), preview].map(stopChild));
|
|
376
420
|
if (stopping)
|
|
377
421
|
return;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
start(children[1]);
|
|
381
|
-
}
|
|
422
|
+
for (const entry of children)
|
|
423
|
+
start(entry);
|
|
382
424
|
start(preview);
|
|
383
425
|
recycling = false;
|
|
384
426
|
console.log(chalk.green(' robot runtime updated and healthy'));
|
|
@@ -396,7 +438,7 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
396
438
|
console.log(` robot: ${chalk.cyan(opts.name)}`);
|
|
397
439
|
console.log(` scheduler: ${chalk.cyan(opts.schedulerUrl)}`);
|
|
398
440
|
console.log(` network: ${chalk.cyan(network)}`);
|
|
399
|
-
console.log(` services: ${opts.vision === false ? 'mesh + preview' : 'mesh + RoboVision camera/audio/motors + preview'}`);
|
|
441
|
+
console.log(` services: ${opts.vision === false ? 'mesh + preview + command supervisor' : 'mesh + RoboVision camera/audio/motors + preview + command supervisor'}`);
|
|
400
442
|
console.log();
|
|
401
443
|
if (opts.enrollmentToken) {
|
|
402
444
|
console.log(chalk.dim(' clearing stale device identity for fresh enrollment…'));
|
|
@@ -405,10 +447,10 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
405
447
|
// Identity and inventory reporting start immediately. RoboVision may still
|
|
406
448
|
// be installing or recovering without making the robot disappear.
|
|
407
449
|
const stopSchedulerHeartbeat = startSchedulerHeartbeat(opts, livekitUrl);
|
|
408
|
-
|
|
450
|
+
for (const entry of children)
|
|
451
|
+
start(entry);
|
|
409
452
|
start(preview);
|
|
410
453
|
if (opts.vision !== false) {
|
|
411
|
-
start(children[1]);
|
|
412
454
|
console.log(chalk.dim(' RoboVision health check running in background…'));
|
|
413
455
|
void waitForVision().then(ready => {
|
|
414
456
|
if (!stopping)
|
package/dist/robopark/setup.d.ts
CHANGED
package/dist/robopark/setup.js
CHANGED
|
@@ -212,6 +212,8 @@ async function setupRobot(config, opts, ctx, internal) {
|
|
|
212
212
|
];
|
|
213
213
|
if (opts.enrollmentToken)
|
|
214
214
|
runtimeArgs.push('--enrollment-token', opts.enrollmentToken);
|
|
215
|
+
if (opts.character)
|
|
216
|
+
runtimeArgs.push('--character', opts.character);
|
|
215
217
|
if (!visionEnabled)
|
|
216
218
|
runtimeArgs.push('--no-vision');
|
|
217
219
|
console.log(chalk.dim(' single robot command:'));
|
package/dist/robopark-cli.js
CHANGED
|
@@ -170,6 +170,7 @@ robotProgram
|
|
|
170
170
|
.requiredOption('--token <token>', 'shared mesh token')
|
|
171
171
|
.option('--scheduler-url <url>', 'scheduler URL (derived from --hub-url when omitted)')
|
|
172
172
|
.option('--enrollment-token <token>', 'one-time scheduler enrollment token for a fresh device')
|
|
173
|
+
.option('--character <id>', 'character preset assigned during canonical device bootstrap')
|
|
173
174
|
.option('--port <port>', 'robot mesh port', '47913')
|
|
174
175
|
.option('--lan', 'use LAN transport (default)')
|
|
175
176
|
.option('--tailscale', 'use Tailscale transport')
|
|
@@ -188,6 +189,7 @@ robotProgram
|
|
|
188
189
|
port: opts.port,
|
|
189
190
|
network: opts.tailscale ? 'tailscale' : 'lan',
|
|
190
191
|
enrollmentToken: opts.enrollmentToken,
|
|
192
|
+
character: opts.character,
|
|
191
193
|
videoDevice: opts.videoDevice,
|
|
192
194
|
audioDevice: opts.audioDevice,
|
|
193
195
|
vision: opts.vision,
|
|
@@ -330,6 +332,7 @@ program
|
|
|
330
332
|
.option('--tailscale-auth <token>', 'Tailscale auth key for the hub')
|
|
331
333
|
.option('--tag <tag>', 'Tailscale tag filter')
|
|
332
334
|
.option('--enrollment-token <token>', 'one-time scheduler enrollment token for robots')
|
|
335
|
+
.option('--character <id>', 'character preset assigned during canonical device bootstrap')
|
|
333
336
|
.option('--lan', 'use LAN discovery and LAN hub/scheduler addresses (default for robots)')
|
|
334
337
|
.option('--tailscale', 'use Tailscale discovery and Tailscale hub/scheduler addresses')
|
|
335
338
|
.option('--video-device <dev>', 'default camera for robot preview agent', 'auto')
|
|
@@ -357,6 +360,7 @@ program
|
|
|
357
360
|
tailscaleAuth: opts.tailscaleAuth,
|
|
358
361
|
tag: opts.tag,
|
|
359
362
|
enrollmentToken: opts.enrollmentToken,
|
|
363
|
+
character: opts.character,
|
|
360
364
|
videoDevice: opts.videoDevice,
|
|
361
365
|
audioDevice: opts.audioDevice,
|
|
362
366
|
vision: opts.vision,
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>RoboPark + RoboVoice Production Simplification Plan</title>
|
|
7
|
+
<style>
|
|
8
|
+
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700;800&display=swap');
|
|
9
|
+
:root{--ink:#070706;--panel:#11110f;--panel2:#181712;--gold:#d8ad52;--gold2:#8d6725;--cream:#f3ead7;--muted:#a89f8e;--line:rgba(216,173,82,.22);--green:#72d39a;--red:#ef7b6f;--blue:#82b8ed}
|
|
10
|
+
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:radial-gradient(circle at 78% 0,rgba(151,105,24,.18),transparent 31rem),radial-gradient(circle at 4% 40%,rgba(82,64,27,.12),transparent 28rem),var(--ink);color:var(--cream);font-family:Manrope,sans-serif;line-height:1.55}
|
|
11
|
+
body:before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.18;background-image:linear-gradient(rgba(255,255,255,.018) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.018) 1px,transparent 1px);background-size:40px 40px}
|
|
12
|
+
.shell{width:min(1280px,calc(100% - 40px));margin:auto;padding:34px 0 90px}.top{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding-bottom:20px;position:sticky;top:0;z-index:5;background:rgba(7,7,6,.88);backdrop-filter:blur(18px)}
|
|
13
|
+
.brand{font-weight:800;letter-spacing:.08em}.brand span{color:var(--gold)}.tag{font:500 11px DM Mono,monospace;text-transform:uppercase;letter-spacing:.12em;border:1px solid var(--line);color:var(--gold);padding:8px 12px;border-radius:99px}.hero{padding:86px 0 54px;display:grid;grid-template-columns:1.25fr .75fr;gap:46px;align-items:end}.eyebrow{color:var(--gold);font:500 12px DM Mono,monospace;letter-spacing:.15em;text-transform:uppercase}.hero h1{font-size:clamp(42px,7vw,92px);line-height:.94;margin:18px 0 24px;letter-spacing:-.055em}.hero p{max-width:760px;font-size:18px;color:#c6bdad}.decision{border:1px solid var(--line);border-radius:22px;padding:26px;background:linear-gradient(145deg,rgba(216,173,82,.1),rgba(255,255,255,.015));box-shadow:0 28px 80px rgba(0,0,0,.35)}
|
|
14
|
+
h2{font-size:30px;letter-spacing:-.035em;margin:0 0 20px}h3{font-size:17px;margin:0 0 10px}.section{padding:54px 0;border-top:1px solid rgba(216,173,82,.13)}.section-head{display:grid;grid-template-columns:180px 1fr;gap:22px;margin-bottom:28px}.num{color:var(--gold);font:500 12px DM Mono,monospace;letter-spacing:.16em}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.card{background:linear-gradient(145deg,rgba(255,255,255,.035),rgba(255,255,255,.012));border:1px solid rgba(216,173,82,.16);border-radius:18px;padding:22px}.card p,.muted{color:var(--muted);font-size:14px}.owner{display:inline-flex;padding:5px 8px;border-radius:6px;background:rgba(216,173,82,.09);color:var(--gold);font:500 10px DM Mono,monospace;text-transform:uppercase;margin-bottom:16px}
|
|
15
|
+
.flow{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}.node{position:relative;padding:20px 16px;min-height:135px;border-radius:16px;background:var(--panel);border:1px solid var(--line)}.node:not(:last-child):after{content:">";position:absolute;right:-10px;top:49%;z-index:2;color:var(--gold);font:600 16px DM Mono}.node b{display:block;margin-bottom:8px}.node small{color:var(--muted)}
|
|
16
|
+
.split{display:grid;grid-template-columns:1fr 1fr;gap:18px}.good,.remove{border-radius:18px;padding:24px;border:1px solid}.good{border-color:rgba(114,211,154,.25);background:rgba(114,211,154,.055)}.remove{border-color:rgba(239,123,111,.24);background:rgba(239,123,111,.045)}ul{padding-left:19px;margin:12px 0 0}li{margin:8px 0;color:#c9c0af}.command{font:400 12px/1.65 DM Mono,monospace;background:#050504;border:1px solid var(--line);border-radius:13px;padding:16px;overflow:auto;color:#ead59f;white-space:pre-wrap}.commands{display:grid;grid-template-columns:repeat(2,1fr);gap:14px}.cmd-title{display:flex;justify-content:space-between;align-items:center;margin-bottom:9px}.cmd-title span{font:500 10px DM Mono;color:var(--muted);text-transform:uppercase}
|
|
17
|
+
table{width:100%;border-collapse:collapse;background:rgba(255,255,255,.018);border-radius:18px;overflow:hidden}th,td{text-align:left;padding:15px;border-bottom:1px solid rgba(216,173,82,.12);vertical-align:top}th{font:500 10px DM Mono;color:var(--gold);letter-spacing:.1em;text-transform:uppercase}td{font-size:14px;color:#c9c0af}td:first-child{font-weight:700;color:var(--cream)}
|
|
18
|
+
.phase{display:grid;grid-template-columns:90px 1fr 160px;gap:20px;padding:22px 0;border-bottom:1px solid rgba(216,173,82,.12)}.phase-id{font:500 12px DM Mono;color:var(--gold)}.gate{color:var(--green);font:500 11px DM Mono}.state{display:flex;flex-wrap:wrap;gap:8px}.state span{padding:8px 11px;border-radius:8px;border:1px solid var(--line);background:var(--panel);font:500 11px DM Mono}.state i{color:var(--gold);font-style:normal;padding-top:7px}.callout{margin-top:22px;padding:20px 22px;border-left:3px solid var(--gold);background:rgba(216,173,82,.06);color:#d7cdbb}.footer{padding-top:36px;color:var(--muted);font-size:12px;font-family:DM Mono,monospace}
|
|
19
|
+
@media(max-width:850px){.hero,.section-head,.split{grid-template-columns:1fr}.grid,.flow,.commands{grid-template-columns:1fr}.node:not(:last-child):after{content:"v";right:50%;top:auto;bottom:-14px}.phase{grid-template-columns:1fr}.top{position:relative}.shell{width:min(100% - 24px,1280px)}}
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
<body><main class="shell">
|
|
23
|
+
<header class="top"><div class="brand">ROBO<span>PARK</span> / ROBOVOICE</div><div class="tag">Production simplification plan</div></header>
|
|
24
|
+
<section class="hero">
|
|
25
|
+
<div><div class="eyebrow">One proven call path. One control plane.</div><h1>Remove orchestration from the conversation.</h1><p>Keep the existing RoboVoice call stack exactly where it works. Make each Pi a headless participant with local motion detection, camera, microphone and speaker. Rebuild RoboPark as fleet control, monitoring, history and remote operations around that path.</p></div>
|
|
26
|
+
<aside class="decision"><div class="owner">Architecture decision</div><h3>RoboVoice owns every call</h3><p class="muted">RoboPark will no longer allocate, duplicate or reinterpret voice sessions. It observes and controls the same call lifecycle used by the existing RoboVoice web UI.</p></aside>
|
|
27
|
+
</section>
|
|
28
|
+
|
|
29
|
+
<section class="section"><div class="section-head"><div class="num">01 / TARGET</div><div><h2>Simplified production topology</h2><p class="muted">The session scheduler leaves the critical path. LiveKit remains media transport, not business orchestration.</p></div></div>
|
|
30
|
+
<div class="flow">
|
|
31
|
+
<div class="node"><b>Pi motion</b><small>Local detector decides when to start. No remote polling required.</small></div>
|
|
32
|
+
<div class="node"><b>Headless Pi call</b><small>Camera + mic publish. Speaker subscribes. Stable hardware profile.</small></div>
|
|
33
|
+
<div class="node"><b>LiveKit room</b><small>One robot and one RoboVoice agent. Strict 1:1 session.</small></div>
|
|
34
|
+
<div class="node"><b>RoboVoice</b><small>Existing STT, LLM, TTS, VAD, interruption and character behavior.</small></div>
|
|
35
|
+
<div class="node"><b>Control Center</b><small>Observes events, video, transcripts, health and remote commands.</small></div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="callout"><b>Control path:</b> browser -> livekit-1 -> robot LAN satellite. <b>Voice path:</b> Pi -> LiveKit -> RoboVoice -> LiveKit -> Pi. The Control Center is never required for an onsite conversation to continue.</div>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
<section class="section"><div class="section-head"><div class="num">02 / OWNERSHIP</div><div><h2>One owner for every responsibility</h2></div></div>
|
|
41
|
+
<div class="grid">
|
|
42
|
+
<article class="card"><div class="owner">RoboVoice-main</div><h3>Conversation authority</h3><p>Characters, prompts, voice stacks, STT, LLM, TTS, VAD, endpointing, interruptions, LiveKit agent jobs, transcripts and call timing.</p></article>
|
|
43
|
+
<article class="card"><div class="owner">Robot Pi</div><h3>Hardware authority</h3><p>Motion detection, camera capture, microphone capture, speaker playback, local media locks, reconnects and device health.</p></article>
|
|
44
|
+
<article class="card"><div class="owner">RoboPark Control</div><h3>Operations authority</h3><p>Fleet identity, production toggles, health, history, audit, always-on preview, tests, configuration editing and remote lifecycle commands.</p></article>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="split" style="margin-top:18px"><div class="good"><h3>Keep</h3><ul><li>Existing RoboVoice web-call AgentSession behavior</li><li>Existing Pi-local motion and trigger logic</li><li>Existing RoboVision camera feed and hardware access</li><li>LiveKit as the proven bidirectional audio transport</li><li>Infinicode mesh for remote management through livekit-1</li></ul></div><div class="remove"><h3>Remove from the critical path</h3><ul><li>Scheduler session allocation before a robot can talk</li><li>Duplicate character and voice-stack interpretations</li><li>Multiple media owners opening the same ALSA/V4L2 devices</li><li>Camera availability tied to conversation sessions</li><li>Alias records such as separate BMW and VIXEN devices</li></ul></div></div>
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<section class="section"><div class="section-head"><div class="num">03 / CONTRACT</div><div><h2>Direct 1:1 call lifecycle</h2></div></div>
|
|
50
|
+
<div class="state"><span>IDLE</span><i>></i><span>MOTION</span><i>></i><span>CONNECTING</span><i>></i><span>GREETING</span><i>></i><span>LISTENING</span><i>></i><span>THINKING</span><i>></i><span>SPEAKING</span><i>></i><span>ENDED</span></div>
|
|
51
|
+
<table style="margin-top:24px"><thead><tr><th>Rule</th><th>Production contract</th></tr></thead><tbody>
|
|
52
|
+
<tr><td>Room identity</td><td>One deterministic room per active robot call. The second start request returns the current call instead of creating another room.</td></tr>
|
|
53
|
+
<tr><td>Motion mode</td><td>Local motion opens the same RoboVoice call used by the web UI, with the robot as the headless browser participant.</td></tr>
|
|
54
|
+
<tr><td>Manual mode</td><td>The operator starts the same call from Control Center or CLI. Only the initiation source differs.</td></tr>
|
|
55
|
+
<tr><td>Camera</td><td>RoboVision MJPEG remains always-on and independent of LiveKit calls. LiveKit video is optional for vision tools, not dashboard preview.</td></tr>
|
|
56
|
+
<tr><td>Events</td><td>Robot and RoboVoice emit state transitions to a thin event API. Events never block audio.</td></tr>
|
|
57
|
+
<tr><td>Failure</td><td>If Control Center or its database is down, the local motion-to-RoboVoice call still works.</td></tr>
|
|
58
|
+
</tbody></table>
|
|
59
|
+
</section>
|
|
60
|
+
|
|
61
|
+
<section class="section"><div class="section-head"><div class="num">04 / CONTROL API</div><div><h2>Thin management API inside RoboVoice</h2><p class="muted">Extend the proven backend instead of running a second scheduler database and state machine.</p></div></div>
|
|
62
|
+
<div class="grid">
|
|
63
|
+
<article class="card"><h3>Robot registry</h3><p>Canonical ID, display name, character, hardware profile, site, production mode and last heartbeat.</p></article>
|
|
64
|
+
<article class="card"><h3>Session history</h3><p>Call source, timestamps, state events, transcript, latency, errors, end reason and selected voice stack.</p></article>
|
|
65
|
+
<article class="card"><h3>Remote commands</h3><p>Start/stop call, arm motion, reconnect camera, media tests, restart runtime and update package.</p></article>
|
|
66
|
+
</div>
|
|
67
|
+
<table style="margin-top:18px"><thead><tr><th>Endpoint family</th><th>Purpose</th></tr></thead><tbody>
|
|
68
|
+
<tr><td>/api/control/robots</td><td>Registry, heartbeat, production mode, hardware inventory and effective configuration.</td></tr>
|
|
69
|
+
<tr><td>/api/control/sessions</td><td>Start, stop, active call, history, transcript, latency and end reason.</td></tr>
|
|
70
|
+
<tr><td>/api/control/events</td><td>Append-only lifecycle events and server-sent live updates.</td></tr>
|
|
71
|
+
<tr><td>/api/control/media</td><td>Always-on camera relay, microphone diagnostics and speaker test requests.</td></tr>
|
|
72
|
+
<tr><td>/api/control/config</td><td>Existing RoboVoice characters, prompts, voice stacks and provider settings.</td></tr>
|
|
73
|
+
</tbody></table>
|
|
74
|
+
</section>
|
|
75
|
+
|
|
76
|
+
<section class="section"><div class="section-head"><div class="num">05 / UI</div><div><h2>Port the complete Control Center</h2></div></div>
|
|
77
|
+
<div class="grid">
|
|
78
|
+
<article class="card"><h3>Fleet</h3><p>Rich robot cards, live state, production mode, camera thumbnail, current character, latency, health and alerts.</p></article>
|
|
79
|
+
<article class="card"><h3>Park</h3><p>Spatial robot view, always-on camera, fullscreen operations, current conversation stage and quick controls.</p></article>
|
|
80
|
+
<article class="card"><h3>Live operations</h3><p>Current transcript, listening/thinking/speaking state, audio levels, errors, timers and remote stop.</p></article>
|
|
81
|
+
<article class="card"><h3>Sessions</h3><p>Persistent paged history, full transcripts, timeline, latency breakdown, selected stack and end reason.</p></article>
|
|
82
|
+
<article class="card"><h3>Voice Studio</h3><p>Keep the original RoboVoice call UI, browser devices, character editor, prompt editor and voice-stack editor.</p></article>
|
|
83
|
+
<article class="card"><h3>Setup + diagnostics</h3><p>Copy-ready commands, camera/audio tests, heartbeat, logs, update, permissions and audit trail.</p></article>
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
<section class="section"><div class="section-head"><div class="num">06 / CLI</div><div><h2>Small idempotent command surface</h2><p class="muted">One control URL, one robot token, one runtime per Pi. Re-running setup repairs configuration instead of creating duplicates.</p></div></div>
|
|
88
|
+
<div class="commands">
|
|
89
|
+
<div class="card"><div class="cmd-title"><b>Start control host</b><span>livekit-1</span></div><div class="command">robopark hub up --robovoice-path C:\ROBOVOICE-main --port 47913 --lan --tailscale --auto-start</div></div>
|
|
90
|
+
<div class="card"><div class="cmd-title"><b>Provision robot</b><span>Pi</span></div><div class="command">robopark robot install --name bmw --control-url http://192.168.0.9:47913 --token <robot-token> --profile usb-standard --motion always --auto-start</div></div>
|
|
91
|
+
<div class="card"><div class="cmd-title"><b>Always-on production</b><span>Operator</span></div><div class="command">robopark production on bmw
|
|
92
|
+
robopark production status bmw</div></div>
|
|
93
|
+
<div class="card"><div class="cmd-title"><b>Regular call</b><span>Operator</span></div><div class="command">robopark session start bmw
|
|
94
|
+
robopark session stop bmw</div></div>
|
|
95
|
+
<div class="card"><div class="cmd-title"><b>Verify hardware</b><span>Remote</span></div><div class="command">robopark test camera bmw
|
|
96
|
+
robopark test audio bmw
|
|
97
|
+
robopark test motion bmw
|
|
98
|
+
robopark test call bmw --require-turn</div></div>
|
|
99
|
+
<div class="card"><div class="cmd-title"><b>Operate runtime</b><span>Remote</span></div><div class="command">robopark status bmw
|
|
100
|
+
robopark logs bmw --follow
|
|
101
|
+
robopark restart bmw
|
|
102
|
+
robopark update bmw</div></div>
|
|
103
|
+
</div>
|
|
104
|
+
</section>
|
|
105
|
+
|
|
106
|
+
<section class="section"><div class="section-head"><div class="num">07 / RELIABILITY</div><div><h2>Hard production rules</h2></div></div>
|
|
107
|
+
<div class="grid">
|
|
108
|
+
<article class="card"><h3>Single media owner</h3><p>One Pi runtime opens camera, microphone and speaker. All tests request access through that owner.</p></article>
|
|
109
|
+
<article class="card"><h3>Stable hardware profile</h3><p>Match USB product identity and udev aliases, not volatile hw:2,0 indexes. Persist tested gain and volume.</p></article>
|
|
110
|
+
<article class="card"><h3>Independent watchdogs</h3><p>Camera, call and control reconnect independently. A camera fault does not restart voice; a UI fault does not stop motion.</p></article>
|
|
111
|
+
<article class="card"><h3>Offline tolerance</h3><p>Local motion continues using cached effective config. Events queue locally and upload after reconnect.</p></article>
|
|
112
|
+
<article class="card"><h3>Canonical identity</h3><p>One immutable robot ID with editable display name. BMW and VIXEN cannot become separate device records.</p></article>
|
|
113
|
+
<article class="card"><h3>Evidence-based tests</h3><p>A call passes only after transcript input, LLM output, TTS output and physical playback are all recorded.</p></article>
|
|
114
|
+
</div>
|
|
115
|
+
</section>
|
|
116
|
+
|
|
117
|
+
<section class="section"><div class="section-head"><div class="num">08 / MIGRATION</div><div><h2>BMW-first migration with rollback</h2></div></div>
|
|
118
|
+
<div class="phase"><div class="phase-id">PHASE 0</div><div><h3>Freeze and measure</h3><p class="muted">Record a successful RoboVoice web call and Pi local motion call. Capture configuration, latency and device paths. No architecture changes yet.</p></div><div class="gate">Gate: baseline saved</div></div>
|
|
119
|
+
<div class="phase"><div class="phase-id">PHASE 1</div><div><h3>Direct headless call</h3><p class="muted">Point BMW motion directly at the existing RoboVoice call entrypoint. Remove scheduler allocation from this path while retaining current rollback command.</p></div><div class="gate">Gate: 10 real turns</div></div>
|
|
120
|
+
<div class="phase"><div class="phase-id">PHASE 2</div><div><h3>Native control events</h3><p class="muted">Persist call lifecycle and transcripts from RoboVoice. Add Pi heartbeat and effective hardware/config reporting.</p></div><div class="gate">Gate: full history</div></div>
|
|
121
|
+
<div class="phase"><div class="phase-id">PHASE 3</div><div><h3>Port Control Center</h3><p class="muted">Connect Fleet, Park, Sessions, Voice Studio and Setup pages to the RoboVoice-native control API and camera relay.</p></div><div class="gate">Gate: no old API calls</div></div>
|
|
122
|
+
<div class="phase"><div class="phase-id">PHASE 4</div><div><h3>Unattended BMW soak</h3><p class="muted">Run motion sessions for four hours. Test reconnects, silence endings, repeated triggers, camera continuity and Control Center restart.</p></div><div class="gate">Gate: zero manual repair</div></div>
|
|
123
|
+
<div class="phase"><div class="phase-id">PHASE 5</div><div><h3>Fleet rollout</h3><p class="muted">Apply the identical USB hardware profile and runtime to each robot. Keep per-robot differences limited to identity, character and site.</p></div><div class="gate">Gate: all five green</div></div>
|
|
124
|
+
<div class="phase"><div class="phase-id">PHASE 6</div><div><h3>Deprecate legacy scheduler path</h3><p class="muted">Make old session-allocation endpoints read-only, export historical data, remove UI calls, then delete only after the rollback window.</p></div><div class="gate">Gate: 7-day stability</div></div>
|
|
125
|
+
</section>
|
|
126
|
+
|
|
127
|
+
<section class="section"><div class="section-head"><div class="num">09 / MVP EXIT</div><div><h2>Definition of production-ready</h2></div></div>
|
|
128
|
+
<table><thead><tr><th>Acceptance gate</th><th>Required evidence</th></tr></thead><tbody>
|
|
129
|
+
<tr><td>Motion-to-greeting</td><td>BMW begins audible greeting within 1.5 seconds of accepted motion.</td></tr>
|
|
130
|
+
<tr><td>Real conversation</td><td>At least ten consecutive onsite turns persist user transcript, LLM response, TTS completion and robot playback.</td></tr>
|
|
131
|
+
<tr><td>Call parity</td><td>Same RoboVoice AgentSession, character, model, endpointing and interruption behavior as the web UI call.</td></tr>
|
|
132
|
+
<tr><td>Always-on video</td><td>Camera remains visible before, during and after calls and reconnects automatically.</td></tr>
|
|
133
|
+
<tr><td>Failure isolation</td><td>Restarting Control Center does not stop motion detection or an active conversation.</td></tr>
|
|
134
|
+
<tr><td>Remote recovery</td><td>Operator can inspect logs, test hardware, restart and update without onsite assistance.</td></tr>
|
|
135
|
+
<tr><td>Soak</td><td>Four-hour BMW run and one-hour five-robot run complete without duplicate sessions or media ownership conflicts.</td></tr>
|
|
136
|
+
</tbody></table>
|
|
137
|
+
</section>
|
|
138
|
+
<footer class="footer">ROBOpark production architecture / RoboVoice-first control plane / generated 2026-07-18</footer>
|
|
139
|
+
</main></body></html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinicode",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.113",
|
|
4
4
|
"description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/kernel/index.js",
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"packages/robopark/pi-client",
|
|
32
32
|
"packages/robopark/vision",
|
|
33
33
|
"docs/ROBOPARK_PRODUCTION_MEMORY.md",
|
|
34
|
+
"docs/ROBOPARK_ROBOVOICE_SIMPLIFICATION_PLAN.html",
|
|
35
|
+
"scripts/integrate-robovoice-control-center.mjs",
|
|
34
36
|
".opencode/plugins",
|
|
35
37
|
".opencode/themes",
|
|
36
38
|
".opencode/tui.json",
|
|
@@ -43,7 +45,8 @@
|
|
|
43
45
|
"test": "npm run build && node --test \"test/**/*.test.mjs\"",
|
|
44
46
|
"prepublishOnly": "npm run build",
|
|
45
47
|
"kernel": "node bin/infinicode.js mission",
|
|
46
|
-
"typecheck": "tsc --noEmit"
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"robovoice:integrate": "node scripts/integrate-robovoice-control-center.mjs"
|
|
47
50
|
},
|
|
48
51
|
"keywords": [
|
|
49
52
|
"ai",
|
|
@@ -10,7 +10,8 @@ Command wire format (JSON on the data channel):
|
|
|
10
10
|
{"op": "stop"}
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
import asyncio
|
|
13
|
+
import asyncio
|
|
14
|
+
import os
|
|
14
15
|
import json
|
|
15
16
|
import logging
|
|
16
17
|
from typing import Awaitable, Callable, Optional
|
|
@@ -20,10 +21,12 @@ import httpx
|
|
|
20
21
|
log = logging.getLogger("robopark-pi.motor")
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
class MotorBridge:
|
|
24
|
-
def __init__(self, motor_server_url: Optional[str], dry_run: bool = False):
|
|
24
|
+
class MotorBridge:
|
|
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
|
-
self.dry_run = dry_run
|
|
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}"
|