infinicode 2.8.98 → 2.8.99
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 +225 -51
- package/docs/ROBOPARK_PRODUCTION_MEMORY.md +3 -1
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +20 -0
- package/packages/robopark/scheduler/robot_supervisor.py +21 -0
|
@@ -4079,25 +4079,24 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
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
4081
|
// a per-car speed (world units / second), a direction (+1 = right, -1 =
|
|
4082
|
-
// left)
|
|
4083
|
-
//
|
|
4084
|
-
//
|
|
4085
|
-
//
|
|
4086
|
-
// it wraps to the opposite side with a small y jitter so the flow
|
|
4087
|
-
// doesn't visibly reset.
|
|
4082
|
+
// left). y is one of two lane bands (≈ -0.65 for outbound, ≈ -1.18 for
|
|
4083
|
+
// inbound) so cars don't overlap. Speeds are deliberately varied and
|
|
4084
|
+
// fast enough to read as movement at 60fps — the road is short, so
|
|
4085
|
+
// a 4-7s pass across the screen feels natural.
|
|
4088
4086
|
var ROAD_Y0=-1.5, ROAD_Y1=-0.35;
|
|
4089
4087
|
var CARS=[
|
|
4090
|
-
{x:
|
|
4091
|
-
{x:
|
|
4092
|
-
{x:
|
|
4093
|
-
{x:
|
|
4094
|
-
{x:11.
|
|
4095
|
-
{x:
|
|
4096
|
-
{x:13.
|
|
4097
|
-
{x:
|
|
4088
|
+
{x:1.5, y:-0.65, color:'#f2c14e', speed:3.2, dir:+1},
|
|
4089
|
+
{x:4.0, y:-1.18, color:'#7fb4ff', speed:3.6, dir:-1},
|
|
4090
|
+
{x:6.5, y:-0.65, color:'#e0a23c', speed:2.8, dir:+1},
|
|
4091
|
+
{x:9.0, y:-1.18, color:'#ff7e6e', speed:4.0, dir:-1},
|
|
4092
|
+
{x:11.5, y:-0.65, color:'#d8d8e0', speed:3.0, dir:+1},
|
|
4093
|
+
{x:0.5, y:-1.18, color:'#5b8bd6', speed:3.4, dir:-1},
|
|
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},
|
|
4098
4096
|
];
|
|
4099
|
-
// Per-car phase offsets so
|
|
4100
|
-
// in lockstep — gives a natural "
|
|
4097
|
+
// Per-car phase offsets so headlights / taillights / wheels don't all
|
|
4098
|
+
// blink in lockstep — gives a natural "different cars, different
|
|
4099
|
+
// speeds" feel.
|
|
4101
4100
|
var _carPhase = CARS.map(function(_, i){ return i * 0.7; });
|
|
4102
4101
|
// ── Walking people — looping sprites that stroll along fixed plaza paths.
|
|
4103
4102
|
// Each walker is a small canvas-drawn figure (head + body + two
|
|
@@ -4497,37 +4496,24 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4497
4496
|
if(typeof now !== 'number') now = performance.now();
|
|
4498
4497
|
var _now = now;
|
|
4499
4498
|
var z=cam.z;
|
|
4500
|
-
// ── grass base —
|
|
4501
|
-
//
|
|
4502
|
-
//
|
|
4503
|
-
//
|
|
4499
|
+
// ── grass base — single uniform mid-green, no radial brightening ──
|
|
4500
|
+
// Park sits on a floating island now, so we want a flat, calm
|
|
4501
|
+
// grass tone across the whole field. The previous radial gradient
|
|
4502
|
+
// pushed a lighter green toward the center which read as a "spot"
|
|
4503
|
+
// — replaced with a single soft fill so the island feels like a
|
|
4504
|
+
// single piece of turf.
|
|
4504
4505
|
var parkCx=PARK_W/2, parkCy=PARK_D/2;
|
|
4505
4506
|
var gCent=iso(parkCx,parkCy);
|
|
4506
4507
|
var gRad=X.createRadialGradient(gCent.x,gCent.y,12*z,gCent.x,gCent.y,Math.max(W,H)*0.62);
|
|
4507
|
-
gRad.addColorStop(0,'rgba(
|
|
4508
|
-
gRad.addColorStop(0.
|
|
4509
|
-
gRad.addColorStop(1,'rgba(
|
|
4508
|
+
gRad.addColorStop(0,'rgba(94,150,84,0.95)');
|
|
4509
|
+
gRad.addColorStop(0.5,'rgba(86,140,76,0.95)');
|
|
4510
|
+
gRad.addColorStop(1,'rgba(78,128,68,0.95)');
|
|
4510
4511
|
// grass field — tight margin around the plaza + lot, NOT covering the road or the beach
|
|
4511
4512
|
var GF_X0=0.0, GF_X1=PARK_W, GF_Y0=-0.32, GF_Y1=PARK_D-0.55;
|
|
4512
4513
|
poly([[GF_X0,GF_Y0],[GF_X1,GF_Y0],[GF_X1,GF_Y1],[GF_X0,GF_Y1]], gRad, 'rgba(140,200,150,0.10)',1);
|
|
4513
|
-
//
|
|
4514
|
-
//
|
|
4515
|
-
|
|
4516
|
-
var stripeColors=['rgba(108,162,90,0.45)','rgba(86,140,72,0.35)'];
|
|
4517
|
-
var stripeCount=5;
|
|
4518
|
-
var stripeW=(GF_X1-GF_X0+GF_Y1-GF_Y0+2)/stripeCount;
|
|
4519
|
-
for(var si=0;si<stripeCount;si++){
|
|
4520
|
-
var off=si*stripeW - ((GF_X1-GF_X0)+(GF_Y1-GF_Y0))*0.5;
|
|
4521
|
-
// Build a parallelogram in world coords that runs from left-bottom to right-top
|
|
4522
|
-
// at a slight angle (skewed +X, -Y direction) — looks like a stadium mowed lawn.
|
|
4523
|
-
// Tightly fitted to the grass field bounds.
|
|
4524
|
-
var p1=[GF_X0 + off, GF_Y1];
|
|
4525
|
-
var p2=[GF_X0 + off + 1.0, GF_Y0];
|
|
4526
|
-
var p3=[GF_X0 + off + 1.0 + (GF_X1-GF_X0), GF_Y0];
|
|
4527
|
-
var p4=[GF_X0 + off + (GF_X1-GF_X0), GF_Y1];
|
|
4528
|
-
poly([p1,p2,p3,p4], stripeColors[si%2], null, 0);
|
|
4529
|
-
}
|
|
4530
|
-
X.restore();
|
|
4514
|
+
// (mowed-stripe effect removed — was the lighter green alternating
|
|
4515
|
+
// bands on the sides of the park. Keeping the lawn as a single
|
|
4516
|
+
// uniform tone so the park reads as a clean floating island.)
|
|
4531
4517
|
// ── outer hedge / foliage border just inside the grass field ──
|
|
4532
4518
|
// Slightly ragged inner edge: extra points along each side to break the straight line.
|
|
4533
4519
|
// The hedge wraps the grass field, which now hugs the plaza footprint (no side overhang).
|
|
@@ -4598,16 +4584,197 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4598
4584
|
X.closePath();
|
|
4599
4585
|
X.strokeStyle='rgba(96,148,86,0.7)'; X.lineWidth=0.8*z; X.stroke();
|
|
4600
4586
|
X.restore();
|
|
4601
|
-
// grass noise — scattered
|
|
4587
|
+
// grass noise — scattered darker tufts for texture (no lighter
|
|
4588
|
+
// tufts, so the field stays a single uniform mid-green)
|
|
4602
4589
|
X.save();
|
|
4603
|
-
for(var ni=0;ni<
|
|
4590
|
+
for(var ni=0;ni<40;ni++){
|
|
4604
4591
|
var gnx=GF_X0 + ((ni*37)%100)/100*(GF_X1-GF_X0);
|
|
4605
4592
|
var gny=GF_Y0 + ((ni*53)%100)/100*(GF_Y1-GF_Y0);
|
|
4606
4593
|
var gp=iso(gnx,gny);
|
|
4607
|
-
X.fillStyle=
|
|
4594
|
+
X.fillStyle='rgba(56,112,62,0.22)';
|
|
4608
4595
|
X.beginPath(); X.arc(gp.x+((ni*7)%13-6)*z,gp.y+((ni*11)%9-4)*z,1.6*z,0,7); X.fill();
|
|
4609
4596
|
}
|
|
4610
4597
|
X.restore();
|
|
4598
|
+
// ── GOLD BORDER + FLOATING ISLAND DEPTH ──
|
|
4599
|
+
// The whole park now reads as a single floating slab of turf in
|
|
4600
|
+
// the sky. We render (in world space, around the grass field):
|
|
4601
|
+
// 1. a soft drop-shadow ellipse on the sky below the island
|
|
4602
|
+
// 2. a tapered rocky underside (darker, hanging from the edge)
|
|
4603
|
+
// 3. a thick gold rim tracing the grass perimeter
|
|
4604
|
+
// 4. a thin warm-glow outer ring (sells the gold as "lit by
|
|
4605
|
+
// the warm sun")
|
|
4606
|
+
// The underside and drop shadow give the park real depth so it
|
|
4607
|
+
// doesn't look like a flat sticker on the sky.
|
|
4608
|
+
// Build the grass perimeter polygon once (offset slightly outward
|
|
4609
|
+
// so the gold rim sits just outside the turf edge).
|
|
4610
|
+
var GRASS_RIM = 0.18; // world units of gold rim thickness
|
|
4611
|
+
function grassPerim(offset){
|
|
4612
|
+
// Use the same world corners as the grass field, offset outward.
|
|
4613
|
+
var o = offset;
|
|
4614
|
+
return [
|
|
4615
|
+
[GF_X0-o, GF_Y0-o],
|
|
4616
|
+
[GF_X1+o, GF_Y0-o],
|
|
4617
|
+
[GF_X1+o, GF_Y1+o],
|
|
4618
|
+
[GF_X0-o, GF_Y1+o]
|
|
4619
|
+
];
|
|
4620
|
+
}
|
|
4621
|
+
// 1. drop shadow on the sky — a wide soft ellipse below the
|
|
4622
|
+
// island's bottom edge (in screen space, large + soft).
|
|
4623
|
+
var botL = iso(GF_X0, GF_Y1);
|
|
4624
|
+
var botR = iso(GF_X1, GF_Y1);
|
|
4625
|
+
var botM = { x:(botL.x+botR.x)/2, y:(botL.y+botR.y)/2 };
|
|
4626
|
+
var islandWidth = Math.abs(botR.x - botL.x);
|
|
4627
|
+
var shadowCx = botM.x;
|
|
4628
|
+
var shadowCy = botM.y + 28*z;
|
|
4629
|
+
var shadowRX = islandWidth * 0.55;
|
|
4630
|
+
var shadowRY = 22 * z;
|
|
4631
|
+
var dropShadow = X.createRadialGradient(shadowCx, shadowCy, 0, shadowCx, shadowCy, shadowRX);
|
|
4632
|
+
dropShadow.addColorStop(0, 'rgba(20, 24, 36, 0.55)');
|
|
4633
|
+
dropShadow.addColorStop(0.5, 'rgba(20, 24, 36, 0.25)');
|
|
4634
|
+
dropShadow.addColorStop(1, 'rgba(20, 24, 36, 0)');
|
|
4635
|
+
X.fillStyle = dropShadow;
|
|
4636
|
+
X.beginPath();
|
|
4637
|
+
X.ellipse(shadowCx, shadowCy, shadowRX, shadowRY, 0, 0, Math.PI*2);
|
|
4638
|
+
X.fill();
|
|
4639
|
+
// 2. rocky underside — draw the grass field polygon again, but
|
|
4640
|
+
// offset down by ~14*z and slightly darker, then close it into
|
|
4641
|
+
// a wedge. We do this with two filled paths:
|
|
4642
|
+
// - the bottom face: same perimeter polygon shifted down
|
|
4643
|
+
// - the side rim: a band that traces from the top of the
|
|
4644
|
+
// bottom face up to the top of the island (so the side
|
|
4645
|
+
// of the slab reads as dirt/rock)
|
|
4646
|
+
var UNDERSIDE_LIFT = 16 * z;
|
|
4647
|
+
var topPerim = grassPerim(0);
|
|
4648
|
+
var botPerim = topPerim.map(function(p){ return [p[0], p[1] + UNDERSIDE_LIFT / (TH*z) * (1/22)]; });
|
|
4649
|
+
// Convert botPerim into screen-space points (slightly down from top).
|
|
4650
|
+
var topScreen = topPerim.map(function(p){ return iso(p[0], p[1]); });
|
|
4651
|
+
var botScreen = topPerim.map(function(p){ return iso(p[0], p[1] + 1.0); });
|
|
4652
|
+
// right side face
|
|
4653
|
+
X.save();
|
|
4654
|
+
X.fillStyle = 'rgba(38, 50, 36, 0.92)';
|
|
4655
|
+
X.beginPath();
|
|
4656
|
+
X.moveTo(topScreen[1].x, topScreen[1].y);
|
|
4657
|
+
X.lineTo(topScreen[2].x, topScreen[2].y);
|
|
4658
|
+
X.lineTo(botScreen[2].x, botScreen[2].y + UNDERSIDE_LIFT);
|
|
4659
|
+
X.lineTo(botScreen[1].x, botScreen[1].y + UNDERSIDE_LIFT);
|
|
4660
|
+
X.closePath();
|
|
4661
|
+
X.fill();
|
|
4662
|
+
// bottom-right edge highlight (slight bevel)
|
|
4663
|
+
X.strokeStyle = 'rgba(80, 96, 60, 0.55)';
|
|
4664
|
+
X.lineWidth = 1.0*z;
|
|
4665
|
+
X.beginPath();
|
|
4666
|
+
X.moveTo(topScreen[2].x, topScreen[2].y);
|
|
4667
|
+
X.lineTo(botScreen[2].x, botScreen[2].y + UNDERSIDE_LIFT);
|
|
4668
|
+
X.stroke();
|
|
4669
|
+
// left side face
|
|
4670
|
+
X.fillStyle = 'rgba(28, 38, 26, 0.95)';
|
|
4671
|
+
X.beginPath();
|
|
4672
|
+
X.moveTo(topScreen[0].x, topScreen[0].y);
|
|
4673
|
+
X.lineTo(topScreen[1].x, topScreen[1].y);
|
|
4674
|
+
X.lineTo(botScreen[1].x, botScreen[1].y + UNDERSIDE_LIFT);
|
|
4675
|
+
X.lineTo(botScreen[0].x, botScreen[0].y + UNDERSIDE_LIFT);
|
|
4676
|
+
X.closePath();
|
|
4677
|
+
X.fill();
|
|
4678
|
+
// bottom-left edge highlight
|
|
4679
|
+
X.strokeStyle = 'rgba(70, 84, 52, 0.45)';
|
|
4680
|
+
X.beginPath();
|
|
4681
|
+
X.moveTo(topScreen[0].x, topScreen[0].y);
|
|
4682
|
+
X.lineTo(botScreen[0].x, botScreen[0].y + UNDERSIDE_LIFT);
|
|
4683
|
+
X.stroke();
|
|
4684
|
+
// bottom face — slightly tapered into a rough point so the slab
|
|
4685
|
+
// doesn't look like a flat box. We taper the bottom by ~25%.
|
|
4686
|
+
var tapered = topScreen.map(function(s, i){
|
|
4687
|
+
if(i === 0) return { x: s.x + 40*z, y: s.y + UNDERSIDE_LIFT*0.95 };
|
|
4688
|
+
if(i === 1) return { x: s.x - 40*z, y: s.y + UNDERSIDE_LIFT*0.95 };
|
|
4689
|
+
if(i === 2) return { x: s.x - 60*z, y: s.y + UNDERSIDE_LIFT*0.85 };
|
|
4690
|
+
if(i === 3) return { x: s.x + 60*z, y: s.y + UNDERSIDE_LIFT*0.85 };
|
|
4691
|
+
return { x: s.x, y: s.y + UNDERSIDE_LIFT };
|
|
4692
|
+
});
|
|
4693
|
+
X.fillStyle = 'rgba(20, 28, 20, 0.92)';
|
|
4694
|
+
X.beginPath();
|
|
4695
|
+
X.moveTo(topScreen[0].x, topScreen[0].y);
|
|
4696
|
+
X.lineTo(topScreen[1].x, topScreen[1].y);
|
|
4697
|
+
X.lineTo(tapered[1].x, tapered[1].y);
|
|
4698
|
+
X.lineTo(tapered[2].x, tapered[2].y);
|
|
4699
|
+
X.lineTo(tapered[3].x, tapered[3].y);
|
|
4700
|
+
X.lineTo(tapered[0].x, tapered[0].y);
|
|
4701
|
+
X.closePath();
|
|
4702
|
+
X.fill();
|
|
4703
|
+
// a few rocky stalactite bumps hanging from the underside so it
|
|
4704
|
+
// doesn't look like a perfect box either
|
|
4705
|
+
var bumpSpots = [
|
|
4706
|
+
{ px: 0.18, len: 0.65 },
|
|
4707
|
+
{ px: 0.34, len: 0.85 },
|
|
4708
|
+
{ px: 0.52, len: 0.55 },
|
|
4709
|
+
{ px: 0.68, len: 0.95 },
|
|
4710
|
+
{ px: 0.86, len: 0.70 }
|
|
4711
|
+
];
|
|
4712
|
+
for(var bpi=0; bpi<bumpSpots.length; bpi++){
|
|
4713
|
+
var bx = GF_X0 + bumpSpots[bpi].px * (GF_X1-GF_X0);
|
|
4714
|
+
var by = GF_Y1;
|
|
4715
|
+
var bp = iso(bx, by);
|
|
4716
|
+
var bpLen = bumpSpots[bpi].len * UNDERSIDE_LIFT;
|
|
4717
|
+
X.fillStyle = 'rgba(24, 32, 22, 0.88)';
|
|
4718
|
+
X.beginPath();
|
|
4719
|
+
X.moveTo(bp.x - 14*z, bp.y + 2*z);
|
|
4720
|
+
// 2-sided stalactite that tapers to a point
|
|
4721
|
+
X.quadraticCurveTo(bp.x, bp.y + bpLen*0.5, bp.x, bp.y + bpLen);
|
|
4722
|
+
X.quadraticCurveTo(bp.x, bp.y + bpLen*0.5, bp.x + 14*z, bp.y + 2*z);
|
|
4723
|
+
X.closePath();
|
|
4724
|
+
X.fill();
|
|
4725
|
+
}
|
|
4726
|
+
X.restore();
|
|
4727
|
+
// 3. GOLD RIM — thick gold band tracing the grass perimeter.
|
|
4728
|
+
// Drawn AFTER the underside so the top edge of the gold
|
|
4729
|
+
// sits flush with the top of the grass. Two strokes: a wide
|
|
4730
|
+
// gold core + a thin lighter inner highlight on the top edge.
|
|
4731
|
+
var rimOuter = grassPerim(GRASS_RIM);
|
|
4732
|
+
var rimInner = grassPerim(0);
|
|
4733
|
+
var rimOuterScreen = rimOuter.map(function(p){ return iso(p[0], p[1]); });
|
|
4734
|
+
var rimInnerScreen = rimInner.map(function(p){ return iso(p[0], p[1]); });
|
|
4735
|
+
X.save();
|
|
4736
|
+
// outer glow
|
|
4737
|
+
var glowGrad = X.createLinearGradient(0, rimOuterScreen[0].y - 30, 0, rimOuterScreen[3].y + 30);
|
|
4738
|
+
X.fillStyle = 'rgba(255, 200, 110, 0.0)';
|
|
4739
|
+
// fill the rim band as a quad
|
|
4740
|
+
X.beginPath();
|
|
4741
|
+
X.moveTo(rimOuterScreen[0].x, rimOuterScreen[0].y);
|
|
4742
|
+
X.lineTo(rimOuterScreen[1].x, rimOuterScreen[1].y);
|
|
4743
|
+
X.lineTo(rimOuterScreen[2].x, rimOuterScreen[2].y);
|
|
4744
|
+
X.lineTo(rimOuterScreen[3].x, rimOuterScreen[3].y);
|
|
4745
|
+
X.closePath();
|
|
4746
|
+
X.fill();
|
|
4747
|
+
// gold band via two strokes: thick base + thin highlight
|
|
4748
|
+
X.strokeStyle = 'rgba(255, 196, 92, 0.95)'; // base gold
|
|
4749
|
+
X.lineWidth = 5.0*z;
|
|
4750
|
+
X.beginPath();
|
|
4751
|
+
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
4752
|
+
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
4753
|
+
X.lineTo(rimInnerScreen[2].x, rimInnerScreen[2].y);
|
|
4754
|
+
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4755
|
+
X.closePath();
|
|
4756
|
+
X.stroke();
|
|
4757
|
+
// inner highlight (thinner, brighter)
|
|
4758
|
+
X.strokeStyle = 'rgba(255, 232, 158, 0.95)';
|
|
4759
|
+
X.lineWidth = 1.4*z;
|
|
4760
|
+
X.beginPath();
|
|
4761
|
+
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
4762
|
+
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
4763
|
+
X.lineTo(rimInnerScreen[2].x, rimInnerScreen[2].y);
|
|
4764
|
+
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4765
|
+
X.closePath();
|
|
4766
|
+
X.stroke();
|
|
4767
|
+
// outer deep-gold edge (slightly darker outer stroke for definition)
|
|
4768
|
+
X.strokeStyle = 'rgba(190, 134, 48, 0.85)';
|
|
4769
|
+
X.lineWidth = 1.2*z;
|
|
4770
|
+
X.beginPath();
|
|
4771
|
+
X.moveTo(rimOuterScreen[0].x, rimOuterScreen[0].y);
|
|
4772
|
+
X.lineTo(rimOuterScreen[1].x, rimOuterScreen[1].y);
|
|
4773
|
+
X.lineTo(rimOuterScreen[2].x, rimOuterScreen[2].y);
|
|
4774
|
+
X.lineTo(rimOuterScreen[3].x, rimOuterScreen[3].y);
|
|
4775
|
+
X.closePath();
|
|
4776
|
+
X.stroke();
|
|
4777
|
+
X.restore();
|
|
4611
4778
|
// ── flower spots — deterministic small dots in 3 colors, scattered in the lawn only ──
|
|
4612
4779
|
X.save();
|
|
4613
4780
|
var flowerColors=['rgba(232,124,164,0.85)','rgba(248,220,96,0.88)','rgba(248,248,240,0.85)'];
|
|
@@ -6095,21 +6262,21 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6095
6262
|
}
|
|
6096
6263
|
// Cars share the same elapsed-dt clock so a tab-switch doesn't
|
|
6097
6264
|
// catapult every car off the road at once. speed is world units
|
|
6098
|
-
// per second, dt is milliseconds.
|
|
6265
|
+
// per second, dt is milliseconds. cdt is the already-clamped dt.
|
|
6099
6266
|
var cdt = Math.min(64, dt);
|
|
6100
6267
|
for(var ci=0; ci<CARS.length; ci++){
|
|
6101
6268
|
var car = CARS[ci];
|
|
6102
6269
|
car.x += car.dir * car.speed * (cdt/1000);
|
|
6103
|
-
// Wrap the car around the road.
|
|
6104
|
-
// re-
|
|
6270
|
+
// Wrap the car around the road. When it leaves one end, it
|
|
6271
|
+
// re-enters the opposite end on the other lane so oncoming
|
|
6272
|
+
// traffic feels continuous.
|
|
6105
6273
|
if(car.dir > 0 && car.x > PARK_W + 0.5){
|
|
6106
6274
|
car.x = -0.5;
|
|
6107
|
-
|
|
6108
|
-
car.y = (car.y < -0.9) ? -0.78 : -1.02;
|
|
6275
|
+
car.y = (car.y < -0.9) ? -0.65 : -1.18;
|
|
6109
6276
|
car.dir = (car.y < -0.9) ? +1 : -1;
|
|
6110
6277
|
} else if(car.dir < 0 && car.x < -0.5){
|
|
6111
6278
|
car.x = PARK_W + 0.5;
|
|
6112
|
-
car.y = (car.y < -0.9) ? -0.
|
|
6279
|
+
car.y = (car.y < -0.9) ? -0.65 : -1.18;
|
|
6113
6280
|
car.dir = (car.y < -0.9) ? +1 : -1;
|
|
6114
6281
|
}
|
|
6115
6282
|
}
|
|
@@ -7025,7 +7192,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7025
7192
|
+ buildShellSection(schedId(n))
|
|
7026
7193
|
+ 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
7194
|
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>');
|
|
7195
|
+
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><button class="rp-btn danger" id="pk-motor-test-all">Test all registered GPIO</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-note">Full GPIO test pulses registered relays only: 300 ms on, 200 ms pause, one at a time.</div><div class="pk-motor-status" id="pk-motor-status">Loading motor profile...</div>');
|
|
7029
7196
|
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>');
|
|
7030
7197
|
// Put live hardware selection beside the camera and audio tests,
|
|
7031
7198
|
// ahead of persona and service administration in the Park drawer.
|
|
@@ -7055,6 +7222,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7055
7222
|
var motorStep=$('pk-motor-step-add');if(motorStep)motorStep.addEventListener('click',function(){motorEditorAddStep();});
|
|
7056
7223
|
var motorSave=$('pk-motor-save');if(motorSave)motorSave.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}saveMotorProfile(n);});
|
|
7057
7224
|
var motorRun=$('pk-motor-run');if(motorRun)motorRun.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}runMotorSequence(n);});
|
|
7225
|
+
var motorTestAll=$('pk-motor-test-all');if(motorTestAll)motorTestAll.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}testAllMotorRelays(n);});
|
|
7058
7226
|
var motorSeq=$('pk-motor-sequence');if(motorSeq)motorSeq.addEventListener('change',function(){selectMotorSequence(motorSeq.value);});
|
|
7059
7227
|
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
7228
|
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();}});
|
|
@@ -7165,6 +7333,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7165
7333
|
captureMotorEditor();var motor=motorProfile.registry[index];if(!motor)return;var robotId=schedId(n);motorStatus('Saving profile before safe 300 ms '+motor.name+' test...');
|
|
7166
7334
|
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
7335
|
}
|
|
7336
|
+
function testAllMotorRelays(n){
|
|
7337
|
+
captureMotorEditor();if(!motorProfile.registry.length){motorStatus('Register at least one relay before running the GPIO test.','bad');return;}
|
|
7338
|
+
if(!window.confirm('Pulse all '+motorProfile.registry.length+' registered relays one at a time? Ensure the motor area is clear.'))return;
|
|
7339
|
+
var robotId=schedId(n);motorStatus('Saving profile and testing '+motorProfile.registry.length+' registered GPIO relay(s), one at a time...');
|
|
7340
|
+
saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/test-all?duration_ms=300&pause_ms=200',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','gpio-test-all');}).then(function(result){motorStatus(result.ok?'Full registered GPIO test completed: '+(result.completed_steps||[]).length+' relay(s).':'GPIO test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');}).catch(function(error){motorStatus('GPIO test failed: '+error.message,'bad');});
|
|
7341
|
+
}
|
|
7168
7342
|
|
|
7169
7343
|
var pipelineHistoryPage=1;
|
|
7170
7344
|
var PIPELINE_LABELS={
|
|
@@ -179,12 +179,14 @@ Required result: `playback_started ok`.
|
|
|
179
179
|
measurement, and fallback resampling with internal PCM16 helpers; production
|
|
180
180
|
capture must not import `audioop`.
|
|
181
181
|
|
|
182
|
-
## Motor and Relay Contract (2.8.
|
|
182
|
+
## Motor and Relay Contract (2.8.99+)
|
|
183
183
|
|
|
184
184
|
- Each robot persists its own motor registry, timed sequences, local motor API
|
|
185
185
|
URL, and optional greeting sequence in the scheduler database.
|
|
186
186
|
- The Park robot drawer is the control surface for registry edits, BCM relay
|
|
187
187
|
tests, sequence authoring, manual runs, and greeting assignment.
|
|
188
|
+
- The full GPIO diagnostic requires operator confirmation and pulses every
|
|
189
|
+
registered relay sequentially. It never scans or energizes unregistered pins.
|
|
188
190
|
- Greeting motion queues only after the robot reports `playback_started`, so
|
|
189
191
|
movement aligns with audible TTS instead of session allocation.
|
|
190
192
|
- Voice agents may invoke only saved sequences for their active LiveKit room
|
package/package.json
CHANGED
|
@@ -4030,6 +4030,26 @@ async def test_robot_motor(robot_id: str, motor_id: str, duration_ms: int = 300)
|
|
|
4030
4030
|
"steps": [{"motor_id": motor_id, "delay_ms": 0, "duration_ms": duration_ms}],
|
|
4031
4031
|
})
|
|
4032
4032
|
return {"queued": True, "request_id": request_id, "device_id": model.id}
|
|
4033
|
+
|
|
4034
|
+
@app.post("/api/robots/{robot_id}/motors/test-all")
|
|
4035
|
+
async def test_all_robot_motors(robot_id: str, duration_ms: int = 300, pause_ms: int = 200):
|
|
4036
|
+
device = await _resolve_device_by_id_or_name(robot_id)
|
|
4037
|
+
model = _device_row_to_model(device)
|
|
4038
|
+
if not model.motor_registry or not model.motor_server_url:
|
|
4039
|
+
raise HTTPException(409, "Robot motor registry or motor server is not configured")
|
|
4040
|
+
duration_ms = max(50, min(1000, int(duration_ms)))
|
|
4041
|
+
pause_ms = max(0, min(2000, int(pause_ms)))
|
|
4042
|
+
request_id = await _queue_shell_request_async(model.id, "motor_sequence", "motor_server", {
|
|
4043
|
+
"motor_server_url": model.motor_server_url,
|
|
4044
|
+
"sequence_id": "registered-gpio-test",
|
|
4045
|
+
"registry": model.motor_registry,
|
|
4046
|
+
"steps": [],
|
|
4047
|
+
"test_all": True,
|
|
4048
|
+
"duration_ms": duration_ms,
|
|
4049
|
+
"pause_ms": pause_ms,
|
|
4050
|
+
})
|
|
4051
|
+
return {"queued": True, "request_id": request_id, "device_id": model.id,
|
|
4052
|
+
"motor_count": len(model.motor_registry)}
|
|
4033
4053
|
|
|
4034
4054
|
@app.post("/api/devices/{device_id}/request-session")
|
|
4035
4055
|
async def device_request_session(device_id: str,
|
|
@@ -453,6 +453,27 @@ def _run_motor_sequence(params: dict) -> dict:
|
|
|
453
453
|
response = (client.put(f"{base}/update-motor/{motor_id}", json=body)
|
|
454
454
|
if motor_id in existing_names else client.post(f"{base}/add-motor", json=body))
|
|
455
455
|
response.raise_for_status()
|
|
456
|
+
if params.get("test_all"):
|
|
457
|
+
duration_ms = max(50, min(1000, int(params.get("duration_ms", 300))))
|
|
458
|
+
pause_ms = max(0, min(2000, int(params.get("pause_ms", 200))))
|
|
459
|
+
response = client.post(f"{base}/test", json={
|
|
460
|
+
"seconds_on": duration_ms / 1000.0,
|
|
461
|
+
"seconds_pause": pause_ms / 1000.0,
|
|
462
|
+
"pins": [int(motor["gpio"]) for motor in registry.values()],
|
|
463
|
+
})
|
|
464
|
+
response.raise_for_status()
|
|
465
|
+
deadline = time.monotonic() + len(registry) * ((duration_ms + pause_ms) / 1000.0) + 3.0
|
|
466
|
+
while time.monotonic() < deadline:
|
|
467
|
+
status = client.get(f"{base}/status").json()
|
|
468
|
+
if status.get("status") == "idle":
|
|
469
|
+
if status.get("error"):
|
|
470
|
+
raise RuntimeError(f"registered GPIO test failed: {status['error']}")
|
|
471
|
+
completed = [{"motor_id": motor_id, "gpio": int(motor["gpio"])}
|
|
472
|
+
for motor_id, motor in registry.items()]
|
|
473
|
+
break
|
|
474
|
+
time.sleep(0.05)
|
|
475
|
+
else:
|
|
476
|
+
raise TimeoutError("registered GPIO test did not return to idle")
|
|
456
477
|
for index, step in enumerate(steps):
|
|
457
478
|
motor_id = str(step.get("motor_id"))
|
|
458
479
|
motor = registry.get(motor_id)
|