Preserve RRD preview frames and clarify onboard K1 status
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user