infinicode 2.8.54 → 2.8.56
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 +40 -16
- package/dist/robopark/auto-start.d.ts +1 -1
- package/dist/robopark/auto-start.js +29 -3
- package/dist/robopark/robot-runtime.d.ts +14 -0
- package/dist/robopark/robot-runtime.js +163 -0
- package/dist/robopark/setup.js +25 -112
- package/dist/robopark/stop-all.js +3 -0
- package/dist/robopark-cli.js +37 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +68 -29
- package/packages/robopark/scheduler/preview_agent.py +21 -13
|
@@ -1881,10 +1881,13 @@ async def robot_stream(robot_id: str,
|
|
|
1881
1881
|
if not await get_production_mode():
|
|
1882
1882
|
return {"active": False, "reason": "production_mode_off"}
|
|
1883
1883
|
|
|
1884
|
-
async with aiosqlite.connect(DB_PATH) as db:
|
|
1885
|
-
db.row_factory = aiosqlite.Row
|
|
1886
|
-
|
|
1887
|
-
|
|
1884
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
1885
|
+
db.row_factory = aiosqlite.Row
|
|
1886
|
+
resolved_id = await _resolve_robot_reference(db, robot_id)
|
|
1887
|
+
if not resolved_id:
|
|
1888
|
+
raise HTTPException(404, "Robot not found")
|
|
1889
|
+
async with db.execute("SELECT * FROM robots WHERE id = ?", (resolved_id,)) as c:
|
|
1890
|
+
robot = await c.fetchone()
|
|
1888
1891
|
if not robot:
|
|
1889
1892
|
raise HTTPException(404, "Robot not found")
|
|
1890
1893
|
if not robot["current_session_id"]:
|
|
@@ -1909,7 +1912,7 @@ async def robot_stream(robot_id: str,
|
|
|
1909
1912
|
except ImportError:
|
|
1910
1913
|
raise HTTPException(503, "livekit-api not installed on scheduler")
|
|
1911
1914
|
|
|
1912
|
-
identity = f"viewer:{
|
|
1915
|
+
identity = f"viewer:{resolved_id}:{int(datetime.utcnow().timestamp())}"
|
|
1913
1916
|
grants = VideoGrants(
|
|
1914
1917
|
room=room_name,
|
|
1915
1918
|
room_join=True,
|
|
@@ -1947,7 +1950,7 @@ async def robot_stream(robot_id: str,
|
|
|
1947
1950
|
PREVIEW_TTL_SECONDS = 45 # preview auto-expires this long after the last keepalive
|
|
1948
1951
|
DEFAULT_VISION_PORT = 5000 # RoboVisionAI_PI's default Flask port (/video_feed, /api/motion/*)
|
|
1949
1952
|
|
|
1950
|
-
async def _mirror_device_to_robot(db, device_id: str):
|
|
1953
|
+
async def _mirror_device_to_robot(db, device_id: str):
|
|
1951
1954
|
"""Ensure a `robots` row exists for an enrolled device, so id-keyed routes
|
|
1952
1955
|
(/stream, /preview/*) work even before the device's first real voice
|
|
1953
1956
|
session (device_request_session normally creates this mirror, but only on
|
|
@@ -1965,8 +1968,30 @@ async def _mirror_device_to_robot(db, device_id: str):
|
|
|
1965
1968
|
(device_id, device["name"] or device_id, device["character_id"]),
|
|
1966
1969
|
)
|
|
1967
1970
|
await db.commit()
|
|
1968
|
-
async with db.execute("SELECT * FROM robots WHERE id = ?", (device_id,)) as c:
|
|
1969
|
-
return await c.fetchone()
|
|
1971
|
+
async with db.execute("SELECT * FROM robots WHERE id = ?", (device_id,)) as c:
|
|
1972
|
+
return await c.fetchone()
|
|
1973
|
+
|
|
1974
|
+
async def _resolve_robot_reference(db, robot_ref: str) -> Optional[str]:
|
|
1975
|
+
"""Resolve a display name to its active enrolled device id when needed."""
|
|
1976
|
+
async with db.execute("SELECT id FROM devices WHERE id = ?", (robot_ref,)) as c:
|
|
1977
|
+
row = await c.fetchone()
|
|
1978
|
+
if row:
|
|
1979
|
+
return row["id"]
|
|
1980
|
+
async with db.execute(
|
|
1981
|
+
"""SELECT id FROM devices WHERE lower(name) = lower(?)
|
|
1982
|
+
ORDER BY CASE lower(status) WHEN 'online' THEN 0 WHEN 'running' THEN 1 ELSE 2 END,
|
|
1983
|
+
last_heartbeat DESC, enrolled_at DESC LIMIT 1""",
|
|
1984
|
+
(robot_ref,),
|
|
1985
|
+
) as c:
|
|
1986
|
+
row = await c.fetchone()
|
|
1987
|
+
if row:
|
|
1988
|
+
return row["id"]
|
|
1989
|
+
async with db.execute(
|
|
1990
|
+
"SELECT id FROM robots WHERE id = ? OR lower(name) = lower(?) LIMIT 1",
|
|
1991
|
+
(robot_ref, robot_ref),
|
|
1992
|
+
) as c:
|
|
1993
|
+
row = await c.fetchone()
|
|
1994
|
+
return row["id"] if row else None
|
|
1970
1995
|
|
|
1971
1996
|
async def _pick_preview_server(db, prefer_id: Optional[str] = None):
|
|
1972
1997
|
"""Return a LiveKit server row for a preview: a preferred one if still present,
|
|
@@ -2010,15 +2035,18 @@ def _mint_lk(server, room, identity, name, publish: bool, ttl_min: int):
|
|
|
2010
2035
|
)
|
|
2011
2036
|
|
|
2012
2037
|
@app.post("/api/robots/{robot_id}/preview/start")
|
|
2013
|
-
async def preview_start(robot_id: str):
|
|
2038
|
+
async def preview_start(robot_id: str):
|
|
2014
2039
|
"""Operator dashboard: begin an on-demand live camera preview for a robot.
|
|
2015
2040
|
Returns a SUBSCRIBE-only viewer token + the preview room."""
|
|
2016
|
-
if not await get_production_mode():
|
|
2017
|
-
return {"active": False, "reason": "production_mode_off"}
|
|
2018
|
-
async with aiosqlite.connect(DB_PATH) as db:
|
|
2019
|
-
db.row_factory = aiosqlite.Row
|
|
2020
|
-
|
|
2021
|
-
|
|
2041
|
+
if not await get_production_mode():
|
|
2042
|
+
return {"active": False, "reason": "production_mode_off"}
|
|
2043
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
2044
|
+
db.row_factory = aiosqlite.Row
|
|
2045
|
+
robot_id = await _resolve_robot_reference(db, robot_id)
|
|
2046
|
+
if not robot_id:
|
|
2047
|
+
raise HTTPException(404, "Robot not found")
|
|
2048
|
+
if not await _mirror_device_to_robot(db, robot_id):
|
|
2049
|
+
raise HTTPException(404, "Robot not found")
|
|
2022
2050
|
async with db.execute("SELECT * FROM previews WHERE robot_id = ?", (robot_id,)) as c:
|
|
2023
2051
|
existing = await c.fetchone()
|
|
2024
2052
|
server = await _pick_preview_server(db, existing["server_id"] if existing else None)
|
|
@@ -2043,11 +2071,15 @@ async def preview_start(robot_id: str):
|
|
|
2043
2071
|
"identity": identity, "expires_in": PREVIEW_TTL_SECONDS}
|
|
2044
2072
|
|
|
2045
2073
|
@app.post("/api/robots/{robot_id}/preview/keepalive")
|
|
2046
|
-
async def preview_keepalive(robot_id: str):
|
|
2047
|
-
"""Dashboard pings while the operator is watching, to keep the preview alive."""
|
|
2048
|
-
expires = (datetime.utcnow() + timedelta(seconds=PREVIEW_TTL_SECONDS)).isoformat()
|
|
2049
|
-
async with aiosqlite.connect(DB_PATH) as db:
|
|
2050
|
-
|
|
2074
|
+
async def preview_keepalive(robot_id: str):
|
|
2075
|
+
"""Dashboard pings while the operator is watching, to keep the preview alive."""
|
|
2076
|
+
expires = (datetime.utcnow() + timedelta(seconds=PREVIEW_TTL_SECONDS)).isoformat()
|
|
2077
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
2078
|
+
db.row_factory = aiosqlite.Row
|
|
2079
|
+
robot_id = await _resolve_robot_reference(db, robot_id)
|
|
2080
|
+
if not robot_id:
|
|
2081
|
+
return {"active": False}
|
|
2082
|
+
cur = await db.execute("UPDATE previews SET expires_at = ? WHERE robot_id = ?", (expires, robot_id))
|
|
2051
2083
|
await db.commit()
|
|
2052
2084
|
if cur.rowcount == 0:
|
|
2053
2085
|
return {"active": False}
|
|
@@ -2055,10 +2087,14 @@ async def preview_keepalive(robot_id: str):
|
|
|
2055
2087
|
return {"active": True, "expires_in": PREVIEW_TTL_SECONDS}
|
|
2056
2088
|
|
|
2057
2089
|
@app.post("/api/robots/{robot_id}/preview/stop")
|
|
2058
|
-
async def preview_stop(robot_id: str):
|
|
2059
|
-
"""Dashboard closed / operator stopped watching -> end the preview now."""
|
|
2060
|
-
async with aiosqlite.connect(DB_PATH) as db:
|
|
2061
|
-
|
|
2090
|
+
async def preview_stop(robot_id: str):
|
|
2091
|
+
"""Dashboard closed / operator stopped watching -> end the preview now."""
|
|
2092
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
2093
|
+
db.row_factory = aiosqlite.Row
|
|
2094
|
+
robot_id = await _resolve_robot_reference(db, robot_id)
|
|
2095
|
+
if not robot_id:
|
|
2096
|
+
return {"stopped": False}
|
|
2097
|
+
await db.execute("DELETE FROM previews WHERE robot_id = ?", (robot_id,))
|
|
2062
2098
|
await db.commit()
|
|
2063
2099
|
await _log_history("preview", "robot", robot_id, "stop", "operator")
|
|
2064
2100
|
return {"stopped": True}
|
|
@@ -2068,11 +2104,14 @@ async def preview_agent(robot_id: str, authorization: Optional[str] = Header(def
|
|
|
2068
2104
|
"""The ROBOT polls this. While an operator preview is active it returns a
|
|
2069
2105
|
PUBLISH token + room so the Pi opens its cam and joins; otherwise
|
|
2070
2106
|
{active:false} so the Pi stops publishing. Enrolled-device auth."""
|
|
2071
|
-
if not await _authorize_fleet(authorization):
|
|
2072
|
-
raise HTTPException(401, "Invalid or missing device token")
|
|
2073
|
-
async with aiosqlite.connect(DB_PATH) as db:
|
|
2074
|
-
db.row_factory = aiosqlite.Row
|
|
2075
|
-
|
|
2107
|
+
if not await _authorize_fleet(authorization):
|
|
2108
|
+
raise HTTPException(401, "Invalid or missing device token")
|
|
2109
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
2110
|
+
db.row_factory = aiosqlite.Row
|
|
2111
|
+
robot_id = await _resolve_robot_reference(db, robot_id)
|
|
2112
|
+
if not robot_id:
|
|
2113
|
+
return {"active": False}
|
|
2114
|
+
async with db.execute("SELECT * FROM previews WHERE robot_id = ?", (robot_id,)) as c:
|
|
2076
2115
|
row = await c.fetchone()
|
|
2077
2116
|
if not _preview_active(row):
|
|
2078
2117
|
return {"active": False}
|
|
@@ -302,19 +302,27 @@ def _get_lan_ip() -> Optional[str]:
|
|
|
302
302
|
return None
|
|
303
303
|
|
|
304
304
|
|
|
305
|
-
async def _send_heartbeat(scheduler_url: str, device_id: str, token: str) -> Optional[bool]:
|
|
306
|
-
try:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
305
|
+
async def _send_heartbeat(scheduler_url: str, device_id: str, token: str) -> Optional[bool]:
|
|
306
|
+
try:
|
|
307
|
+
inventory = _get_device_inventory()
|
|
308
|
+
async with httpx.AsyncClient() as client:
|
|
309
|
+
response = await client.post(
|
|
310
|
+
f"{scheduler_url.rstrip('/')}/api/devices/{device_id}/heartbeat",
|
|
311
|
+
json={"status": "online", "ip": _get_lan_ip(), "device_inventory": inventory},
|
|
312
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
313
|
+
timeout=10.0,
|
|
314
|
+
)
|
|
315
|
+
response.raise_for_status()
|
|
316
|
+
logger.info(
|
|
317
|
+
"heartbeat accepted: inventory source=%s camera=%d mic=%d speaker=%d",
|
|
318
|
+
inventory.get("source", "local"),
|
|
319
|
+
len(inventory.get("video", [])),
|
|
320
|
+
len(inventory.get("audio_input", [])),
|
|
321
|
+
len(inventory.get("audio_output", [])),
|
|
322
|
+
)
|
|
323
|
+
return bool(response.json().get("production_mode", False))
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.warning(f"heartbeat failed: {e}")
|
|
318
326
|
return None
|
|
319
327
|
|
|
320
328
|
|