infinicode 2.8.13 β†’ 2.8.16

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 (38) hide show
  1. package/dist/kernel/federation/dashboard-html.d.ts +1 -1
  2. package/dist/kernel/federation/dashboard-html.js +32 -1
  3. package/dist/robopark/auto-start.d.ts +1 -1
  4. package/dist/robopark/preview-agent-launcher.d.ts +3 -0
  5. package/dist/robopark/preview-agent-launcher.js +8 -0
  6. package/dist/robopark/python-env.d.ts +10 -4
  7. package/dist/robopark/python-env.js +28 -16
  8. package/dist/robopark/setup.d.ts +1 -0
  9. package/dist/robopark/setup.js +22 -0
  10. package/dist/robopark/vision-agent-launcher.d.ts +7 -0
  11. package/dist/robopark/vision-agent-launcher.js +55 -0
  12. package/dist/robopark-cli.js +16 -0
  13. package/package.json +3 -1
  14. package/packages/robopark/pi-client/README.md +17 -0
  15. package/packages/robopark/pi-client/_install_steps.sh +29 -0
  16. package/packages/robopark/pi-client/client.py +305 -0
  17. package/packages/robopark/pi-client/install.sh +41 -0
  18. package/packages/robopark/pi-client/join_convo.sh +55 -0
  19. package/packages/robopark/pi-client/livekit_bridge.py +289 -0
  20. package/packages/robopark/pi-client/motor_bridge.py +82 -0
  21. package/packages/robopark/pi-client/pi_ui.py +375 -0
  22. package/packages/robopark/pi-client/requirements.txt +10 -0
  23. package/packages/robopark/pi-client/robopark-pi-client.service +26 -0
  24. package/packages/robopark/pi-client/robopark-pi-ui.service +24 -0
  25. package/packages/robopark/pi-client/smoke_bridge.py +15 -0
  26. package/packages/robopark/pi-client/stub_motor_server.py +46 -0
  27. package/packages/robopark/scheduler/main.py +45 -9
  28. package/packages/robopark/scheduler/preview_agent.py +132 -3
  29. package/packages/robopark/vision/.env.example +7 -0
  30. package/packages/robopark/vision/README.md +66 -0
  31. package/packages/robopark/vision/app_pi_clean.py +326 -0
  32. package/packages/robopark/vision/audio_server_pi.py +751 -0
  33. package/packages/robopark/vision/install.sh +34 -0
  34. package/packages/robopark/vision/motor_server.py +304 -0
  35. package/packages/robopark/vision/requirements-demo.txt +12 -0
  36. package/packages/robopark/vision/requirements_pi_unified.txt +101 -0
  37. package/packages/robopark/vision/run.sh +244 -0
  38. package/packages/robopark/vision/services/services.sh +12 -0
@@ -825,6 +825,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
825
825
  var nodes=[], byId={}, orbs=[], pulses=[], t0=0, raf=null, running=false, es=null, animSeq=-1;
826
826
  var drawerId=null, camRAF=null, meterRAF=null, camAbort=null, drawerTimer=null;
827
827
  var previewOn=false, previewTimer=null; // on-demand operator camera preview
828
+ var visionOn=false; // RoboVisionAI_PI overlay feed (direct MJPEG, not LiveKit)
828
829
  var lkRoom=null, micAnalyser=null, audioCtx=null;
829
830
  var imgs={}; // preloaded robot concept avatars
830
831
  var TW=44, TH=22, PARK_W=15, PARK_D=8;
@@ -1189,6 +1190,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1189
1190
  // tell the scheduler to end any live preview so the robot stops its cam
1190
1191
  if(previewOn && drawerId){ var pn=byId[drawerId]; if(pn){ try{ fetch('/robopark/api/robots/'+encodeURIComponent(schedId(pn))+'/preview/stop'+QS,{method:'POST',keepalive:true}); }catch(e){} } }
1191
1192
  previewOn=false; if(previewTimer){ clearInterval(previewTimer); previewTimer=null; }
1193
+ stopVision();
1192
1194
  $('pk-drawer').classList.remove('open'); drawerId=null;
1193
1195
  if(camRAF)cancelAnimationFrame(camRAF); camRAF=null;
1194
1196
  if(meterRAF)cancelAnimationFrame(meterRAF); meterRAF=null;
@@ -1225,10 +1227,34 @@ button.act:disabled{opacity:0.5;cursor:default;}
1225
1227
  var note=$('pk-golivenote'); if(note) note.textContent='';
1226
1228
  idleCam('idle');
1227
1229
  }
1230
+ // RoboVisionAI_PI serves its own MJPEG stream directly (not via LiveKit),
1231
+ // so the browser hits the robot's LAN IP straight on, no scheduler proxy.
1232
+ // The scheduler learns that IP from the device's own heartbeats.
1233
+ function toggleVision(n){
1234
+ var img=$('pk-vision-img'), note=$('pk-vision-note'), btn=$('pk-vision-toggle');
1235
+ if(visionOn){
1236
+ visionOn=false; if(img){ img.src=''; img.style.display='none'; }
1237
+ if(btn) btn.textContent='πŸ” Show overlay';
1238
+ if(note) note.textContent="object/motion detection overlay from the robot's vision server (RoboVisionAI_PI)";
1239
+ return;
1240
+ }
1241
+ var row=schedRow(n);
1242
+ var ip=row&&row.lan_ip;
1243
+ if(!ip){ if(note) note.textContent='no known IP for this robot yet β€” waiting for a heartbeat with lan_ip'; return; }
1244
+ var port=(row&&row.vision_port)||5000;
1245
+ visionOn=true;
1246
+ if(btn) btn.textContent='β–  Hide overlay';
1247
+ if(note) note.textContent='streaming from http://'+ip+':'+port+'/video_feed';
1248
+ if(img){ img.src='http://'+ip+':'+port+'/video_feed'; img.style.display='block'; }
1249
+ }
1250
+ function stopVision(){
1251
+ visionOn=false;
1252
+ var img=$('pk-vision-img'); if(img){ img.src=''; img.style.display='none'; }
1253
+ }
1228
1254
  function pollRoboparkQuiet(){
1229
1255
  fetch('/robopark/api/telemetry'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){
1230
1256
  var list=(Array.isArray(d)?d:(d&&d.robots)||[]); roboparkByName={};
1231
- list.forEach(function(x){ roboparkByName[x.robot_id||x.id||x.name||'robot']=x; });
1257
+ list.forEach(function(x){ roboparkByName[x.robot_id||x.id||x.name||'robot']=x; if(x.name) roboparkByName[x.name]=x; });
1232
1258
  refreshDrawer();
1233
1259
  }).catch(function(){});
1234
1260
  }
@@ -1247,6 +1273,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
1247
1273
  +'<div class="hud"><div class="rec"><i></i>REC Β· '+esc(n.name)+'</div><div class="tr" id="pk-camres">β€” Β· β€”</div><div class="br" id="pk-camts">--:--:--</div></div></div>'
1248
1274
  +'<div class="pk-note" id="pk-camnote">seeking LiveKit track…</div>'
1249
1275
  +'<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn" id="pk-golive" data-robot="'+esc(n.name)+'">β–Ά Go live</button><span class="pk-note" id="pk-golivenote" style="margin:0"></span></div>')
1276
+ + sect('πŸ” vision overlay',
1277
+ '<div class="pk-cam" id="pk-vision-wrap"><img id="pk-vision-img" style="width:100%;display:none;border-radius:inherit" alt="vision feed"/></div>'
1278
+ +'<div class="pk-note" id="pk-vision-note">object/motion detection overlay from the robot\'s vision server (RoboVisionAI_PI)</div>'
1279
+ +'<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn" id="pk-vision-toggle">πŸ” Show overlay</button></div>')
1250
1280
  + sect('πŸŽ™ microphone','<div class="pk-meter"><span class="lab">input</span><div class="pk-bars" id="pk-micbars"></div><span class="pk-val" id="pk-micval">β€”</span></div><div class="pk-note" id="pk-micstate">awaiting robot audio plugin</div>')
1251
1281
  + sect('πŸ”Š speaker','<canvas class="pk-wave" id="pk-spkwave" width="360" height="32"></canvas><div class="pk-meter"><span class="lab">output</span><div class="pk-bars" id="pk-spkbars"></div><span class="pk-val" id="pk-spkval">β€”</span></div><div class="pk-note" id="pk-spkstate">awaiting robot audio plugin</div>')
1252
1282
  + sect('πŸ“Š hardware','<div class="pk-hwgrid"><div><div class="l">load</div><div class="track"><div class="fill" id="pk-hw-load"></div></div></div><div><div class="l">status</div><div class="track"><div class="fill" id="pk-hw-conn" style="background:'+(n.connected?'var(--ok)':'var(--muted)')+'"></div></div></div></div><div class="pk-note">remote node β€” full CPU/mem telemetry shows on the node itself</div>')
@@ -1254,6 +1284,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1254
1284
  + sect('βš™ controls','<div class="pk-ctl"><button class="rp-btn danger" id="pk-end" data-robot="'+esc(n.name)+'">End session</button><button class="rp-btn" id="pk-follow">Follow run</button></div><div class="pk-note">end-session is live via the scheduler; cam/mic/speaker fill in as the robot plugin publishes them</div>');
1255
1285
  startCam(n); startMeters(n);
1256
1286
  var gl=$('pk-golive'); if(gl) gl.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } goLive(n); });
1287
+ var vt=$('pk-vision-toggle'); if(vt) vt.addEventListener('click', function(){ toggleVision(n); });
1257
1288
  var eb=$('pk-end'); if(eb) eb.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } endSession(schedId(n)); });
1258
1289
  var fb=$('pk-follow'); if(fb) fb.addEventListener('click', function(){ selectTab('fleet'); $('cmdInput').value='/follow'; selectNode(n.id,n.name); showOut('type a runId after /follow, or run /status on '+n.name); });
1259
1290
  } else {
@@ -1,5 +1,5 @@
1
1
  export interface ServiceArgs {
2
- role: 'hub' | 'robot' | 'control' | 'hub-scheduler' | 'robot-preview-agent';
2
+ role: 'hub' | 'robot' | 'control' | 'hub-scheduler' | 'robot-preview-agent' | 'robot-vision-agent';
3
3
  name: string;
4
4
  command: string;
5
5
  args: string[];
@@ -8,6 +8,9 @@ export interface PreviewAgentOptions {
8
8
  width: string;
9
9
  height: string;
10
10
  fps: string;
11
+ visionWebhookPort?: string;
12
+ visionTriggerCooldown?: string;
13
+ visionSessionSeconds?: string;
11
14
  saveConfig?: boolean;
12
15
  foreground?: boolean;
13
16
  }
@@ -41,6 +41,12 @@ export async function roboparkPreviewAgent(opts) {
41
41
  args.push('--device-token', opts.deviceToken);
42
42
  if (opts.enrollmentToken)
43
43
  args.push('--enrollment-token', opts.enrollmentToken);
44
+ if (opts.visionWebhookPort)
45
+ args.push('--vision-webhook-port', opts.visionWebhookPort);
46
+ if (opts.visionTriggerCooldown)
47
+ args.push('--vision-trigger-cooldown', opts.visionTriggerCooldown);
48
+ if (opts.visionSessionSeconds)
49
+ args.push('--vision-session-seconds', opts.visionSessionSeconds);
44
50
  if (opts.saveConfig)
45
51
  args.push('--save-config');
46
52
  console.log(chalk.bold('\n robopark preview-agent'));
@@ -49,6 +55,8 @@ export async function roboparkPreviewAgent(opts) {
49
55
  console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
50
56
  console.log(` video: ${chalk.cyan(opts.videoDevice)}`);
51
57
  console.log(` audio: ${chalk.cyan(opts.audioDevice)}`);
58
+ if (opts.visionWebhookPort)
59
+ console.log(` vision webhook: ${chalk.cyan(':' + opts.visionWebhookPort)} (point RoboVisionAI_PI's motion webhook here)`);
52
60
  console.log();
53
61
  if (opts.foreground) {
54
62
  const proc = spawn(python, args, { stdio: 'inherit' });
@@ -1,9 +1,15 @@
1
+ /** Modules app_pi_clean.py imports β€” kept in sync with requirements_pi_unified.txt. */
2
+ export declare const VISION_REQUIRED_MODULES: string[];
1
3
  export declare function findSchedulerPath(): Promise<string | null>;
4
+ export declare function findVisionPath(): Promise<string | null>;
2
5
  export declare function findPython(): string;
3
6
  /**
4
- * Ensure preview_agent.py's Python deps are importable, installing them from
5
- * requirements-robot.txt (next to the script) if not. Returns false β€” with a
6
- * message already printed β€” if deps are still missing after the install
7
+ * Ensure a robot-side script's Python deps are importable, installing them
8
+ * from a requirements file (next to the script) if not. Returns false β€” with
9
+ * a message already printed β€” if deps are still missing after the install
7
10
  * attempt, so callers can bail out before hitting a raw traceback.
11
+ *
12
+ * Defaults match preview_agent.py; pass `modules`/`reqFile` for other scripts
13
+ * (e.g. app_pi_clean.py + requirements_pi_unified.txt).
8
14
  */
9
- export declare function ensurePythonDeps(python: string, scriptPath: string): boolean;
15
+ export declare function ensurePythonDeps(python: string, scriptPath: string, modules?: string[], reqFile?: string): boolean;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * RoboPark β€” shared Python environment helpers for `enroll` and
3
- * `preview-agent`, which both spawn `preview_agent.py`.
2
+ * RoboPark β€” shared Python environment helpers for `enroll`, `preview-agent`,
3
+ * and `vision-agent`, which spawn preview_agent.py / app_pi_clean.py.
4
4
  */
5
5
  import { execSync, spawnSync } from 'node:child_process';
6
6
  import { existsSync } from 'node:fs';
@@ -8,10 +8,13 @@ import { join } from 'node:path';
8
8
  import chalk from 'chalk';
9
9
  /** Modules preview_agent.py imports β€” kept in sync with requirements-robot.txt. */
10
10
  const REQUIRED_MODULES = ['httpx', 'cv2', 'pyaudio', 'livekit'];
11
- export async function findSchedulerPath() {
11
+ /** Modules app_pi_clean.py imports β€” kept in sync with requirements_pi_unified.txt. */
12
+ export const VISION_REQUIRED_MODULES = ['flask', 'flask_cors', 'cv2', 'numpy', 'requests'];
13
+ async function findPackageScript(relativePath) {
14
+ const parts = relativePath.split('/');
12
15
  const candidates = [
13
- join(process.cwd(), 'packages', 'robopark', 'scheduler', 'preview_agent.py'),
14
- join(process.cwd(), 'scheduler', 'preview_agent.py'),
16
+ join(process.cwd(), 'packages', 'robopark', ...parts),
17
+ join(process.cwd(), ...parts),
15
18
  ];
16
19
  for (const p of candidates) {
17
20
  if (existsSync(p))
@@ -21,13 +24,19 @@ export async function findSchedulerPath() {
21
24
  const { createRequire } = await import('node:module');
22
25
  const require = createRequire(import.meta.url);
23
26
  const pkgMain = require.resolve('infinicode/package.json');
24
- const p = join(pkgMain, '..', 'packages', 'robopark', 'scheduler', 'preview_agent.py');
27
+ const p = join(pkgMain, '..', 'packages', 'robopark', ...relativePath.split('/'));
25
28
  if (existsSync(p))
26
29
  return p;
27
30
  }
28
31
  catch { /* ignore */ }
29
32
  return null;
30
33
  }
34
+ export async function findSchedulerPath() {
35
+ return findPackageScript('scheduler/preview_agent.py');
36
+ }
37
+ export async function findVisionPath() {
38
+ return findPackageScript('vision/app_pi_clean.py');
39
+ }
31
40
  export function findPython() {
32
41
  const names = process.platform === 'win32'
33
42
  ? ['python', 'python3', 'py']
@@ -43,25 +52,28 @@ export function findPython() {
43
52
  }
44
53
  return process.platform === 'win32' ? 'python' : 'python3';
45
54
  }
46
- function missingModules(python) {
47
- return REQUIRED_MODULES.filter(mod => {
55
+ function missingModules(python, modules) {
56
+ return modules.filter(mod => {
48
57
  const res = spawnSync(python, ['-c', `import ${mod}`], { stdio: 'ignore' });
49
58
  return res.status !== 0;
50
59
  });
51
60
  }
52
61
  /**
53
- * Ensure preview_agent.py's Python deps are importable, installing them from
54
- * requirements-robot.txt (next to the script) if not. Returns false β€” with a
55
- * message already printed β€” if deps are still missing after the install
62
+ * Ensure a robot-side script's Python deps are importable, installing them
63
+ * from a requirements file (next to the script) if not. Returns false β€” with
64
+ * a message already printed β€” if deps are still missing after the install
56
65
  * attempt, so callers can bail out before hitting a raw traceback.
66
+ *
67
+ * Defaults match preview_agent.py; pass `modules`/`reqFile` for other scripts
68
+ * (e.g. app_pi_clean.py + requirements_pi_unified.txt).
57
69
  */
58
- export function ensurePythonDeps(python, scriptPath) {
59
- const missing = missingModules(python);
70
+ export function ensurePythonDeps(python, scriptPath, modules = REQUIRED_MODULES, reqFile = 'requirements-robot.txt') {
71
+ const missing = missingModules(python, modules);
60
72
  if (missing.length === 0)
61
73
  return true;
62
- const reqPath = join(scriptPath, '..', 'requirements-robot.txt');
74
+ const reqPath = join(scriptPath, '..', reqFile);
63
75
  if (!existsSync(reqPath)) {
64
- console.log(chalk.red(` βœ— missing Python modules: ${missing.join(', ')} (and requirements-robot.txt not found next to ${scriptPath})`));
76
+ console.log(chalk.red(` βœ— missing Python modules: ${missing.join(', ')} (and ${reqFile} not found next to ${scriptPath})`));
65
77
  return false;
66
78
  }
67
79
  console.log(chalk.dim(` installing Python deps (${missing.join(', ')})…`));
@@ -70,7 +82,7 @@ export function ensurePythonDeps(python, scriptPath) {
70
82
  console.log(chalk.red(` βœ— pip install failed β€” install manually: ${python} -m pip install -r "${reqPath}"`));
71
83
  return false;
72
84
  }
73
- const stillMissing = missingModules(python);
85
+ const stillMissing = missingModules(python, modules);
74
86
  if (stillMissing.length > 0) {
75
87
  console.log(chalk.red(` βœ— still missing after install: ${stillMissing.join(', ')} β€” install manually: ${python} -m pip install -r "${reqPath}"`));
76
88
  return false;
@@ -16,6 +16,7 @@ export interface SetupOptions {
16
16
  enrollmentToken?: string;
17
17
  videoDevice?: string;
18
18
  audioDevice?: string;
19
+ vision?: boolean;
19
20
  start?: boolean;
20
21
  autoStart?: boolean;
21
22
  yes?: boolean;
@@ -171,9 +171,16 @@ async function setupRobot(config, opts, ctx, internal) {
171
171
  ];
172
172
  if (opts.enrollmentToken)
173
173
  previewAgentArgs.push('--enrollment-token', opts.enrollmentToken);
174
+ // Vision (camera/motion detection -> presence-triggered session) is the
175
+ // production trigger β€” on by default. --no-vision opts out (e.g. no camera
176
+ // on this box, or RoboVisionAI_PI isn't installed).
177
+ const visionEnabled = opts.vision !== false;
178
+ const visionAgentArgs = ['vision-agent'];
174
179
  console.log(chalk.dim(' robot command:'));
175
180
  console.log(' ' + chalk.cyan(`infinicode ${args.join(' ')}`));
176
181
  console.log(' ' + chalk.cyan(`robopark ${previewAgentArgs.join(' ')}`));
182
+ if (visionEnabled)
183
+ console.log(' ' + chalk.cyan(`robopark ${visionAgentArgs.join(' ')}`));
177
184
  console.log();
178
185
  if (opts.start) {
179
186
  const robotArgv = await infinicodeArgv(args);
@@ -191,6 +198,11 @@ async function setupRobot(config, opts, ctx, internal) {
191
198
  console.log(chalk.dim(' starting preview agent…'));
192
199
  const previewArgv = [process.execPath, process.argv[1], ...previewAgentArgs];
193
200
  runDetached(previewArgv[0], previewArgv.slice(1));
201
+ if (visionEnabled) {
202
+ console.log(chalk.dim(' starting vision agent…'));
203
+ const visionArgv = [process.execPath, process.argv[1], ...visionAgentArgs];
204
+ runDetached(visionArgv[0], visionArgv.slice(1));
205
+ }
194
206
  }
195
207
  if (opts.autoStart) {
196
208
  const robotArgv = await infinicodeArgv(args);
@@ -209,6 +221,16 @@ async function setupRobot(config, opts, ctx, internal) {
209
221
  args: previewArgv.slice(1),
210
222
  });
211
223
  console.log(reg2.ok ? chalk.green(` βœ“ ${reg2.message}`) : chalk.yellow(` ⚠ ${reg2.message}`));
224
+ if (visionEnabled) {
225
+ const visionArgv = [process.execPath, process.argv[1], ...visionAgentArgs, '--foreground'];
226
+ const reg3 = await registerAutoStart({
227
+ role: 'robot-vision-agent',
228
+ name: internal.name,
229
+ command: visionArgv[0],
230
+ args: visionArgv.slice(1),
231
+ });
232
+ console.log(reg3.ok ? chalk.green(` βœ“ ${reg3.message}`) : chalk.yellow(` ⚠ ${reg3.message}`));
233
+ }
212
234
  }
213
235
  }
214
236
  async function setupControl(config, opts, ctx, internal) {
@@ -0,0 +1,7 @@
1
+ export interface VisionAgentOptions {
2
+ port?: string;
3
+ motionWebhookUrl?: string;
4
+ motionActive?: boolean;
5
+ foreground?: boolean;
6
+ }
7
+ export declare function roboparkVisionAgent(opts: VisionAgentOptions): Promise<void>;
@@ -0,0 +1,55 @@
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, ensurePythonDeps, 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 python = 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_pi_unified.txt' : 'requirements-demo.txt';
27
+ if (!ensurePythonDeps(python, script, VISION_REQUIRED_MODULES, reqFile))
28
+ process.exit(1);
29
+ const port = opts.port ?? String(DEFAULT_VISION_PORT);
30
+ const webhookUrl = opts.motionWebhookUrl ?? `http://127.0.0.1:${DEFAULT_MOTION_WEBHOOK_PORT}/`;
31
+ const motionActive = opts.motionActive ?? true; // armed by default β€” "vision trigger as production system"
32
+ const args = [script, '--port', port, '--motion-webhook-url', webhookUrl];
33
+ if (motionActive)
34
+ args.push('--motion-active');
35
+ console.log(chalk.bold('\n robopark vision-agent'));
36
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
37
+ console.log(` port: ${chalk.cyan(port)}`);
38
+ console.log(` webhook: ${chalk.cyan(webhookUrl)}`);
39
+ console.log(` motion: ${motionActive ? chalk.green('armed') : chalk.dim('off')}`);
40
+ console.log();
41
+ if (opts.foreground) {
42
+ const proc = spawn(python, args, { stdio: 'inherit' });
43
+ await new Promise((resolve) => {
44
+ proc.on('close', () => resolve());
45
+ });
46
+ return;
47
+ }
48
+ const proc = spawn(python, args, {
49
+ stdio: 'ignore',
50
+ detached: true,
51
+ env: { ...process.env, ROBOPARK_VISION_AGENT: '1' },
52
+ });
53
+ proc.unref();
54
+ console.log(chalk.green(' βœ“ vision agent started in background'));
55
+ }
@@ -99,12 +99,26 @@ program
99
99
  .option('--width <px>', 'video width', '640')
100
100
  .option('--height <px>', 'video height', '480')
101
101
  .option('--fps <n>', 'video fps', '15')
102
+ .option('--vision-webhook-port <port>', 'local port for RoboVisionAI_PI\'s motion webhook (0 disables)', '5057')
103
+ .option('--vision-trigger-cooldown <sec>', 'min seconds between motion-triggered sessions', '20')
104
+ .option('--vision-session-seconds <sec>', 'how long a motion-triggered session holds the publisher', '90')
102
105
  .option('--save-config', 'write settings to ~/.robopark/preview_agent.json')
103
106
  .option('--foreground', 'stay in foreground; do not detach')
104
107
  .action(async (opts) => {
105
108
  const { roboparkPreviewAgent } = await import('./robopark/preview-agent-launcher.js');
106
109
  await roboparkPreviewAgent(opts);
107
110
  });
111
+ program
112
+ .command('vision-agent')
113
+ .description('Start the robot-side camera/motion detector (RoboVisionAI_PI) β€” self-arms and wires to preview-agent\'s vision webhook')
114
+ .option('--port <port>', 'vision server port', '5000')
115
+ .option('--motion-webhook-url <url>', 'where to POST on motion (defaults to the local preview-agent\'s vision webhook, http://127.0.0.1:5057/)')
116
+ .option('--no-motion-active', 'do not arm motion detection on startup (armed by default)')
117
+ .option('--foreground', 'stay in foreground; do not detach')
118
+ .action(async (opts) => {
119
+ const { roboparkVisionAgent } = await import('./robopark/vision-agent-launcher.js');
120
+ await roboparkVisionAgent(opts);
121
+ });
108
122
  program
109
123
  .command('setup')
110
124
  .description('Set up this device in one pass')
@@ -125,6 +139,7 @@ program
125
139
  .option('--enrollment-token <token>', 'one-time scheduler enrollment token for robots')
126
140
  .option('--video-device <dev>', 'default camera for robot preview agent', 'auto')
127
141
  .option('--audio-device <dev>', 'default microphone for robot preview agent', 'default')
142
+ .option('--no-vision', 'do not start the vision (camera/motion trigger) agent for a robot β€” armed by default')
128
143
  .option('--start', 'launch the node now')
129
144
  .option('--auto-start', 'register to start on boot')
130
145
  .option('--yes', 'non-interactive mode')
@@ -149,6 +164,7 @@ program
149
164
  enrollmentToken: opts.enrollmentToken,
150
165
  videoDevice: opts.videoDevice,
151
166
  audioDevice: opts.audioDevice,
167
+ vision: opts.vision,
152
168
  start: opts.start,
153
169
  autoStart: opts.autoStart,
154
170
  yes: opts.yes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.13",
3
+ "version": "2.8.16",
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",
@@ -27,6 +27,8 @@
27
27
  "bin/robopark.js",
28
28
  "dist",
29
29
  "packages/robopark/scheduler",
30
+ "packages/robopark/pi-client",
31
+ "packages/robopark/vision",
30
32
  ".opencode/plugins",
31
33
  ".opencode/themes",
32
34
  ".opencode/tui.json",
@@ -0,0 +1,17 @@
1
+ # robopark-pi-client β€” full robot client (reference)
2
+
3
+ The full on-robot client: enrollment, heartbeat, motor control, and a
4
+ LiveKit publish loop (`livekit_bridge.py`) that joins a standing per-device
5
+ room whenever `production_mode` is on. `pi_ui.py` serves a touchscreen UI
6
+ with a manual "Join Conversation" button β€” the human-in-the-loop trigger.
7
+
8
+ This is a reference implementation shipped for Pi deployment; it is not
9
+ currently wired into `robopark setup --robot` (which uses `../scheduler/
10
+ preview_agent.py` for the on-demand preview + vision-trigger flow β€” see
11
+ `../vision/README.md`). `client.py`'s standing-room model and the scheduler's
12
+ `request-session`-per-trigger model are two different session strategies;
13
+ reconciling them into one is follow-up work, not done here.
14
+
15
+ Install: `pip install -r requirements.txt` (`livekit`/`livekit-api` are
16
+ commented out β€” Pi-only, install separately with the `livekit` SDK).
17
+ `install.sh`/`*.service` are Linux/Pi systemd wrappers.
@@ -0,0 +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"