infinicode 2.8.65 → 2.8.66
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 +4 -5
- package/dist/kernel/federation/dashboard-html.js +30 -7
- package/dist/kernel/federation/federation.js +1 -0
- package/dist/kernel/federation/transport-http.d.ts +6 -1
- package/dist/kernel/federation/transport-http.js +75 -0
- package/package.json +1 -1
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
* for its cam / mic / speaker / hardware drawer)
|
|
15
15
|
* Sessions & Telemetry — RoboPark scheduler stats + production_mode + controls
|
|
16
16
|
*
|
|
17
|
-
* The Park cam panel
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* That token/track is produced by the robot-side RoboPark plugin.
|
|
17
|
+
* The Park cam panel relays RoboVisionAI_PI's existing MJPEG feed through the
|
|
18
|
+
* authenticated mesh. LiveKit remains the voice transport but is deliberately
|
|
19
|
+
* not used for dashboard video, avoiding camera ownership conflicts on robots.
|
|
21
20
|
*
|
|
22
21
|
* Kept free of backticks and ${...} so the whole page fits inside this TS
|
|
23
22
|
* template literal without escaping; the client JS uses string concatenation.
|
|
@@ -5446,7 +5445,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5446
5445
|
// ── on-demand operator preview: open the robot's cam without a scene-detection session ──
|
|
5447
5446
|
function updateGoLive(){ var b=$('pk-golive'); if(b){ b.textContent = previewOn?'■ Stop live':'▶ Go live'; b.classList.toggle('danger', previewOn); } }
|
|
5448
5447
|
function goLive(n){ if(previewOn) stopPreview(n); else startPreview(n); }
|
|
5449
|
-
function startPreview(n){
|
|
5448
|
+
function startPreview(n){
|
|
5449
|
+
previewOn=true; updateGoLive();
|
|
5450
|
+
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
|
|
5451
|
+
idleCam('connecting');
|
|
5452
|
+
tryVisionOverlay(n, true);
|
|
5453
|
+
return;
|
|
5450
5454
|
previewOn=true; updateGoLive();
|
|
5451
5455
|
var note=$('pk-golivenote'); if(note) note.textContent='requesting preview…';
|
|
5452
5456
|
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; } // drop any session cam first
|
|
@@ -5463,7 +5467,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5463
5467
|
})
|
|
5464
5468
|
.catch(function(e){ idleCam('unreachable'); if(note) note.textContent='preview request failed'; if(window.console) console.error('RoboPark preview request failed',e); previewOn=false; updateGoLive(); });
|
|
5465
5469
|
}
|
|
5466
|
-
function stopPreview(n){
|
|
5470
|
+
function stopPreview(n){
|
|
5471
|
+
previewOn=false; updateGoLive();
|
|
5472
|
+
if(previewTimer){ clearInterval(previewTimer); previewTimer=null; }
|
|
5473
|
+
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
|
|
5474
|
+
stopVision();
|
|
5475
|
+
var directNote=$('pk-golivenote'); if(directNote) directNote.textContent='';
|
|
5476
|
+
idleCam('idle');
|
|
5477
|
+
return;
|
|
5467
5478
|
previewOn=false; updateGoLive();
|
|
5468
5479
|
if(previewTimer){ clearInterval(previewTimer); previewTimer=null; }
|
|
5469
5480
|
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
|
|
@@ -5478,6 +5489,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5478
5489
|
// server is opt-in because running it beside preview_agent would open the
|
|
5479
5490
|
// same Windows camera twice and make both feeds unreliable.
|
|
5480
5491
|
function tryVisionOverlay(n, force){
|
|
5492
|
+
var directImg=$('pk-vision-img'); if(!directImg) return;
|
|
5493
|
+
var directRef=n.nodeId||n.id||n.name;
|
|
5494
|
+
if(!directRef){ visionOn=false; directImg.src=''; directImg.style.display='none'; return; }
|
|
5495
|
+
var directCamNote=$('pk-camnote'), directLiveNote=$('pk-golivenote'), directRes=$('pk-camres');
|
|
5496
|
+
visionOn=true;
|
|
5497
|
+
directImg.onerror=function(){ visionOn=false; directImg.style.display='none'; directImg.src=''; if(directCamNote) directCamNote.textContent='RoboVision feed unavailable on robot'; if(directLiveNote) directLiveNote.textContent='camera unavailable'; if(directRes) directRes.textContent='OFFLINE'; };
|
|
5498
|
+
directImg.onload=function(){ directImg.style.display='block'; if(directCamNote) directCamNote.textContent='live camera via RoboVision mesh relay'; if(directLiveNote) directLiveNote.textContent='live'; if(directRes) directRes.textContent='LIVE'; };
|
|
5499
|
+
directImg.src='/fed/media/robots/'+encodeURIComponent(directRef)+'/video_feed'+QS+(QS?'&':'?')+'_='+Date.now();
|
|
5500
|
+
return;
|
|
5481
5501
|
var img=$('pk-vision-img'); if(!img) return;
|
|
5482
5502
|
if(!force && String(window.localStorage.getItem('robopark.visionOverlay')||'').toLowerCase()!=='true'){
|
|
5483
5503
|
visionOn=false; img.src=''; img.style.display='none'; return;
|
|
@@ -6084,7 +6104,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6084
6104
|
function setFill(id,pct){ var e=$(id); if(!e||pct==null) return; e.style.width=Math.max(2,Math.min(100,pct))+'%'; e.style.background=pct>=75?'var(--bad)':pct>=45?'var(--warn)':'var(--gold-bright)'; }
|
|
6085
6105
|
|
|
6086
6106
|
// ── cam: target a LiveKit track; fall back to a styled preview ──
|
|
6087
|
-
function startCam(n){
|
|
6107
|
+
function startCam(n){
|
|
6108
|
+
idleCam('connecting');
|
|
6109
|
+
tryVisionOverlay(n, true);
|
|
6110
|
+
return;
|
|
6088
6111
|
var note=$('pk-camnote');
|
|
6089
6112
|
camAbort = ('AbortController' in window)?new AbortController():null;
|
|
6090
6113
|
idleCam('connecting');
|
|
@@ -86,6 +86,7 @@ export class Federation {
|
|
|
86
86
|
logger,
|
|
87
87
|
getManifest: () => buildManifest(identity, this.describeWithBackends()),
|
|
88
88
|
getNodes: () => this.nodeStatuses(),
|
|
89
|
+
getPeers: () => this.peers(),
|
|
89
90
|
getStatus: () => this.statusSnapshot(),
|
|
90
91
|
runCommand: (input) => this.deps.kernel.command(input),
|
|
91
92
|
commandGate: options.commandGate,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Logger } from '../types.js';
|
|
2
|
-
import type { FederationEnvelope, NodeManifest, StreamFrame } from './types.js';
|
|
2
|
+
import type { FederationEnvelope, NodeManifest, PeerInfo, StreamFrame } from './types.js';
|
|
3
3
|
export type RpcHandler = (env: FederationEnvelope, remote: string) => Promise<FederationEnvelope | void>;
|
|
4
4
|
export interface MeshServerOptions {
|
|
5
5
|
host?: string;
|
|
@@ -9,6 +9,8 @@ export interface MeshServerOptions {
|
|
|
9
9
|
getManifest: () => NodeManifest;
|
|
10
10
|
/** Optional: full mesh view (self + peers) for the GET /fed/nodes probe. */
|
|
11
11
|
getNodes?: () => unknown;
|
|
12
|
+
/** Connected peers and their reachable mesh URLs. */
|
|
13
|
+
getPeers?: () => PeerInfo[];
|
|
12
14
|
/** Optional: consolidated fleet snapshot (self + nodes + hardware + roles +
|
|
13
15
|
* runs + recent activity) for the dashboard and `robopark fleet` (GET /fed/status). */
|
|
14
16
|
getStatus?: () => unknown;
|
|
@@ -50,6 +52,9 @@ export declare class MeshServer {
|
|
|
50
52
|
private seq;
|
|
51
53
|
constructor(opts: MeshServerOptions);
|
|
52
54
|
get streamCount(): number;
|
|
55
|
+
private streamHttp;
|
|
56
|
+
private streamLocalRoboVision;
|
|
57
|
+
private streamPeerRoboVision;
|
|
53
58
|
start(): Promise<void>;
|
|
54
59
|
/** Proxy a dashboard request to the RoboPark scheduler (product data). */
|
|
55
60
|
private proxyToScheduler;
|
|
@@ -20,6 +20,7 @@ import { promisify } from 'node:util';
|
|
|
20
20
|
import { timingSafeEqual } from 'node:crypto';
|
|
21
21
|
import { readFileSync } from 'node:fs';
|
|
22
22
|
import { createRequire } from 'node:module';
|
|
23
|
+
import { once } from 'node:events';
|
|
23
24
|
import { frameToSSE, parseSSE } from './protocol.js';
|
|
24
25
|
const execFileAsync = promisify(execFile);
|
|
25
26
|
/** Read the livekit-client UMD bundle from the installed dependency, once.
|
|
@@ -82,6 +83,68 @@ export class MeshServer {
|
|
|
82
83
|
get streamCount() {
|
|
83
84
|
return this.sse.size;
|
|
84
85
|
}
|
|
86
|
+
async streamHttp(req, res, target, headers = {}) {
|
|
87
|
+
const ctrl = new AbortController();
|
|
88
|
+
req.once('close', () => ctrl.abort());
|
|
89
|
+
try {
|
|
90
|
+
const upstream = await fetch(target, { headers, signal: ctrl.signal });
|
|
91
|
+
if (!upstream.ok || !upstream.body) {
|
|
92
|
+
const detail = await upstream.text().catch(() => '');
|
|
93
|
+
res.writeHead(upstream.status, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
|
|
94
|
+
res.end(detail || `media upstream returned ${upstream.status}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
res.writeHead(200, {
|
|
98
|
+
'content-type': upstream.headers.get('content-type') ?? 'multipart/x-mixed-replace; boundary=frame',
|
|
99
|
+
'cache-control': 'no-store, no-cache, must-revalidate',
|
|
100
|
+
pragma: 'no-cache',
|
|
101
|
+
connection: 'keep-alive',
|
|
102
|
+
});
|
|
103
|
+
const reader = upstream.body.getReader();
|
|
104
|
+
for (;;) {
|
|
105
|
+
const { done, value } = await reader.read();
|
|
106
|
+
if (done || res.writableEnded)
|
|
107
|
+
break;
|
|
108
|
+
if (!res.write(Buffer.from(value)))
|
|
109
|
+
await once(res, 'drain');
|
|
110
|
+
}
|
|
111
|
+
if (!res.writableEnded)
|
|
112
|
+
res.end();
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (ctrl.signal.aborted)
|
|
116
|
+
return;
|
|
117
|
+
if (!res.headersSent) {
|
|
118
|
+
res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
|
|
119
|
+
res.end(`robot camera unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
120
|
+
}
|
|
121
|
+
else if (!res.writableEnded) {
|
|
122
|
+
res.end();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
streamLocalRoboVision(req, res) {
|
|
127
|
+
void this.streamHttp(req, res, 'http://127.0.0.1:5000/video_feed');
|
|
128
|
+
}
|
|
129
|
+
streamPeerRoboVision(req, res, nodeRef) {
|
|
130
|
+
const ref = decodeURIComponent(nodeRef).toLowerCase();
|
|
131
|
+
const self = this.opts.getManifest();
|
|
132
|
+
if (self.nodeId.toLowerCase() === ref || self.displayName.toLowerCase() === ref) {
|
|
133
|
+
this.streamLocalRoboVision(req, res);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const peer = (this.opts.getPeers?.() ?? []).find(candidate => candidate.connected &&
|
|
137
|
+
(candidate.nodeId.toLowerCase() === ref || candidate.displayName.toLowerCase() === ref));
|
|
138
|
+
if (!peer) {
|
|
139
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
|
|
140
|
+
res.end(`connected robot mesh node not found: ${nodeRef}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const headers = {};
|
|
144
|
+
if (this.opts.token)
|
|
145
|
+
headers.authorization = `Bearer ${this.opts.token}`;
|
|
146
|
+
void this.streamHttp(req, res, `${peer.url.replace(/\/+$/, '')}/fed/media/video_feed`, headers);
|
|
147
|
+
}
|
|
85
148
|
async start() {
|
|
86
149
|
const handler = (req, res) => this.handle(req, res);
|
|
87
150
|
this.server = this.opts.tls
|
|
@@ -344,6 +407,18 @@ export class MeshServer {
|
|
|
344
407
|
res.end(js);
|
|
345
408
|
return;
|
|
346
409
|
}
|
|
410
|
+
// RoboVision owns the robot camera. Satellites expose the existing local
|
|
411
|
+
// MJPEG feed, while the hub relays it over the authenticated mesh. This
|
|
412
|
+
// avoids opening /dev/video0 again in LiveKit and works across private LANs.
|
|
413
|
+
if (req.method === 'GET' && path === '/fed/media/video_feed') {
|
|
414
|
+
this.streamLocalRoboVision(req, res);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const robotVideo = path.match(/^\/fed\/media\/robots\/([^/]+)\/video_feed$/);
|
|
418
|
+
if (req.method === 'GET' && robotVideo) {
|
|
419
|
+
this.streamPeerRoboVision(req, res, robotVideo[1]);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
347
422
|
if (req.method === 'GET' && path === '/fed/status') {
|
|
348
423
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
349
424
|
res.end(JSON.stringify(this.opts.getStatus?.() ?? {}));
|
package/package.json
CHANGED