infinicode 2.8.59 → 2.8.60
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.
|
@@ -73,7 +73,7 @@ Wants=network-online.target
|
|
|
73
73
|
[Service]
|
|
74
74
|
Type=simple
|
|
75
75
|
ExecStart=${service.command} ${service.args.map(shellQuote).join(' ')}
|
|
76
|
-
WorkingDirectory=${service.workingDir ??
|
|
76
|
+
WorkingDirectory=${service.workingDir ?? homedir()}
|
|
77
77
|
Restart=always
|
|
78
78
|
RestartSec=5
|
|
79
79
|
${envLines}
|
|
@@ -83,7 +83,15 @@ WantedBy=multi-user.target
|
|
|
83
83
|
`;
|
|
84
84
|
try {
|
|
85
85
|
writeFileSync(unitPath, unit, 'utf8');
|
|
86
|
-
|
|
86
|
+
const reload = spawnSync('systemctl', ['daemon-reload'], { encoding: 'utf8' });
|
|
87
|
+
if (reload.status !== 0) {
|
|
88
|
+
return { ok: false, message: `wrote ${unitPath}, but systemctl daemon-reload failed: ${reload.stderr || reload.stdout}` };
|
|
89
|
+
}
|
|
90
|
+
const enabled = spawnSync('systemctl', ['enable', '--now', unitName], { encoding: 'utf8' });
|
|
91
|
+
if (enabled.status !== 0) {
|
|
92
|
+
return { ok: false, message: `wrote ${unitPath}, but systemctl enable --now failed: ${enabled.stderr || enabled.stdout}` };
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, message: `enabled and started systemd unit ${unitName}` };
|
|
87
95
|
}
|
|
88
96
|
catch (e) {
|
|
89
97
|
return { ok: false, message: `could not write ${unitPath}: ${e instanceof Error ? e.message : String(e)}` };
|
|
@@ -101,8 +101,6 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
101
101
|
'--scheduler-url', opts.schedulerUrl, '--robot-id', opts.name,
|
|
102
102
|
'--video-device', opts.videoDevice ?? 'auto', '--audio-device', opts.audioDevice ?? 'default',
|
|
103
103
|
'--save-config'];
|
|
104
|
-
if (opts.vision !== false)
|
|
105
|
-
previewArgs.push('--robovision-url', 'http://127.0.0.1:5000');
|
|
106
104
|
if (opts.enrollmentToken)
|
|
107
105
|
previewArgs.push('--enrollment-token', opts.enrollmentToken);
|
|
108
106
|
const preview = {
|
|
@@ -113,6 +111,13 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
113
111
|
// Keep this in the child environment, never in the persisted robot config.
|
|
114
112
|
env: {
|
|
115
113
|
ROBOPARK_MESH_TOKEN: opts.token,
|
|
114
|
+
// Keep internal service wiring out of the Commander argument boundary.
|
|
115
|
+
// This also lets preview start while RoboVision is still warming up.
|
|
116
|
+
ROBOVISION_URL: opts.vision !== false ? 'http://127.0.0.1:5000' : '',
|
|
117
|
+
ROBOVISION_MEDIA_URL: opts.vision !== false
|
|
118
|
+
? 'http://127.0.0.1:5000/api/media/inventory'
|
|
119
|
+
: '',
|
|
120
|
+
ROBOVISION_CAMERA: opts.vision !== false ? '1' : '0',
|
|
116
121
|
// Inventory changes should reach the Control Center promptly.
|
|
117
122
|
HEARTBEAT_INTERVAL: '5',
|
|
118
123
|
},
|
package/dist/robopark/setup.js
CHANGED
|
@@ -189,7 +189,10 @@ async function setupRobot(config, opts, ctx, internal) {
|
|
|
189
189
|
console.log(' ' + chalk.cyan(`robopark ${runtimeArgs.join(' ')}`));
|
|
190
190
|
console.log(chalk.dim(` network: ${network} (scheduler: ${schedulerUrl})`));
|
|
191
191
|
console.log();
|
|
192
|
-
|
|
192
|
+
// On Linux, registerAutoStart enables and starts the systemd service. Do not
|
|
193
|
+
// also launch a detached copy that can leave mesh healthy while preview is
|
|
194
|
+
// duplicated, blocked, or running under a different HOME.
|
|
195
|
+
if (opts.start && !(opts.autoStart && process.platform === 'linux')) {
|
|
193
196
|
const robotArgv = [process.execPath, process.argv[1], ...runtimeArgs];
|
|
194
197
|
console.log(chalk.dim(' starting unified robot runtime…'));
|
|
195
198
|
runDetached(robotArgv[0], robotArgv.slice(1));
|
package/dist/robopark-cli.js
CHANGED
|
@@ -135,6 +135,7 @@ program
|
|
|
135
135
|
.option('--enrollment-token <token>', 'one-time enrollment token')
|
|
136
136
|
.option('--video-device <dev>', 'camera: auto, /dev/video0, 0, picamera', 'auto')
|
|
137
137
|
.option('--audio-device <dev>', 'microphone: default, none, or name/index', 'default')
|
|
138
|
+
.option('--robovision-url <url>', 'RoboVision base URL used for camera streaming and hardware inventory')
|
|
138
139
|
.option('--width <px>', 'video width', '640')
|
|
139
140
|
.option('--height <px>', 'video height', '480')
|
|
140
141
|
.option('--fps <n>', 'video fps', '15')
|
package/package.json
CHANGED
|
@@ -337,7 +337,9 @@ def _get_lan_ip() -> Optional[str]:
|
|
|
337
337
|
return None
|
|
338
338
|
|
|
339
339
|
|
|
340
|
-
async def _send_heartbeat(
|
|
340
|
+
async def _send_heartbeat(
|
|
341
|
+
scheduler_url: str, device_id: str, token: str
|
|
342
|
+
) -> tuple[Optional[bool], bool]:
|
|
341
343
|
try:
|
|
342
344
|
inventory = _get_device_inventory()
|
|
343
345
|
headers = {"Authorization": f"Bearer {token}", **_mesh_proxy_headers()}
|
|
@@ -348,6 +350,13 @@ async def _send_heartbeat(scheduler_url: str, device_id: str, token: str) -> Opt
|
|
|
348
350
|
headers=headers,
|
|
349
351
|
timeout=10.0,
|
|
350
352
|
)
|
|
353
|
+
if response.status_code in (401, 404):
|
|
354
|
+
logger.warning(
|
|
355
|
+
"heartbeat credentials rejected for %s (%s)",
|
|
356
|
+
device_id,
|
|
357
|
+
response.status_code,
|
|
358
|
+
)
|
|
359
|
+
return None, False
|
|
351
360
|
response.raise_for_status()
|
|
352
361
|
logger.info(
|
|
353
362
|
"heartbeat accepted: inventory source=%s camera=%d mic=%d speaker=%d",
|
|
@@ -356,10 +365,10 @@ async def _send_heartbeat(scheduler_url: str, device_id: str, token: str) -> Opt
|
|
|
356
365
|
len(inventory.get("audio_input", [])),
|
|
357
366
|
len(inventory.get("audio_output", [])),
|
|
358
367
|
)
|
|
359
|
-
return bool(response.json().get("production_mode", False))
|
|
368
|
+
return bool(response.json().get("production_mode", False)), True
|
|
360
369
|
except Exception as e:
|
|
361
370
|
logger.warning(f"heartbeat failed: {e}")
|
|
362
|
-
return None
|
|
371
|
+
return None, True
|
|
363
372
|
|
|
364
373
|
|
|
365
374
|
@dataclass
|
|
@@ -420,18 +429,34 @@ class PreviewAgent:
|
|
|
420
429
|
self._last_device_config_poll = 0.0
|
|
421
430
|
self._motion_reference = None
|
|
422
431
|
self._last_motion_sample = 0.0
|
|
423
|
-
self._motion_capture: Optional[VideoCapture] = None
|
|
424
|
-
self._motion_capture_lock = asyncio.Lock()
|
|
425
|
-
|
|
432
|
+
self._motion_capture: Optional[VideoCapture] = None
|
|
433
|
+
self._motion_capture_lock = asyncio.Lock()
|
|
434
|
+
self._mesh_bootstrap_ready = False
|
|
435
|
+
self._next_mesh_bootstrap = 0.0
|
|
436
|
+
|
|
437
|
+
async def _ensure_mesh_identity(self) -> bool:
|
|
438
|
+
if not _mesh_proxy_headers():
|
|
439
|
+
return bool(self.device_id and self.device_token)
|
|
440
|
+
now = time.monotonic()
|
|
441
|
+
if self._mesh_bootstrap_ready:
|
|
442
|
+
return True
|
|
443
|
+
if now < self._next_mesh_bootstrap:
|
|
444
|
+
return False
|
|
445
|
+
self._next_mesh_bootstrap = now + 5.0
|
|
446
|
+
try:
|
|
447
|
+
self.device_id, self.device_token = await _bootstrap_mesh_device(
|
|
448
|
+
self.scheduler_url, self.robot_id
|
|
449
|
+
)
|
|
450
|
+
self._mesh_bootstrap_ready = True
|
|
451
|
+
return True
|
|
452
|
+
except Exception as exc:
|
|
453
|
+
logger.warning("mesh device bootstrap failed: %s", exc)
|
|
454
|
+
return False
|
|
455
|
+
|
|
426
456
|
async def run(self) -> None:
|
|
427
457
|
enrollment_token = os.getenv("ENROLLMENT_TOKEN") or self.enrollment_token
|
|
428
458
|
if _mesh_proxy_headers():
|
|
429
|
-
|
|
430
|
-
self.device_id, self.device_token = await _bootstrap_mesh_device(
|
|
431
|
-
self.scheduler_url, self.robot_id
|
|
432
|
-
)
|
|
433
|
-
except Exception as exc:
|
|
434
|
-
logger.warning("mesh device bootstrap failed: %s", exc)
|
|
459
|
+
await self._ensure_mesh_identity()
|
|
435
460
|
|
|
436
461
|
if not self.device_token and enrollment_token:
|
|
437
462
|
self.device_token = await _enroll(self.scheduler_url, enrollment_token, self.robot_id)
|
|
@@ -747,13 +772,18 @@ class PreviewAgent:
|
|
|
747
772
|
await asyncio.to_thread(_play_audio_effect, self.audio_output_device, "disconnect")
|
|
748
773
|
self._current_state = PreviewState()
|
|
749
774
|
|
|
750
|
-
async def _heartbeat_loop(self) -> None:
|
|
751
|
-
while not self._shutdown.is_set():
|
|
752
|
-
if
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
775
|
+
async def _heartbeat_loop(self) -> None:
|
|
776
|
+
while not self._shutdown.is_set():
|
|
777
|
+
if _mesh_proxy_headers() and not self._mesh_bootstrap_ready:
|
|
778
|
+
await self._ensure_mesh_identity()
|
|
779
|
+
if self.device_id and self.device_token:
|
|
780
|
+
production_mode, credentials_ok = await _send_heartbeat(
|
|
781
|
+
self.scheduler_url, self.device_id, self.device_token
|
|
782
|
+
)
|
|
783
|
+
if not credentials_ok:
|
|
784
|
+
self._mesh_bootstrap_ready = False
|
|
785
|
+
self._next_mesh_bootstrap = 0.0
|
|
786
|
+
if production_mode is not None:
|
|
757
787
|
self.production_mode = production_mode
|
|
758
788
|
if not production_mode and self._vision_session_id:
|
|
759
789
|
await self._end_vision_session()
|
|
@@ -1674,7 +1704,9 @@ def main() -> None:
|
|
|
1674
1704
|
"video_device": args.video_device,
|
|
1675
1705
|
"audio_device": args.audio_device,
|
|
1676
1706
|
"robovision_url": args.robovision_url or os.getenv("ROBOVISION_URL", "http://127.0.0.1:5000"),
|
|
1677
|
-
"use_robovision_camera": bool(args.robovision_url)
|
|
1707
|
+
"use_robovision_camera": bool(args.robovision_url) or os.getenv(
|
|
1708
|
+
"ROBOVISION_CAMERA", ""
|
|
1709
|
+
).lower() in ("1", "true", "yes", "on"),
|
|
1678
1710
|
"video_width": args.width,
|
|
1679
1711
|
"video_height": args.height,
|
|
1680
1712
|
"video_fps": args.fps,
|