infinicode 2.8.102 → 2.8.103
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 +65 -33
- package/dist/robopark/add-robot.d.ts +2 -0
- package/dist/robopark/add-robot.js +39 -8
- package/dist/robopark/robot-runtime.js +1 -1
- package/dist/robopark-cli.js +2 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +127 -10
- package/packages/robopark/scheduler/preview_agent.py +2 -2
|
@@ -26,7 +26,7 @@ export const DASHBOARD_HTML = `<!doctype html>
|
|
|
26
26
|
<head>
|
|
27
27
|
<meta charset="utf-8">
|
|
28
28
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
29
|
-
<meta name="robopark-ui-build" content="
|
|
29
|
+
<meta name="robopark-ui-build" content="provisioning-token-20260717-1">
|
|
30
30
|
<title>RoboPark Control</title>
|
|
31
31
|
<style>
|
|
32
32
|
/* ── premium tech glass theme ── dark is the committed default; light is a frosted toggle ── */
|
|
@@ -363,20 +363,24 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
363
363
|
.pk-ctl{display:flex;gap:0.45rem;flex-wrap:wrap;}
|
|
364
364
|
.pk-note{font-family:var(--mono);font-size:0.58rem;color:var(--gold);opacity:0.85;margin-top:0.4rem;}
|
|
365
365
|
.pk-pipeline{display:grid;gap:0.42rem;margin-top:0.7rem;position:relative;}
|
|
366
|
+
.pk-live-stage-focus{--stage-color:var(--gold-bright);position:relative;overflow:hidden;margin-top:.58rem;padding:.78rem .85rem .78rem 1rem;border:1px solid var(--stage-color);border-radius:13px;background:linear-gradient(105deg,color-mix(in srgb,var(--stage-color) 24%,transparent),rgba(8,8,8,.68));box-shadow:0 10px 30px rgba(0,0,0,.28),inset 4px 0 0 var(--stage-color);}
|
|
367
|
+
.pk-live-stage-focus:before{content:'';position:absolute;width:150px;height:150px;right:-65px;top:-95px;border-radius:50%;background:var(--stage-color);opacity:.12;filter:blur(5px);animation:pulse 1.3s infinite;}.pk-live-stage-focus .eyebrow{display:block;font-family:var(--mono);font-size:.49rem;letter-spacing:.13em;text-transform:uppercase;color:var(--stage-color);}.pk-live-stage-focus b{display:block;margin-top:.2rem;font-size:1rem;letter-spacing:.015em;color:var(--ink);}.pk-live-stage-focus span:last-child{display:block;margin-top:.18rem;font-family:var(--mono);font-size:.57rem;color:var(--muted);}.pk-live-stage-focus.blocked,.pk-live-stage-focus.failed{--stage-color:var(--bad)}.pk-live-stage-focus.waiting,.pk-live-stage-focus.paused{--stage-color:var(--muted)}.pk-live-stage-focus.active,.pk-live-stage-focus.running{--stage-color:var(--warn)}
|
|
366
368
|
.pk-pipe-step{--phase:var(--gold);display:grid;grid-template-columns:2rem minmax(7.2rem,.72fr) minmax(9rem,1.35fr);gap:0.55rem;align-items:center;padding:0.55rem 0.62rem;border-radius:11px;border:1px solid rgba(255,255,255,.06);background:linear-gradient(110deg,color-mix(in srgb,var(--phase) 7%,transparent),rgba(255,255,255,.018));font-family:var(--mono);font-size:0.58rem;color:var(--muted);transition:transform .18s,border-color .18s,background .18s;position:relative;overflow:hidden;}
|
|
367
369
|
.pk-pipe-step.phase-hardware{--phase:#63d5c6}.pk-pipe-step.phase-trigger{--phase:#efb94c}.pk-pipe-step.phase-session{--phase:#66a8ff}.pk-pipe-step.phase-media{--phase:#68d48f}.pk-pipe-step.phase-intelligence{--phase:#e38bff}.pk-pipe-step.phase-playback{--phase:#ff9b62}.pk-pipe-step.phase-close{--phase:#a8b0bd}
|
|
368
370
|
.pk-pipe-step .step-index{display:grid;place-items:center;width:1.65rem;height:1.65rem;border-radius:8px;border:1px solid color-mix(in srgb,var(--phase) 35%,transparent);color:var(--phase);background:color-mix(in srgb,var(--phase) 10%,transparent);font-weight:700;}
|
|
369
371
|
.pk-pipe-step .stage{color:var(--text);text-transform:uppercase;letter-spacing:.055em;font-weight:650;display:block;}.pk-pipe-step .phase{display:block;color:var(--phase);font-size:.49rem;letter-spacing:.08em;margin-top:.14rem;}
|
|
370
|
-
.pk-pipe-step .detail{text-align:right;overflow-wrap:anywhere;line-height:1.35;}.pk-pipe-step .time{display:block;color:color-mix(in srgb,var(--muted) 75%,transparent);font-size:.5rem;margin-top:.12rem;}
|
|
372
|
+
.pk-pipe-step .detail{text-align:right;overflow-wrap:anywhere;line-height:1.35;}.pk-pipe-step .time{display:block;color:color-mix(in srgb,var(--muted) 75%,transparent);font-size:.5rem;margin-top:.12rem;}.pk-pipe-step .step-state{display:inline-block;margin-left:.35rem;padding:.1rem .3rem;border:1px solid currentColor;border-radius:999px;font-size:.43rem;letter-spacing:.07em;vertical-align:1px;color:var(--muted)}
|
|
371
373
|
.pk-pipe-step.ok{border-color:color-mix(in srgb,var(--ok) 22%,transparent)}
|
|
374
|
+
.pk-pipe-step.ok .step-state{color:var(--ok)}.pk-pipe-step.pending{opacity:.52}.pk-pipe-step.pending .step-index{filter:saturate(.3)}
|
|
372
375
|
.pk-pipe-step.failed,.pk-pipe-step.blocked{border-color:rgba(255,90,90,.48);background:linear-gradient(110deg,rgba(255,70,70,.14),rgba(255,255,255,.02));}
|
|
373
|
-
.pk-pipe-step.current{transform:translateX(
|
|
376
|
+
.pk-pipe-step.current{transform:translateX(5px) scale(1.012);border-color:var(--phase);background:linear-gradient(105deg,color-mix(in srgb,var(--phase) 24%,transparent),rgba(255,255,255,.045));box-shadow:0 0 0 1px color-mix(in srgb,var(--phase) 28%,transparent),0 10px 32px rgba(0,0,0,.3);opacity:1;z-index:2;}
|
|
374
377
|
.pk-pipe-step.current:after{content:'ACTIVE';position:absolute;right:-1.7rem;top:.45rem;transform:rotate(38deg);padding:.12rem 2rem;background:var(--phase);color:#080808;font-size:.42rem;font-weight:800;letter-spacing:.08em;}
|
|
375
378
|
.pk-pipe-step.current .step-index{box-shadow:0 0 14px color-mix(in srgb,var(--phase) 55%,transparent);animation:pulse 1.15s infinite;}
|
|
376
379
|
.pk-pipeline-summary{padding:0.7rem 0.75rem;border:1px solid var(--glass-border);border-radius:11px;background:linear-gradient(120deg,rgba(212,170,72,.08),rgba(255,255,255,.018));font-family:var(--mono);font-size:0.62rem;color:var(--muted);line-height:1.45;}
|
|
377
380
|
.pk-pipeline-summary.ready{color:var(--ok);border-color:rgba(70,220,150,.35);}
|
|
378
381
|
.pk-pipeline-summary.running{color:var(--warn);border-color:rgba(255,196,70,.35);}
|
|
379
382
|
.pk-pipeline-summary.blocked{color:var(--bad);border-color:rgba(255,90,90,.4);}
|
|
383
|
+
.fleet-stage-focus{margin-top:.48rem;padding:.48rem .58rem;border:1px solid color-mix(in srgb,var(--warn) 48%,transparent);border-radius:9px;background:linear-gradient(100deg,rgba(255,184,62,.14),rgba(255,255,255,.02));font-family:var(--mono)}.fleet-stage-focus span{display:block;color:var(--warn);font-size:.45rem;letter-spacing:.1em;text-transform:uppercase}.fleet-stage-focus b{display:block;margin-top:.12rem;color:var(--ink);font-size:.66rem}.fleet-stage-focus.blocked,.fleet-stage-focus.failed{border-color:color-mix(in srgb,var(--bad) 55%,transparent)}.fleet-stage-focus.blocked span,.fleet-stage-focus.failed span{color:var(--bad)}.fleet-stage-focus.waiting,.fleet-stage-focus.paused{border-color:var(--glass-border)}.fleet-stage-focus.waiting span,.fleet-stage-focus.paused span{color:var(--muted)}
|
|
380
384
|
.pk-pipe-history{margin-top:.75rem;border-top:1px solid var(--glass-border);padding-top:.7rem}.pk-pipe-history-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem;margin-bottom:.5rem}.pk-pipe-history-head b{font-family:var(--mono);font-size:.65rem;color:var(--ink);text-transform:uppercase;letter-spacing:.06em}.pk-pipe-history-list{display:grid;gap:.45rem}.pk-run-card{border:1px solid var(--glass-border);border-radius:10px;background:var(--panel-2);padding:.55rem .62rem;cursor:pointer}.pk-run-card:hover{border-color:var(--gold)}.pk-run-top{display:grid;grid-template-columns:1fr auto;gap:.5rem;align-items:start}.pk-run-title{font-family:var(--mono);font-size:.61rem;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pk-run-meta{font-family:var(--mono);font-size:.53rem;color:var(--muted);margin-top:.17rem}.pk-run-state{font-family:var(--mono);font-size:.5rem;text-transform:uppercase;border:1px solid var(--glass-border);border-radius:999px;padding:.18rem .4rem}.pk-run-state.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent)}.pk-run-state.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent)}.pk-run-state.running{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}.pk-run-stages{display:flex;gap:3px;margin-top:.45rem}.pk-run-stages i{height:4px;flex:1;border-radius:3px;background:rgba(255,255,255,.07)}.pk-run-stages i.ok{background:var(--ok)}.pk-run-stages i.failed,.pk-run-stages i.blocked{background:var(--bad)}.pk-history-pager{display:flex;align-items:center;gap:.35rem}.pk-history-pager span{font-family:var(--mono);font-size:.54rem;color:var(--muted)}
|
|
381
385
|
.pk-mic-suite{margin-top:.6rem;display:grid;gap:.55rem}.pk-mic-health{display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem}.pk-mic-check{padding:.48rem;border:1px solid var(--glass-border);border-radius:9px;background:var(--panel-2)}.pk-mic-check .k{font-family:var(--mono);font-size:.49rem;color:var(--muted);text-transform:uppercase}.pk-mic-check .v{font-family:var(--mono);font-size:.62rem;color:var(--ink);margin-top:.16rem;overflow-wrap:anywhere}.pk-mic-check.ok{border-color:color-mix(in srgb,var(--ok) 30%,transparent)}.pk-mic-check.bad{border-color:color-mix(in srgb,var(--bad) 45%,transparent)}.pk-mic-result{border:1px solid var(--glass-border);border-radius:10px;padding:.58rem;background:var(--panel-2);font-family:var(--mono);font-size:.58rem}.pk-mic-result.ok{border-color:var(--ok);background:rgba(60,180,110,.08)}.pk-mic-result.bad{border-color:var(--bad);background:rgba(220,80,80,.08)}.pk-mic-readings{display:grid;grid-template-columns:repeat(4,1fr);gap:.35rem;margin-top:.45rem}.pk-mic-reading{padding:.35rem;background:rgba(0,0,0,.16);border-radius:7px}.pk-mic-reading span{display:block;color:var(--muted);font-size:.48rem;text-transform:uppercase}.pk-mic-reading b{display:block;color:var(--ink);font-size:.67rem;margin-top:.1rem}
|
|
382
386
|
.pk-motor-registry{display:grid;gap:.4rem}.pk-motor-row{display:grid;grid-template-columns:1fr 4.5rem 5.5rem auto;gap:.35rem;align-items:end;padding:.45rem;border:1px solid var(--glass-border);border-radius:9px;background:var(--panel-2)}.pk-motor-canvas{display:flex;align-items:stretch;gap:.55rem;overflow-x:auto;padding:.65rem .2rem;min-height:130px}.pk-motor-node{min-width:165px;padding:.55rem;border:1px solid rgba(255,205,93,.35);border-radius:12px;background:linear-gradient(145deg,rgba(201,150,46,.13),rgba(255,255,255,.025));position:relative}.pk-motor-node:not(:last-child):after{content:'';position:absolute;right:-.56rem;top:50%;width:.56rem;border-top:2px solid var(--gold)}.pk-motor-node .node-num{font-family:var(--mono);font-size:.48rem;color:var(--gold);text-transform:uppercase;margin-bottom:.35rem}.pk-motor-node .rp-field{margin-bottom:.3rem}.pk-motor-empty{display:grid;place-items:center;width:100%;min-height:100px;border:1px dashed var(--glass-border);border-radius:10px;color:var(--muted);font-family:var(--mono);font-size:.58rem}.pk-motor-status{font-family:var(--mono);font-size:.58rem;padding:.5rem;border-radius:8px;background:var(--panel-2);border:1px solid var(--glass-border)}.pk-motor-status.ok{color:var(--ok);border-color:var(--ok)}.pk-motor-status.bad{color:var(--bad);border-color:var(--bad)}
|
|
@@ -1126,18 +1130,21 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1126
1130
|
<div class="rp-form" id="rp-addrobot">
|
|
1127
1131
|
<h3>Add robot (mint enrollment token)</h3>
|
|
1128
1132
|
<div class="rp-quickrow">
|
|
1129
|
-
<div class="rp-field"><label>Robot name</label><input id="rb-name" placeholder="jaguar"></div>
|
|
1130
|
-
<div class="rp-field"><label>Character preset</label><select id="rb-char"><option value="">— auto (matched from name) —</option></select></div>
|
|
1131
|
-
<
|
|
1133
|
+
<div class="rp-field"><label>Robot name</label><input id="rb-name" placeholder="jaguar"></div>
|
|
1134
|
+
<div class="rp-field"><label>Character preset</label><select id="rb-char"><option value="">— auto (matched from name) —</option></select></div>
|
|
1135
|
+
<div class="rp-field"><label>Robot network</label><select id="rb-network"><option value="auto">Auto from hub address</option><option value="lan">LAN</option><option value="tailscale">Tailscale</option></select></div>
|
|
1136
|
+
<div class="rp-field"><label>Mesh token</label><input id="rb-token" spellcheck="false" autocomplete="off" placeholder="required in generated command"></div>
|
|
1137
|
+
<button class="rp-btn primary" id="rb-add" style="height:fit-content">Add robot</button>
|
|
1132
1138
|
</div>
|
|
1133
1139
|
<div class="rp-autonote" id="rb-autonote">the robot self-reports its IP and hardware on first heartbeat — you only need a name and (optionally) a character preset to get started.</div>
|
|
1134
1140
|
<div class="rp-quickrow" id="rb-clicmd-row" style="display:none;">
|
|
1135
|
-
<div class="rp-field" style="flex:1;"><label>run this ON THE ROBOT
|
|
1141
|
+
<div class="rp-field" style="flex:1;"><label>complete production one-liner — run this ON THE ROBOT</label><input id="rb-clicmd" readonly style="cursor:text;"></div>
|
|
1136
1142
|
<button class="rp-btn" id="rb-clicmd-copy" style="height:fit-content">📋 Copy</button>
|
|
1137
1143
|
</div>
|
|
1138
1144
|
<details class="rp-advanced">
|
|
1139
|
-
<summary>Advanced — pre-set network/voice config before the robot connects</summary>
|
|
1140
|
-
<div class="rp-advanced-grid">
|
|
1145
|
+
<summary>Advanced — pre-set network/voice config before the robot connects</summary>
|
|
1146
|
+
<div class="rp-advanced-grid">
|
|
1147
|
+
<div class="rp-field"><label>Site</label><input id="rb-site" placeholder="Tel Aviv"></div>
|
|
1141
1148
|
<div class="rp-field"><label>Tailscale IP</label><input id="rb-tsip" placeholder="auto-detected once connected"></div>
|
|
1142
1149
|
<div class="rp-field"><label>LAN IP</label><input id="rb-lanip" placeholder="auto-detected once connected"></div>
|
|
1143
1150
|
<div class="rp-field"><label>Motor server URL</label><input id="rb-motor" placeholder="http://...:5000"></div>
|
|
@@ -1241,6 +1248,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1241
1248
|
(function(){
|
|
1242
1249
|
var TOKEN = new URLSearchParams(location.search).get('token') || '';
|
|
1243
1250
|
var QS = TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '';
|
|
1251
|
+
function dashboardMeshToken(){
|
|
1252
|
+
if(TOKEN)return TOKEN;
|
|
1253
|
+
try{var saved=JSON.parse(localStorage.getItem('rp-command-env-v1')||'null');return saved&&String(saved.token||'').trim()||'';}catch(e){return '';}
|
|
1254
|
+
}
|
|
1244
1255
|
var UI_BUILD=(document.querySelector('meta[name="robopark-ui-build"]')||{}).content||'unknown';
|
|
1245
1256
|
fetch('/fed/ui-build'+QS,{cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(info){
|
|
1246
1257
|
if(!info||!info.build||info.build===UI_BUILD)return;
|
|
@@ -1532,6 +1543,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1532
1543
|
+ '<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>'
|
|
1533
1544
|
+ (avatar?('<div class="node-hero"><img class="node-avatar" alt="'+esc(n.displayName)+'" src="'+esc(avatar+QS)+'"><div class="node-health"><b>'+esc(String(readiness).replace(/_/g,' '))+'</b><br>'
|
|
1534
1545
|
+(tele?('sessions '+esc(tele.sessions_total||0)+' / triggers '+esc(tele.trigger_count||0)+'<br>latency '+esc(tele.avg_latency_ms!=null?Math.round(tele.avg_latency_ms)+'ms':'n/a')+' / drops '+esc(tele.drop_rate!=null?Math.round(tele.drop_rate*100)+'%':'n/a')+'<br>heartbeat '+esc(hb!=null?hb+'s ago':'unavailable')):'scheduler telemetry pending')+'</div></div>'):'');
|
|
1546
|
+
if(tele){var fleetStage=tele.current_stage||{},fleetStageStatus=fleetStage.status||tele.current_stage_status||'waiting',fleetStageLabel=fleetStage.label||tele.current_stage_label||'Waiting for pipeline telemetry';html+='<div class="fleet-stage-focus '+esc(fleetStageStatus)+'"><span>Live production stage</span><b>'+esc(fleetStageLabel)+'</b></div>';}
|
|
1535
1547
|
|
|
1536
1548
|
if((tele||String(n.role||'').toLowerCase()==='satellite'||String(n.role||'').toLowerCase()==='robot')&&cameraRef){
|
|
1537
1549
|
html += '<div class="fleet-live-camera" data-fleet-camera-host="'+esc(cameraRef)+'"><img alt="Live camera for '+esc(n.displayName)+'" data-fleet-camera="'+esc(cameraRef)+'"><span class="state">'+(up?'connecting camera':'robot offline')+'</span></div>';
|
|
@@ -2248,16 +2260,20 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2248
2260
|
|
|
2249
2261
|
// ── robust CLI one-liner generation, live as you type — no API call
|
|
2250
2262
|
// needed to see it; 'robopark add-robot' mints its own token when run ──
|
|
2251
|
-
function buildAddRobotCommand(name, characterId){
|
|
2252
|
-
if(!name) return '';
|
|
2253
|
-
var hubUrl=location.origin;
|
|
2254
|
-
var schedulerUrl=
|
|
2255
|
-
var
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
if(
|
|
2259
|
-
if(
|
|
2260
|
-
argv.push('--
|
|
2263
|
+
function buildAddRobotCommand(name, characterId){
|
|
2264
|
+
if(!name) return '';
|
|
2265
|
+
var hubUrl=location.origin.replace(/\\/+$/,'');
|
|
2266
|
+
var schedulerUrl=hubUrl+'/robopark';
|
|
2267
|
+
var requestedNetwork=$('rb-network')&&$('rb-network').value||'auto';
|
|
2268
|
+
var network=requestedNetwork==='auto'?(/^100\\./.test(location.hostname)||/\\.ts\\.net$/i.test(location.hostname)?'tailscale':'lan'):requestedNetwork;
|
|
2269
|
+
var argv=['robopark','add-robot',name];
|
|
2270
|
+
if(characterId) argv.push('--character', characterId);
|
|
2271
|
+
var site=$('rb-site')&&$('rb-site').value.trim();if(site)argv.push('--site',site);
|
|
2272
|
+
if(hubUrl) argv.push('--hub-url', hubUrl);
|
|
2273
|
+
var meshToken=$('rb-token')&&$('rb-token').value.trim()||dashboardMeshToken()||'<mesh-token>';
|
|
2274
|
+
argv.push('--token', meshToken);
|
|
2275
|
+
if(schedulerUrl) argv.push('--scheduler-url', schedulerUrl);
|
|
2276
|
+
argv.push(network==='tailscale'?'--tailscale':'--lan','--start','--auto-start','--yes');
|
|
2261
2277
|
return argv.map(function(a){ return /[^a-zA-Z0-9_./:=,-]/.test(a) ? '"'+a.replace(/"/g,'\\"')+'"' : a; }).join(' ');
|
|
2262
2278
|
}
|
|
2263
2279
|
function refreshRobotCliCommand(){
|
|
@@ -2267,8 +2283,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2267
2283
|
inp.value=buildAddRobotCommand(name, cid);
|
|
2268
2284
|
row.style.display='flex';
|
|
2269
2285
|
}
|
|
2270
|
-
var rbNameEl=$('rb-name'); if(rbNameEl) rbNameEl.addEventListener('input', refreshRobotCliCommand);
|
|
2271
|
-
var rbCharEl=$('rb-char'); if(rbCharEl) rbCharEl.addEventListener('change', refreshRobotCliCommand);
|
|
2286
|
+
var rbNameEl=$('rb-name'); if(rbNameEl) rbNameEl.addEventListener('input', refreshRobotCliCommand);
|
|
2287
|
+
var rbCharEl=$('rb-char'); if(rbCharEl) rbCharEl.addEventListener('change', refreshRobotCliCommand);
|
|
2288
|
+
var rbNetworkEl=$('rb-network'); if(rbNetworkEl) rbNetworkEl.addEventListener('change', refreshRobotCliCommand);
|
|
2289
|
+
var rbSiteEl=$('rb-site'); if(rbSiteEl) rbSiteEl.addEventListener('input', refreshRobotCliCommand);
|
|
2290
|
+
var rbTokenEl=$('rb-token'); if(rbTokenEl){ rbTokenEl.value=dashboardMeshToken(); rbTokenEl.addEventListener('input', refreshRobotCliCommand); }
|
|
2272
2291
|
var rbCliCopy=$('rb-clicmd-copy'); if(rbCliCopy) rbCliCopy.addEventListener('click', function(){ var i=$('rb-clicmd'); i.select(); try{document.execCommand('copy');}catch(e){} showOut('command copied — run it on the robot itself'); });
|
|
2273
2292
|
|
|
2274
2293
|
function addRobot(){
|
|
@@ -2653,7 +2672,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2653
2672
|
var RP_COMMAND_ENV_KEY='rp-command-env-v1';
|
|
2654
2673
|
var RP_CUSTOM_COMMANDS_KEY='rp-custom-commands-v1';
|
|
2655
2674
|
var rpVisibleCommands=[];
|
|
2656
|
-
function rpCommandDefaults(){return {lan:'192.168.1.12',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:
|
|
2675
|
+
function rpCommandDefaults(){return {lan:'192.168.1.12',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:dashboardMeshToken()};}
|
|
2657
2676
|
function rpLoadStoredJson(key,fallback){try{var value=JSON.parse(localStorage.getItem(key)||'null');return value&&typeof value==='object'?value:fallback;}catch(e){return fallback;}}
|
|
2658
2677
|
function rpCommandEnv(){
|
|
2659
2678
|
var d=rpCommandDefaults(),s=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
|
|
@@ -2705,7 +2724,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2705
2724
|
}
|
|
2706
2725
|
function initCommandSheet(){
|
|
2707
2726
|
var fields=['lan','tail','site','robot','enrollment','token'],defaults=rpCommandDefaults(),saved=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
|
|
2708
|
-
fields.forEach(function(name){var input=$('rp-command-'+name);if(!input)return;input.value=saved[name]!=null?saved[name]:defaults[name];input.addEventListener('input',function(){rpSaveCommandEnv();renderCommandSheet();});});
|
|
2727
|
+
fields.forEach(function(name){var input=$('rp-command-'+name);if(!input)return;input.value=name==='token'&&TOKEN?TOKEN:(saved[name]!=null?saved[name]:defaults[name]);input.addEventListener('input',function(){rpSaveCommandEnv();renderCommandSheet();});});
|
|
2709
2728
|
var search=$('rp-command-search'),category=$('rp-command-category'),grid=$('rp-command-grid'),reset=$('rp-command-reset'),copyAll=$('rp-command-copy-all'),form=$('rp-command-custom-form');
|
|
2710
2729
|
if(search)search.addEventListener('input',renderCommandSheet);if(category)category.addEventListener('change',renderCommandSheet);
|
|
2711
2730
|
if(reset)reset.addEventListener('click',function(){try{localStorage.removeItem(RP_COMMAND_ENV_KEY);}catch(e){}fields.forEach(function(name){var input=$('rp-command-'+name);if(input)input.value=defaults[name];});renderCommandSheet();showOut('command environment reset');});
|
|
@@ -6517,13 +6536,24 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6517
6536
|
// cars that arrived at the boundary together don't land on
|
|
6518
6537
|
// top of each other.
|
|
6519
6538
|
if(car.dir > 0 && car.x > PARK_W + 0.5){
|
|
6539
|
+
// Outbound (right-bound) car reached the right edge —
|
|
6540
|
+
// re-enter on the left. We swap to the OTHER lane so two
|
|
6541
|
+
// lanes of traffic are visible at all times; the wrap dir
|
|
6542
|
+
// is explicitly set to +1 so the car continues in its
|
|
6543
|
+
// original direction on the new lane. (Previous version
|
|
6544
|
+
// computed dir from the *post-wrap* y, which always
|
|
6545
|
+
// flipped the car to the opposite direction — cars would
|
|
6546
|
+
// oscillate at the road edge instead of crossing it.)
|
|
6520
6547
|
car.x = -0.5 - Math.random() * (PARK_W * 0.5);
|
|
6521
6548
|
car.y = (car.y < -0.9) ? -0.65 : -1.18;
|
|
6522
|
-
car.dir =
|
|
6549
|
+
car.dir = +1;
|
|
6523
6550
|
} else if(car.dir < 0 && car.x < -0.5){
|
|
6551
|
+
// Inbound (left-bound) car reached the left edge —
|
|
6552
|
+
// re-enter on the right, swap to the other lane, keep
|
|
6553
|
+
// direction at -1. (See note above about the prior bug.)
|
|
6524
6554
|
car.x = PARK_W + 0.5 + Math.random() * (PARK_W * 0.5);
|
|
6525
6555
|
car.y = (car.y < -0.9) ? -0.65 : -1.18;
|
|
6526
|
-
car.dir =
|
|
6556
|
+
car.dir = -1;
|
|
6527
6557
|
}
|
|
6528
6558
|
}
|
|
6529
6559
|
}
|
|
@@ -7429,7 +7459,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7429
7459
|
+'<div class="pk-note" id="pk-camnote">seeking live feed…</div>'
|
|
7430
7460
|
+'<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn" id="pk-golive" data-robot="'+esc(n.name)+'">↻ Reconnect camera</button><button class="rp-btn primary" id="pk-camera-fullscreen">Fullscreen operations view</button><span class="pk-note" id="pk-golivenote" style="margin:0">always-on RoboVision relay</span></div>'
|
|
7431
7461
|
+'<label class="pk-prodtoggle"><input type="checkbox" id="pk-prodmode"'+(devForPm.production_mode?' checked':'')+'><span>Production mode — continuous motion-triggered loop, camera always live</span></label>')
|
|
7432
|
-
+ sect('Remote production test','<div id="pk-pipeline-summary" class="pk-pipeline-summary">Checking robot, camera, scheduler and LiveKit...</div>'
|
|
7462
|
+
+ sect('Remote production test','<div id="pk-live-stage" class="pk-live-stage-focus waiting"><span class="eyebrow">Current production stage</span><b>Checking robot state...</b><span>Waiting for scheduler telemetry</span></div><div id="pk-pipeline-summary" class="pk-pipeline-summary">Checking robot, camera, scheduler and LiveKit...</div>'
|
|
7433
7463
|
+ '<div class="pk-ctl" style="margin-top:0.55rem"><button class="rp-btn primary" id="pk-unattended-test">Run unattended readiness</button><button class="rp-btn" id="pk-pipeline-start">Start full test</button><button class="rp-btn danger" id="pk-pipeline-stop">Stop conversation</button></div>'
|
|
7434
7464
|
+ '<div class="pk-note" id="pk-unattended-result">Unattended readiness runs an acoustic speaker/mic round-trip, triggers the production motion path, and requires greeting playback on the robot.</div>'
|
|
7435
7465
|
+ '<div class="pk-pipeline" id="pk-pipeline"></div>')
|
|
@@ -7621,7 +7651,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7621
7651
|
return stages[Math.min(stages.length-1,lastOk+1)];
|
|
7622
7652
|
}
|
|
7623
7653
|
function renderPipeline(data){
|
|
7624
|
-
var host=$('pk-pipeline'), summary=$('pk-pipeline-summary'); if(!host||!summary) return;
|
|
7654
|
+
var host=$('pk-pipeline'), summary=$('pk-pipeline-summary'),focus=$('pk-live-stage'); if(!host||!summary) return;
|
|
7625
7655
|
var latest={}, prereqs=data.prerequisites||{}, session=data.latest_session||{}, sid=session.id||null;
|
|
7626
7656
|
(data.events||[]).forEach(function(ev){
|
|
7627
7657
|
if(ev.session_id && sid && ev.session_id!==sid) return;
|
|
@@ -7630,15 +7660,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7630
7660
|
['robot_online','camera_ready','microphone_ready','speaker_ready'].forEach(function(stage){
|
|
7631
7661
|
if(!latest[stage]) latest[stage]={stage:stage,status:prereqs[stage]?'ok':'blocked',message:prereqs[stage]?'Ready':'Not reported'};
|
|
7632
7662
|
});
|
|
7633
|
-
var running=data.overall==='running',stages=data.stages||Object.keys(PIPELINE_LABELS),current=pipelineCurrentStage(stages,latest,running);
|
|
7663
|
+
var running=data.overall==='running',stages=data.stages||Object.keys(PIPELINE_LABELS),authoritative=data.current_stage||{},current=authoritative.stage||pipelineCurrentStage(stages,latest,running);
|
|
7634
7664
|
host.innerHTML=stages.map(function(stage,index){
|
|
7635
|
-
var ev=latest[stage],isCurrent=stage===current,status=ev&&ev.status||(isCurrent?'running':'pending');
|
|
7636
|
-
var detail=ev&&(ev.message||ev.timestamp)||'waiting';
|
|
7665
|
+
var ev=latest[stage],isCurrent=stage===current,status=(isCurrent&&authoritative.status)||ev&&ev.status||(isCurrent?'running':'pending');
|
|
7666
|
+
var detail=isCurrent&&(authoritative.message||authoritative.label)||ev&&(ev.message||ev.timestamp)||'waiting';
|
|
7637
7667
|
var time=ev&&ev.timestamp?new Date(ev.timestamp+'Z').toLocaleString():'';
|
|
7638
7668
|
var phase=PIPELINE_PHASES[stage]||'session';
|
|
7639
|
-
return '<div class="pk-pipe-step phase-'+phase+' '+esc(status)+(isCurrent?' current':'')+'"><span class="step-index">'+String(index+1).padStart(2,'0')+'</span><span><span class="stage">'+esc(PIPELINE_LABELS[stage]||stage)+'</span><span class="phase">'+esc(phase)+'</span></span><span class="detail">'+esc(detail)+(time?'<span class="time">'+esc(time)+'</span>':'')+'</span></div>';
|
|
7669
|
+
return '<div class="pk-pipe-step phase-'+phase+' '+esc(status)+(isCurrent?' current':'')+'"'+(isCurrent?' aria-current="step"':'')+'><span class="step-index">'+String(index+1).padStart(2,'0')+'</span><span><span class="stage">'+esc(PIPELINE_LABELS[stage]||stage)+'<em class="step-state">'+esc(isCurrent?'ACTIVE':status)+'</em></span><span class="phase">'+esc(phase)+'</span></span><span class="detail">'+esc(detail)+(time?'<span class="time">'+esc(time)+'</span>':'')+'</span></div>';
|
|
7640
7670
|
}).join('');
|
|
7641
7671
|
var blockers=data.blockers||[];
|
|
7672
|
+
if(focus){var focusStatus=authoritative.status||blockers.length?'blocked':running?'active':'waiting';if(authoritative.status)focusStatus=authoritative.status;focus.className='pk-live-stage-focus '+esc(focusStatus);focus.innerHTML='<span class="eyebrow">Current production stage</span><b>'+esc(authoritative.label||PIPELINE_LABELS[current]||current||'Standing by')+'</b><span>'+esc(authoritative.message||blockers[0]||'Robot is armed and waiting for motion')+'</span>';}
|
|
7642
7673
|
summary.className='pk-pipeline-summary '+esc(data.overall||'blocked');
|
|
7643
7674
|
summary.textContent=blockers.length?('BLOCKED: '+blockers[0]):(running?('ACTIVE: '+(PIPELINE_LABELS[current]||current||'starting')+' | session '+(sid||'allocating')):'READY: camera-to-speaker remote test can start');
|
|
7644
7675
|
var micHealth=$('pk-mic-health');if(micHealth){
|
|
@@ -7784,11 +7815,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7784
7815
|
function refreshCameraOperations(){
|
|
7785
7816
|
var n=cameraOpsNode;if(!n)return;var row=schedRow(n)||{},device=schedulerDevice(n)||{},hw=hwByNode[n.nodeId]||{};
|
|
7786
7817
|
var readiness=String(row.readiness_code||(!n.connected?'offline':'mesh_online')),active=!!(row.active_session||row.in_session||row.session_active||row.current_session_id||row.status==='running'||row.status==='connecting');
|
|
7818
|
+
var currentStage=row.current_stage||{},currentStageLabel=currentStage.label||row.current_stage_label||(active?'Conversation starting':device.production_mode?'Armed - waiting for motion':'Production paused'),currentStageStatus=currentStage.status||row.current_stage_status||(active?'active':'waiting');
|
|
7787
7819
|
var img=$('pk-camera-ops-img'),cameraLive=!!(img&&img.dataset.live==='true'),clockEl=$('pk-camera-ops-clock');if(clockEl)clockEl.textContent=clock(Date.now());
|
|
7788
7820
|
var rec=$('pk-camera-ops-rec');if(rec)rec.textContent=(cameraLive?'REC / LIVE':'REC / CONNECTING')+' / '+String(n.displayName||n.name||'robot').toUpperCase();
|
|
7789
|
-
var overlay=$('pk-camera-ops-overlay');if(overlay)overlay.innerHTML='<div class="metric"><b>'+esc(
|
|
7821
|
+
var overlay=$('pk-camera-ops-overlay');if(overlay)overlay.innerHTML='<div class="metric"><b>'+esc(currentStageLabel)+'</b><span>active stage</span></div><div class="metric"><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>readiness</span></div><div class="metric"><b>'+(row.avg_latency_ms!=null?Math.round(row.avg_latency_ms)+'ms':'--')+'</b><span>average latency</span></div><div class="metric"><b>'+(row.drop_rate!=null?Math.round(row.drop_rate*100)+'%':'--')+'</b><span>session drops</span></div>';
|
|
7790
7822
|
var insights=$('pk-camera-ops-insights');if(insights)insights.innerHTML='<span>Camera relay</span><b class="'+(cameraLive?'ok':'bad')+'">'+(cameraLive?'LIVE':'CONNECTING')+'</b><span>Mesh node</span><b class="'+(n.connected?'ok':'bad')+'">'+(n.connected?'connected':'offline')+'</b><span>Production mode</span><b class="'+(device.production_mode?'ok':'')+'">'+(device.production_mode?'armed':'disabled')+'</b><span>Readiness</span><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>Heartbeat</span><b>'+(row.heartbeat_age_seconds!=null?Math.round(row.heartbeat_age_seconds)+'s ago':'--')+'</b><span>Current room</span><b>'+esc(String(row.current_session_id||row.active_session_id||'none').slice(0,24))+'</b><span>Triggers</span><b>'+esc(row.trigger_count!=null?row.trigger_count:0)+'</b><span>Sessions</span><b>'+esc(row.sessions_total!=null?row.sessions_total:0)+'</b>';
|
|
7791
|
-
var stages=[['Robot online',!!n.connected],['Camera streaming',cameraLive],['Motion detector armed',!!device.production_mode],['Scheduler session',active
|
|
7823
|
+
var stages=[[currentStageLabel,true,'ACTIVE'],['Robot online',!!n.connected,'ready'],['Camera streaming',cameraLive,'ready'],['Motion detector armed',!!device.production_mode,'armed'],['Scheduler session',active,'active']],pipe=$('pk-camera-ops-pipeline');if(pipe)pipe.innerHTML=stages.map(function(s,index){return '<div class="'+(index===0?'fleet-stage-focus '+esc(currentStageStatus):'')+'"><i class="'+(s[1]?'ok':'warn')+'"></i><span>'+esc(s[0])+'</span><b>'+(s[1]?s[2]:'waiting')+'</b></div>';}).join('');
|
|
7792
7824
|
var inv=device.device_inventory||{},mh=inv.media_health||{},hardware=$('pk-camera-ops-hardware');if(hardware)hardware.innerHTML='<span>CPU</span><b>'+(hw.cpu&&hw.cpu.percent!=null?Math.round(hw.cpu.percent)+'%':'--')+'</b><span>Memory</span><b>'+(hw.memory&&hw.memory.percent!=null?Math.round(hw.memory.percent)+'%':'--')+'</b><span>Temperature</span><b>'+(hw.temperatureC!=null?Math.round(hw.temperatureC)+'C':'--')+'</b><span>Camera inputs</span><b>'+((inv.video||[]).length||0)+'</b><span>Microphones</span><b>'+((inv.audio_input||[]).length||0)+'</b><span>Speaker outputs</span><b>'+((inv.audio_output||[]).length||0)+'</b><span>Camera permission</span><b class="'+(mh.camera_access===false?'bad':'ok')+'">'+(mh.camera_access===false?'DENIED':mh.camera_access===true?'granted':'--')+'</b><span>Audio permission</span><b class="'+(mh.audio_access===false?'bad':'ok')+'">'+(mh.audio_access===false?'DENIED':mh.audio_access===true?'granted':'--')+'</b><span>Camera worker</span><b class="'+(mh.camera_stalled?'bad':'ok')+'">'+(mh.camera_stalled?'STALLED':mh.camera_worker?'streaming':'--')+'</b><span>Frame age</span><b>'+(mh.camera_frame_age_seconds!=null?Number(mh.camera_frame_age_seconds).toFixed(1)+'s':'--')+'</b><span>Selected camera</span><b>'+esc(device.video_device||'auto')+'</b><span>Selected microphone</span><b>'+esc(device.audio_device||'default')+'</b>';
|
|
7793
7825
|
var prod=$('pk-camera-ops-production');if(prod){prod.textContent=device.production_mode?'Disable production':'Enable production';prod.classList.toggle('primary',!!device.production_mode);}
|
|
7794
7826
|
}
|
|
@@ -4,6 +4,8 @@ import { type ExplicitContextOpts } from './profile.js';
|
|
|
4
4
|
export interface AddRobotOptions extends ExplicitContextOpts {
|
|
5
5
|
name: string;
|
|
6
6
|
start?: boolean;
|
|
7
|
+
autoStart?: boolean;
|
|
8
|
+
yes?: boolean;
|
|
7
9
|
/** Force a specific park-map pad/character preset id instead of the
|
|
8
10
|
* fuzzy name-based match below — e.g. `--character jaguar`. */
|
|
9
11
|
character?: string;
|
|
@@ -24,8 +24,32 @@
|
|
|
24
24
|
import chalk from 'chalk';
|
|
25
25
|
import { resolveContext } from './profile.js';
|
|
26
26
|
import { roboparkSetup } from './setup.js';
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
function robotReachableSchedulerUrl(schedulerUrl, hubUrl) {
|
|
28
|
+
if (!hubUrl)
|
|
29
|
+
return schedulerUrl;
|
|
30
|
+
try {
|
|
31
|
+
// Port 8080 is intentionally private to the hub. Enrollment, character
|
|
32
|
+
// lookup, heartbeats, and robot sessions must all use the authenticated
|
|
33
|
+
// mesh proxy, whether the hidden scheduler is loopback or a 192.168.x.x
|
|
34
|
+
// address that is only meaningful on the hub's own LAN.
|
|
35
|
+
new URL(schedulerUrl);
|
|
36
|
+
new URL(hubUrl);
|
|
37
|
+
return `${hubUrl.replace(/\/+$/, '')}/robopark`;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Keep the original value so the existing request reports a useful error.
|
|
41
|
+
}
|
|
42
|
+
return schedulerUrl;
|
|
43
|
+
}
|
|
44
|
+
function schedulerApiUrl(schedulerUrl, path, token) {
|
|
45
|
+
const url = new URL(`${schedulerUrl.replace(/\/+$/, '')}${path}`);
|
|
46
|
+
if (token && url.pathname.startsWith('/robopark/')) {
|
|
47
|
+
url.searchParams.set('token', token);
|
|
48
|
+
}
|
|
49
|
+
return url.toString();
|
|
50
|
+
}
|
|
51
|
+
async function mintEnrollmentToken(schedulerUrl, name, characterId, token) {
|
|
52
|
+
const url = schedulerApiUrl(schedulerUrl, '/api/devices', token);
|
|
29
53
|
const res = await fetch(url, {
|
|
30
54
|
method: 'POST',
|
|
31
55
|
headers: { 'content-type': 'application/json' },
|
|
@@ -37,10 +61,10 @@ async function mintEnrollmentToken(schedulerUrl, name, characterId) {
|
|
|
37
61
|
}
|
|
38
62
|
return (await res.json());
|
|
39
63
|
}
|
|
40
|
-
async function pickCharacterPreset(schedulerUrl, name) {
|
|
64
|
+
async function pickCharacterPreset(schedulerUrl, name, token) {
|
|
41
65
|
const lower = name.toLowerCase();
|
|
42
66
|
try {
|
|
43
|
-
const res = await fetch(
|
|
67
|
+
const res = await fetch(schedulerApiUrl(schedulerUrl, '/api/character-presets', token));
|
|
44
68
|
if (!res.ok)
|
|
45
69
|
return undefined;
|
|
46
70
|
const presets = (await res.json());
|
|
@@ -83,11 +107,16 @@ export async function roboparkAddRobot(config, opts) {
|
|
|
83
107
|
console.log(chalk.red(' ✗ no scheduler URL resolved. Pass --scheduler-url/--hub-url, run `robopark hub-use` first, or run this on/near the hub.'));
|
|
84
108
|
process.exit(1);
|
|
85
109
|
}
|
|
86
|
-
|
|
110
|
+
const schedulerUrl = robotReachableSchedulerUrl(ctx.schedulerUrl, ctx.hubUrl);
|
|
111
|
+
if (schedulerUrl.endsWith('/robopark') && !ctx.token) {
|
|
112
|
+
console.log(chalk.red(' ✗ hub mesh token is required to use the scheduler proxy. Pass --token or run `robopark hub-use` first.'));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
|
|
87
116
|
console.log(` name: ${chalk.cyan(opts.name)}${ctx.site ? chalk.dim(' @ ' + ctx.site) : ''}`);
|
|
88
117
|
console.log(` network: ${chalk.cyan(network)}`);
|
|
89
118
|
console.log();
|
|
90
|
-
const characterId = opts.character?.trim() || await pickCharacterPreset(
|
|
119
|
+
const characterId = opts.character?.trim() || await pickCharacterPreset(schedulerUrl, opts.name, ctx.token);
|
|
91
120
|
if (characterId) {
|
|
92
121
|
console.log(` character: ${chalk.cyan(characterId)}${opts.character ? chalk.dim(' (forced via --character)') : chalk.dim(' (auto-matched)')}`);
|
|
93
122
|
}
|
|
@@ -96,7 +125,7 @@ export async function roboparkAddRobot(config, opts) {
|
|
|
96
125
|
}
|
|
97
126
|
let minted;
|
|
98
127
|
try {
|
|
99
|
-
minted = await mintEnrollmentToken(
|
|
128
|
+
minted = await mintEnrollmentToken(schedulerUrl, opts.name, characterId, ctx.token);
|
|
100
129
|
}
|
|
101
130
|
catch (e) {
|
|
102
131
|
console.log(chalk.red(` ✗ failed to mint enrollment token: ${e.message}`));
|
|
@@ -120,6 +149,8 @@ export async function roboparkAddRobot(config, opts) {
|
|
|
120
149
|
schedulerPort: ctx.schedulerUrl ? String(new URL(ctx.schedulerUrl).port || '8080') : undefined,
|
|
121
150
|
enrollmentToken: minted.enrollment_token,
|
|
122
151
|
start: true,
|
|
152
|
+
autoStart: opts.autoStart !== false,
|
|
153
|
+
yes: opts.yes !== false,
|
|
123
154
|
lan: opts.lan,
|
|
124
155
|
tailscale: opts.tailscale,
|
|
125
156
|
});
|
|
@@ -140,7 +171,7 @@ export async function roboparkAddRobot(config, opts) {
|
|
|
140
171
|
argv.push('--scheduler-port', schedulerPort);
|
|
141
172
|
}
|
|
142
173
|
argv.push(network === 'tailscale' ? '--tailscale' : '--lan');
|
|
143
|
-
argv.push('--enrollment-token', minted.enrollment_token, '--start', '--auto-start');
|
|
174
|
+
argv.push('--enrollment-token', minted.enrollment_token, '--start', '--auto-start', '--yes');
|
|
144
175
|
console.log(chalk.dim(` run this ON THE ROBOT${characterId ? ` (binds to pad: ${characterId})` : ''}:`));
|
|
145
176
|
console.log(' ' + chalk.cyan(commandString(argv)));
|
|
146
177
|
if (!ctx.hubUrl) {
|
|
@@ -146,7 +146,7 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
146
146
|
// only while audible robot speech is playing, then hold briefly for
|
|
147
147
|
// room echo decay so VAD cannot interrupt the robot with its own voice.
|
|
148
148
|
ROBOPARK_HALF_DUPLEX: 'true',
|
|
149
|
-
ROBOPARK_ECHO_TAIL_MS: '
|
|
149
|
+
ROBOPARK_ECHO_TAIL_MS: '350',
|
|
150
150
|
ROBOPARK_ECHO_GATE_PEAK: '96',
|
|
151
151
|
// Keep ordinary speaker echo muted but reopen the microphone when a
|
|
152
152
|
// nearby voice rises decisively above the learned acoustic echo floor.
|
package/dist/robopark-cli.js
CHANGED
|
@@ -284,6 +284,8 @@ program
|
|
|
284
284
|
.option('--lan', 'generate a robot command for the same local network (default)')
|
|
285
285
|
.option('--tailscale', 'generate a robot command for a Tailscale-reachable hub')
|
|
286
286
|
.option('--start', 'start the robot node on this machine now, instead of printing the one-liner')
|
|
287
|
+
.option('--auto-start', 'register the robot runtime to start on boot')
|
|
288
|
+
.option('--yes', 'non-interactive production setup')
|
|
287
289
|
.action(async (name, opts) => {
|
|
288
290
|
await roboparkAddRobot(config, { name, ...opts });
|
|
289
291
|
});
|
package/package.json
CHANGED
|
@@ -3192,6 +3192,93 @@ PIPELINE_STAGES = (
|
|
|
3192
3192
|
)
|
|
3193
3193
|
PIPELINE_STATUSES = {"pending", "running", "ok", "failed", "blocked", "skipped"}
|
|
3194
3194
|
|
|
3195
|
+
PIPELINE_STAGE_LABELS = {
|
|
3196
|
+
"robot_online": "Robot online",
|
|
3197
|
+
"camera_ready": "Camera detected",
|
|
3198
|
+
"microphone_ready": "Microphone detected",
|
|
3199
|
+
"speaker_ready": "Speaker detected",
|
|
3200
|
+
"motion_detected": "Waiting for motion",
|
|
3201
|
+
"scheduler_session": "Allocating session",
|
|
3202
|
+
"livekit_join": "Joining LiveKit",
|
|
3203
|
+
"camera_published": "Publishing camera",
|
|
3204
|
+
"microphone_published": "Publishing microphone",
|
|
3205
|
+
"voice_worker": "Starting voice worker",
|
|
3206
|
+
"stt_listening": "Listening to visitor",
|
|
3207
|
+
"llm_response": "Preparing response",
|
|
3208
|
+
"tts_subscribed": "Preparing robot voice",
|
|
3209
|
+
"playback_started": "Speaking through robot",
|
|
3210
|
+
"motor_sequence": "Running motor sequence",
|
|
3211
|
+
"session_ended": "Closing session",
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
|
|
3215
|
+
def _pipeline_current_state(events: list[dict], *, active: bool,
|
|
3216
|
+
production_mode: bool, online: bool) -> dict:
|
|
3217
|
+
"""Derive one operator-facing live stage from persistent pipeline events."""
|
|
3218
|
+
if not online:
|
|
3219
|
+
return {
|
|
3220
|
+
"stage": "robot_online", "status": "blocked",
|
|
3221
|
+
"label": PIPELINE_STAGE_LABELS["robot_online"],
|
|
3222
|
+
"message": "Robot heartbeat is stale or missing", "since": None,
|
|
3223
|
+
}
|
|
3224
|
+
if not active:
|
|
3225
|
+
return {
|
|
3226
|
+
"stage": "motion_detected",
|
|
3227
|
+
"status": "waiting" if production_mode else "paused",
|
|
3228
|
+
"label": "Armed - waiting for motion" if production_mode else "Production paused",
|
|
3229
|
+
"message": "Motion detection is armed" if production_mode else "Enable production mode to arm this robot",
|
|
3230
|
+
"since": None,
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
scoped = [event for event in events if event.get("session_id")]
|
|
3234
|
+
for event in reversed(scoped):
|
|
3235
|
+
if event.get("status") in {"failed", "blocked"}:
|
|
3236
|
+
return {
|
|
3237
|
+
"stage": event["stage"], "status": event["status"],
|
|
3238
|
+
"label": PIPELINE_STAGE_LABELS.get(event["stage"], event["stage"]),
|
|
3239
|
+
"message": event.get("message") or "Pipeline requires attention",
|
|
3240
|
+
"since": event.get("timestamp"),
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
conversational = {
|
|
3244
|
+
"voice_worker", "stt_listening", "llm_response", "tts_subscribed",
|
|
3245
|
+
"playback_started", "motor_sequence",
|
|
3246
|
+
}
|
|
3247
|
+
latest = next((event for event in reversed(scoped) if event.get("stage") in conversational), None)
|
|
3248
|
+
if latest:
|
|
3249
|
+
stage = latest["stage"]
|
|
3250
|
+
status = latest.get("status") or "running"
|
|
3251
|
+
if status in {"ok", "skipped"}:
|
|
3252
|
+
stage = {
|
|
3253
|
+
"voice_worker": "stt_listening",
|
|
3254
|
+
"stt_listening": "llm_response",
|
|
3255
|
+
"llm_response": "tts_subscribed",
|
|
3256
|
+
"tts_subscribed": "playback_started",
|
|
3257
|
+
"playback_started": "stt_listening",
|
|
3258
|
+
"motor_sequence": "stt_listening",
|
|
3259
|
+
}.get(stage, stage)
|
|
3260
|
+
status = "active"
|
|
3261
|
+
return {
|
|
3262
|
+
"stage": stage, "status": status,
|
|
3263
|
+
"label": PIPELINE_STAGE_LABELS.get(stage, stage),
|
|
3264
|
+
"message": latest.get("message") or PIPELINE_STAGE_LABELS.get(stage, stage),
|
|
3265
|
+
"since": latest.get("timestamp"),
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
startup = (
|
|
3269
|
+
"scheduler_session", "livekit_join", "camera_published",
|
|
3270
|
+
"microphone_published", "voice_worker",
|
|
3271
|
+
)
|
|
3272
|
+
completed = {event.get("stage") for event in scoped if event.get("status") in {"ok", "skipped"}}
|
|
3273
|
+
stage = next((candidate for candidate in startup if candidate not in completed), "voice_worker")
|
|
3274
|
+
latest_startup = next((event for event in reversed(scoped) if event.get("stage") in startup), None)
|
|
3275
|
+
return {
|
|
3276
|
+
"stage": stage, "status": "active",
|
|
3277
|
+
"label": PIPELINE_STAGE_LABELS.get(stage, stage),
|
|
3278
|
+
"message": (latest_startup or {}).get("message") or "Production conversation is starting",
|
|
3279
|
+
"since": (latest_startup or {}).get("timestamp"),
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3195
3282
|
async def _insert_pipeline_event(device_id: str, payload: PipelineEventPayload) -> dict:
|
|
3196
3283
|
if payload.stage not in PIPELINE_STAGES:
|
|
3197
3284
|
raise HTTPException(422, f"Unknown pipeline stage: {payload.stage}")
|
|
@@ -3286,16 +3373,29 @@ async def robot_pipeline_status(robot_id: str, limit: int = 80):
|
|
|
3286
3373
|
blockers.append("Microphone inventory has not been reported")
|
|
3287
3374
|
if not prereqs["speaker_ready"]:
|
|
3288
3375
|
blockers.append("Speaker inventory has not been reported")
|
|
3376
|
+
active = bool(latest_session and not latest_session["ended_at"])
|
|
3377
|
+
active_session_id = latest_session["id"] if active else None
|
|
3378
|
+
current_events = [
|
|
3379
|
+
event for event in events
|
|
3380
|
+
if not active_session_id or event.get("session_id") == active_session_id
|
|
3381
|
+
]
|
|
3382
|
+
current_stage = _pipeline_current_state(
|
|
3383
|
+
current_events,
|
|
3384
|
+
active=active,
|
|
3385
|
+
production_mode=bool(device["production_mode"]),
|
|
3386
|
+
online=prereqs["robot_online"],
|
|
3387
|
+
)
|
|
3289
3388
|
return {
|
|
3290
3389
|
"robot_id": device_id,
|
|
3291
3390
|
"robot_name": device["name"],
|
|
3292
|
-
"overall": "blocked" if blockers else ("running" if
|
|
3391
|
+
"overall": "blocked" if blockers else ("running" if active else "ready"),
|
|
3293
3392
|
"blockers": list(dict.fromkeys(blockers)),
|
|
3294
3393
|
"prerequisites": prereqs,
|
|
3295
3394
|
"telemetry": telemetry,
|
|
3296
3395
|
"latest_session": dict(latest_session) if latest_session else None,
|
|
3297
3396
|
"events": events,
|
|
3298
3397
|
"stages": list(PIPELINE_STAGES),
|
|
3398
|
+
"current_stage": current_stage,
|
|
3299
3399
|
}
|
|
3300
3400
|
|
|
3301
3401
|
@app.get("/api/robots/{robot_id}/pipeline-history")
|
|
@@ -5170,13 +5270,20 @@ async def _robot_telemetry(db, robot_id: str) -> Optional[dict]:
|
|
|
5170
5270
|
|
|
5171
5271
|
device_inventory = parse_json_field(device["device_inventory"], {}) if device else {}
|
|
5172
5272
|
supervisor_status = parse_json_field(device["supervisor_status"], []) if device else []
|
|
5173
|
-
session = None
|
|
5174
|
-
|
|
5273
|
+
session = None
|
|
5274
|
+
current_pipeline_events = []
|
|
5275
|
+
if robot["current_session_id"]:
|
|
5175
5276
|
async with db.execute(
|
|
5176
5277
|
"SELECT started_at, last_activity_at, ended_at FROM sessions WHERE id = ?",
|
|
5177
5278
|
(robot["current_session_id"],),
|
|
5178
|
-
) as c:
|
|
5179
|
-
session = await c.fetchone()
|
|
5279
|
+
) as c:
|
|
5280
|
+
session = await c.fetchone()
|
|
5281
|
+
async with db.execute(
|
|
5282
|
+
"SELECT * FROM pipeline_events WHERE robot_id = ? AND session_id = ? "
|
|
5283
|
+
"ORDER BY timestamp, id",
|
|
5284
|
+
(robot_id, robot["current_session_id"]),
|
|
5285
|
+
) as c:
|
|
5286
|
+
current_pipeline_events = [dict(row) for row in await c.fetchall()]
|
|
5180
5287
|
|
|
5181
5288
|
now = datetime.utcnow()
|
|
5182
5289
|
session_age = None
|
|
@@ -5259,8 +5366,14 @@ async def _robot_telemetry(db, robot_id: str) -> Optional[dict]:
|
|
|
5259
5366
|
(robot_id,),
|
|
5260
5367
|
) as c:
|
|
5261
5368
|
end_reasons = {r["end_reason"]: r["n"] for r in await c.fetchall()}
|
|
5262
|
-
drops = sum(n for reason, n in end_reasons.items() if reason in ABNORMAL_END_REASONS)
|
|
5263
|
-
drop_rate = round(drops / sessions_total, 4) if sessions_total else 0.0
|
|
5369
|
+
drops = sum(n for reason, n in end_reasons.items() if reason in ABNORMAL_END_REASONS)
|
|
5370
|
+
drop_rate = round(drops / sessions_total, 4) if sessions_total else 0.0
|
|
5371
|
+
current_stage = _pipeline_current_state(
|
|
5372
|
+
current_pipeline_events,
|
|
5373
|
+
active=bool(robot["current_session_id"] and session and not session["ended_at"]),
|
|
5374
|
+
production_mode=bool(device["production_mode"]) if device else False,
|
|
5375
|
+
online=heartbeat_age is not None and heartbeat_age <= 30,
|
|
5376
|
+
)
|
|
5264
5377
|
|
|
5265
5378
|
return {
|
|
5266
5379
|
"robot_id": robot_id,
|
|
@@ -5291,9 +5404,13 @@ async def _robot_telemetry(db, robot_id: str) -> Optional[dict]:
|
|
|
5291
5404
|
"supervisor_status": supervisor_status,
|
|
5292
5405
|
"supervisor_status_at": device["supervisor_status_at"] if device else None,
|
|
5293
5406
|
"production_mode": bool(device["production_mode"]) if device else False,
|
|
5294
|
-
"readiness_code": readiness_code,
|
|
5295
|
-
"readiness_message": readiness_message,
|
|
5296
|
-
|
|
5407
|
+
"readiness_code": readiness_code,
|
|
5408
|
+
"readiness_message": readiness_message,
|
|
5409
|
+
"current_stage": current_stage,
|
|
5410
|
+
"current_stage_id": current_stage["stage"],
|
|
5411
|
+
"current_stage_label": current_stage["label"],
|
|
5412
|
+
"current_stage_status": current_stage["status"],
|
|
5413
|
+
}
|
|
5297
5414
|
|
|
5298
5415
|
@app.get("/api/robots/{robot_id}/telemetry")
|
|
5299
5416
|
async def robot_telemetry(robot_id: str):
|
|
@@ -1303,7 +1303,7 @@ class LiveKitPublisher:
|
|
|
1303
1303
|
self._echo_mic_floor = 0.0
|
|
1304
1304
|
self._barge_in_candidate_frames = 0
|
|
1305
1305
|
self._speaker_echo_tail = max(
|
|
1306
|
-
0.1, min(float(os.getenv("ROBOPARK_ECHO_TAIL_MS", "
|
|
1306
|
+
0.1, min(float(os.getenv("ROBOPARK_ECHO_TAIL_MS", "350")) / 1000.0, 2.0)
|
|
1307
1307
|
)
|
|
1308
1308
|
self._adaptive_barge_in = str(
|
|
1309
1309
|
os.getenv("ROBOPARK_ADAPTIVE_BARGE_IN", "true")
|
|
@@ -1553,7 +1553,7 @@ class LiveKitPublisher:
|
|
|
1553
1553
|
import threading
|
|
1554
1554
|
write_queue: "queue.Queue[Optional[bytes]]" = queue.Queue()
|
|
1555
1555
|
written_frames = [0]
|
|
1556
|
-
PREBUFFER_CHUNKS =
|
|
1556
|
+
PREBUFFER_CHUNKS = 3
|
|
1557
1557
|
stream_failed = threading.Event()
|
|
1558
1558
|
playback_reported = threading.Event()
|
|
1559
1559
|
event_loop = asyncio.get_running_loop()
|