infinicode 2.8.102 → 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.
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +78 -39
- package/dist/robopark/add-robot.d.ts +2 -0
- package/dist/robopark/add-robot.js +51 -13
- package/dist/robopark/robot-runtime.js +1 -1
- package/dist/robopark-cli.js +2 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +143 -10
- package/packages/robopark/scheduler/preview_agent.py +2 -2
- package/packages/robopark/scheduler/robot_supervisor.py +41 -0
|
@@ -3192,6 +3192,93 @@ PIPELINE_STAGES = (
|
|
|
3192
3192
|
)
|
|
3193
3193
|
PIPELINE_STATUSES = {"pending", "running", "ok", "failed", "blocked", "skipped"}
|
|
3194
3194
|
|
|
3195
|
+
PIPELINE_STAGE_LABELS = {
|
|
3196
|
+
"robot_online": "Robot online",
|
|
3197
|
+
"camera_ready": "Camera detected",
|
|
3198
|
+
"microphone_ready": "Microphone detected",
|
|
3199
|
+
"speaker_ready": "Speaker detected",
|
|
3200
|
+
"motion_detected": "Waiting for motion",
|
|
3201
|
+
"scheduler_session": "Allocating session",
|
|
3202
|
+
"livekit_join": "Joining LiveKit",
|
|
3203
|
+
"camera_published": "Publishing camera",
|
|
3204
|
+
"microphone_published": "Publishing microphone",
|
|
3205
|
+
"voice_worker": "Starting voice worker",
|
|
3206
|
+
"stt_listening": "Listening to visitor",
|
|
3207
|
+
"llm_response": "Preparing response",
|
|
3208
|
+
"tts_subscribed": "Preparing robot voice",
|
|
3209
|
+
"playback_started": "Speaking through robot",
|
|
3210
|
+
"motor_sequence": "Running motor sequence",
|
|
3211
|
+
"session_ended": "Closing session",
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
|
|
3215
|
+
def _pipeline_current_state(events: list[dict], *, active: bool,
|
|
3216
|
+
production_mode: bool, online: bool) -> dict:
|
|
3217
|
+
"""Derive one operator-facing live stage from persistent pipeline events."""
|
|
3218
|
+
if not online:
|
|
3219
|
+
return {
|
|
3220
|
+
"stage": "robot_online", "status": "blocked",
|
|
3221
|
+
"label": PIPELINE_STAGE_LABELS["robot_online"],
|
|
3222
|
+
"message": "Robot heartbeat is stale or missing", "since": None,
|
|
3223
|
+
}
|
|
3224
|
+
if not active:
|
|
3225
|
+
return {
|
|
3226
|
+
"stage": "motion_detected",
|
|
3227
|
+
"status": "waiting" if production_mode else "paused",
|
|
3228
|
+
"label": "Armed - waiting for motion" if production_mode else "Production paused",
|
|
3229
|
+
"message": "Motion detection is armed" if production_mode else "Enable production mode to arm this robot",
|
|
3230
|
+
"since": None,
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
scoped = [event for event in events if event.get("session_id")]
|
|
3234
|
+
for event in reversed(scoped):
|
|
3235
|
+
if event.get("status") in {"failed", "blocked"}:
|
|
3236
|
+
return {
|
|
3237
|
+
"stage": event["stage"], "status": event["status"],
|
|
3238
|
+
"label": PIPELINE_STAGE_LABELS.get(event["stage"], event["stage"]),
|
|
3239
|
+
"message": event.get("message") or "Pipeline requires attention",
|
|
3240
|
+
"since": event.get("timestamp"),
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
conversational = {
|
|
3244
|
+
"voice_worker", "stt_listening", "llm_response", "tts_subscribed",
|
|
3245
|
+
"playback_started", "motor_sequence",
|
|
3246
|
+
}
|
|
3247
|
+
latest = next((event for event in reversed(scoped) if event.get("stage") in conversational), None)
|
|
3248
|
+
if latest:
|
|
3249
|
+
stage = latest["stage"]
|
|
3250
|
+
status = latest.get("status") or "running"
|
|
3251
|
+
if status in {"ok", "skipped"}:
|
|
3252
|
+
stage = {
|
|
3253
|
+
"voice_worker": "stt_listening",
|
|
3254
|
+
"stt_listening": "llm_response",
|
|
3255
|
+
"llm_response": "tts_subscribed",
|
|
3256
|
+
"tts_subscribed": "playback_started",
|
|
3257
|
+
"playback_started": "stt_listening",
|
|
3258
|
+
"motor_sequence": "stt_listening",
|
|
3259
|
+
}.get(stage, stage)
|
|
3260
|
+
status = "active"
|
|
3261
|
+
return {
|
|
3262
|
+
"stage": stage, "status": status,
|
|
3263
|
+
"label": PIPELINE_STAGE_LABELS.get(stage, stage),
|
|
3264
|
+
"message": latest.get("message") or PIPELINE_STAGE_LABELS.get(stage, stage),
|
|
3265
|
+
"since": latest.get("timestamp"),
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
startup = (
|
|
3269
|
+
"scheduler_session", "livekit_join", "camera_published",
|
|
3270
|
+
"microphone_published", "voice_worker",
|
|
3271
|
+
)
|
|
3272
|
+
completed = {event.get("stage") for event in scoped if event.get("status") in {"ok", "skipped"}}
|
|
3273
|
+
stage = next((candidate for candidate in startup if candidate not in completed), "voice_worker")
|
|
3274
|
+
latest_startup = next((event for event in reversed(scoped) if event.get("stage") in startup), None)
|
|
3275
|
+
return {
|
|
3276
|
+
"stage": stage, "status": "active",
|
|
3277
|
+
"label": PIPELINE_STAGE_LABELS.get(stage, stage),
|
|
3278
|
+
"message": (latest_startup or {}).get("message") or "Production conversation is starting",
|
|
3279
|
+
"since": (latest_startup or {}).get("timestamp"),
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3195
3282
|
async def _insert_pipeline_event(device_id: str, payload: PipelineEventPayload) -> dict:
|
|
3196
3283
|
if payload.stage not in PIPELINE_STAGES:
|
|
3197
3284
|
raise HTTPException(422, f"Unknown pipeline stage: {payload.stage}")
|
|
@@ -3286,16 +3373,29 @@ async def robot_pipeline_status(robot_id: str, limit: int = 80):
|
|
|
3286
3373
|
blockers.append("Microphone inventory has not been reported")
|
|
3287
3374
|
if not prereqs["speaker_ready"]:
|
|
3288
3375
|
blockers.append("Speaker inventory has not been reported")
|
|
3376
|
+
active = bool(latest_session and not latest_session["ended_at"])
|
|
3377
|
+
active_session_id = latest_session["id"] if active else None
|
|
3378
|
+
current_events = [
|
|
3379
|
+
event for event in events
|
|
3380
|
+
if not active_session_id or event.get("session_id") == active_session_id
|
|
3381
|
+
]
|
|
3382
|
+
current_stage = _pipeline_current_state(
|
|
3383
|
+
current_events,
|
|
3384
|
+
active=active,
|
|
3385
|
+
production_mode=bool(device["production_mode"]),
|
|
3386
|
+
online=prereqs["robot_online"],
|
|
3387
|
+
)
|
|
3289
3388
|
return {
|
|
3290
3389
|
"robot_id": device_id,
|
|
3291
3390
|
"robot_name": device["name"],
|
|
3292
|
-
"overall": "blocked" if blockers else ("running" if
|
|
3391
|
+
"overall": "blocked" if blockers else ("running" if active else "ready"),
|
|
3293
3392
|
"blockers": list(dict.fromkeys(blockers)),
|
|
3294
3393
|
"prerequisites": prereqs,
|
|
3295
3394
|
"telemetry": telemetry,
|
|
3296
3395
|
"latest_session": dict(latest_session) if latest_session else None,
|
|
3297
3396
|
"events": events,
|
|
3298
3397
|
"stages": list(PIPELINE_STAGES),
|
|
3398
|
+
"current_stage": current_stage,
|
|
3299
3399
|
}
|
|
3300
3400
|
|
|
3301
3401
|
@app.get("/api/robots/{robot_id}/pipeline-history")
|
|
@@ -4031,6 +4131,22 @@ async def test_robot_motor(robot_id: str, motor_id: str, duration_ms: int = 300)
|
|
|
4031
4131
|
})
|
|
4032
4132
|
return {"queued": True, "request_id": request_id, "device_id": model.id}
|
|
4033
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
|
+
|
|
4034
4150
|
@app.post("/api/robots/{robot_id}/motors/test-all")
|
|
4035
4151
|
async def test_all_robot_motors(robot_id: str, duration_ms: int = 300, pause_ms: int = 200):
|
|
4036
4152
|
device = await _resolve_device_by_id_or_name(robot_id)
|
|
@@ -5170,13 +5286,20 @@ async def _robot_telemetry(db, robot_id: str) -> Optional[dict]:
|
|
|
5170
5286
|
|
|
5171
5287
|
device_inventory = parse_json_field(device["device_inventory"], {}) if device else {}
|
|
5172
5288
|
supervisor_status = parse_json_field(device["supervisor_status"], []) if device else []
|
|
5173
|
-
session = None
|
|
5174
|
-
|
|
5289
|
+
session = None
|
|
5290
|
+
current_pipeline_events = []
|
|
5291
|
+
if robot["current_session_id"]:
|
|
5175
5292
|
async with db.execute(
|
|
5176
5293
|
"SELECT started_at, last_activity_at, ended_at FROM sessions WHERE id = ?",
|
|
5177
5294
|
(robot["current_session_id"],),
|
|
5178
|
-
) as c:
|
|
5179
|
-
session = await c.fetchone()
|
|
5295
|
+
) as c:
|
|
5296
|
+
session = await c.fetchone()
|
|
5297
|
+
async with db.execute(
|
|
5298
|
+
"SELECT * FROM pipeline_events WHERE robot_id = ? AND session_id = ? "
|
|
5299
|
+
"ORDER BY timestamp, id",
|
|
5300
|
+
(robot_id, robot["current_session_id"]),
|
|
5301
|
+
) as c:
|
|
5302
|
+
current_pipeline_events = [dict(row) for row in await c.fetchall()]
|
|
5180
5303
|
|
|
5181
5304
|
now = datetime.utcnow()
|
|
5182
5305
|
session_age = None
|
|
@@ -5259,8 +5382,14 @@ async def _robot_telemetry(db, robot_id: str) -> Optional[dict]:
|
|
|
5259
5382
|
(robot_id,),
|
|
5260
5383
|
) as c:
|
|
5261
5384
|
end_reasons = {r["end_reason"]: r["n"] for r in await c.fetchall()}
|
|
5262
|
-
drops = sum(n for reason, n in end_reasons.items() if reason in ABNORMAL_END_REASONS)
|
|
5263
|
-
drop_rate = round(drops / sessions_total, 4) if sessions_total else 0.0
|
|
5385
|
+
drops = sum(n for reason, n in end_reasons.items() if reason in ABNORMAL_END_REASONS)
|
|
5386
|
+
drop_rate = round(drops / sessions_total, 4) if sessions_total else 0.0
|
|
5387
|
+
current_stage = _pipeline_current_state(
|
|
5388
|
+
current_pipeline_events,
|
|
5389
|
+
active=bool(robot["current_session_id"] and session and not session["ended_at"]),
|
|
5390
|
+
production_mode=bool(device["production_mode"]) if device else False,
|
|
5391
|
+
online=heartbeat_age is not None and heartbeat_age <= 30,
|
|
5392
|
+
)
|
|
5264
5393
|
|
|
5265
5394
|
return {
|
|
5266
5395
|
"robot_id": robot_id,
|
|
@@ -5291,9 +5420,13 @@ async def _robot_telemetry(db, robot_id: str) -> Optional[dict]:
|
|
|
5291
5420
|
"supervisor_status": supervisor_status,
|
|
5292
5421
|
"supervisor_status_at": device["supervisor_status_at"] if device else None,
|
|
5293
5422
|
"production_mode": bool(device["production_mode"]) if device else False,
|
|
5294
|
-
"readiness_code": readiness_code,
|
|
5295
|
-
"readiness_message": readiness_message,
|
|
5296
|
-
|
|
5423
|
+
"readiness_code": readiness_code,
|
|
5424
|
+
"readiness_message": readiness_message,
|
|
5425
|
+
"current_stage": current_stage,
|
|
5426
|
+
"current_stage_id": current_stage["stage"],
|
|
5427
|
+
"current_stage_label": current_stage["label"],
|
|
5428
|
+
"current_stage_status": current_stage["status"],
|
|
5429
|
+
}
|
|
5297
5430
|
|
|
5298
5431
|
@app.get("/api/robots/{robot_id}/telemetry")
|
|
5299
5432
|
async def robot_telemetry(robot_id: str):
|
|
@@ -1303,7 +1303,7 @@ class LiveKitPublisher:
|
|
|
1303
1303
|
self._echo_mic_floor = 0.0
|
|
1304
1304
|
self._barge_in_candidate_frames = 0
|
|
1305
1305
|
self._speaker_echo_tail = max(
|
|
1306
|
-
0.1, min(float(os.getenv("ROBOPARK_ECHO_TAIL_MS", "
|
|
1306
|
+
0.1, min(float(os.getenv("ROBOPARK_ECHO_TAIL_MS", "350")) / 1000.0, 2.0)
|
|
1307
1307
|
)
|
|
1308
1308
|
self._adaptive_barge_in = str(
|
|
1309
1309
|
os.getenv("ROBOPARK_ADAPTIVE_BARGE_IN", "true")
|
|
@@ -1553,7 +1553,7 @@ class LiveKitPublisher:
|
|
|
1553
1553
|
import threading
|
|
1554
1554
|
write_queue: "queue.Queue[Optional[bytes]]" = queue.Queue()
|
|
1555
1555
|
written_frames = [0]
|
|
1556
|
-
PREBUFFER_CHUNKS =
|
|
1556
|
+
PREBUFFER_CHUNKS = 3
|
|
1557
1557
|
stream_failed = threading.Event()
|
|
1558
1558
|
playback_reported = threading.Event()
|
|
1559
1559
|
event_loop = asyncio.get_running_loop()
|
|
@@ -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
|