infinicode 2.8.8 → 2.8.10
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 +77 -25
- package/dist/kernel/federation/transport-http.js +33 -0
- package/dist/robopark/auto-start.d.ts +1 -1
- package/dist/robopark/enroll.d.ts +7 -0
- package/dist/robopark/enroll.js +101 -0
- package/dist/robopark/preview-agent-launcher.d.ts +14 -0
- package/dist/robopark/preview-agent-launcher.js +102 -0
- package/dist/robopark/probe.d.ts +7 -0
- package/dist/robopark/probe.js +138 -0
- package/dist/robopark/serve.js +13 -0
- package/dist/robopark/setup-livekit.d.ts +8 -0
- package/dist/robopark/setup-livekit.js +233 -0
- package/dist/robopark/setup.d.ts +3 -0
- package/dist/robopark/setup.js +32 -0
- package/dist/robopark/verify.d.ts +6 -0
- package/dist/robopark/verify.js +192 -0
- package/dist/robopark-cli.js +68 -1
- package/package.json +1 -1
- package/packages/robopark/scheduler/Dockerfile +6 -1
- package/packages/robopark/scheduler/__pycache__/main.cpython-312.pyc +0 -0
- package/packages/robopark/scheduler/__pycache__/preview_agent.cpython-312.pyc +0 -0
- package/packages/robopark/scheduler/main.py +431 -26
- package/packages/robopark/scheduler/preview_agent.py +644 -0
- package/packages/robopark/scheduler/requirements.txt +4 -0
|
@@ -697,7 +697,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
697
697
|
if (name==='park') Park.activate(); else Park.deactivate();
|
|
698
698
|
}
|
|
699
699
|
var initHash = location.hash.replace('#','');
|
|
700
|
-
|
|
700
|
+
// Defer tab init until Park module is defined (same outer IIFE, later in source).
|
|
701
|
+
function initTab(){
|
|
702
|
+
if (initHash==='robopark'||initHash==='park') selectTab(initHash);
|
|
703
|
+
}
|
|
704
|
+
// initTab() is called at the very bottom of this IIFE, after Park is assigned.
|
|
701
705
|
|
|
702
706
|
function robotsFrom(d){
|
|
703
707
|
if (Array.isArray(d)) return d;
|
|
@@ -822,21 +826,32 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
822
826
|
var drawerId=null, camRAF=null, meterRAF=null, camAbort=null, drawerTimer=null;
|
|
823
827
|
var previewOn=false, previewTimer=null; // on-demand operator camera preview
|
|
824
828
|
var lkRoom=null, micAnalyser=null, audioCtx=null;
|
|
829
|
+
var imgs={}; // preloaded robot concept avatars
|
|
825
830
|
var TW=44, TH=22, PARK_W=15, PARK_D=8;
|
|
826
|
-
// The real park (from the aerial): named robot pads at their ground
|
|
827
|
-
// positions
|
|
828
|
-
//
|
|
831
|
+
// The real park (from the aerial photo): named robot pads at their ground
|
|
832
|
+
// positions along the long Shenzhen Bay promenade plaza, plus palms, pond,
|
|
833
|
+
// street and stage. A mesh node binds to a pad when its display name matches
|
|
834
|
+
// the pad keywords (robobmw → VIXEN/BMW).
|
|
829
835
|
var SLOTS=[
|
|
830
|
-
{label:'VOLT',
|
|
831
|
-
{label:'TESLA',
|
|
832
|
-
{label:'JAGUAR',
|
|
833
|
-
{label:'VIXEN',
|
|
834
|
-
{label:'PANDA',
|
|
836
|
+
{label:'VOLT', keys:['volt'], nx:0.18, ny:0.26, img:'/static/robopark/volt.png'},
|
|
837
|
+
{label:'TESLA', keys:['tesla'], nx:0.38, ny:0.30, img:'/static/robopark/tesla.png'},
|
|
838
|
+
{label:'JAGUAR', keys:['jaguar','jag'], nx:0.58, ny:0.34, img:'/static/robopark/jaguar.png'},
|
|
839
|
+
{label:'VIXEN', keys:['vixen','bmw'], nx:0.54, ny:0.66, sub:'BMW', img:'/static/robopark/bmw.png'},
|
|
840
|
+
{label:'PANDA', keys:['panda'], nx:0.82, ny:0.52, img:'/static/robopark/panda.png'}
|
|
841
|
+
];
|
|
842
|
+
var HUBSLOT={label:'CONTROL', keys:[], nx:0.06, ny:0.16};
|
|
843
|
+
// Palm positions tuned to the aerial: clusters along the back edge and
|
|
844
|
+
// scattered down the center of the paved strip.
|
|
845
|
+
var PALMS=[
|
|
846
|
+
[0.10,0.38],[0.14,0.55],[0.18,0.72],[0.22,0.30],[0.26,0.48],[0.30,0.66],
|
|
847
|
+
[0.36,0.24],[0.40,0.42],[0.44,0.60],[0.50,0.26],[0.54,0.46],[0.58,0.64],
|
|
848
|
+
[0.66,0.30],[0.70,0.50],[0.74,0.68],[0.80,0.36],[0.84,0.56],[0.90,0.40],[0.94,0.62],
|
|
849
|
+
[0.62,0.80],[0.76,0.84],[0.35,0.82]
|
|
835
850
|
];
|
|
836
|
-
|
|
837
|
-
var
|
|
838
|
-
|
|
839
|
-
var CARS=[[4,-0.
|
|
851
|
+
// Pond / curved water feature at the lower-left of the plaza.
|
|
852
|
+
var POND={cx:0.28*15, cy:0.96*8, rx:3.4, ry:1.6};
|
|
853
|
+
// Street traffic along the top road (yellow taxis + private cars).
|
|
854
|
+
var CARS=[[2.2,-0.92,'#f2c14e'],[4.4,-0.78,'#e0a23c'],[6.6,-0.95,'#7fb4ff'],[8.8,-1.02,'#ff7e6e'],[11.0,-0.84,'#d8d8e0'],[13.2,-0.98,'#f2c14e']];
|
|
840
855
|
|
|
841
856
|
function mono(){ return 'ui-monospace,Menlo,Consolas,monospace'; }
|
|
842
857
|
function W2(nx,ny){ return [nx*PARK_W, ny*PARK_D]; }
|
|
@@ -986,17 +1001,39 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
986
1001
|
X.strokeStyle='rgba(232,210,120,0.5)'; X.setLineDash([9*cam.z,9*cam.z]);
|
|
987
1002
|
var r1=iso(0.2,-0.92), r2=iso(PARK_W-0.2,-0.92); X.beginPath(); X.moveTo(r1.x,r1.y); X.lineTo(r2.x,r2.y); X.stroke(); X.setLineDash([]);
|
|
988
1003
|
CARS.forEach(function(c){ drawCar(c[0],c[1],c[2]); });
|
|
989
|
-
//
|
|
990
|
-
|
|
1004
|
+
// white event tent / stage on the far left of the plaza (matches aerial)
|
|
1005
|
+
drawTent(0.85, 0.18);
|
|
1006
|
+
// paved plaza — long rectangular promenade matching the aerial footprint
|
|
1007
|
+
var px0=0.2,py0=-0.10,px1=PARK_W-0.2,py1=PARK_D-0.45;
|
|
991
1008
|
var pg=X.createLinearGradient(0,0,0,H); pg.addColorStop(0,'rgba(74,70,92,0.66)'); pg.addColorStop(1,'rgba(46,42,60,0.62)');
|
|
992
1009
|
poly([[px0,py0],[px1,py0],[px1,py1],[px0,py1]], pg, 'rgba(255,255,255,0.07)',1);
|
|
1010
|
+
// plaza grid (tile joints)
|
|
993
1011
|
X.strokeStyle='rgba(255,255,255,0.05)'; X.lineWidth=1;
|
|
994
|
-
for(var gx=Math.ceil(px0);gx<px1;gx
|
|
995
|
-
for(var gy=Math.ceil(py0);gy<py1;gy
|
|
996
|
-
// pond
|
|
997
|
-
var pts=[]; for(var t=0;t<Math.PI*2;t+=Math.PI/
|
|
1012
|
+
for(var gx=Math.ceil(px0);gx<px1;gx+=1.5){ var a=iso(gx,py0), b=iso(gx,py1); X.beginPath(); X.moveTo(a.x,a.y); X.lineTo(b.x,b.y); X.stroke(); }
|
|
1013
|
+
for(var gy=Math.ceil(py0);gy<py1;gy+=1.5){ var c=iso(px0,gy), d=iso(px1,gy); X.beginPath(); X.moveTo(c.x,c.y); X.lineTo(d.x,d.y); X.stroke(); }
|
|
1014
|
+
// curved pond / water feature at bottom-left
|
|
1015
|
+
var pts=[]; for(var t=0;t<Math.PI*2;t+=Math.PI/12){ pts.push([POND.cx+POND.rx*Math.cos(t), POND.cy+POND.ry*Math.sin(t)*0.7]); }
|
|
998
1016
|
var wg=X.createLinearGradient(0,0,0,H); wg.addColorStop(0,'rgba(44,116,156,0.72)'); wg.addColorStop(1,'rgba(26,74,116,0.72)');
|
|
999
1017
|
poly(pts, wg, 'rgba(130,205,235,0.32)',1.5);
|
|
1018
|
+
// winding footpath along the pond
|
|
1019
|
+
X.strokeStyle='rgba(210,190,150,0.35)'; X.lineWidth=2.5*cam.z; X.setLineDash([]);
|
|
1020
|
+
var pathPts=[[0.15*PARK_W,0.82*PARK_D],[0.22*PARK_W,0.88*PARK_D],[0.32*PARK_W,0.92*PARK_D],[0.45*PARK_W,0.95*PARK_D]];
|
|
1021
|
+
X.beginPath();
|
|
1022
|
+
for(var pi=0;pi<pathPts.length;pi++){ var pp=iso(pathPts[pi][0],pathPts[pi][1]); if(pi===0)X.moveTo(pp.x,pp.y); else X.lineTo(pp.x,pp.y); }
|
|
1023
|
+
X.stroke();
|
|
1024
|
+
}
|
|
1025
|
+
function drawTent(wx,wy){
|
|
1026
|
+
var p=iso(wx,wy), z=cam.z;
|
|
1027
|
+
// canopy shadow
|
|
1028
|
+
X.fillStyle='rgba(0,0,0,0.28)'; X.beginPath(); X.ellipse(p.x,p.y+4*z,22*z,8*z,0,0,7); X.fill();
|
|
1029
|
+
// white peaked tent
|
|
1030
|
+
X.fillStyle='rgba(235,235,240,0.92)'; X.strokeStyle='rgba(180,180,190,0.6)'; X.lineWidth=1;
|
|
1031
|
+
X.beginPath(); X.moveTo(p.x-20*z,p.y); X.lineTo(p.x,p.y-26*z); X.lineTo(p.x+20*z,p.y); X.closePath(); X.fill(); X.stroke();
|
|
1032
|
+
X.beginPath(); X.moveTo(p.x-20*z,p.y); X.lineTo(p.x-12*z,p.y+16*z); X.lineTo(p.x+12*z,p.y+16*z); X.lineTo(p.x+20*z,p.y); X.closePath(); X.fill(); X.stroke();
|
|
1033
|
+
// support poles
|
|
1034
|
+
X.strokeStyle='rgba(160,160,170,0.7)'; X.lineWidth=1.5*z;
|
|
1035
|
+
X.beginPath(); X.moveTo(p.x-10*z,p.y+16*z); X.lineTo(p.x-10*z,p.y+5*z); X.stroke();
|
|
1036
|
+
X.beginPath(); X.moveTo(p.x+10*z,p.y+16*z); X.lineTo(p.x+10*z,p.y+5*z); X.stroke();
|
|
1000
1037
|
}
|
|
1001
1038
|
function drawCar(wx,wy,col){
|
|
1002
1039
|
var p=iso(wx,wy), z=cam.z;
|
|
@@ -1039,12 +1076,22 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1039
1076
|
X.fillStyle='rgba(0,0,0,0.3)'; X.beginPath(); X.ellipse(bx,p.y+3*z,13*z,6*z,0,0,7); X.fill();
|
|
1040
1077
|
var col=n.connected?getVar('--gold-bright'):'rgba(150,143,120,0.5)';
|
|
1041
1078
|
X.globalAlpha=n.connected?1:0.7;
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1079
|
+
var img = n.connected && imgs[n.label];
|
|
1080
|
+
if(img && img.complete && img.naturalWidth>0){
|
|
1081
|
+
var iw=26*z, ih=26*z*img.naturalHeight/img.naturalWidth;
|
|
1082
|
+
X.save();
|
|
1083
|
+
X.beginPath(); X.roundRect(bx-13*z,by-38*z,iw,ih+12*z,6*z); X.clip();
|
|
1084
|
+
X.drawImage(img, bx-13*z, by-44*z, iw, ih+18*z);
|
|
1085
|
+
X.restore();
|
|
1086
|
+
X.strokeStyle=col; X.lineWidth=1.6; X.beginPath(); X.roundRect(bx-13*z,by-38*z,iw,ih+12*z,6*z); X.stroke();
|
|
1087
|
+
} else {
|
|
1088
|
+
X.fillStyle='rgba(24,20,38,0.92)'; X.strokeStyle=col; X.lineWidth=1.6;
|
|
1089
|
+
X.beginPath(); X.roundRect(bx-11*z,by-24*z,22*z,25*z,5*z); X.fill(); X.stroke();
|
|
1090
|
+
X.beginPath(); X.roundRect(bx-8*z,by-37*z,16*z,14*z,4*z); X.fillStyle='rgba(30,24,44,0.95)'; X.fill(); X.stroke();
|
|
1091
|
+
X.fillStyle=n.connected?getVar('--ok'):'#655'; X.beginPath(); X.arc(bx-3.5*z,by-30*z,2*z,0,7); X.arc(bx+3.5*z,by-30*z,2*z,0,7); X.fill();
|
|
1092
|
+
X.strokeStyle=col; X.beginPath(); X.moveTo(bx,by-37*z); X.lineTo(bx,by-44*z); X.stroke();
|
|
1093
|
+
X.fillStyle=n.connected?'#FF5B4A':'#655'; X.beginPath(); X.arc(bx,by-45*z,2*z,0,7); X.fill();
|
|
1094
|
+
}
|
|
1048
1095
|
X.globalAlpha=1;
|
|
1049
1096
|
var sub=n.connected?((n.sub&&n.sub!==n.name?n.sub+' · ':'')+Math.round(n.load)+'%'):'not joined';
|
|
1050
1097
|
label(bx,p.y+17*z,n.label,col,sub);
|
|
@@ -1100,9 +1147,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1100
1147
|
var s=document.createElement('script'); s.src='/vendor/livekit-client.umd.js'+QS; s.async=true;
|
|
1101
1148
|
s.onerror=function(){ sdkLoading=false; }; document.head.appendChild(s);
|
|
1102
1149
|
}
|
|
1150
|
+
function loadImages(){
|
|
1151
|
+
SLOTS.forEach(function(s){ if(!s.img) return; var i=new Image(); i.src=s.img+QS; i.onload=function(){ imgs[s.label]=i; }; i.onerror=function(){ imgs[s.label]=null; }; });
|
|
1152
|
+
}
|
|
1103
1153
|
function activate(){
|
|
1104
1154
|
ensure();
|
|
1105
1155
|
loadSDK();
|
|
1156
|
+
loadImages();
|
|
1106
1157
|
if(lastStatus) onStatus(lastStatus);
|
|
1107
1158
|
resize();
|
|
1108
1159
|
connectStream();
|
|
@@ -1319,6 +1370,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1319
1370
|
poll();
|
|
1320
1371
|
setInterval(poll, 2000);
|
|
1321
1372
|
setInterval(function(){ if (activeTab==='robopark') pollRobopark(); }, 3000);
|
|
1373
|
+
initTab();
|
|
1322
1374
|
})();
|
|
1323
1375
|
</script>
|
|
1324
1376
|
</body>
|
|
@@ -38,6 +38,28 @@ function loadLiveKitUmd() {
|
|
|
38
38
|
}
|
|
39
39
|
return _lkUmd;
|
|
40
40
|
}
|
|
41
|
+
/** In-memory cache of small static assets embedded next to the dashboard so the
|
|
42
|
+
* Park map can show per-robot concept avatars without external URLs. */
|
|
43
|
+
const _staticCache = {};
|
|
44
|
+
function loadStaticAsset(relPath) {
|
|
45
|
+
if (_staticCache[relPath])
|
|
46
|
+
return _staticCache[relPath];
|
|
47
|
+
try {
|
|
48
|
+
const require = createRequire(import.meta.url);
|
|
49
|
+
// Resolve from dist/kernel/federation/transport-http.js → project root.
|
|
50
|
+
const here = require.resolve('./transport-http.js');
|
|
51
|
+
const base = here.replace(/dist[\\/]kernel[\\/]federation[\\/]transport-http\.js$/, 'static');
|
|
52
|
+
const file = base + '/' + relPath;
|
|
53
|
+
const data = readFileSync(file);
|
|
54
|
+
const ext = file.split('.').pop()?.toLowerCase();
|
|
55
|
+
const type = ext === 'png' ? 'image/png' : ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'application/octet-stream';
|
|
56
|
+
_staticCache[relPath] = { type, data };
|
|
57
|
+
return _staticCache[relPath];
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
41
63
|
/** Constant-time string compare — avoids leaking the token via response timing. */
|
|
42
64
|
function safeEqual(a, b) {
|
|
43
65
|
const ab = Buffer.from(a);
|
|
@@ -152,6 +174,17 @@ export class MeshServer {
|
|
|
152
174
|
res.end(this.opts.dashboardHtml);
|
|
153
175
|
return;
|
|
154
176
|
}
|
|
177
|
+
// Static assets bundled with the dashboard (robot concept avatars, etc.).
|
|
178
|
+
if (req.method === 'GET' && path.startsWith('/static/')) {
|
|
179
|
+
const asset = loadStaticAsset(path.slice(8));
|
|
180
|
+
if (!asset) {
|
|
181
|
+
res.writeHead(404).end('asset not found');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
res.writeHead(200, { 'content-type': asset.type, 'cache-control': 'public, max-age=86400' });
|
|
185
|
+
res.end(asset.data);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
155
188
|
// LiveKit browser SDK, served from the installed dependency so the Park
|
|
156
189
|
// tab can subscribe to a robot's live camera track. Loaded by the dashboard
|
|
157
190
|
// with the ?token= appended (so it passes the auth gate above). Optional:
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoboPark — `robopark enroll`.
|
|
3
|
+
*
|
|
4
|
+
* Enrolls this machine as a scheduler device. Either uses an explicit
|
|
5
|
+
* enrollment token, or reads it from ~/.robopark/enrollment_token.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
9
|
+
import { homedir, hostname } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
const ROBOPARK_DIR = join(homedir(), '.robopark');
|
|
13
|
+
const ENROLLMENT_FILE = join(ROBOPARK_DIR, 'enrollment_token');
|
|
14
|
+
function ensureDir() {
|
|
15
|
+
mkdirSync(ROBOPARK_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
function readEnrollmentToken() {
|
|
18
|
+
if (existsSync(ENROLLMENT_FILE)) {
|
|
19
|
+
return readFileSync(ENROLLMENT_FILE, 'utf8').trim();
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
function saveEnrollmentToken(token) {
|
|
24
|
+
ensureDir();
|
|
25
|
+
writeFileSync(ENROLLMENT_FILE, token, { mode: 0o600 });
|
|
26
|
+
}
|
|
27
|
+
async function findSchedulerPath() {
|
|
28
|
+
const candidates = [
|
|
29
|
+
join(process.cwd(), 'packages', 'robopark', 'scheduler', 'preview_agent.py'),
|
|
30
|
+
join(process.cwd(), 'scheduler', 'preview_agent.py'),
|
|
31
|
+
];
|
|
32
|
+
for (const p of candidates) {
|
|
33
|
+
if (existsSync(p))
|
|
34
|
+
return p;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const { createRequire } = await import('node:module');
|
|
38
|
+
const require = createRequire(import.meta.url);
|
|
39
|
+
const pkgMain = require.resolve('infinicode/package.json');
|
|
40
|
+
const p = join(pkgMain, '..', 'packages', 'robopark', 'scheduler', 'preview_agent.py');
|
|
41
|
+
if (existsSync(p))
|
|
42
|
+
return p;
|
|
43
|
+
}
|
|
44
|
+
catch { /* ignore */ }
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function findPython() {
|
|
48
|
+
const names = process.platform === 'win32'
|
|
49
|
+
? ['python', 'python3', 'py']
|
|
50
|
+
: ['python3', 'python'];
|
|
51
|
+
for (const name of names) {
|
|
52
|
+
try {
|
|
53
|
+
const { execSync } = require('node:child_process');
|
|
54
|
+
execSync(`${name} --version`, { stdio: 'ignore' });
|
|
55
|
+
return name;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return process.platform === 'win32' ? 'python' : 'python3';
|
|
62
|
+
}
|
|
63
|
+
export async function roboparkEnroll(opts) {
|
|
64
|
+
const token = opts.enrollmentToken ?? readEnrollmentToken();
|
|
65
|
+
if (!token) {
|
|
66
|
+
console.log(chalk.red(' ✗ pass --enrollment-token <token> or write it to ~/.robopark/enrollment_token'));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
saveEnrollmentToken(token);
|
|
70
|
+
const robotId = opts.robotId ?? hostname().split('.')[0];
|
|
71
|
+
const schedulerUrl = opts.schedulerUrl;
|
|
72
|
+
const script = await findSchedulerPath();
|
|
73
|
+
if (!script) {
|
|
74
|
+
console.log(chalk.red(' ✗ could not find preview_agent.py'));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const python = findPython();
|
|
78
|
+
const args = [
|
|
79
|
+
script,
|
|
80
|
+
'--scheduler-url', schedulerUrl,
|
|
81
|
+
'--robot-id', robotId,
|
|
82
|
+
'--enrollment-token', token,
|
|
83
|
+
];
|
|
84
|
+
if (opts.saveConfig)
|
|
85
|
+
args.push('--save-config');
|
|
86
|
+
console.log(chalk.bold('\n robopark enroll'));
|
|
87
|
+
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
88
|
+
console.log(` robot: ${chalk.cyan(robotId)}`);
|
|
89
|
+
console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
|
|
90
|
+
console.log();
|
|
91
|
+
const proc = spawn(python, args, { stdio: 'inherit' });
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
proc.on('error', reject);
|
|
94
|
+
proc.on('close', (code) => {
|
|
95
|
+
if (code === 0)
|
|
96
|
+
resolve();
|
|
97
|
+
else
|
|
98
|
+
reject(new Error(`enrollment exited ${code ?? ''}`));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface PreviewAgentOptions {
|
|
2
|
+
schedulerUrl: string;
|
|
3
|
+
robotId?: string;
|
|
4
|
+
deviceToken?: string;
|
|
5
|
+
enrollmentToken?: string;
|
|
6
|
+
videoDevice: string;
|
|
7
|
+
audioDevice: string;
|
|
8
|
+
width: string;
|
|
9
|
+
height: string;
|
|
10
|
+
fps: string;
|
|
11
|
+
saveConfig?: boolean;
|
|
12
|
+
foreground?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function roboparkPreviewAgent(opts: PreviewAgentOptions): Promise<void>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoboPark — `robopark preview-agent` launcher.
|
|
3
|
+
*
|
|
4
|
+
* Starts preview_agent.py with the right Python interpreter, auto-discovers
|
|
5
|
+
* the scheduler URL from mesh/hub, and detaches unless --foreground.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { hostname } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { discoverContext } from './discovery.js';
|
|
13
|
+
const DEFAULT_SCHEDULER_PORT = 8080;
|
|
14
|
+
async function findSchedulerPath() {
|
|
15
|
+
const candidates = [
|
|
16
|
+
join(process.cwd(), 'packages', 'robopark', 'scheduler', 'preview_agent.py'),
|
|
17
|
+
join(process.cwd(), 'scheduler', 'preview_agent.py'),
|
|
18
|
+
];
|
|
19
|
+
for (const p of candidates) {
|
|
20
|
+
if (existsSync(p))
|
|
21
|
+
return p;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const { createRequire } = await import('node:module');
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const pkgMain = require.resolve('infinicode/package.json');
|
|
27
|
+
const p = join(pkgMain, '..', 'packages', 'robopark', 'scheduler', 'preview_agent.py');
|
|
28
|
+
if (existsSync(p))
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore */ }
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function findPython() {
|
|
35
|
+
const names = process.platform === 'win32'
|
|
36
|
+
? ['python', 'python3', 'py']
|
|
37
|
+
: ['python3', 'python'];
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
try {
|
|
40
|
+
const { execSync } = require('node:child_process');
|
|
41
|
+
execSync(`${name} --version`, { stdio: 'ignore' });
|
|
42
|
+
return name;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return process.platform === 'win32' ? 'python' : 'python3';
|
|
49
|
+
}
|
|
50
|
+
export async function roboparkPreviewAgent(opts) {
|
|
51
|
+
const ctx = await discoverContext();
|
|
52
|
+
const port = DEFAULT_SCHEDULER_PORT;
|
|
53
|
+
const schedulerUrl = opts.schedulerUrl !== 'http://localhost:8080'
|
|
54
|
+
? opts.schedulerUrl
|
|
55
|
+
: ctx.hub
|
|
56
|
+
? `http://${ctx.hub.ip}:${port}`
|
|
57
|
+
: 'http://localhost:8080';
|
|
58
|
+
const robotId = opts.robotId ?? hostname().split('.')[0];
|
|
59
|
+
const script = await findSchedulerPath();
|
|
60
|
+
if (!script) {
|
|
61
|
+
console.log(chalk.red(' ✗ could not find preview_agent.py'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const python = findPython();
|
|
65
|
+
const args = [
|
|
66
|
+
script,
|
|
67
|
+
'--scheduler-url', schedulerUrl,
|
|
68
|
+
'--robot-id', robotId,
|
|
69
|
+
'--video-device', opts.videoDevice,
|
|
70
|
+
'--audio-device', opts.audioDevice,
|
|
71
|
+
'--width', opts.width,
|
|
72
|
+
'--height', opts.height,
|
|
73
|
+
'--fps', opts.fps,
|
|
74
|
+
];
|
|
75
|
+
if (opts.deviceToken)
|
|
76
|
+
args.push('--device-token', opts.deviceToken);
|
|
77
|
+
if (opts.enrollmentToken)
|
|
78
|
+
args.push('--enrollment-token', opts.enrollmentToken);
|
|
79
|
+
if (opts.saveConfig)
|
|
80
|
+
args.push('--save-config');
|
|
81
|
+
console.log(chalk.bold('\n robopark preview-agent'));
|
|
82
|
+
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
83
|
+
console.log(` robot: ${chalk.cyan(robotId)}`);
|
|
84
|
+
console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
|
|
85
|
+
console.log(` video: ${chalk.cyan(opts.videoDevice)}`);
|
|
86
|
+
console.log(` audio: ${chalk.cyan(opts.audioDevice)}`);
|
|
87
|
+
console.log();
|
|
88
|
+
if (opts.foreground) {
|
|
89
|
+
const proc = spawn(python, args, { stdio: 'inherit' });
|
|
90
|
+
await new Promise((resolve) => {
|
|
91
|
+
proc.on('close', () => resolve());
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const proc = spawn(python, args, {
|
|
96
|
+
stdio: 'ignore',
|
|
97
|
+
detached: true,
|
|
98
|
+
env: { ...process.env, ROBOPARK_PREVIEW_AGENT: '1' },
|
|
99
|
+
});
|
|
100
|
+
proc.unref();
|
|
101
|
+
console.log(chalk.green(' ✓ preview agent started in background'));
|
|
102
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoboPark — `robopark probe`.
|
|
3
|
+
*
|
|
4
|
+
* Spawn opencode agents (locally or over mesh) to build an accurate
|
|
5
|
+
* infrastructure map. Discovers peers via Tailscale, then runs a structured
|
|
6
|
+
* prompt through the locally installed `opencode` CLI for each node.
|
|
7
|
+
*
|
|
8
|
+
* The agent is asked only to report facts — it does not mutate anything.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { hostname, networkInterfaces } from 'node:os';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { discoverContext } from './discovery.js';
|
|
14
|
+
function getLanIps() {
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const [name, addrs] of Object.entries(networkInterfaces())) {
|
|
17
|
+
if (!addrs)
|
|
18
|
+
continue;
|
|
19
|
+
for (const a of addrs) {
|
|
20
|
+
if (a.family === 'IPv4' && !a.internal)
|
|
21
|
+
out.push(a.address);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function findOpencode() {
|
|
27
|
+
const names = process.platform === 'win32' ? ['opencode.exe', 'opencode'] : ['opencode'];
|
|
28
|
+
for (const name of names) {
|
|
29
|
+
try {
|
|
30
|
+
const { execSync } = require('node:child_process');
|
|
31
|
+
execSync(`${name} --version`, { stdio: 'ignore' });
|
|
32
|
+
return name;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function runOpencodePrompt(prompt, timeoutMs) {
|
|
41
|
+
const bin = findOpencode();
|
|
42
|
+
if (!bin)
|
|
43
|
+
return Promise.resolve('opencode CLI not found; install opencode to run probes');
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const proc = spawn(bin, ['--no-project', '--non-interactive'], {
|
|
46
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
|
+
env: { ...process.env, OPENCODE_QUIET: '1' },
|
|
48
|
+
});
|
|
49
|
+
let stdout = '';
|
|
50
|
+
let stderr = '';
|
|
51
|
+
let killed = false;
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
killed = true;
|
|
54
|
+
proc.kill('SIGTERM');
|
|
55
|
+
}, timeoutMs);
|
|
56
|
+
proc.stdin?.write(prompt);
|
|
57
|
+
proc.stdin?.end();
|
|
58
|
+
proc.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
59
|
+
proc.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
60
|
+
proc.on('close', () => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
if (killed)
|
|
63
|
+
resolve(stdout + '\n[probe timed out]');
|
|
64
|
+
else
|
|
65
|
+
resolve(stdout || stderr || '(no output)');
|
|
66
|
+
});
|
|
67
|
+
proc.on('error', (err) => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve(`opencode error: ${err.message}`);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function probePrompt(target, token) {
|
|
74
|
+
const tokenHint = token ? `mesh token is "${token}"` : 'no mesh token provided';
|
|
75
|
+
return `You are an infrastructure probe for the RoboPark fleet.
|
|
76
|
+
Target node: ${target.name} (${target.ip}) via ${target.source}.
|
|
77
|
+
${tokenHint}.
|
|
78
|
+
|
|
79
|
+
Do NOT modify anything. Only report facts. Keep your answer under 600 words.
|
|
80
|
+
|
|
81
|
+
Please report:
|
|
82
|
+
1. Hostname, OS, architecture.
|
|
83
|
+
2. LAN IPs and Tailscale IP if available.
|
|
84
|
+
3. Whether these processes are running: infinicode node, robopark scheduler, robopark preview_agent.
|
|
85
|
+
4. If this node is the hub, whether the scheduler at http://localhost:8080/api/settings responds and what production_mode is.
|
|
86
|
+
5. If this node is a robot/satellite, whether it has enrolled to the scheduler and whether ~/.robopark/device_token exists.
|
|
87
|
+
6. Any recent errors in journalctl/systemctl for infinicode or robopark services (last 10 lines).
|
|
88
|
+
7. Whether a camera (/dev/video0) or microphone is detected.
|
|
89
|
+
|
|
90
|
+
Answer as concise structured Markdown bullet list.`;
|
|
91
|
+
}
|
|
92
|
+
async function probeTarget(target, opts) {
|
|
93
|
+
const timeoutMs = opts.timeout ? parseInt(opts.timeout, 10) * 1000 : 60000;
|
|
94
|
+
try {
|
|
95
|
+
const summary = await runOpencodePrompt(probePrompt(target, opts.token), timeoutMs);
|
|
96
|
+
return { target, ok: true, summary, timestamp: new Date().toISOString() };
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
return { target, ok: false, error: e.message, summary: '', timestamp: new Date().toISOString() };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function roboparkProbe(opts) {
|
|
103
|
+
const ctx = await discoverContext();
|
|
104
|
+
const self = { name: hostname().split('.')[0], ip: getLanIps()[0] || '127.0.0.1', source: 'self' };
|
|
105
|
+
const rawTargets = opts.local ? [self] : [self, ...(ctx.robots || []), ...(ctx.hub ? [ctx.hub] : [])];
|
|
106
|
+
const targets = rawTargets.map((t) => ({
|
|
107
|
+
name: t.name,
|
|
108
|
+
ip: t.ip,
|
|
109
|
+
source: t === self ? 'self' : 'tailscale',
|
|
110
|
+
}));
|
|
111
|
+
console.log(chalk.bold('\n robopark probe'));
|
|
112
|
+
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
113
|
+
console.log(` targets: ${chalk.cyan(targets.length)}`);
|
|
114
|
+
console.log();
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const t of targets) {
|
|
117
|
+
process.stdout.write(chalk.dim(` probing ${t.name}… `));
|
|
118
|
+
const r = await probeTarget(t, opts);
|
|
119
|
+
results.push(r);
|
|
120
|
+
console.log(r.ok ? chalk.green('ok') : chalk.red('failed'));
|
|
121
|
+
}
|
|
122
|
+
console.log();
|
|
123
|
+
console.log(chalk.bold(' Probe results'));
|
|
124
|
+
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
125
|
+
for (const r of results) {
|
|
126
|
+
console.log(`\n ${chalk.cyan(r.target.name)} (${r.target.ip})`);
|
|
127
|
+
if (!r.ok) {
|
|
128
|
+
console.log(chalk.red(` error: ${r.error}`));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
console.log(r.summary.split('\n').map(l => ' ' + l).join('\n'));
|
|
132
|
+
}
|
|
133
|
+
if (opts.output) {
|
|
134
|
+
const { writeFileSync } = await import('node:fs');
|
|
135
|
+
writeFileSync(opts.output, JSON.stringify(results, null, 2), 'utf8');
|
|
136
|
+
console.log(`\n ${chalk.green('✓')} wrote ${opts.output}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
package/dist/robopark/serve.js
CHANGED
|
@@ -108,6 +108,19 @@ export async function roboparkServe(opts) {
|
|
|
108
108
|
const port = opts.port ? parseInt(opts.port, 10) : 8080;
|
|
109
109
|
const host = opts.host ?? '0.0.0.0';
|
|
110
110
|
const python = findPython();
|
|
111
|
+
const schedulerDir = dirname(schedulerPath);
|
|
112
|
+
const requirements = join(schedulerDir, 'requirements.txt');
|
|
113
|
+
// Ensure scheduler Python dependencies are installed before starting.
|
|
114
|
+
if (existsSync(requirements)) {
|
|
115
|
+
console.log(chalk.dim(' ensuring scheduler Python deps…'));
|
|
116
|
+
try {
|
|
117
|
+
execSync(`${python} -m pip install -r "${requirements}" --user --quiet`, { stdio: 'pipe' });
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.log(chalk.yellow(` ⚠ pip install failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
121
|
+
console.log(chalk.dim(' continuing; scheduler may fail if deps are missing.'));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
111
124
|
const env = {
|
|
112
125
|
...process.env,
|
|
113
126
|
SCHEDULER_HOST: host,
|