infinicode 2.8.99 → 2.8.102
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 +322 -57
- package/dist/robopark/agent-ctl.js +42 -26
- package/dist/robopark/robot-runtime.js +15 -0
- package/docs/ROBOPARK_PRODUCTION_MEMORY.md +39 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +47 -0
- package/packages/robopark/scheduler/preview_agent.py +139 -23
|
@@ -790,7 +790,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
790
790
|
<h3><span>Live activity</span><span style="color:var(--muted)" id="pk-src">/fed/stream</span></h3>
|
|
791
791
|
<div class="rows" id="pk-rows"></div>
|
|
792
792
|
</aside>
|
|
793
|
-
<div class="pk-hint">drag to pan · scroll to zoom · <b>click a robot</b> for cam / mic / speaker</div>
|
|
793
|
+
<div class="pk-hint">drag to pan · scroll to zoom · <b>click a robot</b> for cam / mic / speaker · <b>ctrl+drag a robot</b> to reposition (persists)</div>
|
|
794
794
|
</div>
|
|
795
795
|
<div class="pk-drawer" id="pk-drawer">
|
|
796
796
|
<div class="pk-dh">
|
|
@@ -4078,21 +4078,27 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4078
4078
|
// Pond / curved water feature at the lower-left of the plaza.
|
|
4079
4079
|
var POND={cx:0.18*15, cy:0.84*8, rx:2.4, ry:1.1};
|
|
4080
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,
|
|
4082
|
-
// left)
|
|
4083
|
-
//
|
|
4084
|
-
//
|
|
4085
|
-
//
|
|
4081
|
+
// a per-car base speed (world units / second), a direction (+1 = right,
|
|
4082
|
+
// -1 = left), and a per-frame jitter range so cars never settle into
|
|
4083
|
+
// a stable cluster. y is one of two lane bands (≈ -0.65 for outbound,
|
|
4084
|
+
// ≈ -1.18 for inbound) so cars don't overlap.
|
|
4085
|
+
//
|
|
4086
|
+
// Stability fix: the previous version always re-spawned a wrapped car
|
|
4087
|
+
// at x = -0.5 (or PARK_W + 0.5) — every car in the same lane teleported
|
|
4088
|
+
// to the same spot, so the slowest car instantly caught the fastest
|
|
4089
|
+
// one and the whole lane collapsed into a clump. We now (a) spawn
|
|
4090
|
+
// each car at a random x within the visible road, and (b) add a
|
|
4091
|
+
// ±5% per-frame speed jitter so cars gradually drift apart. Speeds
|
|
4092
|
+
// are also more spread (2.2 → 4.6) so the natural ordering stays
|
|
4093
|
+
// stable long enough to look like real traffic.
|
|
4086
4094
|
var ROAD_Y0=-1.5, ROAD_Y1=-0.35;
|
|
4087
4095
|
var CARS=[
|
|
4088
|
-
{x:1.5, y:-0.65, color:'#f2c14e', speed:
|
|
4089
|
-
{x:
|
|
4090
|
-
{x:
|
|
4091
|
-
{x:
|
|
4092
|
-
{x:
|
|
4093
|
-
{x:0.5, y:-1.18, color:'#5b8bd6', speed:
|
|
4094
|
-
{x:13.0, y:-0.65, color:'#c87ed4', speed:2.6, dir:+1},
|
|
4095
|
-
{x:7.0, y:-1.18, color:'#f2c14e', speed:3.8, dir:-1},
|
|
4096
|
+
{x:1.5, y:-0.65, color:'#f2c14e', speed:2.4, dir:+1},
|
|
4097
|
+
{x:6.5, y:-0.65, color:'#e0a23c', speed:3.6, dir:+1},
|
|
4098
|
+
{x:11.5, y:-0.65, color:'#d8d8e0', speed:4.4, dir:+1},
|
|
4099
|
+
{x:4.0, y:-1.18, color:'#7fb4ff', speed:2.6, dir:-1},
|
|
4100
|
+
{x:9.0, y:-1.18, color:'#ff7e6e', speed:3.4, dir:-1},
|
|
4101
|
+
{x:0.5, y:-1.18, color:'#5b8bd6', speed:4.2, dir:-1},
|
|
4096
4102
|
];
|
|
4097
4103
|
// Per-car phase offsets so headlights / taillights / wheels don't all
|
|
4098
4104
|
// blink in lockstep — gives a natural "different cars, different
|
|
@@ -4369,12 +4375,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4369
4375
|
connected: match?!!match.connected:false,
|
|
4370
4376
|
load: (match&&match.load!=null)?match.load:0,
|
|
4371
4377
|
platform: match?(match.platform||''):'', version: match?(match.version||'?'):'—',
|
|
4372
|
-
site: match?(match.site||''):'', caps: match?(match.capabilities||[]):[],
|
|
4373
|
-
schedulerRobotId: match?(match.schedulerRobotId||match.schedulerDeviceId||''):'',
|
|
4374
|
-
schedulerDeviceId: match?(match.schedulerDeviceId||match.schedulerRobotId||''):'',
|
|
4378
|
+
site: match?(match.site||''):'', caps: match?(match.capabilities||[]):[],
|
|
4379
|
+
schedulerRobotId: match?(match.schedulerRobotId||match.schedulerDeviceId||''):'',
|
|
4380
|
+
schedulerDeviceId: match?(match.schedulerDeviceId||match.schedulerRobotId||''):'',
|
|
4375
4381
|
telemKeys: [slot.label.toLowerCase()].concat(slot.keys||[]).concat(match?[(match.displayName||'').toLowerCase()]:[]),
|
|
4382
|
+
// slotId is the stable handle we use to persist operator-
|
|
4383
|
+
// chosen positions. slotNx/slotNy are the preset's default
|
|
4384
|
+
// coordinates so the user can always reset back.
|
|
4385
|
+
slotId: slot.id, slotNx: slot.nx, slotNy: slot.ny,
|
|
4376
4386
|
wx:wx, wy:wy, _hit:prevHit(id)
|
|
4377
|
-
};
|
|
4387
|
+
};
|
|
4378
4388
|
// Attach a status derived from the latest scheduler telemetry
|
|
4379
4389
|
// (push-poll, so it can lag up to 3s behind /fed/status).
|
|
4380
4390
|
if(kind === 'robot'){
|
|
@@ -4406,6 +4416,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4406
4416
|
if(!s) return;
|
|
4407
4417
|
nodes = layout(s.nodes||[]);
|
|
4408
4418
|
byId={}; nodes.forEach(function(n){ byId[n.id]=n; });
|
|
4419
|
+
// re-apply any persisted Ctrl+drag overrides on top of the
|
|
4420
|
+
// fresh layout (the server may have also returned updated
|
|
4421
|
+
// slot nx/ny — those go through the override map too)
|
|
4422
|
+
_applyPosOverrides();
|
|
4409
4423
|
// if not streaming, animate from the polled activity tail
|
|
4410
4424
|
if(!es || es.readyState!==1){ (s.activity||[]).forEach(function(e){ ingest(e.f||e, true); }); }
|
|
4411
4425
|
}
|
|
@@ -4450,16 +4464,187 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4450
4464
|
}
|
|
4451
4465
|
|
|
4452
4466
|
// ── canvas + projection ──
|
|
4467
|
+
// State for Ctrl+drag robot repositioning on the park map. When
|
|
4468
|
+
// the user holds Ctrl and mousedown's a robot, the canvas stops
|
|
4469
|
+
// panning and instead drags that node around. On mouseup, the
|
|
4470
|
+
// new (nx, ny) is written to _parkPosOverrides and pushed to
|
|
4471
|
+
// the scheduler via a debounced PUT. Survives reloads because
|
|
4472
|
+
// the server stores nx/ny in the character_presets table.
|
|
4473
|
+
var _parkPosOverrides = {}; // presetId -> {nx, ny} (normalized 0..1)
|
|
4474
|
+
var _dragNode = null; // the robot node currently being dragged
|
|
4475
|
+
var _dragOffset = {x:0, y:0}; // screen-space offset from node center
|
|
4476
|
+
var _posSaveTimer = null; // debounce timer for the server PUT
|
|
4477
|
+
var _posSaveQueue = {}; // presetId -> {nx, ny} pending save
|
|
4478
|
+
function _isoToWorld(sx, sy){
|
|
4479
|
+
// Inverse of iso() in the same coordinate system. Given a
|
|
4480
|
+
// screen point, return the (wx, wy) world point. Used by the
|
|
4481
|
+
// Ctrl+drag handler to translate a mouse position back into
|
|
4482
|
+
// the iso coordinate space so the robot follows the cursor.
|
|
4483
|
+
var dx = (sx - (W-300)/2 - cam.x) / (TW*cam.z);
|
|
4484
|
+
var dy = (sy - H*0.42 - cam.y) / (TH*cam.z);
|
|
4485
|
+
// from iso(): dx = (wx-PARK_W/2) - (wy-PARK_D/2); dy = (wx-PARK_W/2) + (wy-PARK_D/2)
|
|
4486
|
+
// so wx = (dx+dy)/2 + PARK_W/2; wy = (dy-dx)/2 + PARK_D/2
|
|
4487
|
+
return { x: (dx+dy)*0.5 + PARK_W*0.5, y: (dy-dx)*0.5 + PARK_D*0.5 };
|
|
4488
|
+
}
|
|
4489
|
+
function _applyPosOverrides(){
|
|
4490
|
+
// Walk nodes and override their wx/wy from _parkPosOverrides if
|
|
4491
|
+
// we have a stored position. The slot's default nx/ny stays in
|
|
4492
|
+
// node.slotNx/slotNy so the user can "reset" back to defaults
|
|
4493
|
+
// if we ever expose that control.
|
|
4494
|
+
for(var i=0; i<nodes.length; i++){
|
|
4495
|
+
var n = nodes[i];
|
|
4496
|
+
if(!n.slotId) continue;
|
|
4497
|
+
var ov = _parkPosOverrides[n.slotId];
|
|
4498
|
+
if(ov){
|
|
4499
|
+
n.wx = Math.max(0, Math.min(PARK_W, ov.nx * PARK_W));
|
|
4500
|
+
n.wy = Math.max(0, Math.min(PARK_D, ov.ny * PARK_D));
|
|
4501
|
+
n._posOverridden = true;
|
|
4502
|
+
} else if(n._posOverridden){
|
|
4503
|
+
// The server just gave us a fresh layout and we don't have
|
|
4504
|
+
// a local override — fall back to the slot's default position.
|
|
4505
|
+
n.wx = n.slotNx * PARK_W;
|
|
4506
|
+
n.wy = n.slotNy * PARK_D;
|
|
4507
|
+
n._posOverridden = false;
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
function _schedulePosSave(){
|
|
4512
|
+
// Debounce: collapse a flurry of drag-end events into one
|
|
4513
|
+
// PUT after 350ms of quiet. Saves queue is merged so the final
|
|
4514
|
+
// PUT always reflects the latest position for each preset.
|
|
4515
|
+
if(_posSaveTimer) clearTimeout(_posSaveTimer);
|
|
4516
|
+
_posSaveTimer = setTimeout(_flushPosSaves, 350);
|
|
4517
|
+
}
|
|
4518
|
+
function _flushPosSaves(){
|
|
4519
|
+
var entries = [];
|
|
4520
|
+
for(var id in _posSaveQueue){
|
|
4521
|
+
if(Object.prototype.hasOwnProperty.call(_posSaveQueue, id)){
|
|
4522
|
+
entries.push({ id: id, nx: _posSaveQueue[id].nx, ny: _posSaveQueue[id].ny });
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
_posSaveQueue = {};
|
|
4526
|
+
_posSaveTimer = null;
|
|
4527
|
+
if(!entries.length) return;
|
|
4528
|
+
// Persist to the scheduler. We POST to the dedicated
|
|
4529
|
+
// /api/character-presets/positions batch endpoint which clamps
|
|
4530
|
+
// nx/ny to [0.04, 0.96] so a stray drag can't park a robot
|
|
4531
|
+
// off the island.
|
|
4532
|
+
var body = JSON.stringify({ positions: entries });
|
|
4533
|
+
try{
|
|
4534
|
+
fetch('/api/character-presets/positions'+QS, {
|
|
4535
|
+
method: 'PUT',
|
|
4536
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4537
|
+
body: body,
|
|
4538
|
+
keepalive: true
|
|
4539
|
+
}).then(function(r){
|
|
4540
|
+
if(r.ok) return r.json();
|
|
4541
|
+
throw new Error('HTTP '+r.status);
|
|
4542
|
+
}).then(function(j){
|
|
4543
|
+
if(typeof showOut === 'function'){
|
|
4544
|
+
showOut('park position saved · '+(j.updated||entries.length)+' preset'+(entries.length===1?'':'s'));
|
|
4545
|
+
}
|
|
4546
|
+
}).catch(function(err){
|
|
4547
|
+
if(typeof showOut === 'function') showOut('park position save failed: '+err.message);
|
|
4548
|
+
});
|
|
4549
|
+
}catch(e){ /* offline — keep override in memory */ }
|
|
4550
|
+
}
|
|
4551
|
+
function _hitTestNode(mx, my){
|
|
4552
|
+
// Walk all visible nodes and return the first whose hit-box
|
|
4553
|
+
// contains (mx, my). Mirrors the existing click hit-test
|
|
4554
|
+
// but returns the node directly (and skips early if a drag
|
|
4555
|
+
// is already in progress).
|
|
4556
|
+
for(var i=0; i<nodes.length; i++){
|
|
4557
|
+
var n = nodes[i];
|
|
4558
|
+
var hb = n._hit;
|
|
4559
|
+
if(hb && mx>=hb.x && mx<=hb.x+hb.w && my>=hb.y && my<=hb.y+hb.h){
|
|
4560
|
+
return n;
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
return null;
|
|
4564
|
+
}
|
|
4453
4565
|
function ensure(){
|
|
4454
4566
|
if(C) return;
|
|
4455
4567
|
C=$('pk-canvas'); X=C.getContext('2d'); t0=performance.now();
|
|
4456
4568
|
DPR=Math.max(1,Math.min(2,window.devicePixelRatio||1));
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4569
|
+
// Panning: drag on the canvas (no Ctrl) pans the iso camera.
|
|
4570
|
+
// Ctrl+drag on a robot reposition the robot on the park map.
|
|
4571
|
+
// Each gesture is mutually exclusive — we keep one _isPanning
|
|
4572
|
+
// flag and one _dragNode at a time.
|
|
4573
|
+
var _isPanning = false;
|
|
4574
|
+
var _px = 0, _py = 0;
|
|
4575
|
+
C.addEventListener('mousedown',function(e){
|
|
4576
|
+
var r = C.getBoundingClientRect();
|
|
4577
|
+
var mx = e.clientX - r.left;
|
|
4578
|
+
var my = e.clientY - r.top;
|
|
4579
|
+
// Ctrl+drag: if a robot is under the cursor, pick it up.
|
|
4580
|
+
// Only ROBOT nodes are draggable — the hub, parked cars in the
|
|
4581
|
+
// lot, and ghosts for offline nodes stay locked to their slot.
|
|
4582
|
+
if(e.ctrlKey){
|
|
4583
|
+
var hit = _hitTestNode(mx, my);
|
|
4584
|
+
if(hit && hit.slotId && hit.kind === 'robot'){
|
|
4585
|
+
_dragNode = hit;
|
|
4586
|
+
var p = iso(hit.wx, hit.wy);
|
|
4587
|
+
_dragOffset.x = mx - p.x;
|
|
4588
|
+
_dragOffset.y = my - p.y;
|
|
4589
|
+
// prevent text selection while dragging
|
|
4590
|
+
e.preventDefault();
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
// otherwise: start a pan
|
|
4595
|
+
_isPanning = true;
|
|
4596
|
+
_px = e.clientX; _py = e.clientY;
|
|
4597
|
+
});
|
|
4598
|
+
window.addEventListener('mouseup',function(){
|
|
4599
|
+
if(_dragNode){
|
|
4600
|
+
// commit the drag: record override, schedule server save
|
|
4601
|
+
var nx = _dragNode.wx / PARK_W;
|
|
4602
|
+
var ny = _dragNode.wy / PARK_D;
|
|
4603
|
+
var id = _dragNode.slotId;
|
|
4604
|
+
if(id){
|
|
4605
|
+
_parkPosOverrides[id] = { nx: nx, ny: ny };
|
|
4606
|
+
_posSaveQueue[id] = { nx: nx, ny: ny };
|
|
4607
|
+
_schedulePosSave();
|
|
4608
|
+
}
|
|
4609
|
+
_dragNode = null;
|
|
4610
|
+
}
|
|
4611
|
+
_isPanning = false;
|
|
4612
|
+
});
|
|
4613
|
+
window.addEventListener('mousemove',function(e){
|
|
4614
|
+
if(_dragNode){
|
|
4615
|
+
var r = C.getBoundingClientRect();
|
|
4616
|
+
var mx = e.clientX - r.left;
|
|
4617
|
+
var my = e.clientY - r.top;
|
|
4618
|
+
// screen point = mouse minus the original grab offset, so
|
|
4619
|
+
// the robot doesn't snap its center to the cursor
|
|
4620
|
+
var worldPt = _isoToWorld(mx - _dragOffset.x, my - _dragOffset.y);
|
|
4621
|
+
// clamp to the visible grass rectangle (with a small
|
|
4622
|
+
// inner margin so the avatar doesn't get clipped by the
|
|
4623
|
+
// gold wall or the hedge)
|
|
4624
|
+
var MARGIN = 0.4;
|
|
4625
|
+
_dragNode.wx = Math.max(MARGIN, Math.min(PARK_W - MARGIN, worldPt.x));
|
|
4626
|
+
_dragNode.wy = Math.max(MARGIN, Math.min(PARK_D - MARGIN, worldPt.y));
|
|
4627
|
+
return;
|
|
4628
|
+
}
|
|
4629
|
+
if(!_isPanning) return;
|
|
4630
|
+
cam.x += e.clientX - _px;
|
|
4631
|
+
cam.y += e.clientY - _py;
|
|
4632
|
+
_px = e.clientX; _py = e.clientY;
|
|
4633
|
+
});
|
|
4634
|
+
// Escape cancels an in-progress drag without saving
|
|
4635
|
+
window.addEventListener('keydown',function(e){
|
|
4636
|
+
if((e.key === 'Escape' || e.key === 'Esc') && _dragNode){
|
|
4637
|
+
// restore to the original override (or slot default)
|
|
4638
|
+
_dragNode.wx = _dragNode.slotNx * PARK_W;
|
|
4639
|
+
_dragNode.wy = _dragNode.slotNy * PARK_D;
|
|
4640
|
+
_dragNode = null;
|
|
4641
|
+
}
|
|
4642
|
+
});
|
|
4461
4643
|
C.addEventListener('wheel',function(e){ e.preventDefault(); var f=e.deltaY<0?1.08:0.925; cam.z=Math.max(.5,Math.min(2.4,cam.z*f)); },{passive:false});
|
|
4462
4644
|
C.addEventListener('click',function(e){
|
|
4645
|
+
// Suppress the click that fires at the end of a drag — we
|
|
4646
|
+
// don't want a single drag-release to also open the drawer.
|
|
4647
|
+
if(_dragNode) return;
|
|
4463
4648
|
var r=C.getBoundingClientRect(), mx=e.clientX-r.left, my=e.clientY-r.top, hit=null;
|
|
4464
4649
|
for(var i=0;i<nodes.length;i++){ var hb=nodes[i]._hit; if(hb&&mx>=hb.x&&mx<=hb.x+hb.w&&my>=hb.y&&my<=hb.y+hb.h) hit=nodes[i]; }
|
|
4465
4650
|
if(hit){ openDrawer(hit.id); if(String(hit.id).indexOf('pad:')!==0) selectNode(hit.id, hit.name); }
|
|
@@ -4605,9 +4790,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4605
4790
|
// the warm sun")
|
|
4606
4791
|
// The underside and drop shadow give the park real depth so it
|
|
4607
4792
|
// doesn't look like a flat sticker on the sky.
|
|
4608
|
-
// Build the grass perimeter polygon once (
|
|
4609
|
-
// so the gold
|
|
4610
|
-
|
|
4793
|
+
// Build the grass perimeter polygon once (the same corners the
|
|
4794
|
+
// grass field uses, so the gold wall's top edge sits flush
|
|
4795
|
+
// against the grass). We use grassPerim(0) — no offset — for
|
|
4796
|
+
// the walls, and the perimeter itself is used again later for
|
|
4797
|
+
// the bottom face of the floating island (see UNDERSIDE_LIFT).
|
|
4611
4798
|
function grassPerim(offset){
|
|
4612
4799
|
// Use the same world corners as the grass field, offset outward.
|
|
4613
4800
|
var o = offset;
|
|
@@ -4724,39 +4911,80 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4724
4911
|
X.fill();
|
|
4725
4912
|
}
|
|
4726
4913
|
X.restore();
|
|
4727
|
-
// 3. GOLD
|
|
4728
|
-
//
|
|
4729
|
-
//
|
|
4730
|
-
//
|
|
4731
|
-
|
|
4914
|
+
// 3. RAISED GOLD WALLS — proper 3D parapet around the park.
|
|
4915
|
+
// The wall follows the grass perimeter (4-corner rectangle).
|
|
4916
|
+
// Each side has a visible front face that extends downward in
|
|
4917
|
+
// screen space, plus a thin top-edge highlight that catches
|
|
4918
|
+
// the warm sun light. Drawn AFTER the underside so the top
|
|
4919
|
+
// edge of the wall sits flush with the top of the grass.
|
|
4920
|
+
//
|
|
4921
|
+
// Geometry: the wall's TOP edge is the grass perimeter line.
|
|
4922
|
+
// Its BOTTOM edge is the same line shifted down by WALL_H
|
|
4923
|
+
// pixels. We draw each of the 4 sides as a quad (TL, TR,
|
|
4924
|
+
// BR, BL), then trace a thin gold line along the top of
|
|
4925
|
+
// each side for the highlight.
|
|
4732
4926
|
var rimInner = grassPerim(0);
|
|
4733
|
-
var rimOuterScreen = rimOuter.map(function(p){ return iso(p[0], p[1]); });
|
|
4734
4927
|
var rimInnerScreen = rimInner.map(function(p){ return iso(p[0], p[1]); });
|
|
4928
|
+
var WALL_H = 26 * z; // wall height in screen pixels
|
|
4929
|
+
// Build bottom-edge screen points by shifting each top point
|
|
4930
|
+
// down by WALL_H.
|
|
4931
|
+
var rimBotScreen = rimInnerScreen.map(function(s){
|
|
4932
|
+
return { x: s.x, y: s.y + WALL_H };
|
|
4933
|
+
});
|
|
4934
|
+
// Front face: a single closed polygon (the grass perimeter top
|
|
4935
|
+
// + the grass perimeter bottom, traversed in reverse). Drawn
|
|
4936
|
+
// in two passes — first a darker base, then a lighter overlay
|
|
4937
|
+
// stripe near the top to give the wall some "warm-light from
|
|
4938
|
+
// above" shading.
|
|
4735
4939
|
X.save();
|
|
4736
|
-
//
|
|
4737
|
-
|
|
4738
|
-
X.fillStyle = 'rgba(255, 200, 110, 0.0)';
|
|
4739
|
-
// fill the rim band as a quad
|
|
4940
|
+
// base wall (slightly darker gold)
|
|
4941
|
+
X.fillStyle = 'rgba(176, 124, 44, 0.96)';
|
|
4740
4942
|
X.beginPath();
|
|
4741
|
-
X.moveTo(
|
|
4742
|
-
X.lineTo(
|
|
4743
|
-
X.lineTo(
|
|
4744
|
-
X.lineTo(
|
|
4943
|
+
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
4944
|
+
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
4945
|
+
X.lineTo(rimInnerScreen[2].x, rimInnerScreen[2].y);
|
|
4946
|
+
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4947
|
+
X.lineTo(rimBotScreen[3].x, rimBotScreen[3].y);
|
|
4948
|
+
X.lineTo(rimBotScreen[2].x, rimBotScreen[2].y);
|
|
4949
|
+
X.lineTo(rimBotScreen[1].x, rimBotScreen[1].y);
|
|
4950
|
+
X.lineTo(rimBotScreen[0].x, rimBotScreen[0].y);
|
|
4745
4951
|
X.closePath();
|
|
4746
4952
|
X.fill();
|
|
4747
|
-
//
|
|
4748
|
-
|
|
4749
|
-
|
|
4953
|
+
// warm-light gradient overlay — fades from bright gold at the
|
|
4954
|
+
// top of the wall to transparent at the bottom, so the wall
|
|
4955
|
+
// reads as if the top is catching the sun and the bottom is
|
|
4956
|
+
// in shadow.
|
|
4957
|
+
var wallGrad = X.createLinearGradient(0, rimInnerScreen[0].y, 0, rimInnerScreen[0].y + WALL_H);
|
|
4958
|
+
wallGrad.addColorStop(0, 'rgba(255, 226, 132, 0.85)');
|
|
4959
|
+
wallGrad.addColorStop(0.35, 'rgba(255, 196, 92, 0.55)');
|
|
4960
|
+
wallGrad.addColorStop(1, 'rgba(176, 124, 44, 0)');
|
|
4961
|
+
X.fillStyle = wallGrad;
|
|
4750
4962
|
X.beginPath();
|
|
4751
4963
|
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
4752
4964
|
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
4753
4965
|
X.lineTo(rimInnerScreen[2].x, rimInnerScreen[2].y);
|
|
4754
4966
|
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4967
|
+
X.lineTo(rimBotScreen[3].x, rimBotScreen[3].y);
|
|
4968
|
+
X.lineTo(rimBotScreen[2].x, rimBotScreen[2].y);
|
|
4969
|
+
X.lineTo(rimBotScreen[1].x, rimBotScreen[1].y);
|
|
4970
|
+
X.lineTo(rimBotScreen[0].x, rimBotScreen[0].y);
|
|
4971
|
+
X.closePath();
|
|
4972
|
+
X.fill();
|
|
4973
|
+
// outer dark base line at the bottom of the wall (the bottom
|
|
4974
|
+
// edge against the sky) — sells the "raised" effect
|
|
4975
|
+
X.strokeStyle = 'rgba(124, 84, 28, 0.95)';
|
|
4976
|
+
X.lineWidth = 1.4 * z;
|
|
4977
|
+
X.beginPath();
|
|
4978
|
+
X.moveTo(rimBotScreen[0].x, rimBotScreen[0].y);
|
|
4979
|
+
X.lineTo(rimBotScreen[1].x, rimBotScreen[1].y);
|
|
4980
|
+
X.lineTo(rimBotScreen[2].x, rimBotScreen[2].y);
|
|
4981
|
+
X.lineTo(rimBotScreen[3].x, rimBotScreen[3].y);
|
|
4755
4982
|
X.closePath();
|
|
4756
4983
|
X.stroke();
|
|
4757
|
-
//
|
|
4758
|
-
|
|
4759
|
-
X.
|
|
4984
|
+
// top edge — gold band right at the grass line, with a thin
|
|
4985
|
+
// lighter highlight on top to catch the sun.
|
|
4986
|
+
X.strokeStyle = 'rgba(196, 138, 48, 0.95)';
|
|
4987
|
+
X.lineWidth = 4.0 * z;
|
|
4760
4988
|
X.beginPath();
|
|
4761
4989
|
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
4762
4990
|
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
@@ -4764,16 +4992,26 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4764
4992
|
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4765
4993
|
X.closePath();
|
|
4766
4994
|
X.stroke();
|
|
4767
|
-
//
|
|
4768
|
-
X.strokeStyle = 'rgba(
|
|
4769
|
-
X.lineWidth = 1.2*z;
|
|
4995
|
+
// bright top highlight (thin, hottest color)
|
|
4996
|
+
X.strokeStyle = 'rgba(255, 232, 158, 0.95)';
|
|
4997
|
+
X.lineWidth = 1.2 * z;
|
|
4770
4998
|
X.beginPath();
|
|
4771
|
-
X.moveTo(
|
|
4772
|
-
X.lineTo(
|
|
4773
|
-
X.lineTo(
|
|
4774
|
-
X.lineTo(
|
|
4999
|
+
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
5000
|
+
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
5001
|
+
X.lineTo(rimInnerScreen[2].x, rimInnerScreen[2].y);
|
|
5002
|
+
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4775
5003
|
X.closePath();
|
|
4776
5004
|
X.stroke();
|
|
5005
|
+
// corner studs — small gold dots at each of the 4 perimeter
|
|
5006
|
+
// vertices so the wall reads as constructed panels, not a
|
|
5007
|
+
// single continuous block.
|
|
5008
|
+
for(var sci=0; sci<rimInnerScreen.length; sci++){
|
|
5009
|
+
var sPt = rimInnerScreen[sci];
|
|
5010
|
+
X.fillStyle = 'rgba(255, 220, 140, 0.95)';
|
|
5011
|
+
X.beginPath(); X.arc(sPt.x, sPt.y, 2.2 * z, 0, Math.PI*2); X.fill();
|
|
5012
|
+
X.fillStyle = 'rgba(196, 138, 48, 0.85)';
|
|
5013
|
+
X.beginPath(); X.arc(sPt.x, sPt.y, 3.4 * z, 0, Math.PI*2); X.stroke();
|
|
5014
|
+
}
|
|
4777
5015
|
X.restore();
|
|
4778
5016
|
// ── flower spots — deterministic small dots in 3 colors, scattered in the lawn only ──
|
|
4779
5017
|
X.save();
|
|
@@ -6263,19 +6501,27 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6263
6501
|
// Cars share the same elapsed-dt clock so a tab-switch doesn't
|
|
6264
6502
|
// catapult every car off the road at once. speed is world units
|
|
6265
6503
|
// per second, dt is milliseconds. cdt is the already-clamped dt.
|
|
6504
|
+
// Per-frame speed jitter (±5%) so the same lane never settles into
|
|
6505
|
+
// a stable cluster — the fastest car gradually pulls away from the
|
|
6506
|
+
// slowest one and the spread stays alive even after many wraps.
|
|
6266
6507
|
var cdt = Math.min(64, dt);
|
|
6267
6508
|
for(var ci=0; ci<CARS.length; ci++){
|
|
6268
6509
|
var car = CARS[ci];
|
|
6269
|
-
|
|
6510
|
+
// Deterministic per-car jitter based on wall-clock + index so it
|
|
6511
|
+
// changes every frame but isn't pure noise (smooth-ish accel).
|
|
6512
|
+
var jitter = 1 + (Math.sin(now/380 + ci*1.7) * 0.05);
|
|
6513
|
+
car.x += car.dir * car.speed * jitter * (cdt/1000);
|
|
6270
6514
|
// Wrap the car around the road. When it leaves one end, it
|
|
6271
|
-
// re-enters the opposite end on the other lane
|
|
6272
|
-
//
|
|
6515
|
+
// re-enters the opposite end on the other lane — and the
|
|
6516
|
+
// re-entry x is randomized across the visible road so two
|
|
6517
|
+
// cars that arrived at the boundary together don't land on
|
|
6518
|
+
// top of each other.
|
|
6273
6519
|
if(car.dir > 0 && car.x > PARK_W + 0.5){
|
|
6274
|
-
car.x = -0.5;
|
|
6520
|
+
car.x = -0.5 - Math.random() * (PARK_W * 0.5);
|
|
6275
6521
|
car.y = (car.y < -0.9) ? -0.65 : -1.18;
|
|
6276
6522
|
car.dir = (car.y < -0.9) ? +1 : -1;
|
|
6277
6523
|
} else if(car.dir < 0 && car.x < -0.5){
|
|
6278
|
-
car.x = PARK_W + 0.5;
|
|
6524
|
+
car.x = PARK_W + 0.5 + Math.random() * (PARK_W * 0.5);
|
|
6279
6525
|
car.y = (car.y < -0.9) ? -0.65 : -1.18;
|
|
6280
6526
|
car.dir = (car.y < -0.9) ? +1 : -1;
|
|
6281
6527
|
}
|
|
@@ -6489,6 +6735,25 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6489
6735
|
ensure();
|
|
6490
6736
|
loadSDK();
|
|
6491
6737
|
loadImages();
|
|
6738
|
+
// Pull the latest operator-saved positions from the scheduler
|
|
6739
|
+
// and seed the in-memory override map. Without this step the
|
|
6740
|
+
// dashboard would only know about local (this-session) drags
|
|
6741
|
+
// and would reset to slot defaults on every reload.
|
|
6742
|
+
try{
|
|
6743
|
+
getJson('/api/character-presets'+QS).then(function(list){
|
|
6744
|
+
if(Array.isArray(list)){
|
|
6745
|
+
for(var pi=0; pi<list.length; pi++){
|
|
6746
|
+
var p = list[pi];
|
|
6747
|
+
if(p && p.id && typeof p.nx === 'number' && typeof p.ny === 'number'){
|
|
6748
|
+
_parkPosOverrides[p.id] = { nx: p.nx, ny: p.ny };
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6752
|
+
// re-apply on the live node list (if it's already populated
|
|
6753
|
+
// by a prior /fed/status frame)
|
|
6754
|
+
if(typeof _applyPosOverrides === 'function') _applyPosOverrides();
|
|
6755
|
+
}).catch(function(){ /* offline — use slot defaults */ });
|
|
6756
|
+
}catch(e){}
|
|
6492
6757
|
if(lastStatus) onStatus(lastStatus);
|
|
6493
6758
|
resize();
|
|
6494
6759
|
connectStream();
|
|
@@ -115,35 +115,51 @@ function ensureRobovoiceSingleAudioTrack(path) {
|
|
|
115
115
|
return { patched: false, warning: 'voice_agent.py was not found; skipping RoboPark greeting compatibility patch' };
|
|
116
116
|
}
|
|
117
117
|
const source = readFileSync(file, 'utf8');
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
let updated = source;
|
|
119
|
+
let patched = false;
|
|
120
|
+
const newline = source.includes('\r\n') ? '\r\n' : '\n';
|
|
121
|
+
const singleTrackMarker = 'Initial greeting completed through AgentSession audio track';
|
|
121
122
|
const startMarker = ' # Synthesize the greeting first';
|
|
122
123
|
const endMarker = ' except Exception as e:';
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
if (!updated.includes(singleTrackMarker)) {
|
|
125
|
+
const start = updated.indexOf(startMarker);
|
|
126
|
+
const end = start >= 0 ? updated.indexOf(endMarker, start) : -1;
|
|
127
|
+
if (start < 0 || end < 0) {
|
|
128
|
+
return {
|
|
129
|
+
patched: false,
|
|
130
|
+
file,
|
|
131
|
+
warning: 'voice_agent.py does not contain the known duplicate greeting-track block; no automatic patch applied',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const replacement = [
|
|
135
|
+
' # Use the existing conversation track. A dedicated greeting',
|
|
136
|
+
' # track makes Linux robots compete for the same ALSA device.',
|
|
137
|
+
' import inspect as _inspect',
|
|
138
|
+
' speech = session.say(greeting, allow_interruptions=False)',
|
|
139
|
+
' if _inspect.isawaitable(speech):',
|
|
140
|
+
' await speech',
|
|
141
|
+
" elif hasattr(speech, 'wait_for_playout'):",
|
|
142
|
+
' await speech.wait_for_playout()',
|
|
143
|
+
" logger.info('Initial greeting completed through AgentSession audio track')",
|
|
144
|
+
'',
|
|
145
|
+
].join(newline);
|
|
146
|
+
updated = updated.slice(0, start) + replacement + updated.slice(end);
|
|
147
|
+
patched = true;
|
|
131
148
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return { patched: true, file };
|
|
149
|
+
// The job is dispatched after the robot participant joins. Waiting for its
|
|
150
|
+
// camera adds camera startup to voice latency even though audio is ready.
|
|
151
|
+
const cameraGate = / await asyncio\.wait_for\(robot_camera_ready\.wait\(\), timeout\s*=\s*[0-9.]+\)/;
|
|
152
|
+
if (cameraGate.test(updated)) {
|
|
153
|
+
updated = updated.replace(cameraGate, [
|
|
154
|
+
' # The robot is already in the room when this job is dispatched.',
|
|
155
|
+
' # Yield once for subscription signalling; do not gate voice on camera.',
|
|
156
|
+
' await asyncio.sleep(0)',
|
|
157
|
+
].join(newline));
|
|
158
|
+
patched = true;
|
|
159
|
+
}
|
|
160
|
+
if (patched)
|
|
161
|
+
writeFileSync(file, updated, 'utf8');
|
|
162
|
+
return { patched, file };
|
|
147
163
|
}
|
|
148
164
|
export async function roboparkAgentUp(opts) {
|
|
149
165
|
console.log(chalk.bold('\n robopark agent up'));
|
|
@@ -135,10 +135,25 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
135
135
|
// Motion remains enabled, but it reads RoboVision's shared stream. Only
|
|
136
136
|
// RoboVision may open the physical V4L2 device.
|
|
137
137
|
LOCAL_CAMERA_MOTION: 'true',
|
|
138
|
+
// The first production sound is the greeting. A beep would occupy the
|
|
139
|
+
// same exclusive ALSA device and delay or clip TTS.
|
|
140
|
+
ROBOPARK_MOTION_CUE: 'false',
|
|
138
141
|
// Fleet hardware profile: resolve volatile ALSA hw:X,Y coordinates
|
|
139
142
|
// from these USB product labels on every inventory refresh.
|
|
140
143
|
ROBOPARK_AUDIO_INPUT_MATCH: 'Usb Audio Device: USB Audio',
|
|
141
144
|
ROBOPARK_AUDIO_OUTPUT_MATCH: 'USB Audio Device: -',
|
|
145
|
+
// Launch hardware has no acoustic echo cancellation. Suppress mic PCM
|
|
146
|
+
// only while audible robot speech is playing, then hold briefly for
|
|
147
|
+
// room echo decay so VAD cannot interrupt the robot with its own voice.
|
|
148
|
+
ROBOPARK_HALF_DUPLEX: 'true',
|
|
149
|
+
ROBOPARK_ECHO_TAIL_MS: '700',
|
|
150
|
+
ROBOPARK_ECHO_GATE_PEAK: '96',
|
|
151
|
+
// Keep ordinary speaker echo muted but reopen the microphone when a
|
|
152
|
+
// nearby voice rises decisively above the learned acoustic echo floor.
|
|
153
|
+
ROBOPARK_ADAPTIVE_BARGE_IN: 'true',
|
|
154
|
+
ROBOPARK_BARGE_IN_MIN_PEAK: '2200',
|
|
155
|
+
ROBOPARK_BARGE_IN_ECHO_RATIO: '2.4',
|
|
156
|
+
ROBOPARK_BARGE_IN_HOLD_MS: '1400',
|
|
142
157
|
// Inventory changes should reach the Control Center promptly.
|
|
143
158
|
HEARTBEAT_INTERVAL: '5',
|
|
144
159
|
},
|
|
@@ -199,3 +199,42 @@ Required result: `playback_started ok`.
|
|
|
199
199
|
rejects duplicate assignments, limits pulses to 10 seconds, serializes all
|
|
200
200
|
actuation, and de-energizes every registered relay on errors and shutdown.
|
|
201
201
|
- A missing or busy GPIO driver is a failed test, never a simulated success.
|
|
202
|
+
|
|
203
|
+
## Robot Speaker Echo Contract (2.8.100+)
|
|
204
|
+
|
|
205
|
+
- The launch USB speaker/microphone stack has no hardware acoustic echo
|
|
206
|
+
cancellation, so production uses robot-side half-duplex echo suppression.
|
|
207
|
+
- While outbound TTS contains audible PCM, the Pi publishes timing-correct
|
|
208
|
+
silence instead of amplified microphone PCM. The microphone track remains
|
|
209
|
+
connected and resumes after a 700 ms acoustic decay tail.
|
|
210
|
+
- Silent frames on the persistent TTS track do not hold the gate open. Output
|
|
211
|
+
peak detection releases the visitor microphone between responses.
|
|
212
|
+
- `ROBOPARK_HALF_DUPLEX`, `ROBOPARK_ECHO_TAIL_MS`, and
|
|
213
|
+
`ROBOPARK_ECHO_GATE_PEAK` are explicit runtime controls. Disable half-duplex
|
|
214
|
+
only after deploying real acoustic echo cancellation.
|
|
215
|
+
|
|
216
|
+
## Instant Greeting Contract (2.8.101+)
|
|
217
|
+
|
|
218
|
+
- Motion reporting is asynchronous and never delays session allocation.
|
|
219
|
+
- RoboVision's shared camera stream is not stopped or reopened on a trigger.
|
|
220
|
+
- The old fixed 500 ms trigger delay is removed.
|
|
221
|
+
- The local motion beep is disabled by default because it occupies the same
|
|
222
|
+
exclusive ALSA speaker as TTS. `ROBOPARK_MOTION_CUE=true` is diagnostics only.
|
|
223
|
+
- The voice worker uses the persistent AgentSession audio track and starts the
|
|
224
|
+
greeting after robot join; it does not wait for camera subscription.
|
|
225
|
+
- Network, scheduler, LiveKit, and TTS synthesis still impose real latency, but
|
|
226
|
+
RoboPark adds no intentional serial delay before greeting generation.
|
|
227
|
+
|
|
228
|
+
## Adaptive Interruption Contract (2.8.102+)
|
|
229
|
+
|
|
230
|
+
- The preset greeting remains non-interruptible so ambient startup noise cannot
|
|
231
|
+
cancel it before the first audible frame.
|
|
232
|
+
- Normal responses retain LiveKit AgentSession interruption handling.
|
|
233
|
+
- Robot half-duplex suppression learns the microphone's active speaker-echo
|
|
234
|
+
floor. Three consecutive near-field peaks above both the absolute threshold
|
|
235
|
+
and the adaptive echo ratio reopen microphone PCM for the VAD.
|
|
236
|
+
- The barge-in window remains open for 1.4 seconds, allowing LiveKit to cancel
|
|
237
|
+
TTS; ordinary speaker echo remains suppressed with the existing decay tail.
|
|
238
|
+
- Vision responses inherit the selected voice-stack output language. With an
|
|
239
|
+
automatic profile, the original user transcript language is authoritative,
|
|
240
|
+
including direct terminal vision-tool responses.
|
package/package.json
CHANGED