feat(node): push USB changes and preserve sensor setup across sessions
This commit is contained in:
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user