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.
@@ -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), 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.
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: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},
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 the headlights / taillights don't all blink
4100
- // in lockstep — gives a natural "moving at different speeds" feel.
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 — multi-stop radial sun-lit gradient on a slightly larger field ──
4501
- // Field is now sized to the plaza footprint + a small margin. The previous
4502
- // version extended from x=-0.5..PARK_W+0.5, which left a wide empty green
4503
- // strip on either side of the plaza; trimmed here to remove that overhang.
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(154,196,108,0.95)');
4508
- gRad.addColorStop(0.45,'rgba(86,142,80,0.95)');
4509
- gRad.addColorStop(1,'rgba(44,98,52,0.95)');
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
- // ── "mowed stripes" effect — wide diagonal bands of alternating green tones ──
4514
- // Drawn as full-width rectangles in world space, slightly rotated via a parallelogram skew.
4515
- X.save();
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 lighter tufts to add texture (warm bright-green tints)
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<60;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=(ni%2)?'rgba(150,200,110,0.20)':'rgba(56,112,62,0.22)';
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. Add a small y-jitter so the
6104
- // re-entry point doesn't visibly snap.
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
- // bounce across the centerline by switching y + dir
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.78 : -1.02;
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.98+)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.98",
3
+ "version": "2.8.99",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",
@@ -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)