Preserve RRD preview frames and clarify onboard K1 status

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 15:27:13 +03:00
parent 67398fef10
commit 92b60de945
19 changed files with 516 additions and 94 deletions
@@ -838,6 +838,18 @@ async def _retrieve_bluez_device(address: str, details: object = None) -> BLEDev
return None
async def status_read_device_is_current(device: BLEDevice) -> bool:
"""Check a BlueZ handle before the one explicit status-read connection.
BlueZ may remove an unpaired object after a completed scan session. This
cache check neither scans nor connects and never changes macOS selection.
"""
details = getattr(device, "details", None)
if not sys.platform.startswith("linux") or not isinstance(details, dict):
return True
return await _retrieve_bluez_device(device.address, details) is not None
async def _retrieve_corebluetooth_device(
captured: CapturedDiscoveredDevice,
) -> BLEDevice | None:
@@ -26,6 +26,7 @@ from k1link.device_plugins.xgrids_k1.ble.scanner import (
mark_captured_device_gatt_validated,
retrieve_connected_device_capture,
retrieve_known_device_capture_for_status_read,
status_read_device_is_current,
)
PROFILE_ID = "xgrids-k1-fw3-wifi-v1"
@@ -390,6 +391,18 @@ async def _read_wifi_status_impl(
"Exact BLE device is unavailable; run an explicit recovery or scan.",
)
if not await status_read_device_is_current(device):
# One explicit read may refresh the exact vanished BlueZ object
# before GATT. No failed connect/write is retried; public discovery
# generations, the pinned target and macOS behavior are unchanged.
progress.operation_stage = "exact-uuid-scan"
active_captured_device = await discover_known_device_capture_for_status_read(
device_macos_uuid, timeout_seconds=min(timeout_seconds, 8.0),
)
device = (captured_device_handle(active_captured_device)
if active_captured_device is not None else None)
if device is None:
raise BleakDeviceNotFoundError(device_macos_uuid, "Exact BLE device unavailable")
progress.operation_stage = "connect"
async with BleakClient(device, timeout=timeout_seconds, pair=False) as client:
progress.operation_stage = "gatt-contract"
+44 -10
View File
@@ -2,14 +2,22 @@
import asyncio
import ipaddress
import json
import logging
import queue
import time
from contextlib import suppress
from itertools import chain
from uuid import uuid4
import aioice.ice
from aiortc import RTCConfiguration, RTCPeerConnection, RTCSessionDescription
MEDIA_PROTOCOL = "missioncore.node-preview/v1"
MAX_PAYLOAD = 8 * 1024 * 1024
FRAGMENT_BYTES = 16384
logger = logging.getLogger(__name__)
PRIVATE_NETWORKS = tuple(
ipaddress.ip_network(v)
for v in ("127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10")
@@ -90,12 +98,11 @@ class NodeMediaPeers:
await self.close(identifier)
entry["tasks"].append(asyncio.create_task(expiry()))
snapshot = self.camera.snapshot()
return {
"peer_id": identifier,
"sdp": pc.localDescription.sdp,
"type": "answer",
"camera_mime": (snapshot.get("delivery") or {}).get("media_type"),
"media_protocol": MEDIA_PROTOCOL,
"profile": "live-acquisition",
"transport": "webrtc-rrd-fmp4",
}
@@ -104,17 +111,43 @@ class NodeMediaPeers:
raise
async def send(self, channel, payload):
if len(payload) > 8 * 1024 * 1024:
if not 0 < len(payload) <= MAX_PAYLOAD:
raise RuntimeError("Preview fragment exceeds bound")
for offset in range(0, len(payload), 16384):
# Each binary_stream.read() is an independent RRD. SCTP messages are
# transport fragments, never independently decodable RRD files.
parts = (payload[offset:offset + FRAGMENT_BYTES]
for offset in range(0, len(payload), FRAGMENT_BYTES))
for part in chain((b"MCF1" + len(payload).to_bytes(4, "big"),), parts):
deadline = time.monotonic() + 2
while channel.readyState != "open" or channel.bufferedAmount > 1024 * 1024:
if channel.readyState in {"closed", "closing"} or time.monotonic() > deadline:
raise RuntimeError("Preview consumer unavailable")
await asyncio.sleep(0.01)
channel.send(payload[offset : offset + 16384])
channel.send(part)
await asyncio.sleep(0)
async def camera_delivery(self, identifier, channel):
# Camera activation follows calibration / first PCL. Opening the
# viewer does not select or restart a camera producer.
deadline = time.monotonic() + 45
while identifier in self.items and time.monotonic() < deadline:
state = self.camera.snapshot()
delivery = state.get("delivery") or {}
if state.get("generation") is not None and delivery.get("media_type"):
lease = await asyncio.to_thread(self.camera.open_delivery, state["generation"])
try:
channel.send(json.dumps({
"type": "camera-ready", "mime": delivery["media_type"],
}))
except BaseException:
self.camera.release_delivery(lease, client_closed=True)
raise
return lease
if state.get("phase") == "error":
break
await asyncio.sleep(0.25)
return None
async def deliver(self, identifier, channel):
subscriber = lease = None
try:
@@ -122,11 +155,9 @@ class NodeMediaPeers:
if channel.label == "rrd":
subscriber = await asyncio.to_thread(self.hub.subscribe)
else:
generation = self.camera.snapshot().get("generation")
if generation is None:
channel.close()
lease = await self.camera_delivery(identifier, channel)
if lease is None:
return
lease = await asyncio.to_thread(self.camera.open_delivery, generation)
while identifier in self.items and time.monotonic() - entry["seen"] < 30:
if subscriber:
payload = await asyncio.to_thread(subscriber.read)
@@ -144,8 +175,11 @@ class NodeMediaPeers:
break
if payload:
await self.send(channel, payload)
except (Exception, asyncio.CancelledError):
except asyncio.CancelledError:
pass
except Exception as error:
logger.warning("Node preview delivery failed channel=%s exception=%s",
channel.label, type(error).__name__)
finally:
if subscriber:
subscriber.close()