feat(node): push USB changes and preserve sensor setup across sessions

This commit is contained in:
DCCONSTRUCTIONS
2026-09-06 00:24:48 +03:00
parent a8647c4d87
commit 22d69107ba
24 changed files with 636 additions and 60 deletions
+34
View File
@@ -0,0 +1,34 @@
"""Bounded notifications from the registry writer to local operator streams."""
import asyncio
import threading
class FleetEvents:
def __init__(self):
self.lock = threading.Lock()
self.listeners = set()
def subscribe(self):
loop = asyncio.get_running_loop()
queue = asyncio.Queue(maxsize=1)
def put_latest():
if not queue.full():
queue.put_nowait(None)
listener = (loop, put_latest)
with self.lock:
self.listeners.add(listener)
def close():
with self.lock:
self.listeners.discard(listener)
return queue, close
def notify(self):
with self.lock:
for loop, callback in self.listeners:
if not loop.is_closed():
loop.call_soon_threadsafe(callback)
+4
View File
@@ -27,6 +27,9 @@ class FleetRegistry:
self.started_at = time.time()
self.root = root
self.lock = threading.RLock()
from .events import FleetEvents
self.events = FleetEvents()
self.trust = CoreTrust(root)
path = root / "fleet.sqlite3"
if path.is_symlink():
@@ -58,6 +61,7 @@ class FleetRegistry:
"ON CONFLICT(id) DO UPDATE SET body=excluded.body",
(row["id"], row["node_id"], json.dumps(row)),
)
self.events.notify()
def find(self, identifier):
result = self.db.execute("SELECT body FROM vehicles WHERE id=?", (identifier,)).fetchone()
+29
View File
@@ -2,11 +2,15 @@
from __future__ import annotations
import asyncio
import ipaddress
import json
from contextlib import suppress
from typing import Annotated
from urllib.parse import urlsplit
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, ConfigDict, Field
from k1link.fleet.registry import FleetRegistry
@@ -53,6 +57,31 @@ def fleet_list(response: Response, fleet: Annotated[FleetRegistry, Depends(local
return fleet.listing()
@router.get("/events")
async def fleet_events(fleet: Annotated[FleetRegistry, Depends(local_operator)]):
async def stream():
queue, close = fleet.events.subscribe()
try:
while not fleet.stop.is_set():
# Full replacement snapshots recover missed events/reconnections.
# The timeout updates link expiry locally; it never queries a Node.
value = await asyncio.to_thread(fleet.listing)
yield "retry: 3000\ndata: " + json.dumps(value) + "\n\n"
with suppress(TimeoutError):
await asyncio.wait_for(queue.get(), timeout=5)
finally:
close()
return StreamingResponse(
stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-store",
"X-Accel-Buffering": "no",
},
)
@router.post("/preview")
def fleet_preview(
body: PreviewRequest,