infinicode 2.8.97 → 2.8.98
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 +471 -54
- package/dist/robopark/python-env.d.ts +1 -0
- package/dist/robopark/python-env.js +3 -0
- package/dist/robopark/robot-runtime.js +1 -1
- package/dist/robopark/vision-agent-launcher.js +19 -3
- package/docs/ROBOPARK_PRODUCTION_MEMORY.md +19 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +278 -22
- package/packages/robopark/scheduler/robot_supervisor.py +62 -5
- package/packages/robopark/vision/motor_server.py +204 -88
- package/packages/robopark/vision/requirements_vision_agent.txt +1 -0
|
@@ -362,18 +362,25 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
362
362
|
.pk-kv .l{font-size:0.56rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--muted);}
|
|
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
|
-
.pk-pipeline{display:grid;gap:0.
|
|
366
|
-
.pk-pipe-step{display:grid;grid-template-columns:
|
|
367
|
-
.pk-pipe-step
|
|
368
|
-
.pk-pipe-step.
|
|
369
|
-
.pk-pipe-step.
|
|
370
|
-
.pk-pipe-step.
|
|
371
|
-
.pk-pipe-step
|
|
372
|
-
.pk-pipe-step
|
|
373
|
-
.pk-
|
|
365
|
+
.pk-pipeline{display:grid;gap:0.42rem;margin-top:0.7rem;position:relative;}
|
|
366
|
+
.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
|
+
.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
|
+
.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
|
+
.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;}
|
|
371
|
+
.pk-pipe-step.ok{border-color:color-mix(in srgb,var(--ok) 22%,transparent)}
|
|
372
|
+
.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(4px);border-color:var(--phase);background:linear-gradient(105deg,color-mix(in srgb,var(--phase) 19%,transparent),rgba(255,255,255,.035));box-shadow:0 0 0 1px color-mix(in srgb,var(--phase) 18%,transparent),0 8px 28px rgba(0,0,0,.22);}
|
|
374
|
+
.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
|
+
.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
|
+
.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;}
|
|
374
377
|
.pk-pipeline-summary.ready{color:var(--ok);border-color:rgba(70,220,150,.35);}
|
|
375
378
|
.pk-pipeline-summary.running{color:var(--warn);border-color:rgba(255,196,70,.35);}
|
|
376
379
|
.pk-pipeline-summary.blocked{color:var(--bad);border-color:rgba(255,90,90,.4);}
|
|
380
|
+
.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
|
+
.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
|
+
.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)}
|
|
383
|
+
@media(max-width:720px){.pk-pipe-step{grid-template-columns:1.8rem 1fr}.pk-pipe-step .detail{grid-column:2;text-align:left}.pk-mic-health{grid-template-columns:1fr}.pk-mic-readings{grid-template-columns:1fr 1fr}.pk-motor-row{grid-template-columns:1fr 4.5rem}}
|
|
377
384
|
.pk-prodtoggle{display:flex;align-items:center;gap:0.5rem;margin-top:0.6rem;padding:0.45rem 0.6rem;border-radius:8px;border:1px solid var(--border-strong);background:var(--panel-2);cursor:pointer;}
|
|
378
385
|
.pk-prodtoggle input{width:15px;height:15px;accent-color:var(--gold-bright,var(--gold));cursor:pointer;}
|
|
379
386
|
.pk-prodtoggle span{font-family:var(--mono);font-size:0.66rem;color:var(--ink);}
|
|
@@ -4070,8 +4077,28 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4070
4077
|
];
|
|
4071
4078
|
// Pond / curved water feature at the lower-left of the plaza.
|
|
4072
4079
|
var POND={cx:0.18*15, cy:0.84*8, rx:2.4, ry:1.1};
|
|
4073
|
-
// Street traffic along the top road
|
|
4074
|
-
|
|
4080
|
+
// Street traffic along the top road. Each car has x, y (world), color,
|
|
4081
|
+
// a per-car speed (world units / second), a direction (+1 = right, -1 =
|
|
4082
|
+
// left), and an "oncoming" flag so the car sprite can face the right way
|
|
4083
|
+
// (headlight forward, taillight rear). y is one of two lane bands
|
|
4084
|
+
// (≈ -0.78 for outbound, ≈ -1.02 for inbound) so cars don't overlap.
|
|
4085
|
+
// When a car crosses either end of the road (x < -0.5 or x > PARK_W+0.5)
|
|
4086
|
+
// it wraps to the opposite side with a small y jitter so the flow
|
|
4087
|
+
// doesn't visibly reset.
|
|
4088
|
+
var ROAD_Y0=-1.5, ROAD_Y1=-0.35;
|
|
4089
|
+
var CARS=[
|
|
4090
|
+
{x:2.2, y:-0.78, color:'#f2c14e', speed:1.6, dir:+1},
|
|
4091
|
+
{x:5.6, y:-1.02, color:'#7fb4ff', speed:1.8, dir:-1},
|
|
4092
|
+
{x:7.4, y:-0.78, color:'#e0a23c', speed:1.4, dir:+1},
|
|
4093
|
+
{x:10.2, y:-1.02, color:'#ff7e6e', speed:2.0, dir:-1},
|
|
4094
|
+
{x:11.8, y:-0.78, color:'#d8d8e0', speed:1.5, dir:+1},
|
|
4095
|
+
{x:1.0, y:-1.02, color:'#5b8bd6', speed:1.7, dir:-1},
|
|
4096
|
+
{x:13.6, y:-0.78, color:'#c87ed4', speed:1.3, dir:+1},
|
|
4097
|
+
{x:8.4, y:-1.02, color:'#f2c14e', speed:1.9, dir:-1},
|
|
4098
|
+
];
|
|
4099
|
+
// Per-car phase offsets so the headlights / taillights don't all blink
|
|
4100
|
+
// in lockstep — gives a natural "moving at different speeds" feel.
|
|
4101
|
+
var _carPhase = CARS.map(function(_, i){ return i * 0.7; });
|
|
4075
4102
|
// ── Walking people — looping sprites that stroll along fixed plaza paths.
|
|
4076
4103
|
// Each walker is a small canvas-drawn figure (head + body + two
|
|
4077
4104
|
// swinging legs + swinging arms) with a vertical bob and a slight
|
|
@@ -4463,7 +4490,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4463
4490
|
if(fill){ X.fillStyle=fill; X.fill(); }
|
|
4464
4491
|
if(stroke){ X.strokeStyle=stroke; X.lineWidth=lw||1; X.stroke(); }
|
|
4465
4492
|
}
|
|
4466
|
-
function drawGround(){
|
|
4493
|
+
function drawGround(now){
|
|
4494
|
+
// now is optional — only the road cars and water effects need a
|
|
4495
|
+
// wall-clock. We pass performance.now() in from frame() so every
|
|
4496
|
+
// animation reads the same time.
|
|
4497
|
+
if(typeof now !== 'number') now = performance.now();
|
|
4498
|
+
var _now = now;
|
|
4467
4499
|
var z=cam.z;
|
|
4468
4500
|
// ── grass base — multi-stop radial sun-lit gradient on a slightly larger field ──
|
|
4469
4501
|
// Field is now sized to the plaza footprint + a small margin. The previous
|
|
@@ -4690,7 +4722,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4690
4722
|
poly([[0,-1.5],[PARK_W,-1.5],[PARK_W,-0.35],[0,-0.35]], 'rgba(26,26,32,0.92)', 'rgba(255,255,255,0.08)',1);
|
|
4691
4723
|
X.strokeStyle='rgba(232,210,120,0.55)'; X.setLineDash([9*z,9*z]);
|
|
4692
4724
|
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([]);
|
|
4693
|
-
|
|
4725
|
+
// animated road cars — positions updated each frame by tickCars()
|
|
4726
|
+
// draw order: outbound lane first (front), then inbound (back) for natural overlap
|
|
4727
|
+
for(var cai=0; cai<CARS.length; cai++){
|
|
4728
|
+
var car = CARS[cai];
|
|
4729
|
+
drawCar(car.x, car.y, car.color, car.dir, _carPhase[cai]||0, _now);
|
|
4730
|
+
}
|
|
4694
4731
|
// white event tent in the left grass strip (west of the plaza)
|
|
4695
4732
|
drawTent(-0.15, 0.5);
|
|
4696
4733
|
// ── lake — wider sandy shore with soft radial fade to grass ──
|
|
@@ -4743,7 +4780,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4743
4780
|
X.restore();
|
|
4744
4781
|
// ── animated ripple/highlight — thin white elliptical arcs that drift across the lake ──
|
|
4745
4782
|
var pondCent=iso(POND.cx, POND.cy);
|
|
4746
|
-
var ripplePhase=Math.sin(
|
|
4783
|
+
var ripplePhase=Math.sin(_now/1500);
|
|
4747
4784
|
X.save();
|
|
4748
4785
|
X.strokeStyle='rgba(232,248,252,0.7)'; X.lineWidth=1.0*z; X.lineCap='round';
|
|
4749
4786
|
for(var rk=0;rk<4;rk++){
|
|
@@ -4754,7 +4791,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4754
4791
|
var rCy2=POND.cy - 0.10 + rk*0.12;
|
|
4755
4792
|
var rA2=iso(rCx2-rxr2, rCy2);
|
|
4756
4793
|
var rB2=iso(rCx2+rxr2, rCy2);
|
|
4757
|
-
X.globalAlpha=0.35 + 0.4*Math.abs(Math.sin(
|
|
4794
|
+
X.globalAlpha=0.35 + 0.4*Math.abs(Math.sin(_now/900 + rk));
|
|
4758
4795
|
X.beginPath();
|
|
4759
4796
|
X.moveTo(rA2.x, rA2.y);
|
|
4760
4797
|
X.quadraticCurveTo(pondCent.x, rA2.y - 3*z, rB2.x, rB2.y);
|
|
@@ -4801,7 +4838,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4801
4838
|
// ── tiny fountain — 3 concentric rings of decreasing opacity around lake centroid ──
|
|
4802
4839
|
X.save();
|
|
4803
4840
|
for(var fk=0;fk<3;fk++){
|
|
4804
|
-
var fr=((fk+1)*0.18) + 0.06*Math.abs(Math.sin(
|
|
4841
|
+
var fr=((fk+1)*0.18) + 0.06*Math.abs(Math.sin(_now/600 + fk));
|
|
4805
4842
|
var falpha=0.55 - fk*0.18;
|
|
4806
4843
|
var fcol='rgba(220,240,248,'+falpha.toFixed(3)+')';
|
|
4807
4844
|
var fringPts=[];
|
|
@@ -4937,54 +4974,271 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4937
4974
|
X.fillStyle='rgba(232,72,72,0.95)';
|
|
4938
4975
|
X.beginPath(); X.moveTo(rdR.x,rdR.y-4*z); X.lineTo(rdR.x+2.5*z,rdR.y-3*z); X.lineTo(rdR.x,rdR.y-2*z); X.closePath(); X.fill();
|
|
4939
4976
|
}
|
|
4940
|
-
function drawCar(wx,wy,col){
|
|
4977
|
+
function drawCar(wx,wy,col,dir,phase,now){
|
|
4941
4978
|
var p=iso(wx,wy), z=cam.z;
|
|
4942
|
-
//
|
|
4979
|
+
// Optional animation params: dir (+1 right / -1 left), phase (per-car
|
|
4980
|
+
// offset in seconds), now (performance.now in ms). Falls back to the
|
|
4981
|
+
// static parked-car look when these are undefined.
|
|
4982
|
+
var moving = (dir===1 || dir===-1) && typeof now === 'number';
|
|
4983
|
+
// gentle vertical bob so a moving car looks like it has suspension
|
|
4984
|
+
var bob = moving ? Math.sin((now/180 + (phase||0)*1000)) * 0.5*z : 0;
|
|
4985
|
+
// soft drop shadow — slight motion blur for moving cars
|
|
4943
4986
|
X.fillStyle='rgba(0,0,0,0.32)';
|
|
4944
4987
|
X.beginPath(); X.ellipse(p.x,p.y+4*z,13*z,5.5*z,0,0,7); X.fill();
|
|
4988
|
+
if(moving){
|
|
4989
|
+
// ground shadow streak behind the car (opposite to direction of travel)
|
|
4990
|
+
var sx = p.x - dir*8*z;
|
|
4991
|
+
X.fillStyle='rgba(0,0,0,0.10)';
|
|
4992
|
+
X.beginPath(); X.ellipse(sx, p.y+4.5*z, 9*z, 4*z, 0, 0, 7); X.fill();
|
|
4993
|
+
// soft headlight beam projected on the road in front of the car
|
|
4994
|
+
var beamFrontX = p.x + dir*16*z;
|
|
4995
|
+
var beam = X.createRadialGradient(beamFrontX, p.y+1*z, 1*z, beamFrontX, p.y+1*z, 14*z);
|
|
4996
|
+
beam.addColorStop(0, 'rgba(255,242,200,0.32)');
|
|
4997
|
+
beam.addColorStop(0.5, 'rgba(255,242,200,0.10)');
|
|
4998
|
+
beam.addColorStop(1, 'rgba(255,242,200,0)');
|
|
4999
|
+
X.save();
|
|
5000
|
+
X.fillStyle = beam;
|
|
5001
|
+
X.beginPath(); X.ellipse(beamFrontX, p.y+1*z, 14*z, 6*z, 0, 0, Math.PI*2); X.fill();
|
|
5002
|
+
X.restore();
|
|
5003
|
+
}
|
|
4945
5004
|
// chassis (lower body)
|
|
4946
5005
|
X.fillStyle=col;
|
|
4947
|
-
X.beginPath(); X.roundRect(p.x-13*z,p.y-5*z,26*z,11*z,3*z); X.fill();
|
|
5006
|
+
X.beginPath(); X.roundRect(p.x-13*z,p.y-5*z+bob,26*z,11*z,3*z); X.fill();
|
|
4948
5007
|
// chassis bottom shade
|
|
4949
5008
|
X.fillStyle='rgba(0,0,0,0.22)';
|
|
4950
|
-
X.beginPath(); X.roundRect(p.x-13*z,p.y+2*z,26*z,4*z,2*z); X.fill();
|
|
5009
|
+
X.beginPath(); X.roundRect(p.x-13*z,p.y+2*z+bob,26*z,4*z,2*z); X.fill();
|
|
4951
5010
|
// roof (darker top)
|
|
4952
5011
|
X.fillStyle='rgba(0,0,0,0.55)';
|
|
4953
|
-
X.beginPath(); X.roundRect(p.x-8*z,p.y-10*z,16*z,6*z,2.5*z); X.fill();
|
|
5012
|
+
X.beginPath(); X.roundRect(p.x-8*z,p.y-10*z+bob,16*z,6*z,2.5*z); X.fill();
|
|
4954
5013
|
// roof tint with car color
|
|
4955
5014
|
X.fillStyle=col;
|
|
4956
5015
|
X.globalAlpha=0.6;
|
|
4957
|
-
X.beginPath(); X.roundRect(p.x-8*z,p.y-10*z,16*z,6*z,2.5*z); X.fill();
|
|
5016
|
+
X.beginPath(); X.roundRect(p.x-8*z,p.y-10*z+bob,16*z,6*z,2.5*z); X.fill();
|
|
4958
5017
|
X.globalAlpha=1;
|
|
4959
5018
|
// windshield highlight
|
|
4960
5019
|
X.fillStyle='rgba(180,220,255,0.65)';
|
|
4961
|
-
X.beginPath(); X.roundRect(p.x-6*z,p.y-9*z,12*z,3*z,1.5*z); X.fill();
|
|
5020
|
+
X.beginPath(); X.roundRect(p.x-6*z,p.y-9*z+bob,12*z,3*z,1.5*z); X.fill();
|
|
4962
5021
|
// windshield shine
|
|
4963
5022
|
X.fillStyle='rgba(255,255,255,0.45)';
|
|
4964
5023
|
X.beginPath();
|
|
4965
|
-
X.moveTo(p.x-5*z,p.y-9*z); X.lineTo(p.x-2*z,p.y-9*z); X.lineTo(p.x-4*z,p.y-7*z); X.closePath(); X.fill();
|
|
4966
|
-
// wheels
|
|
5024
|
+
X.moveTo(p.x-5*z,p.y-9*z+bob); X.lineTo(p.x-2*z,p.y-9*z+bob); X.lineTo(p.x-4*z,p.y-7*z+bob); X.closePath(); X.fill();
|
|
5025
|
+
// wheels — animated spokes when the car is moving (rotation = distance traveled)
|
|
4967
5026
|
X.fillStyle='#0c0c10';
|
|
4968
|
-
X.beginPath(); X.arc(p.x-8*z,p.y+5*z,2.6*z,0,7); X.fill();
|
|
4969
|
-
X.beginPath(); X.arc(p.x+8*z,p.y+5*z,2.6*z,0,7); X.fill();
|
|
4970
|
-
X.beginPath(); X.arc(p.x-8*z,p.y+6*z,2.6*z,0,7); X.fill();
|
|
4971
|
-
X.beginPath(); X.arc(p.x+8*z,p.y+6*z,2.6*z,0,7); X.fill();
|
|
4972
|
-
// wheel rims
|
|
5027
|
+
X.beginPath(); X.arc(p.x-8*z,p.y+5*z+bob,2.6*z,0,7); X.fill();
|
|
5028
|
+
X.beginPath(); X.arc(p.x+8*z,p.y+5*z+bob,2.6*z,0,7); X.fill();
|
|
5029
|
+
X.beginPath(); X.arc(p.x-8*z,p.y+6*z+bob,2.6*z,0,7); X.fill();
|
|
5030
|
+
X.beginPath(); X.arc(p.x+8*z,p.y+6*z+bob,2.6*z,0,7); X.fill();
|
|
5031
|
+
// wheel rims with a moving spoke mark so wheels visibly spin
|
|
4973
5032
|
X.fillStyle='rgba(200,200,210,0.75)';
|
|
4974
|
-
X.beginPath(); X.arc(p.x-8*z,p.y+5.5*z,1*z,0,7); X.fill();
|
|
4975
|
-
X.beginPath(); X.arc(p.x+8*z,p.y+5.5*z,1*z,0,7); X.fill();
|
|
4976
|
-
//
|
|
5033
|
+
X.beginPath(); X.arc(p.x-8*z,p.y+5.5*z+bob,1*z,0,7); X.fill();
|
|
5034
|
+
X.beginPath(); X.arc(p.x+8*z,p.y+5.5*z+bob,1*z,0,7); X.fill();
|
|
5035
|
+
// wheel spoke (rotates with speed) — gives the impression of motion
|
|
5036
|
+
if(moving){
|
|
5037
|
+
var spokeAng = (now/40 + (phase||0)*1000) % (Math.PI*2);
|
|
5038
|
+
var spokeDx = Math.cos(spokeAng) * 1.6*z;
|
|
5039
|
+
var spokeDy = Math.sin(spokeAng) * 1.6*z;
|
|
5040
|
+
X.strokeStyle='rgba(220,220,230,0.85)'; X.lineWidth=0.5*z;
|
|
5041
|
+
X.beginPath(); X.moveTo(p.x-8*z - spokeDx, p.y+5.5*z+bob - spokeDy); X.lineTo(p.x-8*z + spokeDx, p.y+5.5*z+bob + spokeDy); X.stroke();
|
|
5042
|
+
X.beginPath(); X.moveTo(p.x+8*z - spokeDx, p.y+5.5*z+bob - spokeDy); X.lineTo(p.x+8*z + spokeDx, p.y+5.5*z+bob + spokeDy); X.stroke();
|
|
5043
|
+
}
|
|
5044
|
+
// headlight (front — the side the car is driving toward)
|
|
5045
|
+
var frontX = p.x + (dir||1)*12*z;
|
|
4977
5046
|
X.fillStyle='rgba(255,240,180,0.95)';
|
|
4978
|
-
X.beginPath(); X.arc(
|
|
5047
|
+
X.beginPath(); X.arc(frontX, p.y-1*z+bob, 1.6*z, 0, 7); X.fill();
|
|
4979
5048
|
X.fillStyle='rgba(255,240,180,0.4)';
|
|
4980
|
-
X.beginPath(); X.arc(
|
|
4981
|
-
//
|
|
5049
|
+
X.beginPath(); X.arc(frontX, p.y-1*z+bob, 3*z, 0, 7); X.fill();
|
|
5050
|
+
// taillight (rear — opposite side, slightly redder)
|
|
5051
|
+
var rearX = p.x - (dir||1)*12*z;
|
|
4982
5052
|
X.fillStyle='rgba(255,80,80,0.85)';
|
|
4983
|
-
X.beginPath(); X.arc(
|
|
5053
|
+
X.beginPath(); X.arc(rearX, p.y-1*z+bob, 1.2*z, 0, 7); X.fill();
|
|
5054
|
+
if(moving){
|
|
5055
|
+
// a small red glow on the road behind the car
|
|
5056
|
+
X.fillStyle='rgba(255,80,80,0.25)';
|
|
5057
|
+
X.beginPath(); X.arc(rearX - dir*3*z, p.y+1*z+bob, 2.6*z, 0, 7); X.fill();
|
|
5058
|
+
}
|
|
4984
5059
|
// body highlight
|
|
4985
5060
|
X.fillStyle='rgba(255,255,255,0.18)';
|
|
4986
|
-
X.beginPath(); X.roundRect(p.x-12*z,p.y-4*z,24*z,1.5*z,1*z); X.fill();
|
|
5061
|
+
X.beginPath(); X.roundRect(p.x-12*z,p.y-4*z+bob,24*z,1.5*z,1*z); X.fill();
|
|
5062
|
+
}
|
|
5063
|
+
|
|
5064
|
+
// ── Atmosphere: sky, sun, distant mountains, clouds, haze ──
|
|
5065
|
+
// Painted on top of the cleared canvas and below the rest of the map
|
|
5066
|
+
// so the map looks like it sits in an actual world instead of on a
|
|
5067
|
+
// blank panel. Cheap to render (a handful of full-canvas fills + a
|
|
5068
|
+
// few silhouette paths + a small cloud set) and turns the whole
|
|
5069
|
+
// scene from "miniature on a black velvet" into "park at golden hour".
|
|
5070
|
+
//
|
|
5071
|
+
// Everything is drawn in screen space, not world space, because the
|
|
5072
|
+
// sky doesn't follow the iso projection — it's a backdrop, not a
|
|
5073
|
+
// surface. Distant mountains live just above the road horizon line,
|
|
5074
|
+
// the sun is fixed near the upper-right (a warm focal point), and a
|
|
5075
|
+
// handful of soft clouds drift slowly across the sky.
|
|
5076
|
+
function drawAtmosphere(now){
|
|
5077
|
+
if(!W || !H) return;
|
|
5078
|
+
var z = cam.z;
|
|
5079
|
+
|
|
5080
|
+
// ── sky gradient — warm late-afternoon palette (top to bottom) ──
|
|
5081
|
+
// Top: deep cobalt → mid: soft sky → low: warm gold haze. Anchored
|
|
5082
|
+
// to a virtual horizon line ~38% from the top of the stage so the
|
|
5083
|
+
// map always sits on the same horizon regardless of zoom.
|
|
5084
|
+
var horizon = H * 0.42;
|
|
5085
|
+
var sky = X.createLinearGradient(0, 0, 0, H);
|
|
5086
|
+
sky.addColorStop(0, 'rgba(46, 78, 132, 0.95)'); // deep sky
|
|
5087
|
+
sky.addColorStop(0.30, 'rgba(108, 152, 196, 0.85)'); // mid sky
|
|
5088
|
+
sky.addColorStop(0.46, 'rgba(214, 184, 138, 0.78)'); // warm horizon
|
|
5089
|
+
sky.addColorStop(0.55, 'rgba(244, 222, 168, 0.62)'); // soft golden glow
|
|
5090
|
+
sky.addColorStop(0.75, 'rgba(248, 232, 200, 0.20)'); // fades into the ground area
|
|
5091
|
+
sky.addColorStop(1, 'rgba(248, 232, 200, 0)');
|
|
5092
|
+
X.fillStyle = sky;
|
|
5093
|
+
X.fillRect(0, 0, W, H);
|
|
5094
|
+
|
|
5095
|
+
// ── sun — a soft warm disc with a wide glow halo ──
|
|
5096
|
+
// Anchored at ~78% across, 22% down so it reads as the off-screen
|
|
5097
|
+
// light source warming the whole scene. The glow pulses very
|
|
5098
|
+
// gently so it doesn't feel like a static sticker.
|
|
5099
|
+
var sunX = W * 0.78;
|
|
5100
|
+
var sunY = H * 0.22;
|
|
5101
|
+
var sunR = 28 * (0.95 + 0.05 * Math.sin(now/2400));
|
|
5102
|
+
// outer warm glow
|
|
5103
|
+
var glow = X.createRadialGradient(sunX, sunY, sunR*0.6, sunX, sunY, sunR*7);
|
|
5104
|
+
glow.addColorStop(0, 'rgba(255, 222, 140, 0.55)');
|
|
5105
|
+
glow.addColorStop(0.25,'rgba(255, 210, 130, 0.30)');
|
|
5106
|
+
glow.addColorStop(0.6, 'rgba(255, 200, 120, 0.08)');
|
|
5107
|
+
glow.addColorStop(1, 'rgba(255, 200, 120, 0)');
|
|
5108
|
+
X.fillStyle = glow;
|
|
5109
|
+
X.fillRect(0, 0, W, H);
|
|
5110
|
+
// sun disc
|
|
5111
|
+
var disc = X.createRadialGradient(sunX, sunY, sunR*0.2, sunX, sunY, sunR);
|
|
5112
|
+
disc.addColorStop(0, 'rgba(255, 250, 230, 1)');
|
|
5113
|
+
disc.addColorStop(0.6, 'rgba(255, 232, 170, 0.95)');
|
|
5114
|
+
disc.addColorStop(1, 'rgba(255, 210, 130, 0)');
|
|
5115
|
+
X.fillStyle = disc;
|
|
5116
|
+
X.beginPath(); X.arc(sunX, sunY, sunR, 0, Math.PI*2); X.fill();
|
|
5117
|
+
|
|
5118
|
+
// ── distant mountain silhouette (3 receding layers) ──
|
|
5119
|
+
// Each layer uses a slightly lighter color so the eye reads depth.
|
|
5120
|
+
// Anchored just below the horizon so the iso map sits in front of
|
|
5121
|
+
// them. Heights/shapes are deterministic so the silhouette doesn't
|
|
5122
|
+
// shimmer between frames.
|
|
5123
|
+
function mountainLayer(baseY, amp, segments, color, seedOff){
|
|
5124
|
+
X.fillStyle = color;
|
|
5125
|
+
X.beginPath();
|
|
5126
|
+
X.moveTo(0, H);
|
|
5127
|
+
// sample the silhouette using a sum of low-frequency sines
|
|
5128
|
+
for(var mx=0; mx<=W; mx+=8){
|
|
5129
|
+
var t = mx / W;
|
|
5130
|
+
var h = (
|
|
5131
|
+
Math.sin(t*6.3 + seedOff) * 0.5 +
|
|
5132
|
+
Math.sin(t*12.5 + seedOff*2.3) * 0.28 +
|
|
5133
|
+
Math.sin(t*22.1 + seedOff*0.7) * 0.14
|
|
5134
|
+
) * amp;
|
|
5135
|
+
X.lineTo(mx, baseY - h);
|
|
5136
|
+
}
|
|
5137
|
+
X.lineTo(W, H);
|
|
5138
|
+
X.closePath();
|
|
5139
|
+
X.fill();
|
|
5140
|
+
}
|
|
5141
|
+
// farther layer (lighter haze, lower)
|
|
5142
|
+
mountainLayer(H*0.46, 28, 18, 'rgba(120, 132, 158, 0.45)', 1.7);
|
|
5143
|
+
// middle layer
|
|
5144
|
+
mountainLayer(H*0.48, 36, 16, 'rgba(86, 100, 124, 0.55)', 3.1);
|
|
5145
|
+
// closer ridge (darkest, in front of the plaza)
|
|
5146
|
+
mountainLayer(H*0.50, 22, 14, 'rgba(54, 68, 86, 0.60)', 0.4);
|
|
5147
|
+
|
|
5148
|
+
// ── soft cloud band — 5 elongated ellipses drifting right ──
|
|
5149
|
+
// Drawn as flat white ellipses with low alpha so they look like
|
|
5150
|
+
// wisps, not puffs. Each cloud has its own period so they don't
|
|
5151
|
+
// march in lockstep.
|
|
5152
|
+
var cloudColors = [
|
|
5153
|
+
'rgba(255,255,255,0.55)',
|
|
5154
|
+
'rgba(255,250,240,0.45)',
|
|
5155
|
+
'rgba(248,242,228,0.40)',
|
|
5156
|
+
'rgba(255,250,235,0.35)',
|
|
5157
|
+
'rgba(252,246,232,0.50)',
|
|
5158
|
+
];
|
|
5159
|
+
for(var ci=0; ci<cloudColors.length; ci++){
|
|
5160
|
+
var period = 90 + ci*22; // seconds for a full pass
|
|
5161
|
+
var drift = ((now/1000) / period) % 1; // 0..1
|
|
5162
|
+
var cx = (drift * (W + 240)) - 120; // wrap from -120 to W+120
|
|
5163
|
+
var cy = H*0.10 + ci*16;
|
|
5164
|
+
var rx = 60 + ci*8;
|
|
5165
|
+
var ry = 10 + (ci%3)*3;
|
|
5166
|
+
X.fillStyle = cloudColors[ci];
|
|
5167
|
+
// fluffy edge: 3 overlapping ellipses
|
|
5168
|
+
X.beginPath(); X.ellipse(cx, cy, rx, ry, 0, 0, Math.PI*2); X.fill();
|
|
5169
|
+
X.beginPath(); X.ellipse(cx - rx*0.5, cy + ry*0.4, rx*0.7, ry*0.7, 0, 0, Math.PI*2); X.fill();
|
|
5170
|
+
X.beginPath(); X.ellipse(cx + rx*0.5, cy + ry*0.3, rx*0.7, ry*0.6, 0, 0, Math.PI*2); X.fill();
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
// ── soft warm light wash on the upper third of the map ──
|
|
5174
|
+
// Sits in screen space too: a wide radial glow over the entire
|
|
5175
|
+
// map area that makes the grass + plaza read as if lit from the
|
|
5176
|
+
// direction of the sun (upper-right). Without this the map looks
|
|
5177
|
+
// like it was cut out and pasted in.
|
|
5178
|
+
var wash = X.createRadialGradient(sunX, sunY*0.7, 40, sunX, sunY*0.7, Math.max(W, H)*0.85);
|
|
5179
|
+
wash.addColorStop(0, 'rgba(255, 220, 150, 0.16)');
|
|
5180
|
+
wash.addColorStop(0.35, 'rgba(255, 200, 130, 0.06)');
|
|
5181
|
+
wash.addColorStop(1, 'rgba(255, 200, 130, 0)');
|
|
5182
|
+
X.fillStyle = wash;
|
|
5183
|
+
X.fillRect(0, 0, W, H);
|
|
5184
|
+
|
|
5185
|
+
// ── subtle haze band along the horizon ──
|
|
5186
|
+
// Sits just above the road so the cars pop against a soft
|
|
5187
|
+
// horizon-glow rather than a hard line.
|
|
5188
|
+
var haze = X.createLinearGradient(0, horizon - 30, 0, horizon + 50);
|
|
5189
|
+
haze.addColorStop(0, 'rgba(255, 220, 160, 0)');
|
|
5190
|
+
haze.addColorStop(0.5, 'rgba(255, 220, 160, 0.22)');
|
|
5191
|
+
haze.addColorStop(1, 'rgba(255, 220, 160, 0)');
|
|
5192
|
+
X.fillStyle = haze;
|
|
5193
|
+
X.fillRect(0, horizon - 30, W, 80);
|
|
5194
|
+
|
|
5195
|
+
// ── ambient particles — drifting motes / pollen / fireflies ──
|
|
5196
|
+
// 40 tiny dots that float across the scene. Each one is held in
|
|
5197
|
+
// an array; the array is seeded once (so they don't all start at
|
|
5198
|
+
// the same position) and advanced by a tiny per-frame velocity.
|
|
5199
|
+
// On first call we lazily seed it; on resize we re-distribute.
|
|
5200
|
+
if(!_atmos.particles){
|
|
5201
|
+
_atmos.particles = [];
|
|
5202
|
+
for(var pi=0; pi<40; pi++){
|
|
5203
|
+
_atmos.particles.push({
|
|
5204
|
+
x: Math.random()*W,
|
|
5205
|
+
y: H*0.20 + Math.random()*H*0.55,
|
|
5206
|
+
r: 0.8 + Math.random()*1.6,
|
|
5207
|
+
vx: -6 + Math.random()*12, // px/sec, mostly right
|
|
5208
|
+
vy: -2 + Math.random()*4, // slight up/down sway
|
|
5209
|
+
phase: Math.random()*Math.PI*2,
|
|
5210
|
+
twinkle: 0.5 + Math.random()*0.5, // brightness amplitude
|
|
5211
|
+
hue: Math.random() < 0.35 ? 'warm' : 'cool', // mix of gold + silver
|
|
5212
|
+
});
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
var dt = _atmos.lastNow ? Math.min(64, now - _atmos.lastNow)/1000 : 0.016;
|
|
5216
|
+
_atmos.lastNow = now;
|
|
5217
|
+
for(var pj=0; pj<_atmos.particles.length; pj++){
|
|
5218
|
+
var pt = _atmos.particles[pj];
|
|
5219
|
+
// simple wrap
|
|
5220
|
+
pt.x += pt.vx * dt;
|
|
5221
|
+
pt.y += Math.sin(now/1200 + pt.phase) * 0.3;
|
|
5222
|
+
if(pt.x > W + 8) pt.x = -8;
|
|
5223
|
+
if(pt.x < -8) pt.x = W + 8;
|
|
5224
|
+
if(pt.y < H*0.10) pt.y = H*0.75;
|
|
5225
|
+
if(pt.y > H*0.80) pt.y = H*0.15;
|
|
5226
|
+
// soft twinkle alpha
|
|
5227
|
+
var a = 0.25 + 0.45 * Math.abs(Math.sin(now/700 + pt.phase)) * pt.twinkle;
|
|
5228
|
+
X.fillStyle = pt.hue === 'warm'
|
|
5229
|
+
? 'rgba(255, 230, 160, '+a.toFixed(3)+')'
|
|
5230
|
+
: 'rgba(220, 235, 250, '+a.toFixed(3)+')';
|
|
5231
|
+
X.beginPath(); X.arc(pt.x, pt.y, pt.r, 0, Math.PI*2); X.fill();
|
|
5232
|
+
// tiny halo
|
|
5233
|
+
X.fillStyle = pt.hue === 'warm'
|
|
5234
|
+
? 'rgba(255, 220, 140, '+(a*0.35).toFixed(3)+')'
|
|
5235
|
+
: 'rgba(200, 220, 245, '+(a*0.30).toFixed(3)+')';
|
|
5236
|
+
X.beginPath(); X.arc(pt.x, pt.y, pt.r*2.5, 0, Math.PI*2); X.fill();
|
|
5237
|
+
}
|
|
4987
5238
|
}
|
|
5239
|
+
// Holds the persistent atmospheric state (particle positions + last tick time).
|
|
5240
|
+
var _atmos = { particles: null, lastNow: 0 };
|
|
5241
|
+
|
|
4988
5242
|
// Sun-shade pavilion: 4 thin white poles supporting a flat white square canopy.
|
|
4989
5243
|
function drawPavilion(wx,wy){
|
|
4990
5244
|
var p=iso(wx,wy), z=cam.z;
|
|
@@ -5839,6 +6093,26 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5839
6093
|
}
|
|
5840
6094
|
}
|
|
5841
6095
|
}
|
|
6096
|
+
// Cars share the same elapsed-dt clock so a tab-switch doesn't
|
|
6097
|
+
// catapult every car off the road at once. speed is world units
|
|
6098
|
+
// per second, dt is milliseconds.
|
|
6099
|
+
var cdt = Math.min(64, dt);
|
|
6100
|
+
for(var ci=0; ci<CARS.length; ci++){
|
|
6101
|
+
var car = CARS[ci];
|
|
6102
|
+
car.x += car.dir * car.speed * (cdt/1000);
|
|
6103
|
+
// Wrap the car around the road. Add a small y-jitter so the
|
|
6104
|
+
// re-entry point doesn't visibly snap.
|
|
6105
|
+
if(car.dir > 0 && car.x > PARK_W + 0.5){
|
|
6106
|
+
car.x = -0.5;
|
|
6107
|
+
// bounce across the centerline by switching y + dir
|
|
6108
|
+
car.y = (car.y < -0.9) ? -0.78 : -1.02;
|
|
6109
|
+
car.dir = (car.y < -0.9) ? +1 : -1;
|
|
6110
|
+
} else if(car.dir < 0 && car.x < -0.5){
|
|
6111
|
+
car.x = PARK_W + 0.5;
|
|
6112
|
+
car.y = (car.y < -0.9) ? -0.78 : -1.02;
|
|
6113
|
+
car.dir = (car.y < -0.9) ? +1 : -1;
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
5842
6116
|
}
|
|
5843
6117
|
function walkerPos(w){
|
|
5844
6118
|
// Decode w.distance (in units of "segments") into a (seg, t) so
|
|
@@ -5952,8 +6226,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5952
6226
|
if(!running) return;
|
|
5953
6227
|
var now = performance.now();
|
|
5954
6228
|
X.clearRect(0,0,W,H);
|
|
5955
|
-
|
|
5956
|
-
//
|
|
6229
|
+
// ── atmosphere: sky + sun + clouds + distant mountains + haze ──
|
|
6230
|
+
// Painted first, behind everything else, so the rest of the map
|
|
6231
|
+
// sits on top of an actual horizon instead of on the empty page
|
|
6232
|
+
// background. Drawn every frame so the sun and clouds drift even
|
|
6233
|
+
// when the rest of the scene is static.
|
|
6234
|
+
drawAtmosphere(now);
|
|
6235
|
+
drawGround(now);
|
|
6236
|
+
// step the looping walkers (and road cars) along their paths
|
|
5957
6237
|
tickWalkers(now);
|
|
5958
6238
|
// Re-derive status every frame so the "ended" countdown (a wall-
|
|
5959
6239
|
// clock timer) decrements smoothly between telemetry polls (3s).
|
|
@@ -6721,6 +7001,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6721
7001
|
+ '<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>'
|
|
6722
7002
|
+ '<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>'
|
|
6723
7003
|
+ '<div class="pk-pipeline" id="pk-pipeline"></div>')
|
|
7004
|
+
+ sect('Production run history','<div class="pk-pipe-history" style="margin-top:0;padding-top:0;border-top:0"><div class="pk-pipe-history-head"><b>Persistent sessions</b><div class="pk-history-pager"><button class="rp-btn" id="pk-history-prev">Prev</button><span id="pk-history-page">page 1</span><button class="rp-btn" id="pk-history-next">Next</button></div></div><div class="pk-pipe-history-list" id="pk-pipeline-history"><div class="pk-note">Loading persistent sessions...</div></div></div>')
|
|
6724
7005
|
+ sect('🎙 microphone','<div class="pk-meter"><span class="lab">input</span><div class="pk-bars" id="pk-micbars"></div><span class="pk-val" id="pk-micval">—</span></div><div class="pk-note" id="pk-micstate">awaiting robot audio plugin</div><div class="pk-ctl" style="margin-top:.5rem"><button class="rp-btn" id="pk-mic-listen">Listen in browser</button><label class="pk-note" style="display:flex;align-items:center;gap:.35rem;margin:0">volume <input id="pk-mic-volume" type="range" min="0" max="100" value="80" style="width:90px"></label></div><div class="pk-note" id="pk-mic-listen-note">Muted by default. Use headphones to prevent acoustic feedback.</div>')
|
|
6725
7006
|
+ sect('🔊 speaker','<canvas class="pk-wave" id="pk-spkwave" width="360" height="32"></canvas><div class="pk-meter"><span class="lab">output</span><div class="pk-bars" id="pk-spkbars"></div><span class="pk-val" id="pk-spkval">—</span></div><div class="pk-note" id="pk-spkstate">TTS plays on the robot speaker — use “Test speaker” below to round-trip a tone and confirm the audio path</div>'
|
|
6726
7007
|
+ '<div class="rp-form" id="pk-spk-test" style="grid-template-columns:1fr 1fr;gap:0.4rem;margin-top:0.55rem;padding:0.55rem;border-color:var(--border-strong);">'
|
|
@@ -6743,6 +7024,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6743
7024
|
+ sect('🩺 robot services','<div id="pk-svc-list" class="pk-svc-list"><div class="pk-note">waiting for supervisor report…</div></div><div class="pk-note" id="pk-svc-age" style="margin-top:0.35rem;"></div><div class="rp-field" style="margin-top:0.55rem;"><label>Quick run (on the robot, from packages/robopark/scheduler)</label><input id="pk-svc-clicmd" readonly value="python robot_supervisor.py"></div><button class="rp-btn" id="pk-svc-clicmd-copy" style="margin-top:0.35rem;">📋 Copy command</button>')
|
|
6744
7025
|
+ buildShellSection(schedId(n))
|
|
6745
7026
|
+ sect('🎭 character / voice','<div class="pk-note" id="pk-eff-note" style="margin-bottom:0.45rem;">loading effective config…</div><div class="rp-field"><label>Character preset</label><select class="rp-char" id="pk-eff-char" data-robot="'+esc(schedId(n))+'">'+effPresetOptions+'</select></div><div class="rp-field"><label>Voice stack</label><select class="rp-voice" id="pk-eff-voice" data-robot="'+esc(schedId(n))+'">'+effVoiceOptions+'</select></div><div class="rp-config-actions" style="margin-top:.45rem"><button class="rp-btn primary" id="pk-eff-save" data-robot="'+esc(schedId(n))+'">Apply to robot</button><button class="rp-btn" id="pk-edit-character">Edit character</button><button class="rp-btn" id="pk-edit-stack">Edit stack</button><button class="rp-btn" id="pk-new-stack">New stack</button></div>');
|
|
7027
|
+
b.innerHTML += sect('Microphone setup suite','<div class="pk-mic-suite"><div class="pk-mic-health" id="pk-mic-health"><div class="pk-mic-check"><div class="k">Hardware lock</div><div class="v">USB mic hw:3,0</div></div><div class="pk-mic-check"><div class="k">PCM capture</div><div class="v">not tested</div></div><div class="pk-mic-check"><div class="k">Production track</div><div class="v">waiting</div></div></div><div class="rp-form" style="grid-template-columns:1fr 1fr;gap:.4rem"><div class="rp-field"><label>Ground-truth capture</label><select id="pk-mic-duration"><option value="2">2 seconds</option><option value="4" selected>4 seconds</option><option value="8">8 seconds</option></select></div><div class="rp-field"><label>Locked input</label><div class="pk-note" style="margin:.45rem 0 0">USB microphone (plughw:3,0)</div></div></div><div class="pk-ctl"><button class="rp-btn primary" id="pk-mic-groundtruth">Run raw mic capture</button><button class="rp-btn" id="pk-mic-roundtrip">Run speaker + mic test</button><button class="rp-btn" id="pk-mic-apply">Reapply production profile</button></div><div class="pk-note" id="pk-mic-test-state">Raw capture bypasses LiveKit and tests the exact ALSA path. Round-trip then verifies the physical speaker-to-mic path.</div><div id="pk-mic-test-result"></div></div>');
|
|
7028
|
+
b.innerHTML += sect('Motor registry + sequence canvas','<div class="rp-field"><label>Robot-local motor server</label><input id="pk-motor-url" value="http://127.0.0.1:8001" placeholder="http://127.0.0.1:8001"></div><div class="pk-motor-registry" id="pk-motor-registry"></div><button class="rp-btn" id="pk-motor-add" style="margin-top:.4rem">Add registered motor</button><div class="rp-form" style="grid-template-columns:1fr 1fr;margin-top:.7rem"><div class="rp-field"><label>Saved sequence</label><select id="pk-motor-sequence"></select></div><div class="rp-field"><label>Sequence name</label><input id="pk-motor-sequence-name" placeholder="Greeting motion"></div></div><div class="pk-motor-canvas" id="pk-motor-canvas"></div><div class="pk-ctl"><button class="rp-btn" id="pk-motor-step-add">Add timed node</button><button class="rp-btn primary" id="pk-motor-save">Save motor profile</button><button class="rp-btn" id="pk-motor-run">Run selected sequence</button></div><label class="pk-prodtoggle"><input type="checkbox" id="pk-motor-greeting"><span>Run this sequence automatically with the greeting pipeline</span></label><div class="pk-motor-status" id="pk-motor-status">Loading motor profile...</div>');
|
|
6746
7029
|
b.innerHTML += sect('Production camera + audio','<div class="pk-note" id="pk-device-search" style="margin-bottom:.55rem">Hardware locked for launch. Camera, microphone, and speaker cannot drift between restarts.</div><div class="rp-field"><label>Camera input</label><select id="pk-video-device" disabled></select></div><div class="rp-field"><label>Microphone input</label><select id="pk-audio-input" disabled></select></div><div class="rp-field"><label>Speaker output</label><select id="pk-audio-output" disabled></select></div><button class="rp-btn primary" id="pk-device-test-speaker" style="margin-top:0.35rem">Test USB speaker now</button><div class="rp-field"><label>Preset greeting phrases <span class="pk-note">one per line, cached per voice</span></label><textarea id="pk-greeting-phrases" rows="3" placeholder="Hi, I am Jaguar! / Welcome to RoboPark."></textarea></div><button class="rp-btn" id="pk-device-save" style="margin-top:0.35rem">Save greetings + enforce hardware</button><div class="pk-note" id="pk-device-note">waiting for robot heartbeat inventory</div>');
|
|
6747
7030
|
// Put live hardware selection beside the camera and audio tests,
|
|
6748
7031
|
// ahead of persona and service administration in the Park drawer.
|
|
@@ -6765,6 +7048,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6765
7048
|
});
|
|
6766
7049
|
var micListen=$('pk-mic-listen');if(micListen)micListen.addEventListener('click',function(){setMicMonitorAudible(!micMonitorAudible);});
|
|
6767
7050
|
var micVolume=$('pk-mic-volume');if(micVolume)micVolume.addEventListener('input',function(){micMonitorVolume=Math.max(0,Math.min(1,Number(micVolume.value||80)/100));if(micMonitorGain)micMonitorGain.gain.value=micMonitorAudible?micMonitorVolume:0;});
|
|
7051
|
+
var micGround=$('pk-mic-groundtruth');if(micGround)micGround.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}runMicGroundtruth(n);});
|
|
7052
|
+
var micRound=$('pk-mic-roundtrip');if(micRound)micRound.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}runMicRoundtrip(n);});
|
|
7053
|
+
var micApply=$('pk-mic-apply');if(micApply)micApply.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}saveDeviceSelectors(n);});
|
|
7054
|
+
var motorAdd=$('pk-motor-add');if(motorAdd)motorAdd.addEventListener('click',function(){motorEditorAddMotor();});
|
|
7055
|
+
var motorStep=$('pk-motor-step-add');if(motorStep)motorStep.addEventListener('click',function(){motorEditorAddStep();});
|
|
7056
|
+
var motorSave=$('pk-motor-save');if(motorSave)motorSave.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}saveMotorProfile(n);});
|
|
7057
|
+
var motorRun=$('pk-motor-run');if(motorRun)motorRun.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}runMotorSequence(n);});
|
|
7058
|
+
var motorSeq=$('pk-motor-sequence');if(motorSeq)motorSeq.addEventListener('change',function(){selectMotorSequence(motorSeq.value);});
|
|
7059
|
+
var motorRegistry=$('pk-motor-registry');if(motorRegistry)motorRegistry.addEventListener('click',function(ev){var button=ev.target.closest('[data-motor-action]');if(!button)return;var index=Number(button.dataset.index);if(button.dataset.motorAction==='remove'){motorProfile.registry.splice(index,1);renderMotorEditor();}else if(button.dataset.motorAction==='test'){testMotorRelay(n,index);}});
|
|
7060
|
+
var motorCanvas=$('pk-motor-canvas');if(motorCanvas)motorCanvas.addEventListener('click',function(ev){var button=ev.target.closest('[data-step-remove]');if(button){captureMotorEditor();motorEditorSequence().steps.splice(Number(button.dataset.stepRemove),1);renderMotorEditor();}});
|
|
7061
|
+
loadMotorProfile(n);
|
|
6768
7062
|
startCam(n); startMeters(n); loadEffectiveConfig(schedId(n)); refreshSupervisorPanel(n);
|
|
6769
7063
|
_bindShellSection(n);
|
|
6770
7064
|
_spkTestRenderHistory(schedId(n));
|
|
@@ -6792,6 +7086,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6792
7086
|
var pipelineStart=$('pk-pipeline-start'); if(pipelineStart) pipelineStart.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } startPipelineTest(n); });
|
|
6793
7087
|
var unattended=$('pk-unattended-test'); if(unattended) unattended.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runUnattendedReadiness(n); });
|
|
6794
7088
|
var pipelineStop=$('pk-pipeline-stop'); if(pipelineStop) pipelineStop.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } stopPipelineTest(n); });
|
|
7089
|
+
var historyPrev=$('pk-history-prev');if(historyPrev)historyPrev.addEventListener('click',function(){pipelineHistoryPage=Math.max(1,pipelineHistoryPage-1);loadPipelineHistory(n);});
|
|
7090
|
+
var historyNext=$('pk-history-next');if(historyNext)historyNext.addEventListener('click',function(){pipelineHistoryPage+=1;loadPipelineHistory(n);});
|
|
7091
|
+
var historyList=$('pk-pipeline-history');if(historyList)historyList.addEventListener('click',function(ev){var card=ev.target.closest('.pk-run-card');if(card)openSessionDetailModal({session_id:card.dataset.session});});
|
|
7092
|
+
pipelineHistoryPage=1;
|
|
7093
|
+
loadPipelineHistory(n);
|
|
6795
7094
|
pollPipeline(n);
|
|
6796
7095
|
if(pipelineTimer) clearInterval(pipelineTimer);
|
|
6797
7096
|
pipelineTimer=setInterval(function(){ if(drawerId===n.id) pollPipeline(n); }, 1500);
|
|
@@ -6806,12 +7105,82 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6806
7105
|
refreshDrawer();
|
|
6807
7106
|
}
|
|
6808
7107
|
|
|
7108
|
+
var motorProfile={registry:[],sequences:[],greeting_sequence_id:null,motor_server_url:'http://127.0.0.1:8001'};
|
|
7109
|
+
var motorSequenceId='';
|
|
7110
|
+
function motorEditorSequence(){
|
|
7111
|
+
var sequence=(motorProfile.sequences||[]).find(function(item){return item.id===motorSequenceId;});
|
|
7112
|
+
if(!sequence){sequence={id:motorSequenceId||('sequence-'+Date.now()),name:'New sequence',steps:[]};motorSequenceId=sequence.id;motorProfile.sequences.push(sequence);}
|
|
7113
|
+
if(!Array.isArray(sequence.steps))sequence.steps=[];
|
|
7114
|
+
return sequence;
|
|
7115
|
+
}
|
|
7116
|
+
function captureMotorEditor(){
|
|
7117
|
+
var rows=document.querySelectorAll('#pk-motor-registry .pk-motor-row');
|
|
7118
|
+
if(rows.length)motorProfile.registry=Array.prototype.map.call(rows,function(row,index){
|
|
7119
|
+
var fields=row.querySelectorAll('input');
|
|
7120
|
+
return {id:slugify(fields[0].value)||('motor-'+(index+1)),name:fields[1].value.trim()||('Motor '+(index+1)),gpio:Number(fields[2].value),active_high:fields[3].checked,max_duration_ms:Number(fields[4].value)||3000};
|
|
7121
|
+
});
|
|
7122
|
+
var sequence=(motorProfile.sequences||[]).find(function(item){return item.id===motorSequenceId;});
|
|
7123
|
+
if(sequence){
|
|
7124
|
+
var name=$('pk-motor-sequence-name');if(name)sequence.name=name.value.trim()||sequence.name||'Sequence';
|
|
7125
|
+
sequence.steps=Array.prototype.map.call(document.querySelectorAll('#pk-motor-canvas .pk-motor-node'),function(node){
|
|
7126
|
+
return {motor_id:node.querySelector('[data-step-motor]').value,delay_ms:Number(node.querySelector('[data-step-delay]').value)||0,duration_ms:Number(node.querySelector('[data-step-duration]').value)||300};
|
|
7127
|
+
});
|
|
7128
|
+
var greeting=$('pk-motor-greeting');if(greeting)motorProfile.greeting_sequence_id=greeting.checked?sequence.id:(motorProfile.greeting_sequence_id===sequence.id?null:motorProfile.greeting_sequence_id);
|
|
7129
|
+
}
|
|
7130
|
+
var url=$('pk-motor-url');if(url)motorProfile.motor_server_url=url.value.trim()||'http://127.0.0.1:8001';
|
|
7131
|
+
}
|
|
7132
|
+
function renderMotorEditor(){
|
|
7133
|
+
var registry=$('pk-motor-registry'),canvas=$('pk-motor-canvas'),select=$('pk-motor-sequence');if(!registry||!canvas||!select)return;
|
|
7134
|
+
registry.innerHTML=(motorProfile.registry||[]).map(function(motor,index){return '<div class="pk-motor-row"><input value="'+esc(motor.id)+'" aria-label="Motor id"><input value="'+esc(motor.name)+'" aria-label="Motor name"><input type="number" min="2" max="27" value="'+esc(motor.gpio)+'" aria-label="BCM GPIO"><label title="Active high"><input type="checkbox" '+(motor.active_high!==false?'checked':'')+'> high</label><input type="number" min="50" max="10000" step="50" value="'+esc(motor.max_duration_ms||3000)+'" aria-label="Max pulse milliseconds"><button class="rp-btn" data-motor-action="test" data-index="'+index+'">Test</button><button class="rp-btn danger" data-motor-action="remove" data-index="'+index+'">Remove</button></div>';}).join('')||'<div class="pk-note">No relays registered. Add only wired BCM outputs.</div>';
|
|
7135
|
+
select.innerHTML=(motorProfile.sequences||[]).map(function(sequence){return '<option value="'+esc(sequence.id)+'" '+(sequence.id===motorSequenceId?'selected':'')+'>'+esc(sequence.name||sequence.id)+'</option>';}).join('')+'<option value="__new__">+ New sequence</option>';
|
|
7136
|
+
var sequence=motorEditorSequence(),name=$('pk-motor-sequence-name'),greeting=$('pk-motor-greeting'),url=$('pk-motor-url');
|
|
7137
|
+
if(name)name.value=sequence.name||'';if(greeting)greeting.checked=motorProfile.greeting_sequence_id===sequence.id;if(url)url.value=motorProfile.motor_server_url||'http://127.0.0.1:8001';
|
|
7138
|
+
var motorOptions=(motorProfile.registry||[]).map(function(motor){return '<option value="'+esc(motor.id)+'">'+esc(motor.name+' | BCM '+motor.gpio)+'</option>';}).join('');
|
|
7139
|
+
canvas.innerHTML=(sequence.steps||[]).map(function(step,index){return '<div class="pk-motor-node"><div class="pk-motor-node-num">'+(index+1)+'</div><label>Relay<select data-step-motor>'+motorOptions.replace('value="'+esc(step.motor_id)+'"','value="'+esc(step.motor_id)+'" selected')+'</select></label><label>Delay ms<input data-step-delay type="number" min="0" max="30000" step="50" value="'+esc(step.delay_ms||0)+'"></label><label>Pulse ms<input data-step-duration type="number" min="50" max="10000" step="50" value="'+esc(step.duration_ms||300)+'"></label><button class="rp-btn danger" data-step-remove="'+index+'">Remove</button></div>';}).join('')||'<div class="pk-note">No timed nodes. Add a node to build the sequence.</div>';
|
|
7140
|
+
}
|
|
7141
|
+
function selectMotorSequence(id){
|
|
7142
|
+
captureMotorEditor();
|
|
7143
|
+
if(id==='__new__'){var sequence={id:'sequence-'+Date.now(),name:'New sequence',steps:[]};motorProfile.sequences.push(sequence);motorSequenceId=sequence.id;}else motorSequenceId=id;
|
|
7144
|
+
renderMotorEditor();
|
|
7145
|
+
}
|
|
7146
|
+
function motorEditorAddMotor(){captureMotorEditor();var number=motorProfile.registry.length+1;motorProfile.registry.push({id:'motor-'+number,name:'Motor '+number,gpio:Math.min(27,Math.max(2,number+4)),active_high:true,max_duration_ms:3000});renderMotorEditor();}
|
|
7147
|
+
function motorEditorAddStep(){captureMotorEditor();if(!motorProfile.registry.length){motorStatus('Register a motor before adding a timed node','bad');return;}motorEditorSequence().steps.push({motor_id:motorProfile.registry[0].id,delay_ms:0,duration_ms:300});renderMotorEditor();}
|
|
7148
|
+
function motorStatus(message,kind){var host=$('pk-motor-status');if(host){host.textContent=message;host.className='pk-motor-status '+(kind||'');}}
|
|
7149
|
+
function loadMotorProfile(n){
|
|
7150
|
+
var robotId=schedId(n);motorStatus('Loading saved motor profile...');
|
|
7151
|
+
getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');}).catch(function(error){motorStatus('Motor profile unavailable: '+error.message,'bad');});
|
|
7152
|
+
}
|
|
7153
|
+
function saveMotorProfile(n){
|
|
7154
|
+
captureMotorEditor();var robotId=schedId(n),url=motorProfile.motor_server_url;
|
|
7155
|
+
var parsedUrl=null;try{parsedUrl=new URL(url);}catch(e){}
|
|
7156
|
+
if(!parsedUrl||parsedUrl.protocol!=='http:'||(parsedUrl.hostname!=='127.0.0.1'&&parsedUrl.hostname!=='localhost')||!parsedUrl.port){motorStatus('Motor server must be robot-local: http://127.0.0.1:PORT','bad');return Promise.reject(new Error('motor server must be localhost'));}
|
|
7157
|
+
motorStatus('Validating and saving motor profile...');
|
|
7158
|
+
return rpAction('PATCH','/robopark/api/devices/'+encodeURIComponent(robotId),{motor_server_url:url}).then(function(){return rpAction('PUT','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile',{registry:motorProfile.registry,sequences:motorProfile.sequences,greeting_sequence_id:motorProfile.greeting_sequence_id});}).then(function(data){motorProfile.registry=data.registry||[];motorProfile.sequences=data.sequences||[];motorProfile.greeting_sequence_id=data.greeting_sequence_id||null;renderMotorEditor();motorStatus('Profile saved. Greeting motion will start on first TTS playback.','ok');return data;}).catch(function(error){motorStatus('Save failed: '+error.message,'bad');throw error;});
|
|
7159
|
+
}
|
|
7160
|
+
function runMotorSequence(n){
|
|
7161
|
+
var robotId=schedId(n),sequence=motorEditorSequence();motorStatus('Saving and queueing '+sequence.name+'...');
|
|
7162
|
+
saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-sequences/'+encodeURIComponent(sequence.id)+'/run',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','motor-sequence');}).then(function(result){motorStatus(result.ok?'Sequence completed on robot.':'Sequence failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');}).catch(function(error){motorStatus('Run failed: '+error.message,'bad');});
|
|
7163
|
+
}
|
|
7164
|
+
function testMotorRelay(n,index){
|
|
7165
|
+
captureMotorEditor();var motor=motorProfile.registry[index];if(!motor)return;var robotId=schedId(n);motorStatus('Saving profile before safe 300 ms '+motor.name+' test...');
|
|
7166
|
+
saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/'+encodeURIComponent(motor.id)+'/test?duration_ms=300',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','motor-test');}).then(function(result){motorStatus(result.ok?motor.name+' relay test completed.':'Relay test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');}).catch(function(error){motorStatus('Relay test failed: '+error.message,'bad');});
|
|
7167
|
+
}
|
|
7168
|
+
|
|
7169
|
+
var pipelineHistoryPage=1;
|
|
6809
7170
|
var PIPELINE_LABELS={
|
|
6810
7171
|
robot_online:'Robot online',camera_ready:'Camera detected',microphone_ready:'Microphone detected',speaker_ready:'Speaker detected',
|
|
6811
7172
|
motion_detected:'Motion received',scheduler_session:'Scheduler session',livekit_join:'LiveKit joined',camera_published:'Camera published',
|
|
6812
7173
|
microphone_published:'Microphone published',voice_worker:'Voice worker',stt_listening:'STT listening',llm_response:'LLM response',
|
|
6813
|
-
tts_subscribed:'TTS subscribed',playback_started:'Robot playback',session_ended:'Session ended'
|
|
7174
|
+
tts_subscribed:'TTS subscribed',playback_started:'Robot playback',motor_sequence:'Greeting motion',session_ended:'Session ended'
|
|
6814
7175
|
};
|
|
7176
|
+
var PIPELINE_PHASES={robot_online:'hardware',camera_ready:'hardware',microphone_ready:'hardware',speaker_ready:'hardware',motion_detected:'trigger',scheduler_session:'session',livekit_join:'session',camera_published:'media',microphone_published:'media',voice_worker:'intelligence',stt_listening:'intelligence',llm_response:'intelligence',tts_subscribed:'playback',playback_started:'playback',motor_sequence:'playback',session_ended:'close'};
|
|
7177
|
+
function pipelineCurrentStage(stages,latest,running){
|
|
7178
|
+
for(var i=0;i<stages.length;i++){var bad=latest[stages[i]];if(bad&&(bad.status==='failed'||bad.status==='blocked'))return stages[i];}
|
|
7179
|
+
if(!running)return null;
|
|
7180
|
+
var lastOk=-1;
|
|
7181
|
+
for(var j=0;j<stages.length;j++){var ev=latest[stages[j]];if(ev&&(ev.status==='ok'||ev.status==='skipped'))lastOk=j;if(ev&&ev.status==='running')return stages[j];}
|
|
7182
|
+
return stages[Math.min(stages.length-1,lastOk+1)];
|
|
7183
|
+
}
|
|
6815
7184
|
function renderPipeline(data){
|
|
6816
7185
|
var host=$('pk-pipeline'), summary=$('pk-pipeline-summary'); if(!host||!summary) return;
|
|
6817
7186
|
var latest={}, prereqs=data.prerequisites||{}, session=data.latest_session||{}, sid=session.id||null;
|
|
@@ -6822,16 +7191,37 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6822
7191
|
['robot_online','camera_ready','microphone_ready','speaker_ready'].forEach(function(stage){
|
|
6823
7192
|
if(!latest[stage]) latest[stage]={stage:stage,status:prereqs[stage]?'ok':'blocked',message:prereqs[stage]?'Ready':'Not reported'};
|
|
6824
7193
|
});
|
|
6825
|
-
var running=data.overall==='running';
|
|
6826
|
-
host.innerHTML=
|
|
6827
|
-
var ev=latest[stage],
|
|
7194
|
+
var running=data.overall==='running',stages=data.stages||Object.keys(PIPELINE_LABELS),current=pipelineCurrentStage(stages,latest,running);
|
|
7195
|
+
host.innerHTML=stages.map(function(stage,index){
|
|
7196
|
+
var ev=latest[stage],isCurrent=stage===current,status=ev&&ev.status||(isCurrent?'running':'pending');
|
|
6828
7197
|
var detail=ev&&(ev.message||ev.timestamp)||'waiting';
|
|
6829
|
-
|
|
6830
|
-
|
|
7198
|
+
var time=ev&&ev.timestamp?new Date(ev.timestamp+'Z').toLocaleString():'';
|
|
7199
|
+
var phase=PIPELINE_PHASES[stage]||'session';
|
|
7200
|
+
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>';
|
|
6831
7201
|
}).join('');
|
|
6832
7202
|
var blockers=data.blockers||[];
|
|
6833
7203
|
summary.className='pk-pipeline-summary '+esc(data.overall||'blocked');
|
|
6834
|
-
summary.textContent=blockers.length?('BLOCKED: '+blockers[0]):(running?'
|
|
7204
|
+
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');
|
|
7205
|
+
var micHealth=$('pk-mic-health');if(micHealth){
|
|
7206
|
+
var micEvent=latest.microphone_published,trackOk=micEvent&&micEvent.status==='ok',inventoryOk=!!prereqs.microphone_ready;
|
|
7207
|
+
micHealth.innerHTML='<div class="pk-mic-check ok"><div class="k">Hardware lock</div><div class="v">USB mic hw:3,0</div></div><div class="pk-mic-check '+(inventoryOk?'ok':'bad')+'"><div class="k">Inventory</div><div class="v">'+(inventoryOk?'detected':'missing')+'</div></div><div class="pk-mic-check '+(trackOk?'ok':micEvent?'bad':'')+'"><div class="k">Production track</div><div class="v">'+esc(micEvent?(micEvent.message||micEvent.status):'waiting for session')+'</div></div>';
|
|
7208
|
+
}
|
|
7209
|
+
}
|
|
7210
|
+
function loadPipelineHistory(n){
|
|
7211
|
+
var host=$('pk-pipeline-history'),pageLabel=$('pk-history-page');if(!host)return;
|
|
7212
|
+
host.innerHTML='<div class="pk-note">Loading session page...</div>';
|
|
7213
|
+
getJson('/robopark/api/robots/'+encodeURIComponent(schedId(n))+'/pipeline-history?page='+pipelineHistoryPage+'&page_size=6'+(QS?'&'+QS.slice(1):''))
|
|
7214
|
+
.then(function(data){
|
|
7215
|
+
pipelineHistoryPage=data.page||1;if(pageLabel)pageLabel.textContent='page '+pipelineHistoryPage+' / '+(data.pages||1)+' | '+(data.total||0)+' runs';
|
|
7216
|
+
var prev=$('pk-history-prev'),next=$('pk-history-next');if(prev)prev.disabled=pipelineHistoryPage<=1;if(next)next.disabled=pipelineHistoryPage>=(data.pages||1);
|
|
7217
|
+
host.innerHTML=(data.items||[]).map(function(run){
|
|
7218
|
+
var events=run.events||[],latest={};events.forEach(function(ev){latest[ev.stage]=ev;});
|
|
7219
|
+
var hasError=Number(run.error_count||0)>0,state=!run.ended_at?'running':hasError?'bad':'ok',label=!run.ended_at?'active':hasError?'attention':(run.end_reason||'complete');
|
|
7220
|
+
var started=run.started_at?new Date(run.started_at+'Z'):null,duration=run.duration_seconds!=null?run.duration_seconds:(started?Math.max(0,Math.floor((Date.now()-started.getTime())/1000)):0);
|
|
7221
|
+
var stageBar=Object.keys(PIPELINE_LABELS).map(function(stage){var ev=latest[stage];return '<i class="'+esc(ev&&ev.status||'pending')+'" title="'+esc(PIPELINE_LABELS[stage])+'"></i>';}).join('');
|
|
7222
|
+
return '<div class="pk-run-card" data-session="'+esc(run.session_id)+'"><div class="pk-run-top"><div><div class="pk-run-title">'+esc(run.session_id)+'</div><div class="pk-run-meta">'+esc(started?started.toLocaleString():'unknown time')+' | '+esc(rpFmtDuration(duration))+' | '+esc(run.transcript_count||0)+' transcript turns | '+esc(run.pipeline_event_count||0)+' events</div></div><span class="pk-run-state '+state+'">'+esc(label)+'</span></div><div class="pk-run-stages">'+stageBar+'</div></div>';
|
|
7223
|
+
}).join('')||'<div class="pk-note">No production sessions recorded for this robot.</div>';
|
|
7224
|
+
}).catch(function(e){host.innerHTML='<div class="pk-note" style="color:var(--bad)">History unavailable: '+esc(e.message)+'</div>';});
|
|
6835
7225
|
}
|
|
6836
7226
|
function pollPipeline(n){
|
|
6837
7227
|
fetch('/robopark/api/robots/'+encodeURIComponent(schedId(n))+'/pipeline-status'+QS,{cache:'no-store'})
|
|
@@ -7225,7 +7615,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7225
7615
|
}).join('');
|
|
7226
7616
|
host.innerHTML = '<div class="pk-spk-hist-head">last '+list.length+' tests</div>' + rows;
|
|
7227
7617
|
}
|
|
7228
|
-
function _spkTestRenderResult(robotId, result){
|
|
7618
|
+
function _spkTestRenderResult(robotId, result){
|
|
7229
7619
|
var host = $('pk-spk-test-result'); if(!host) return;
|
|
7230
7620
|
if(!result){
|
|
7231
7621
|
host.innerHTML = '';
|
|
@@ -7252,9 +7642,32 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7252
7642
|
+ '<div><span class="l">format</span><span class="v">'+esc((result.output_sample_rate||result.sample_rate)+' Hz / '+(result.output_channels||1)+'ch out'+(result.playback_only?'':' · '+(result.input_sample_rate||result.sample_rate)+' Hz / '+(result.input_channels||1)+'ch in'))+'</span></div>'
|
|
7253
7643
|
+ '</div>';
|
|
7254
7644
|
var diag = result.diagnostic ? '<div class="pk-spk-diag">'+esc(result.diagnostic)+'</div>' : '';
|
|
7255
|
-
host.innerHTML = '<div class="pk-spk-result '+(result.pass?'ok':'bad')+'">'
|
|
7256
|
-
+ banner + meta + diag + '</div>';
|
|
7257
|
-
}
|
|
7645
|
+
host.innerHTML = '<div class="pk-spk-result '+(result.pass?'ok':'bad')+'">'
|
|
7646
|
+
+ banner + meta + diag + '</div>';
|
|
7647
|
+
}
|
|
7648
|
+
function _micTestRenderResult(result,label){
|
|
7649
|
+
var host=$('pk-mic-test-result'),state=$('pk-mic-test-state');if(!host)return;
|
|
7650
|
+
if(!result||!result.ok){var error=result&&result.error||'robot did not return a result';host.innerHTML='<div class="pk-mic-result bad"><b>TEST FAILED</b><div style="margin-top:.3rem">'+esc(error)+'</div></div>';if(state)state.textContent='Failed | '+error;return;}
|
|
7651
|
+
var passed=result.pass===true,mode=result.mode||label;
|
|
7652
|
+
host.innerHTML='<div class="pk-mic-result '+(passed?'ok':'bad')+'"><b>'+(passed?'MIC PATH VERIFIED':'NO USABLE SIGNAL')+'</b><div class="pk-mic-readings"><div class="pk-mic-reading"><span>Peak</span><b>'+esc(result.recorded_peak_db!=null?result.recorded_peak_db+' dB':'--')+'</b></div><div class="pk-mic-reading"><span>RMS</span><b>'+esc(result.recorded_rms_db!=null?result.recorded_rms_db+' dB':'--')+'</b></div><div class="pk-mic-reading"><span>Nonzero</span><b>'+esc(result.nonzero_percent!=null?result.nonzero_percent+'%':'--')+'</b></div><div class="pk-mic-reading"><span>Format</span><b>'+esc((result.sample_rate||result.input_sample_rate||'--')+' Hz / '+(result.channels||result.input_channels||1)+'ch')+'</b></div></div><div style="margin-top:.38rem;color:var(--muted)">'+esc(result.input_device||'USB microphone')+' | '+esc(result.duration_ms||0)+' ms | '+esc(result.diagnostic||mode)+'</div></div>';
|
|
7653
|
+
if(state)state.textContent=passed?'Passed | '+mode+' confirmed on the robot':'Capture completed but no usable PCM signal was detected';
|
|
7654
|
+
}
|
|
7655
|
+
function setMicTestBusy(busy,text){
|
|
7656
|
+
[$('pk-mic-groundtruth'),$('pk-mic-roundtrip'),$('pk-mic-apply')].forEach(function(button){if(button)button.disabled=busy;});
|
|
7657
|
+
var state=$('pk-mic-test-state');if(state&&text)state.textContent=text;
|
|
7658
|
+
}
|
|
7659
|
+
function runMicGroundtruth(n){
|
|
7660
|
+
var robotId=schedId(n),duration=parseFloat((($('pk-mic-duration')||{}).value)||'4');setMicTestBusy(true,'Recording raw PCM on the robot for '+duration+' seconds...');
|
|
7661
|
+
rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/speaker-test',{mode:'mic_groundtruth',duration:duration,input:'2',input_name:'Usb Audio Device: USB Audio (hw:3,0)'})
|
|
7662
|
+
.then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','mic-groundtruth');})
|
|
7663
|
+
.catch(function(e){_micTestRenderResult({ok:false,error:e.message},'mic_groundtruth');setMicTestBusy(false);});
|
|
7664
|
+
}
|
|
7665
|
+
function runMicRoundtrip(n){
|
|
7666
|
+
var robotId=schedId(n);setMicTestBusy(true,'Playing a reference tone and recording the locked USB microphone...');
|
|
7667
|
+
rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/speaker-test',{mode:'tone',playback_only:false,frequency:1000,duration:1,amplitude:.6,threshold_db:-30,output:'1',input:'2',output_name:'USB Audio Device: - (hw:2,0)',input_name:'Usb Audio Device: USB Audio (hw:3,0)'})
|
|
7668
|
+
.then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','mic-roundtrip');})
|
|
7669
|
+
.catch(function(e){_micTestRenderResult({ok:false,error:e.message},'roundtrip');setMicTestBusy(false);});
|
|
7670
|
+
}
|
|
7258
7671
|
function runSpeakerTest(n, forcedMode){
|
|
7259
7672
|
var robotId = schedId(n), mode=forcedMode||(($('pk-spk-mode')||{}).value||'tts');
|
|
7260
7673
|
var buttons=[$('pk-spk-runtest'),$('pk-spk-tone'),$('pk-device-test-speaker')];buttons.forEach(function(b){if(b)b.disabled=true;});
|
|
@@ -7310,7 +7723,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7310
7723
|
var _origRenderShellResult = _renderShellResult;
|
|
7311
7724
|
_renderShellResult = function(robotId, result, label){
|
|
7312
7725
|
_origRenderShellResult(robotId, result, label);
|
|
7313
|
-
if(label === 'speaker-test'){
|
|
7726
|
+
if(label === 'speaker-test'){
|
|
7314
7727
|
_spkTestSetLive(robotId, {live:false});
|
|
7315
7728
|
_spkTestMark(robotId, result);
|
|
7316
7729
|
_spkTestRenderResult(robotId, result);
|
|
@@ -7323,8 +7736,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7323
7736
|
else { status.textContent = '✗ no signal — '+result.recorded_peak_db+' dB peak'; }
|
|
7324
7737
|
}
|
|
7325
7738
|
var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = false; btn.textContent = (($('pk-spk-mode')||{}).value==='tone'?'Run tone test':'Play cached voice'); }
|
|
7326
|
-
}
|
|
7327
|
-
|
|
7739
|
+
}
|
|
7740
|
+
if(label==='mic-groundtruth'||label==='mic-roundtrip'){
|
|
7741
|
+
_micTestRenderResult(result,label);
|
|
7742
|
+
setMicTestBusy(false);
|
|
7743
|
+
}
|
|
7744
|
+
};
|
|
7328
7745
|
|
|
7329
7746
|
function openDrawerForRobot(name){
|
|
7330
7747
|
var key=String(name||'').toLowerCase(), id='pad:'+name, found=null;
|