infinicode 2.8.103 → 2.8.104

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.
@@ -1132,7 +1132,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1132
1132
  <div class="rp-quickrow">
1133
1133
  <div class="rp-field"><label>Robot name</label><input id="rb-name" placeholder="jaguar"></div>
1134
1134
  <div class="rp-field"><label>Character preset</label><select id="rb-char"><option value="">— auto (matched from name) —</option></select></div>
1135
- <div class="rp-field"><label>Robot network</label><select id="rb-network"><option value="auto">Auto from hub address</option><option value="lan">LAN</option><option value="tailscale">Tailscale</option></select></div>
1135
+ <div class="rp-field"><label>Robot network</label><select id="rb-network"><option value="lan">LAN (onsite default)</option><option value="tailscale">Tailscale</option></select></div>
1136
1136
  <div class="rp-field"><label>Mesh token</label><input id="rb-token" spellcheck="false" autocomplete="off" placeholder="required in generated command"></div>
1137
1137
  <button class="rp-btn primary" id="rb-add" style="height:fit-content">Add robot</button>
1138
1138
  </div>
@@ -1175,7 +1175,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1175
1175
  <div class="rp-config-hero-actions"><button class="rp-btn" id="rp-command-reset">Reset environment</button><button class="rp-btn primary" id="rp-command-copy-all">Copy visible commands</button></div>
1176
1176
  </div>
1177
1177
  <div class="rp-command-env">
1178
- <div class="rp-field"><label>Hub LAN IP</label><input id="rp-command-lan" value="192.168.1.12" spellcheck="false"></div>
1178
+ <div class="rp-field"><label>Hub LAN IP</label><input id="rp-command-lan" value="192.168.0.9" spellcheck="false"></div>
1179
1179
  <div class="rp-field"><label>Hub Tailscale IP / DNS</label><input id="rp-command-tail" value="100.84.147.24" spellcheck="false"></div>
1180
1180
  <div class="rp-field"><label>Site</label><input id="rp-command-site" value="Tel Aviv"></div>
1181
1181
  <div class="rp-field"><label>Robot name</label><input id="rp-command-robot" value="bmw" spellcheck="false"></div>
@@ -2262,10 +2262,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
2262
2262
  // needed to see it; 'robopark add-robot' mints its own token when run ──
2263
2263
  function buildAddRobotCommand(name, characterId){
2264
2264
  if(!name) return '';
2265
- var hubUrl=location.origin.replace(/\\/+$/,'');
2265
+ var network=$('rb-network')&&$('rb-network').value||'lan';
2266
+ var commandEnv=rpCommandEnv();
2267
+ var hubHost=network==='tailscale'?commandEnv.tail:commandEnv.lan;
2268
+ var hubUrl=/^https?:\/\//i.test(hubHost)?hubHost.replace(/\\/+$/,''):'http://'+hubHost.replace(/\\/+$/,'')+':47913';
2266
2269
  var schedulerUrl=hubUrl+'/robopark';
2267
- var requestedNetwork=$('rb-network')&&$('rb-network').value||'auto';
2268
- var network=requestedNetwork==='auto'?(/^100\\./.test(location.hostname)||/\\.ts\\.net$/i.test(location.hostname)?'tailscale':'lan'):requestedNetwork;
2269
2270
  var argv=['robopark','add-robot',name];
2270
2271
  if(characterId) argv.push('--character', characterId);
2271
2272
  var site=$('rb-site')&&$('rb-site').value.trim();if(site)argv.push('--site',site);
@@ -2672,7 +2673,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2672
2673
  var RP_COMMAND_ENV_KEY='rp-command-env-v1';
2673
2674
  var RP_CUSTOM_COMMANDS_KEY='rp-custom-commands-v1';
2674
2675
  var rpVisibleCommands=[];
2675
- function rpCommandDefaults(){return {lan:'192.168.1.12',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:dashboardMeshToken()};}
2676
+ function rpCommandDefaults(){return {lan:'192.168.0.9',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:dashboardMeshToken()};}
2676
2677
  function rpLoadStoredJson(key,fallback){try{var value=JSON.parse(localStorage.getItem(key)||'null');return value&&typeof value==='object'?value:fallback;}catch(e){return fallback;}}
2677
2678
  function rpCommandEnv(){
2678
2679
  var d=rpCommandDefaults(),s=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
@@ -7611,7 +7612,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7611
7612
  function motorStatus(message,kind){var host=$('pk-motor-status');if(host){host.textContent=message;host.className='pk-motor-status '+(kind||'');}}
7612
7613
  function loadMotorProfile(n){
7613
7614
  var robotId=schedId(n);motorStatus('Loading saved motor profile...');
7614
- getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');}).catch(function(error){motorStatus('Motor profile unavailable: '+error.message,'bad');});
7615
+ getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||data.motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();if(motorProfile.registry.length){motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');return;}motorStatus('No central registry; discovering robot-local relays...');return discoverMotorRelays(n,true);}).catch(function(error){motorStatus('Motor profile unavailable: '+error.message,'bad');});
7615
7616
  }
7616
7617
  function saveMotorProfile(n){
7617
7618
  captureMotorEditor();var robotId=schedId(n),url=motorProfile.motor_server_url;
@@ -7628,11 +7629,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
7628
7629
  captureMotorEditor();var motor=motorProfile.registry[index];if(!motor)return;var robotId=schedId(n);motorStatus('Saving profile before safe 300 ms '+motor.name+' test...');
7629
7630
  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');});
7630
7631
  }
7632
+ function discoverMotorRelays(n,automatic){
7633
+ var robotId=schedId(n);motorStatus((automatic?'Automatically discovering':'Discovering')+' relays from the robot motor server...');
7634
+ return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/discover',{}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','motor-discover');}).then(function(result){
7635
+ if(!result.ok)throw new Error(result.error||'robot motor discovery failed');
7636
+ if(!result.registry||!result.registry.length)throw new Error('Robot motor server has no registered relays. Register the wired BCM pins once; unknown pins will not be pulsed.');
7637
+ motorProfile.registry=result.registry;renderMotorEditor();motorStatus('Discovered '+result.registry.length+' robot-local relay(s); saving centrally...','ok');return saveMotorProfile(n);
7638
+ });
7639
+ }
7631
7640
  function testAllMotorRelays(n){
7632
- captureMotorEditor();if(!motorProfile.registry.length){motorStatus('Register at least one relay before running the GPIO test.','bad');return;}
7633
- if(!window.confirm('Pulse all '+motorProfile.registry.length+' registered relays one at a time? Ensure the motor area is clear.'))return;
7634
- var robotId=schedId(n);motorStatus('Saving profile and testing '+motorProfile.registry.length+' registered GPIO relay(s), one at a time...');
7635
- 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');});
7641
+ captureMotorEditor();var ready=motorProfile.registry.length?Promise.resolve():discoverMotorRelays(n,true);
7642
+ ready.then(function(){if(!window.confirm('Pulse all '+motorProfile.registry.length+' registered relays one at a time? Ensure the motor area is clear.'))return null;var robotId=schedId(n);motorStatus('Saving profile and testing '+motorProfile.registry.length+' registered GPIO relay(s), one at a time...');return 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');});
7636
7643
  }
7637
7644
 
7638
7645
  var pipelineHistoryPage=1;
@@ -50,11 +50,18 @@ function schedulerApiUrl(schedulerUrl, path, token) {
50
50
  }
51
51
  async function mintEnrollmentToken(schedulerUrl, name, characterId, token) {
52
52
  const url = schedulerApiUrl(schedulerUrl, '/api/devices', token);
53
- const res = await fetch(url, {
54
- method: 'POST',
55
- headers: { 'content-type': 'application/json' },
56
- body: JSON.stringify({ name, character_id: characterId }),
57
- });
53
+ let res;
54
+ try {
55
+ res = await fetch(url, {
56
+ method: 'POST',
57
+ headers: { 'content-type': 'application/json' },
58
+ body: JSON.stringify({ name, character_id: characterId }),
59
+ });
60
+ }
61
+ catch (error) {
62
+ const cause = error.cause;
63
+ throw new Error(`cannot reach scheduler through ${schedulerUrl}: ${cause?.message ?? error.message}`);
64
+ }
58
65
  if (!res.ok) {
59
66
  const text = await res.text().catch(() => '');
60
67
  throw new Error(`${res.status}: ${text || res.statusText}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.103",
3
+ "version": "2.8.104",
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",
@@ -4131,6 +4131,22 @@ async def test_robot_motor(robot_id: str, motor_id: str, duration_ms: int = 300)
4131
4131
  })
4132
4132
  return {"queued": True, "request_id": request_id, "device_id": model.id}
4133
4133
 
4134
+ @app.post("/api/robots/{robot_id}/motors/discover")
4135
+ async def discover_robot_motors(robot_id: str):
4136
+ """Import relays already registered by the robot-local motor server.
4137
+
4138
+ This is read-only on the robot. It never scans or pulses unregistered GPIO.
4139
+ The dashboard validates and persists the returned registry before testing.
4140
+ """
4141
+ device = await _resolve_device_by_id_or_name(robot_id)
4142
+ model = _device_row_to_model(device)
4143
+ if not model.motor_server_url:
4144
+ raise HTTPException(409, "Robot has no motor_server_url")
4145
+ request_id = await _queue_shell_request_async(model.id, "motor_discover", "motor_server", {
4146
+ "motor_server_url": model.motor_server_url,
4147
+ })
4148
+ return {"queued": True, "request_id": request_id, "device_id": model.id}
4149
+
4134
4150
  @app.post("/api/robots/{robot_id}/motors/test-all")
4135
4151
  async def test_all_robot_motors(robot_id: str, duration_ms: int = 300, pause_ms: int = 200):
4136
4152
  device = await _resolve_device_by_id_or_name(robot_id)
@@ -427,6 +427,8 @@ def _handle_shell_command(identity, cmd: dict) -> None:
427
427
  result = _run_shell_command(params.get("name", ""), params)
428
428
  elif kind == "speaker_test":
429
429
  result = _speaker_roundtrip_test(params)
430
+ elif kind == "motor_discover":
431
+ result = _discover_motor_registry(params)
430
432
  elif kind == "motor_sequence":
431
433
  result = _run_motor_sequence(params)
432
434
  else:
@@ -434,6 +436,45 @@ def _handle_shell_command(identity, cmd: dict) -> None:
434
436
  _post_supervisor_output(identity, kind, service, result, request_id)
435
437
 
436
438
 
439
+ def _discover_motor_registry(params: dict) -> dict:
440
+ """Read the robot-local motor registry without touching any GPIO output."""
441
+ import httpx
442
+ import re
443
+
444
+ base = str(params.get("motor_server_url") or "http://127.0.0.1:8001").rstrip("/")
445
+ if not (base.startswith("http://127.0.0.1:") or base.startswith("http://localhost:")):
446
+ return {"ok": False, "error": "motor server must be robot-local"}
447
+ try:
448
+ with httpx.Client(timeout=5.0) as client:
449
+ response = client.get(f"{base}/list-motors")
450
+ response.raise_for_status()
451
+ raw_motors = response.json().get("motors", [])
452
+ registry, used_ids, used_gpios = [], set(), set()
453
+ for index, raw in enumerate(raw_motors[:32]):
454
+ gpio = int(raw.get("gpio", -1))
455
+ if gpio < 2 or gpio > 27 or gpio in used_gpios:
456
+ continue
457
+ name = str(raw.get("name") or f"Relay {index + 1}").strip()[:60]
458
+ base_id = re.sub(r"[^a-z0-9_-]+", "-", name.lower()).strip("-") or f"relay-{index + 1}"
459
+ motor_id, suffix = base_id[:32], 2
460
+ while motor_id in used_ids:
461
+ tail = f"-{suffix}"
462
+ motor_id = f"{base_id[:32-len(tail)]}{tail}"
463
+ suffix += 1
464
+ used_ids.add(motor_id)
465
+ used_gpios.add(gpio)
466
+ registry.append({
467
+ "id": motor_id,
468
+ "name": name,
469
+ "gpio": gpio,
470
+ "active_high": bool(raw.get("active_high", True)),
471
+ "max_duration_ms": 3000,
472
+ })
473
+ return {"ok": True, "registry": registry, "count": len(registry), "motor_server_url": base}
474
+ except Exception as exc:
475
+ return {"ok": False, "error": f"motor registry discovery failed: {type(exc).__name__}: {exc}"}
476
+
477
+
437
478
  def _run_motor_sequence(params: dict) -> dict:
438
479
  """Run one validated scheduler sequence against the robot-local motor API."""
439
480
  import httpx