infinicode 2.6.1 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +16 -1
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +346 -110
- package/dist/kernel/federation/telemetry.d.ts +5 -0
- package/dist/kernel/federation/telemetry.js +72 -9
- package/dist/kernel/federation/transport-http.js +35 -0
- package/dist/kernel/federation/types.d.ts +3 -0
- package/package.json +8 -7
|
@@ -124,6 +124,25 @@ section h2 .count{color:var(--muted);}
|
|
|
124
124
|
.cap{font-family:var(--mono);font-size:0.58rem;color:var(--muted);background:var(--panel-2);border:1px solid var(--glass-border);border-radius:5px;padding:0.05em 0.4em;}
|
|
125
125
|
.empty{color:var(--muted);font-size:0.82rem;padding:1.4rem;text-align:center;border:1px dashed var(--border);border-radius:11px;}
|
|
126
126
|
|
|
127
|
+
/* fleet card — live per-robot hardware */
|
|
128
|
+
.statdot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--muted);margin-right:0.3rem;vertical-align:middle;flex:none;}
|
|
129
|
+
.statdot.on{background:var(--ok);box-shadow:0 0 7px color-mix(in srgb,var(--ok) 70%,transparent);}
|
|
130
|
+
.statdot.off{background:var(--bad);}
|
|
131
|
+
.node .mrow{display:grid;grid-template-columns:2.3rem 1fr 2.6rem;align-items:center;gap:0.45rem;margin-top:0.42rem;font-family:var(--mono);font-size:0.6rem;color:var(--muted);}
|
|
132
|
+
.node .mrow .ml{letter-spacing:0.04em;}
|
|
133
|
+
.node .mrow .mv{color:var(--ink);font-size:0.66rem;text-align:right;}
|
|
134
|
+
.node .mrow .mv.warn{color:var(--warn);} .node .mrow .mv.bad{color:var(--bad);} .node .mrow .mv.ok{color:var(--ok);}
|
|
135
|
+
.node .mrow .bar-track{margin:0;}
|
|
136
|
+
.node .spark{width:100%;height:20px;display:block;margin:0.28rem 0 0.1rem;opacity:0.92;}
|
|
137
|
+
.node .stats{display:flex;flex-wrap:wrap;gap:0.3rem 0.7rem;margin-top:0.5rem;font-family:var(--mono);font-size:0.6rem;color:var(--muted);}
|
|
138
|
+
.node .stats span b{color:var(--ink);font-weight:600;}
|
|
139
|
+
.node .stats .rx b{color:var(--info);} .node .stats .tx b{color:var(--gold-bright);}
|
|
140
|
+
.tbadge{font-family:var(--mono);font-size:0.6rem;padding:0.08em 0.45em;border-radius:5px;border:1px solid var(--glass-border);color:var(--muted);}
|
|
141
|
+
.tbadge.ok{color:var(--ok);} .tbadge.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 40%,transparent);}
|
|
142
|
+
.tbadge.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent);background:var(--bad-bg);}
|
|
143
|
+
.chip.alert{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent);background:var(--bad-bg);}
|
|
144
|
+
.chip.alert b{color:var(--bad);}
|
|
145
|
+
|
|
127
146
|
/* hardware meters */
|
|
128
147
|
.hw{border-radius:13px;padding:0.85rem;margin-top:1rem;
|
|
129
148
|
background:var(--panel);-webkit-backdrop-filter:blur(14px) saturate(1.25);backdrop-filter:blur(14px) saturate(1.25);
|
|
@@ -443,6 +462,45 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
443
462
|
+ '<div class="bar-track"><div class="bar-fill '+loadClass(v)+'" style="width:'+v+'%"></div></div></div>';
|
|
444
463
|
}
|
|
445
464
|
|
|
465
|
+
// ── per-node hardware store + rolling history (drives the Fleet cards + sparklines) ──
|
|
466
|
+
var hwByNode = {}; // nodeId -> latest HardwareSnapshot (self from /fed/status.hardware, peers from 'hw' frames)
|
|
467
|
+
var hwHist = {}; // nodeId -> { cpu:[], temp:[], net:[] } rolling buffers
|
|
468
|
+
var HWMAX = 48; // ~2.5 min of history at the 3s poll cadence
|
|
469
|
+
function netTotal(hw){
|
|
470
|
+
var t=0; if(hw&&hw.network&&hw.network.interfaces){ for(var i=0;i<hw.network.interfaces.length;i++){ var f=hw.network.interfaces[i]; t+=(f.rxBytesPerSec||0)+(f.txBytesPerSec||0); } } return t;
|
|
471
|
+
}
|
|
472
|
+
function netRxTx(hw){
|
|
473
|
+
var rx=0,tx=0; if(hw&&hw.network&&hw.network.interfaces){ for(var i=0;i<hw.network.interfaces.length;i++){ rx+=hw.network.interfaces[i].rxBytesPerSec||0; tx+=hw.network.interfaces[i].txBytesPerSec||0; } } return {rx:rx,tx:tx};
|
|
474
|
+
}
|
|
475
|
+
function pushHist(id, snap){
|
|
476
|
+
var h = hwHist[id] || (hwHist[id]={cpu:[],temp:[],net:[]});
|
|
477
|
+
var cpu = snap&&snap.cpu?Math.round(snap.cpu.percent):0;
|
|
478
|
+
var temp = snap&&snap.temperatureC!=null?snap.temperatureC:null;
|
|
479
|
+
h.cpu.push(cpu); if(h.cpu.length>HWMAX) h.cpu.shift();
|
|
480
|
+
h.temp.push(temp); if(h.temp.length>HWMAX) h.temp.shift();
|
|
481
|
+
h.net.push(netTotal(snap)); if(h.net.length>HWMAX) h.net.shift();
|
|
482
|
+
}
|
|
483
|
+
function updateHw(s){
|
|
484
|
+
if(s.self && s.hardware){ hwByNode[s.self.nodeId] = s.hardware; }
|
|
485
|
+
var act = s.activity||[];
|
|
486
|
+
for(var i=0;i<act.length;i++){ var f=(act[i].f||act[i]); if(f.t==='hw'&&f.n&&f.d){ hwByNode[f.n]=f.d; } }
|
|
487
|
+
var nodes = s.nodes||[];
|
|
488
|
+
for(var j=0;j<nodes.length;j++){ if(nodes[j].connected) pushHist(nodes[j].nodeId, hwByNode[nodes[j].nodeId]); }
|
|
489
|
+
}
|
|
490
|
+
// Inline sparkline (SVG polyline) from a numeric history buffer.
|
|
491
|
+
function sparkSvg(arr, color, maxv){
|
|
492
|
+
if(!arr || arr.length<2) return '';
|
|
493
|
+
var w=100,h=20,n=arr.length, vals=[];
|
|
494
|
+
for(var i=0;i<n;i++) vals.push(arr[i]==null?0:arr[i]);
|
|
495
|
+
var mx = maxv || Math.max.apply(null, vals); if(mx<=0) mx=1;
|
|
496
|
+
var pts=''; for(var j=0;j<n;j++){ var x=(j/(n-1))*w; var y=h-(vals[j]/mx)*(h-2)-1; pts+=(j?' ':'')+x.toFixed(1)+','+y.toFixed(1); }
|
|
497
|
+
return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" preserveAspectRatio="none" aria-hidden="true">'
|
|
498
|
+
+ '<polyline points="'+pts+'" fill="none" stroke="'+color+'" stroke-width="1.6" stroke-linejoin="round"/></svg>';
|
|
499
|
+
}
|
|
500
|
+
function fmtRate(b){ return fmtBytes(b)+'/s'; }
|
|
501
|
+
function fmtUptime(sec){ if(sec==null) return '—'; var d=Math.floor(sec/86400),h=Math.floor((sec%86400)/3600),m=Math.floor((sec%3600)/60); if(d) return d+'d '+h+'h'; if(h) return h+'h '+m+'m'; return m+'m'; }
|
|
502
|
+
function tempClass(t){ return t==null?'':(t>=75?'bad':(t>=65?'warn':'ok')); }
|
|
503
|
+
|
|
446
504
|
function renderHw(hw){
|
|
447
505
|
var el = $('hw');
|
|
448
506
|
if(!hw){ el.innerHTML = '<div class="meter"><div class="lab"><span>This node</span><span>no telemetry yet</span></div></div>'; return; }
|
|
@@ -450,6 +508,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
450
508
|
html += meter('CPU', hw.cpu?hw.cpu.percent:0, hw.cpu?(hw.cpu.cores+' cores'):'');
|
|
451
509
|
html += meter('Memory', hw.memory?hw.memory.percent:0, hw.memory?(fmtBytes(hw.memory.usedBytes)+' / '+fmtBytes(hw.memory.totalBytes)):'');
|
|
452
510
|
if(hw.gpu){ html += meter('GPU '+esc(hw.gpu.name||hw.gpu.vendor||''), hw.gpu.utilizationPercent, hw.gpu.vramTotalBytes?(fmtBytes(hw.gpu.vramUsedBytes)+' / '+fmtBytes(hw.gpu.vramTotalBytes)+' vram'):''); }
|
|
511
|
+
if(hw.temperatureC!=null){ html += meter('Temp', Math.min(100,(hw.temperatureC/90)*100), Math.round(hw.temperatureC)+'°C'); }
|
|
512
|
+
if(hw.disk&&hw.disk.mounts&&hw.disk.mounts[0]){ var mnt=hw.disk.mounts[0]; html += meter('Disk '+esc(mnt.path), mnt.percent, fmtBytes(mnt.usedBytes)+' / '+fmtBytes(mnt.totalBytes)); }
|
|
513
|
+
var nt=netRxTx(hw); html += '<div class="stats" style="margin-top:0.55rem">'
|
|
514
|
+
+ '<span class="rx">↓ <b>'+fmtRate(nt.rx)+'</b></span><span class="tx">↑ <b>'+fmtRate(nt.tx)+'</b></span>'
|
|
515
|
+
+ (hw.system?('<span>up <b>'+fmtUptime(hw.system.uptimeSec)+'</b></span>'):'') + '</div>';
|
|
453
516
|
el.innerHTML = html;
|
|
454
517
|
}
|
|
455
518
|
|
|
@@ -460,25 +523,57 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
460
523
|
for(var i=0;i<cards.length;i++) cards[i].classList.toggle('sel', cards[i].getAttribute('data-id')===id);
|
|
461
524
|
}
|
|
462
525
|
|
|
526
|
+
function mrow(label, pct, valTxt, cls){
|
|
527
|
+
var v = Math.max(0, Math.min(100, Math.round(pct||0)));
|
|
528
|
+
return '<div class="mrow"><span class="ml">'+label+'</span>'
|
|
529
|
+
+ '<div class="bar-track"><div class="bar-fill '+loadClass(v)+'" style="width:'+v+'%"></div></div>'
|
|
530
|
+
+ '<span class="mv '+(cls||'')+'">'+esc(valTxt)+'</span></div>';
|
|
531
|
+
}
|
|
463
532
|
function renderFleet(nodes){
|
|
464
533
|
var el = $('fleet');
|
|
465
534
|
if(!nodes || !nodes.length){ el.innerHTML = '<div class="empty">No nodes reporting yet. Start peers with <span class="mono">infinicode serve</span> or <span class="mono">robopark setup</span>.</div>'; return; }
|
|
535
|
+
// connected first, then robots/satellites above hubs so the fleet reads robot-first
|
|
466
536
|
nodes.sort(function(a,b){ return (b.connected?1:0)-(a.connected?1:0); });
|
|
537
|
+
var accent = '#6FE0A6';
|
|
538
|
+
try{ accent = getComputedStyle(document.documentElement).getPropertyValue('--ok').trim() || accent; }catch(e){}
|
|
467
539
|
var html = '';
|
|
468
540
|
for(var i=0;i<nodes.length;i++){
|
|
469
541
|
var n = nodes[i];
|
|
470
542
|
var isSelf = n.nodeId===selfId;
|
|
471
|
-
var
|
|
472
|
-
var
|
|
543
|
+
var up = !!n.connected;
|
|
544
|
+
var hw = hwByNode[n.nodeId] || null;
|
|
545
|
+
var hist = hwHist[n.nodeId] || {};
|
|
546
|
+
var cpu = hw&&hw.cpu ? Math.round(hw.cpu.percent) : (n.load!=null?Math.round(n.load):0);
|
|
547
|
+
var cores = hw&&hw.cpu ? hw.cpu.cores : null;
|
|
548
|
+
var mem = hw&&hw.memory ? Math.round(hw.memory.percent) : null;
|
|
549
|
+
var temp = hw&&hw.temperatureC!=null ? hw.temperatureC : null;
|
|
550
|
+
var disk = (hw&&hw.disk&&hw.disk.mounts&&hw.disk.mounts[0]) ? hw.disk.mounts[0].percent : null;
|
|
551
|
+
var nt = netRxTx(hw);
|
|
552
|
+
var upt = hw&&hw.system ? hw.system.uptimeSec : null;
|
|
553
|
+
var caps = (n.capabilities||[]).filter(function(c){ return c.indexOf('backend:')!==0; }).slice(0,5);
|
|
473
554
|
var capHtml=''; for(var c=0;c<caps.length;c++) capHtml += '<span class="cap">'+esc(caps[c])+'</span>';
|
|
474
|
-
var site = n.site?('· '+esc(n.site)):'';
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
+ '<div class="
|
|
478
|
-
+ '<div class="
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
+ '
|
|
555
|
+
var site = n.site?(' · '+esc(n.site)):'';
|
|
556
|
+
|
|
557
|
+
html += '<div class="node '+(up?'up':'down')+(selected===n.nodeId?' sel':'')+'" tabindex="0" role="button" data-id="'+esc(n.nodeId)+'" data-name="'+esc(n.displayName)+'">'
|
|
558
|
+
+ '<div class="top"><span class="name"><span class="statdot '+(up?'on':'off')+'"></span>'+esc(n.displayName)+(isSelf?' <span class="selfbadge">this</span>':'')+'</span><span class="role">'+esc(n.role)+'</span></div>'
|
|
559
|
+
+ '<div class="meta">'+esc(n.platform||'')+' '+esc((hw&&hw.cpu&&hw.cpu.model)?hw.cpu.model.replace(/\\s+/g,' ').slice(0,22):'')+site+' <span class="mono">v'+esc(n.version||'?')+'</span></div>';
|
|
560
|
+
|
|
561
|
+
if(up){
|
|
562
|
+
html += mrow('CPU', cpu, cpu+'%'+(cores?(' /'+cores+'c'):''), loadClass(cpu))
|
|
563
|
+
+ (hist.cpu&&hist.cpu.length>1 ? sparkSvg(hist.cpu, accent, 100) : '')
|
|
564
|
+
+ (mem!=null ? mrow('MEM', mem, mem+'%', loadClass(mem)) : '')
|
|
565
|
+
+ (temp!=null ? mrow('TEMP', Math.min(100,(temp/90)*100), Math.round(temp)+'°C', tempClass(temp)) : '')
|
|
566
|
+
+ '<div class="stats">'
|
|
567
|
+
+ '<span class="rx">↓ <b>'+fmtRate(nt.rx)+'</b></span>'
|
|
568
|
+
+ '<span class="tx">↑ <b>'+fmtRate(nt.tx)+'</b></span>'
|
|
569
|
+
+ (disk!=null?('<span>disk <b>'+disk+'%</b></span>'):'')
|
|
570
|
+
+ (upt!=null?('<span>up <b>'+fmtUptime(upt)+'</b></span>'):'')
|
|
571
|
+
+ '<span><b>'+((n.commands||[]).length)+'</b> cmds</span>'
|
|
572
|
+
+ '</div>';
|
|
573
|
+
} else {
|
|
574
|
+
html += '<div class="stats"><span style="color:var(--muted)">offline'+(n.lastSeenAt?(' · last seen '+clock(n.lastSeenAt)):'')+'</span></div>';
|
|
575
|
+
}
|
|
576
|
+
html += (capHtml?('<div class="caps">'+capHtml+'</div>'):'') + '</div>';
|
|
482
577
|
}
|
|
483
578
|
el.innerHTML = html;
|
|
484
579
|
var cards = el.querySelectorAll('.node');
|
|
@@ -498,11 +593,27 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
498
593
|
var runs = (s.runs||[]).filter(function(r){return r.status==='running';}).length;
|
|
499
594
|
var loads = nodes.filter(function(n){return n.connected&&n.load!=null;}).map(function(n){return n.load;});
|
|
500
595
|
var avg = loads.length?Math.round(loads.reduce(function(a,b){return a+b;},0)/loads.length):0;
|
|
501
|
-
|
|
502
|
-
|
|
596
|
+
// live aggregates + alerts from per-node hardware
|
|
597
|
+
var hotTemp=null, maxCpu=0, alerts=[];
|
|
598
|
+
for(var k=0;k<nodes.length;k++){
|
|
599
|
+
var nn=nodes[k]; if(!nn.connected) continue;
|
|
600
|
+
var hw=hwByNode[nn.nodeId]; if(!hw) continue;
|
|
601
|
+
if(hw.cpu&&hw.cpu.percent>maxCpu) maxCpu=Math.round(hw.cpu.percent);
|
|
602
|
+
if(hw.temperatureC!=null&&(hotTemp==null||hw.temperatureC>hotTemp)) hotTemp=hw.temperatureC;
|
|
603
|
+
if(hw.temperatureC!=null&&hw.temperatureC>=75) alerts.push(nn.displayName+' '+Math.round(hw.temperatureC)+'°C');
|
|
604
|
+
if(hw.cpu&&hw.cpu.percent>=92) alerts.push(nn.displayName+' cpu '+Math.round(hw.cpu.percent)+'%');
|
|
605
|
+
if(hw.memory&&hw.memory.percent>=92) alerts.push(nn.displayName+' mem '+Math.round(hw.memory.percent)+'%');
|
|
606
|
+
if(hw.disk&&hw.disk.mounts&&hw.disk.mounts[0]&&hw.disk.mounts[0].percent>=90) alerts.push(nn.displayName+' disk '+hw.disk.mounts[0].percent+'%');
|
|
607
|
+
}
|
|
608
|
+
if(down>0) alerts.unshift(down+' offline');
|
|
609
|
+
var html = '<span class="chip ok">up <b>'+up+'</b></span>'
|
|
503
610
|
+ '<span class="chip'+(down?' bad':'')+'">down <b>'+down+'</b></span>'
|
|
504
|
-
+ '<span class="chip info">
|
|
505
|
-
+ '<span class="chip">avg load <b>'+avg+'%</b></span>'
|
|
611
|
+
+ '<span class="chip info">runs <b>'+runs+'</b></span>'
|
|
612
|
+
+ '<span class="chip">avg load <b>'+avg+'%</b></span>'
|
|
613
|
+
+ '<span class="chip">peak cpu <b>'+maxCpu+'%</b></span>'
|
|
614
|
+
+ (hotTemp!=null?('<span class="chip">hottest <b>'+Math.round(hotTemp)+'°C</b></span>'):'');
|
|
615
|
+
for(var a2=0;a2<alerts.length&&a2<4;a2++){ html += '<span class="chip alert">⚠ <b>'+esc(alerts[a2])+'</b></span>'; }
|
|
616
|
+
$('summary').innerHTML = html;
|
|
506
617
|
$('fleetcount').textContent = '(' + nodes.length + ')';
|
|
507
618
|
var self = s.self||{};
|
|
508
619
|
var role = s.role||{};
|
|
@@ -522,6 +633,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
522
633
|
everOk = true; setLive(true); lastStatus = s;
|
|
523
634
|
if(!selfId && s.self) selfId = s.self.nodeId;
|
|
524
635
|
if(s.control){ rpReadOnly = !!s.control.readonly; applyReadOnly(); }
|
|
636
|
+
updateHw(s);
|
|
525
637
|
renderSummary(s);
|
|
526
638
|
renderFleet(s.nodes||[]);
|
|
527
639
|
renderHw(s.hardware);
|
|
@@ -705,13 +817,28 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
705
817
|
// Robot cam targets a LiveKit track via /robopark/api/robots/:id/stream.
|
|
706
818
|
// ══════════════════════════════════════════════════════════════════
|
|
707
819
|
var Park = (function(){
|
|
708
|
-
var C, X, DPR=1, W=0, H=0, cam={x:0,y:0,z:1};
|
|
820
|
+
var C, X, DPR=1, W=0, H=0, cam={x:0,y:0,z:1}, fitted=false;
|
|
709
821
|
var nodes=[], byId={}, orbs=[], pulses=[], t0=0, raf=null, running=false, es=null, animSeq=-1;
|
|
710
|
-
var TW=124, TH=62;
|
|
711
822
|
var drawerId=null, camRAF=null, meterRAF=null, camAbort=null, drawerTimer=null;
|
|
712
|
-
var
|
|
823
|
+
var lkRoom=null, micAnalyser=null, audioCtx=null;
|
|
824
|
+
var TW=44, TH=22, PARK_W=15, PARK_D=8;
|
|
825
|
+
// The real park (from the aerial): named robot pads at their ground
|
|
826
|
+
// positions, plus palms, pond, and the street. A mesh node binds to a pad
|
|
827
|
+
// when its display name matches the pad keywords (robobmw → VIXEN/BMW).
|
|
828
|
+
var SLOTS=[
|
|
829
|
+
{label:'VOLT', keys:['volt'], nx:0.30, ny:0.22},
|
|
830
|
+
{label:'TESLA', keys:['tesla'], nx:0.44, ny:0.36},
|
|
831
|
+
{label:'JAGUAR', keys:['jaguar','jag'], nx:0.60, ny:0.40},
|
|
832
|
+
{label:'VIXEN', keys:['vixen','bmw'], nx:0.55, ny:0.66, sub:'BMW'},
|
|
833
|
+
{label:'PANDA', keys:['panda'], nx:0.84, ny:0.58}
|
|
834
|
+
];
|
|
835
|
+
var HUBSLOT={label:'CONTROL', keys:[], nx:0.09, ny:0.12};
|
|
836
|
+
var PALMS=[[0.11,0.55],[0.15,0.74],[0.19,0.42],[0.23,0.64],[0.17,0.31],[0.25,0.82],[0.37,0.24],[0.41,0.56],[0.49,0.74],[0.52,0.29],[0.59,0.62],[0.66,0.26],[0.71,0.69],[0.76,0.42],[0.82,0.74],[0.89,0.42],[0.92,0.68],[0.67,0.83],[0.34,0.71]];
|
|
837
|
+
var POND={cx:0.33*15, cy:0.99*8, rx:3.1, ry:1.4};
|
|
838
|
+
var CARS=[[4,-0.95,'#e0a23c'],[6,-0.8,'#7fb4ff'],[8,-1.0,'#ff7e6e'],[10.5,-0.78,'#d8d8e0'],[12.8,-0.95,'#f2c14e']];
|
|
713
839
|
|
|
714
840
|
function mono(){ return 'ui-monospace,Menlo,Consolas,monospace'; }
|
|
841
|
+
function W2(nx,ny){ return [nx*PARK_W, ny*PARK_D]; }
|
|
715
842
|
function kindOf(n){
|
|
716
843
|
var r=(n.role||'').toLowerCase();
|
|
717
844
|
if(r==='hub'||r==='scheduler') return 'hub';
|
|
@@ -723,19 +850,41 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
723
850
|
function loadColor(l){ return l>=75?getVar('--bad'):l>=45?getVar('--warn'):getVar('--ok'); }
|
|
724
851
|
function getVar(v){ try{ return getComputedStyle(document.documentElement).getPropertyValue(v).trim()||'#ccc'; }catch(e){ return '#ccc'; } }
|
|
725
852
|
|
|
853
|
+
// Bind mesh nodes onto the fixed park pads; unmatched pads render as
|
|
854
|
+
// offline placeholders so the whole park is always visible.
|
|
726
855
|
function layout(src){
|
|
727
|
-
var list=(src||[]).slice()
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
856
|
+
var list=(src||[]).slice(), used={}, out=[];
|
|
857
|
+
function prevHit(id){ for(var p=0;p<nodes.length;p++){ if(nodes[p].id===id) return nodes[p]._hit; } return null; }
|
|
858
|
+
function bind(slot, wx, wy, kind){
|
|
859
|
+
var match=null;
|
|
860
|
+
for(var i=0;i<list.length;i++){ var n=list[i]; if(used[n.nodeId]) continue;
|
|
861
|
+
var nm=(n.displayName||n.nodeId||'').toLowerCase(), isHub=kindOf(n)==='hub';
|
|
862
|
+
if(kind==='hub'){ if(isHub){ match=n; break; } }
|
|
863
|
+
else if(!isHub){ for(var k=0;k<slot.keys.length;k++){ if(slot.keys[k] && nm.indexOf(slot.keys[k])>-1){ match=n; break; } } if(match) break; }
|
|
864
|
+
}
|
|
865
|
+
var id = match?match.nodeId:('pad:'+slot.label);
|
|
866
|
+
if(match) used[match.nodeId]=1;
|
|
867
|
+
return {
|
|
868
|
+
id:id, kind:kind, label:slot.label,
|
|
869
|
+
name: match?(match.displayName||match.nodeId):slot.label.toLowerCase(),
|
|
870
|
+
sub: match?(slot.sub||''):'not joined',
|
|
871
|
+
connected: match?!!match.connected:false,
|
|
872
|
+
load: (match&&match.load!=null)?match.load:0,
|
|
873
|
+
platform: match?(match.platform||''):'', version: match?(match.version||'?'):'—',
|
|
874
|
+
site: match?(match.site||''):'', caps: match?(match.capabilities||[]):[],
|
|
875
|
+
telemKeys: [slot.label.toLowerCase()].concat(slot.keys||[]).concat(match?[(match.displayName||'').toLowerCase()]:[]),
|
|
876
|
+
wx:wx, wy:wy, _hit:prevHit(id)
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
var h=W2(HUBSLOT.nx,HUBSLOT.ny); out.push(bind(HUBSLOT,h[0],h[1],'hub'));
|
|
880
|
+
SLOTS.forEach(function(s){ var w=W2(s.nx,s.ny); out.push(bind(s,w[0],w[1],'robot')); });
|
|
881
|
+
// Any extra live node that matched no pad → line it along the near edge.
|
|
882
|
+
var ex=0;
|
|
883
|
+
list.forEach(function(n){ if(used[n.nodeId]) return; used[n.nodeId]=1;
|
|
884
|
+
var w=W2(0.20+ex*0.13, 0.90); ex++;
|
|
885
|
+
out.push({ id:n.nodeId, kind:kindOf(n), label:(n.displayName||n.nodeId), name:(n.displayName||n.nodeId),
|
|
886
|
+
sub:n.role||'', connected:!!n.connected, load:(n.load!=null?n.load:0), platform:n.platform||'', version:n.version||'?',
|
|
887
|
+
site:n.site||'', caps:n.capabilities||[], telemKeys:[(n.displayName||'').toLowerCase()], wx:w[0], wy:w[1], _hit:prevHit(n.nodeId) });
|
|
739
888
|
});
|
|
740
889
|
return out;
|
|
741
890
|
}
|
|
@@ -750,8 +899,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
750
899
|
|
|
751
900
|
function hub(){ for(var i=0;i<nodes.length;i++) if(nodes[i].kind==='hub') return nodes[i]; return nodes[0]; }
|
|
752
901
|
function findByFrame(f){
|
|
753
|
-
var key=f.n; if(key
|
|
754
|
-
for(var i=0;i<nodes.length;i++){
|
|
902
|
+
var key=(f.n==null?'':String(f.n)).toLowerCase(); if(!key) return null;
|
|
903
|
+
for(var i=0;i<nodes.length;i++){ var n=nodes[i];
|
|
904
|
+
if(n.id===f.n || (n.name||'').toLowerCase()===key) return n;
|
|
905
|
+
for(var k=0;k<n.telemKeys.length;k++){ if(n.telemKeys[k] && key.indexOf(n.telemKeys[k])>-1) return n; }
|
|
906
|
+
}
|
|
755
907
|
return null;
|
|
756
908
|
}
|
|
757
909
|
function ingest(f, fromPoll){
|
|
@@ -797,30 +949,67 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
797
949
|
C.addEventListener('click',function(e){
|
|
798
950
|
var r=C.getBoundingClientRect(), mx=e.clientX-r.left, my=e.clientY-r.top, hit=null;
|
|
799
951
|
for(var i=0;i<nodes.length;i++){ var hb=nodes[i]._hit; if(hb&&mx>=hb.x&&mx<=hb.x+hb.w&&my>=hb.y&&my<=hb.y+hb.h) hit=nodes[i]; }
|
|
800
|
-
if(hit){ openDrawer(hit.id); selectNode(hit.id, hit.name); }
|
|
952
|
+
if(hit){ openDrawer(hit.id); if(String(hit.id).indexOf('pad:')!==0) selectNode(hit.id, hit.name); }
|
|
801
953
|
});
|
|
802
954
|
window.addEventListener('resize', function(){ if(running) resize(); });
|
|
803
955
|
}
|
|
804
956
|
function resize(){
|
|
805
957
|
var st=$('pk-stage'); if(!st) return;
|
|
806
958
|
W=st.clientWidth; H=st.clientHeight; C.width=W*DPR; C.height=H*DPR; X.setTransform(DPR,0,0,DPR,0,0);
|
|
959
|
+
if(!fitted){ fit(); fitted=true; }
|
|
807
960
|
}
|
|
808
|
-
function
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
961
|
+
function fit(){
|
|
962
|
+
var span=PARK_W+PARK_D;
|
|
963
|
+
var zx=(W-330)/(span*TW*0.5+120), zy=H/(span*TH+220);
|
|
964
|
+
cam.z=Math.max(0.5,Math.min(1.5, Math.min(zx,zy))); cam.x=0; cam.y=0;
|
|
965
|
+
}
|
|
966
|
+
// World (park) coords → screen. Origin biased left so the right-docked feed
|
|
967
|
+
// doesn't cover the plaza; the park is centered on (PARK_W/2, PARK_D/2).
|
|
968
|
+
function iso(wx,wy){
|
|
969
|
+
var dx=(wx-PARK_W/2)-(wy-PARK_D/2), dy=(wx-PARK_W/2)+(wy-PARK_D/2);
|
|
970
|
+
return { x:(W-300)/2 + cam.x + dx*TW*cam.z, y:H*0.42 + cam.y + dy*TH*cam.z };
|
|
971
|
+
}
|
|
972
|
+
function poly(pts, fill, stroke, lw){
|
|
973
|
+
X.beginPath();
|
|
974
|
+
for(var i=0;i<pts.length;i++){ var p=iso(pts[i][0],pts[i][1]); if(i===0)X.moveTo(p.x,p.y); else X.lineTo(p.x,p.y); }
|
|
975
|
+
X.closePath();
|
|
976
|
+
if(fill){ X.fillStyle=fill; X.fill(); }
|
|
977
|
+
if(stroke){ X.strokeStyle=stroke; X.lineWidth=lw||1; X.stroke(); }
|
|
978
|
+
}
|
|
979
|
+
function drawGround(){
|
|
980
|
+
// grass base
|
|
981
|
+
var g=X.createLinearGradient(0,0,0,H); g.addColorStop(0,'rgba(38,68,44,0.55)'); g.addColorStop(1,'rgba(22,44,28,0.5)');
|
|
982
|
+
poly([[-2,-2.2],[PARK_W+2,-2.2],[PARK_W+2,PARK_D+2],[-2,PARK_D+2]], g, 'rgba(120,180,120,0.10)',1);
|
|
983
|
+
// street band along the far edge + dashed centerline + cars
|
|
984
|
+
poly([[0,-1.5],[PARK_W,-1.5],[PARK_W,-0.35],[0,-0.35]], 'rgba(26,26,32,0.88)', 'rgba(255,255,255,0.08)',1);
|
|
985
|
+
X.strokeStyle='rgba(232,210,120,0.5)'; X.setLineDash([9*cam.z,9*cam.z]);
|
|
986
|
+
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([]);
|
|
987
|
+
CARS.forEach(function(c){ drawCar(c[0],c[1],c[2]); });
|
|
988
|
+
// paved plaza
|
|
989
|
+
var px0=0.4,py0=-0.15,px1=PARK_W-0.4,py1=PARK_D-0.6;
|
|
990
|
+
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)');
|
|
991
|
+
poly([[px0,py0],[px1,py0],[px1,py1],[px0,py1]], pg, 'rgba(255,255,255,0.07)',1);
|
|
992
|
+
X.strokeStyle='rgba(255,255,255,0.05)'; X.lineWidth=1;
|
|
993
|
+
for(var gx=Math.ceil(px0);gx<px1;gx++){ 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(); }
|
|
994
|
+
for(var gy=Math.ceil(py0);gy<py1;gy++){ 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(); }
|
|
995
|
+
// pond
|
|
996
|
+
var pts=[]; for(var t=0;t<Math.PI*2;t+=Math.PI/9){ pts.push([POND.cx+POND.rx*Math.cos(t), POND.cy+POND.ry*Math.sin(t)]); }
|
|
997
|
+
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)');
|
|
998
|
+
poly(pts, wg, 'rgba(130,205,235,0.32)',1.5);
|
|
999
|
+
}
|
|
1000
|
+
function drawCar(wx,wy,col){
|
|
1001
|
+
var p=iso(wx,wy), z=cam.z;
|
|
1002
|
+
X.fillStyle='rgba(0,0,0,0.3)'; X.beginPath(); X.ellipse(p.x,p.y+3*z,11*z,5*z,0,0,7); X.fill();
|
|
1003
|
+
X.fillStyle=col; X.beginPath(); X.roundRect(p.x-11*z,p.y-7*z,22*z,12*z,3*z); X.fill();
|
|
1004
|
+
X.fillStyle='rgba(255,255,255,0.25)'; X.beginPath(); X.roundRect(p.x-6*z,p.y-6*z,12*z,5*z,2*z); X.fill();
|
|
1005
|
+
}
|
|
1006
|
+
function drawPalm(wx,wy){
|
|
1007
|
+
var p=iso(wx,wy), z=cam.z;
|
|
1008
|
+
X.fillStyle='rgba(0,0,0,0.26)'; X.beginPath(); X.ellipse(p.x,p.y,10*z,4.5*z,0,0,7); X.fill();
|
|
1009
|
+
X.strokeStyle='#6b4a24'; X.lineWidth=3*z; X.beginPath(); X.moveTo(p.x,p.y); X.quadraticCurveTo(p.x+3*z,p.y-14*z,p.x+1*z,p.y-24*z); X.stroke();
|
|
1010
|
+
var tx=p.x+1*z, ty=p.y-24*z; X.strokeStyle='#3f8f4a'; X.lineWidth=2.4*z;
|
|
1011
|
+
for(var a=0;a<6;a++){ var ang=(-Math.PI/2)+(a-2.5)*0.52; X.beginPath(); X.moveTo(tx,ty); X.quadraticCurveTo(tx+Math.cos(ang)*10*z, ty+Math.sin(ang)*8*z-4*z, tx+Math.cos(ang)*20*z, ty+Math.sin(ang)*11*z+3*z); X.stroke(); }
|
|
1012
|
+
X.fillStyle='#caa23a'; X.beginPath(); X.arc(tx,ty+1*z,2*z,0,7); X.fill();
|
|
824
1013
|
}
|
|
825
1014
|
function ring(cx,cy,r,frac,color){
|
|
826
1015
|
X.beginPath(); X.arc(cx,cy,r,0,Math.PI*2); X.strokeStyle='rgba(255,255,255,0.12)'; X.lineWidth=3*cam.z; X.stroke();
|
|
@@ -831,62 +1020,68 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
831
1020
|
X.font='700 '+(11*z)+'px '+mono(); X.fillStyle=col; X.fillText(txt,cx,cy);
|
|
832
1021
|
X.font=(8.5*z)+'px '+mono(); X.fillStyle='rgba(200,185,150,0.9)'; X.fillText(sub,cx,cy+11*z);
|
|
833
1022
|
}
|
|
1023
|
+
function padMark(bx,by,z){ X.strokeStyle='rgba(255,255,255,0.10)'; X.lineWidth=1; X.beginPath(); X.ellipse(bx,by,15*z,7*z,0,0,7); X.stroke(); }
|
|
834
1024
|
function drawHub(n){
|
|
835
|
-
var p=
|
|
836
|
-
X.fillStyle='rgba(
|
|
1025
|
+
var p=iso(n.wx,n.wy), z=cam.z, bob=Math.sin((performance.now()-t0)/700)*2*z, bx=p.x, by=p.y-6*z-bob;
|
|
1026
|
+
X.fillStyle='rgba(0,0,0,0.3)'; X.beginPath(); X.ellipse(bx,p.y+2*z,16*z,7*z,0,0,7); X.fill();
|
|
1027
|
+
X.fillStyle='rgba(40,34,58,0.92)'; X.strokeStyle=getVar('--gold-bright'); X.lineWidth=1.2;
|
|
837
1028
|
X.beginPath(); X.rect(bx-16*z,by-40*z,32*z,44*z); X.fill(); X.stroke();
|
|
838
1029
|
for(var i=0;i<4;i++){ X.fillStyle=i%2?getVar('--ok'):getVar('--gold-bright'); X.globalAlpha=0.4+0.6*Math.abs(Math.sin((performance.now()-t0)/300+i)); X.fillRect(bx-11*z,by-34*z+i*9*z,22*z,4*z); X.globalAlpha=1; }
|
|
839
1030
|
X.strokeStyle=getVar('--gold'); X.beginPath(); X.moveTo(bx,by-40*z); X.lineTo(bx,by-52*z); X.stroke();
|
|
840
1031
|
X.fillStyle=getVar('--gold-bright'); X.beginPath(); X.arc(bx,by-53*z,3*z,0,7); X.fill();
|
|
841
|
-
label(bx,p.y+
|
|
1032
|
+
label(bx,p.y+16*z,n.name,getVar('--gold-bright'), (n.connected?'HUB · '+Math.round(n.load)+'%':'offline'));
|
|
842
1033
|
n._hit={x:bx-20*z,y:by-54*z,w:40*z,h:80*z};
|
|
843
1034
|
}
|
|
844
1035
|
function drawRobot(n){
|
|
845
|
-
var p=
|
|
846
|
-
ring(bx,p.y+
|
|
847
|
-
X.fillStyle='rgba(0,0,0,0.
|
|
848
|
-
var col=n.connected?getVar('--gold-bright'):'rgba(150,
|
|
849
|
-
X.
|
|
850
|
-
X.
|
|
851
|
-
X.beginPath(); X.roundRect(bx-
|
|
852
|
-
X.
|
|
853
|
-
X.
|
|
854
|
-
X.
|
|
855
|
-
|
|
856
|
-
|
|
1036
|
+
var p=iso(n.wx,n.wy), z=cam.z, bob=n.connected?Math.sin((performance.now()-t0)/500+n.wx)*2.3*z:0, bx=p.x, by=p.y-4*z-bob;
|
|
1037
|
+
if(n.connected) ring(bx,p.y+3*z,18*z,n.load/100,loadColor(n.load)); else padMark(bx,p.y+3*z,z);
|
|
1038
|
+
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();
|
|
1039
|
+
var col=n.connected?getVar('--gold-bright'):'rgba(150,143,120,0.5)';
|
|
1040
|
+
X.globalAlpha=n.connected?1:0.7;
|
|
1041
|
+
X.fillStyle='rgba(24,20,38,0.92)'; X.strokeStyle=col; X.lineWidth=1.6;
|
|
1042
|
+
X.beginPath(); X.roundRect(bx-11*z,by-24*z,22*z,25*z,5*z); X.fill(); X.stroke();
|
|
1043
|
+
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();
|
|
1044
|
+
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();
|
|
1045
|
+
X.strokeStyle=col; X.beginPath(); X.moveTo(bx,by-37*z); X.lineTo(bx,by-44*z); X.stroke();
|
|
1046
|
+
X.fillStyle=n.connected?'#FF5B4A':'#655'; X.beginPath(); X.arc(bx,by-45*z,2*z,0,7); X.fill();
|
|
1047
|
+
X.globalAlpha=1;
|
|
1048
|
+
var sub=n.connected?((n.sub&&n.sub!==n.name?n.sub+' · ':'')+Math.round(n.load)+'%'):'not joined';
|
|
1049
|
+
label(bx,p.y+17*z,n.label,col,sub);
|
|
1050
|
+
n._hit={x:bx-15*z,y:by-47*z,w:30*z,h:60*z};
|
|
857
1051
|
}
|
|
858
1052
|
function drawPeer(n){
|
|
859
|
-
var p=
|
|
860
|
-
|
|
1053
|
+
var p=iso(n.wx,n.wy), z=cam.z, bx=p.x, by=p.y-3*z;
|
|
1054
|
+
X.fillStyle='rgba(0,0,0,0.3)'; X.beginPath(); X.ellipse(bx,p.y+2*z,13*z,6*z,0,0,7); X.fill();
|
|
1055
|
+
var col=n.connected?getVar('--info'):'rgba(150,143,120,0.5)';
|
|
861
1056
|
X.fillStyle='rgba(24,22,40,0.9)'; X.strokeStyle=col; X.lineWidth=1.4;
|
|
862
1057
|
X.beginPath(); X.roundRect(bx-13*z,by-22*z,26*z,22*z,4*z); X.fill(); X.stroke();
|
|
863
1058
|
X.fillStyle=col; X.globalAlpha=0.5+0.5*Math.abs(Math.sin((performance.now()-t0)/500)); X.fillRect(bx-8*z,by-14*z,16*z,3*z); X.globalAlpha=1;
|
|
864
|
-
label(bx,p.y+
|
|
1059
|
+
label(bx,p.y+15*z,n.label,col,(n.connected?n.sub:'offline'));
|
|
865
1060
|
n._hit={x:bx-15*z,y:by-24*z,w:30*z,h:42*z};
|
|
866
1061
|
}
|
|
867
1062
|
|
|
868
1063
|
function frame(){
|
|
869
1064
|
if(!running) return;
|
|
870
1065
|
X.clearRect(0,0,W,H);
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
for(var
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1066
|
+
drawGround();
|
|
1067
|
+
// depth-sort palms + nodes so nearer objects overlap farther ones
|
|
1068
|
+
var objs=[];
|
|
1069
|
+
for(var pi=0;pi<PALMS.length;pi++){ var w=W2(PALMS[pi][0],PALMS[pi][1]); objs.push({wx:w[0],wy:w[1],palm:true}); }
|
|
1070
|
+
for(var ni=0;ni<nodes.length;ni++){ objs.push({wx:nodes[ni].wx,wy:nodes[ni].wy,n:nodes[ni]}); }
|
|
1071
|
+
objs.sort(function(a,b){ return (a.wx+a.wy)-(b.wx+b.wy); });
|
|
1072
|
+
objs.forEach(function(o){ if(o.palm) drawPalm(o.wx,o.wy); else { var n=o.n; if(n.kind==='hub')drawHub(n); else if(n.kind==='robot')drawRobot(n); else drawPeer(n); } });
|
|
1073
|
+
for(var i=orbs.length-1;i>=0;i--){ var o2=orbs[i]; o2.t+=0.024; if(o2.t>=1){orbs.splice(i,1);continue;}
|
|
1074
|
+
var a=iso(o2.a.wx,o2.a.wy), b=iso(o2.b.wx,o2.b.wy), e=o2.t<.5?2*o2.t*o2.t:-1+(4-2*o2.t)*o2.t;
|
|
1075
|
+
var x=a.x+(b.x-a.x)*e, y=a.y+(b.y-a.y)*e-Math.sin(o2.t*Math.PI)*40*cam.z;
|
|
1076
|
+
X.beginPath(); X.arc(x,y,4*cam.z,0,7); X.fillStyle=o2.color; X.shadowColor=o2.color; X.shadowBlur=12; X.fill(); X.shadowBlur=0;
|
|
878
1077
|
}
|
|
879
1078
|
for(var j=pulses.length-1;j>=0;j--){ var pu=pulses[j]; pu.t+=0.02; if(pu.t>=1){pulses.splice(j,1);continue;}
|
|
880
|
-
var pp=iso(pu.n.
|
|
1079
|
+
var pp=iso(pu.n.wx,pu.n.wy), yy=pp.y-56*cam.z-pu.t*24*cam.z;
|
|
881
1080
|
X.globalAlpha=1-pu.t; X.font='700 '+(15*cam.z)+'px '+mono(); X.textAlign='center'; X.fillStyle=getVar('--gold-bright'); X.fillText(pu.g,pp.x,yy);
|
|
882
|
-
X.beginPath(); X.arc(pp.x,pp.y-
|
|
1081
|
+
X.beginPath(); X.arc(pp.x,pp.y-18*cam.z,(10+pu.t*22)*cam.z,0,7); X.strokeStyle='rgba(255,214,112,'+(1-pu.t)+')'; X.lineWidth=2; X.stroke(); X.globalAlpha=1;
|
|
883
1082
|
}
|
|
884
1083
|
raf=requestAnimationFrame(frame);
|
|
885
1084
|
}
|
|
886
|
-
function faint(gx,gy){ var p=iso(gx,gy),z=cam.z,hw=TW/2*z,hh=TH/2*z;
|
|
887
|
-
X.beginPath(); X.moveTo(p.x,p.y-hh); X.lineTo(p.x+hw,p.y); X.lineTo(p.x,p.y+hh); X.lineTo(p.x-hw,p.y); X.closePath();
|
|
888
|
-
X.strokeStyle='rgba(255,255,255,0.045)'; X.lineWidth=1; X.stroke();
|
|
889
|
-
}
|
|
890
1085
|
|
|
891
1086
|
function connectStream(){
|
|
892
1087
|
if(es) return;
|
|
@@ -898,8 +1093,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
898
1093
|
}catch(e){ es=null; }
|
|
899
1094
|
}
|
|
900
1095
|
|
|
1096
|
+
var sdkLoading=false;
|
|
1097
|
+
function loadSDK(){
|
|
1098
|
+
if(window.LivekitClient || sdkLoading) return; sdkLoading=true;
|
|
1099
|
+
var s=document.createElement('script'); s.src='/vendor/livekit-client.umd.js'+QS; s.async=true;
|
|
1100
|
+
s.onerror=function(){ sdkLoading=false; }; document.head.appendChild(s);
|
|
1101
|
+
}
|
|
901
1102
|
function activate(){
|
|
902
1103
|
ensure();
|
|
1104
|
+
loadSDK();
|
|
903
1105
|
if(lastStatus) onStatus(lastStatus);
|
|
904
1106
|
resize();
|
|
905
1107
|
connectStream();
|
|
@@ -913,9 +1115,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
913
1115
|
var n=byId[id]; if(!n) return; drawerId=id;
|
|
914
1116
|
$('pk-drawer').classList.add('open');
|
|
915
1117
|
$('pk-av').textContent = n.kind==='hub'?'🖥':(n.kind==='robot'?'🤖':'🧩');
|
|
916
|
-
$('pk-name').textContent=n.name;
|
|
917
|
-
$('pk-sub').textContent=n.
|
|
918
|
-
var st=$('pk-state'); st.textContent=n.connected?'live':'
|
|
1118
|
+
$('pk-name').textContent=n.label||n.name;
|
|
1119
|
+
$('pk-sub').textContent = n.connected ? (n.name+' · '+(n.platform||'?')+' · v'+n.version+(n.site?(' · '+n.site):'')) : 'pad reserved · not joined';
|
|
1120
|
+
var st=$('pk-state'); st.textContent=n.connected?'live':'offline'; st.className='pk-pill '+(n.connected?'live':'idle');
|
|
919
1121
|
buildDrawer(n);
|
|
920
1122
|
pollRoboparkQuiet(); // fill session telemetry right away instead of waiting for the tick
|
|
921
1123
|
if(drawerTimer) clearInterval(drawerTimer);
|
|
@@ -927,6 +1129,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
927
1129
|
if(meterRAF)cancelAnimationFrame(meterRAF); meterRAF=null;
|
|
928
1130
|
if(camAbort){ try{camAbort.abort();}catch(e){} camAbort=null; }
|
|
929
1131
|
if(drawerTimer){ clearInterval(drawerTimer); drawerTimer=null; }
|
|
1132
|
+
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
|
|
1133
|
+
micAnalyser=null;
|
|
930
1134
|
}
|
|
931
1135
|
function pollRoboparkQuiet(){
|
|
932
1136
|
fetch('/robopark/api/telemetry'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){
|
|
@@ -937,6 +1141,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
937
1141
|
}
|
|
938
1142
|
function buildDrawer(n){
|
|
939
1143
|
var b=$('pk-body');
|
|
1144
|
+
if(n.kind==='robot' && !n.connected){
|
|
1145
|
+
b.innerHTML =
|
|
1146
|
+
sect('🅿 park pad · '+esc(n.label), '<div class="pk-note" style="opacity:1;color:var(--muted)">No robot has joined this pad yet. When a node named <b>'+esc((n.telemKeys[1]||n.label).toString())+'</b> connects to the mesh, its live camera, mic, speaker, and telemetry appear here automatically.</div>')
|
|
1147
|
+
+ sect('📍 location', '<div class="pk-grid2"><div class="pk-kv"><div class="v">'+esc(n.label)+'</div><div class="l">pad</div></div><div class="pk-kv"><div class="v" style="color:var(--muted)">idle</div><div class="l">status</div></div></div>');
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
940
1150
|
if(n.kind==='robot'){
|
|
941
1151
|
b.innerHTML =
|
|
942
1152
|
sect('👁 camera feed',
|
|
@@ -966,6 +1176,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
966
1176
|
var lv=$('pk-tv-load'); if(lv) lv.textContent=Math.round(n.load)+'%';
|
|
967
1177
|
var hc=$('pk-hw-conn'); if(hc) hc.style.width=n.connected?'100%':'12%';
|
|
968
1178
|
var row=roboparkByName[n.name];
|
|
1179
|
+
if(!row){ var keys=n.telemKeys||[]; for(var rk in roboparkByName){ var lk=rk.toLowerCase(); for(var ki=0;ki<keys.length;ki++){ if(keys[ki] && (lk===keys[ki]||lk.indexOf(keys[ki])>-1)){ row=roboparkByName[rk]; break; } } if(row) break; } }
|
|
969
1180
|
if(row){
|
|
970
1181
|
set('pk-tv-trig', row.trigger_count!=null?row.trigger_count:0);
|
|
971
1182
|
set('pk-tv-sess', row.sessions_total!=null?row.sessions_total:0);
|
|
@@ -980,30 +1191,54 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
980
1191
|
|
|
981
1192
|
// ── cam: target a LiveKit track; fall back to a styled preview ──
|
|
982
1193
|
function startCam(n){
|
|
983
|
-
var note=$('pk-camnote')
|
|
1194
|
+
var note=$('pk-camnote');
|
|
984
1195
|
camAbort = ('AbortController' in window)?new AbortController():null;
|
|
985
|
-
|
|
1196
|
+
idleCam('connecting');
|
|
1197
|
+
// scheduler mints a subscribe-only viewer token for the robot's current
|
|
1198
|
+
// LiveKit room; the Pi already publishes 'cam' (video) + 'mic' (audio).
|
|
986
1199
|
fetch('/robopark/api/robots/'+encodeURIComponent(n.name)+'/stream'+QS, {cache:'no-store', signal:camAbort?camAbort.signal:undefined})
|
|
987
|
-
.then(function(r){
|
|
1200
|
+
.then(function(r){ return r.json().catch(function(){ throw new Error('bad response'); }); })
|
|
988
1201
|
.then(function(info){
|
|
989
|
-
if(info && info.
|
|
990
|
-
|
|
991
|
-
|
|
1202
|
+
if(info && info.active===false){ idleCam(info.reason||'idle'); return; }
|
|
1203
|
+
if(info && info.url && info.token){
|
|
1204
|
+
if(window.LivekitClient){ attachLiveKit(info, n); }
|
|
1205
|
+
else { idleCam('no-sdk'); }
|
|
1206
|
+
} else { idleCam('idle'); }
|
|
992
1207
|
})
|
|
993
|
-
.catch(function(){
|
|
1208
|
+
.catch(function(){ idleCam('unreachable'); });
|
|
1209
|
+
}
|
|
1210
|
+
function idleCam(reason){
|
|
1211
|
+
var cc=$('pk-camcv'), msg={connecting:'connecting to live session…', idle:'robot idle · no live session', production_mode_off:'production mode off', 'no-sdk':'live session up · loading video…', unreachable:'scheduler unreachable'}[reason]||'no live feed';
|
|
1212
|
+
if(cc&&cc.getContext){ var cx=cc.getContext('2d'); cx.fillStyle='#0b0d12'; cx.fillRect(0,0,cc.width,cc.height); cx.strokeStyle='rgba(255,255,255,0.05)'; for(var y=0;y<cc.height;y+=4){cx.beginPath();cx.moveTo(0,y);cx.lineTo(cc.width,y);cx.stroke();} cx.fillStyle='#5a6a80'; cx.font='11px monospace'; cx.textAlign='center'; cx.fillText(msg, cc.width/2, cc.height/2); }
|
|
1213
|
+
var res=$('pk-camres'); if(res) res.textContent = reason==='idle'?'idle':'—';
|
|
1214
|
+
var note=$('pk-camnote'); if(note) note.textContent = reason==='idle'?'the feed goes live automatically when the robot opens a session':msg;
|
|
994
1215
|
}
|
|
995
1216
|
function attachLiveKit(info, n){
|
|
996
|
-
var
|
|
1217
|
+
var note=$('pk-camnote'), res=$('pk-camres');
|
|
997
1218
|
try{
|
|
998
|
-
var LK=window.LivekitClient
|
|
999
|
-
|
|
1000
|
-
if(
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
});
|
|
1005
|
-
})
|
|
1006
|
-
|
|
1219
|
+
var LK=window.LivekitClient; lkRoom=new LK.Room();
|
|
1220
|
+
lkRoom.on(LK.RoomEvent.TrackSubscribed, function(track){
|
|
1221
|
+
if(track.kind==='video'){
|
|
1222
|
+
var el=track.attach(); el.setAttribute('playsinline',''); el.muted=true; el.autoplay=true;
|
|
1223
|
+
var cv=$('pk-camcv'); if(cv&&cv.parentNode) cv.parentNode.replaceChild(el, cv);
|
|
1224
|
+
if(res) res.textContent='LIVE'; if(note) note.textContent='live camera via LiveKit · '+esc(info.room);
|
|
1225
|
+
} else if(track.kind==='audio'){ hookMicAnalyser(track); track.attach(); }
|
|
1226
|
+
});
|
|
1227
|
+
lkRoom.on(LK.RoomEvent.Disconnected, function(){ micAnalyser=null; if(note) note.textContent='session ended'; if(res) res.textContent='—'; });
|
|
1228
|
+
lkRoom.connect(info.url, info.token).then(function(){ if(note) note.textContent='connected · waiting for camera track…'; })
|
|
1229
|
+
.catch(function(){ idleCam('unreachable'); });
|
|
1230
|
+
}catch(e){ idleCam('no-sdk'); }
|
|
1231
|
+
}
|
|
1232
|
+
// Real mic level: tap the robot's subscribed 'mic' audio track with a Web
|
|
1233
|
+
// Audio analyser and read RMS in startMeters (no invented levels).
|
|
1234
|
+
function hookMicAnalyser(track){
|
|
1235
|
+
try{
|
|
1236
|
+
var AC=window.AudioContext||window.webkitAudioContext; if(!AC) return;
|
|
1237
|
+
audioCtx=audioCtx||new AC();
|
|
1238
|
+
var ms=track.mediaStreamTrack; if(!ms) return;
|
|
1239
|
+
var src=audioCtx.createMediaStreamSource(new MediaStream([ms]));
|
|
1240
|
+
var an=audioCtx.createAnalyser(); an.fftSize=256; src.connect(an); micAnalyser=an;
|
|
1241
|
+
}catch(e){}
|
|
1007
1242
|
}
|
|
1008
1243
|
function previewCam(n){
|
|
1009
1244
|
var cc=$('pk-camcv'); if(!cc||!cc.getContext) return; var cx=cc.getContext('2d'), w=cc.width, h=cc.height, s0=performance.now();
|
|
@@ -1026,18 +1261,19 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1026
1261
|
function bars(el,n){ el.innerHTML=''; for(var i=0;i<n;i++){ var b=document.createElement('i'); el.appendChild(b); } return el.querySelectorAll('i'); }
|
|
1027
1262
|
function startMeters(n){
|
|
1028
1263
|
var mb=$('pk-micbars'), sb=$('pk-spkbars'); if(!mb) return;
|
|
1029
|
-
var mi=bars(mb,18), si=bars(sb,18), sw=$('pk-spkwave'), swx=sw.getContext('2d'),
|
|
1030
|
-
// Preview envelopes until the robot audio plugin streams real levels.
|
|
1264
|
+
var mi=bars(mb,18), si=bars(sb,18), sw=$('pk-spkwave'), swx=sw?sw.getContext('2d'):null, buf=new Uint8Array(128);
|
|
1031
1265
|
(function loop(){ if(!$('pk-micbars')||drawerId==null) return;
|
|
1032
|
-
|
|
1033
|
-
var
|
|
1034
|
-
for(var
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
for(var
|
|
1266
|
+
// MIC — real RMS from the subscribed LiveKit mic track (else no signal).
|
|
1267
|
+
var lvl=0;
|
|
1268
|
+
if(micAnalyser){ micAnalyser.getByteTimeDomainData(buf); var sum=0; for(var q=0;q<buf.length;q++){ var v=(buf[q]-128)/128; sum+=v*v; } lvl=Math.min(1, Math.sqrt(sum/buf.length)*3.4); }
|
|
1269
|
+
for(var i=0;i<mi.length;i++){ var on=i<lvl*mi.length; mi[i].style.height=(3+(on?lvl*13:0))+'px'; mi[i].style.background=on?(i<mi.length*0.7?'var(--ok)':i<mi.length*0.9?'var(--warn)':'var(--bad)'):'var(--glass-border)'; }
|
|
1270
|
+
set('pk-micval', micAnalyser?Math.round(lvl*100)+'%':'—');
|
|
1271
|
+
set('pk-micstate', micAnalyser?(lvl>0.12?'live · hearing audio':'live · quiet'):'no live mic track');
|
|
1272
|
+
// SPEAKER — the Pi plays TTS out its own speaker; it is not published as a
|
|
1273
|
+
// track, so report status honestly instead of inventing a level.
|
|
1274
|
+
for(var j=0;j<si.length;j++){ si[j].style.height='3px'; si[j].style.background='var(--glass-border)'; }
|
|
1275
|
+
set('pk-spkval','—'); set('pk-spkstate','TTS plays on the robot speaker (not streamed)');
|
|
1276
|
+
if(swx){ swx.clearRect(0,0,sw.width,sw.height); swx.strokeStyle='rgba(255,255,255,0.12)'; swx.lineWidth=1.4; swx.beginPath(); for(var x=0;x<sw.width;x++){ var yy=sw.height/2+Math.sin(x*0.1)*1.1; if(x===0)swx.moveTo(x,yy); else swx.lineTo(x,yy); } swx.stroke(); }
|
|
1041
1277
|
meterRAF=requestAnimationFrame(loop);
|
|
1042
1278
|
})();
|
|
1043
1279
|
}
|