fix(k1): stabilize repeated acquisition and live viewer recovery
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import socket
|
||||
import time
|
||||
from collections.abc import Callable, Mapping
|
||||
from contextlib import suppress
|
||||
@@ -31,6 +33,7 @@ TRAJECTORY_MIN_DISTANCE_METERS = 0.02
|
||||
TRAJECTORY_PUBLISH_INTERVAL_NS = 500_000_000
|
||||
LIVE_GRPC_BUFFER_LIMIT = "32MiB"
|
||||
DEFAULT_GRPC_PORT = 9876
|
||||
GRPC_PORT_SEARCH_SPAN = 128
|
||||
DEFAULT_CORS_ORIGINS = (
|
||||
"http://127.0.0.1:5173",
|
||||
"http://localhost:5173",
|
||||
@@ -40,6 +43,8 @@ DEFAULT_CORS_ORIGINS = (
|
||||
"http://localhost:8000",
|
||||
)
|
||||
|
||||
logger = logging.getLogger("k1link.device_plugins.xgrids_k1.viewer_receiver")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RerunSceneSettings:
|
||||
@@ -62,6 +67,34 @@ class RerunSceneSettings:
|
||||
SettingsProvider = Callable[[], RerunSceneSettings]
|
||||
|
||||
|
||||
def _select_available_grpc_port(
|
||||
preferred_port: int,
|
||||
*,
|
||||
search_span: int = GRPC_PORT_SEARCH_SPAN,
|
||||
) -> int:
|
||||
"""Select a local Rerun port without reusing a still-served recording."""
|
||||
|
||||
if not 1 <= preferred_port <= 65_535:
|
||||
raise ValueError("Rerun gRPC port must be between 1 and 65535")
|
||||
if search_span < 1:
|
||||
raise ValueError("Rerun gRPC port search span must be positive")
|
||||
|
||||
last_port = min(preferred_port + search_span, 65_536)
|
||||
for candidate in range(preferred_port, last_port):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
||||
try:
|
||||
# Rerun binds all local interfaces. Probe the same address class
|
||||
# so a previous recording retained by a late viewer is detected.
|
||||
probe.bind(("0.0.0.0", candidate))
|
||||
except OSError:
|
||||
continue
|
||||
return candidate
|
||||
raise RuntimeError(
|
||||
"No local Rerun gRPC port is available in "
|
||||
f"{preferred_port}..{last_port - 1}"
|
||||
)
|
||||
|
||||
|
||||
class RerunBridge:
|
||||
"""Publish transport-neutral canonical envelopes to a Rerun recording."""
|
||||
|
||||
@@ -85,9 +118,20 @@ class RerunBridge:
|
||||
else:
|
||||
recording = recording_factory("nodedc_mission_core_spatial")
|
||||
try:
|
||||
selected_grpc_port = _select_available_grpc_port(grpc_port)
|
||||
if selected_grpc_port != grpc_port:
|
||||
logger.info(
|
||||
"Mission Core selected a new Rerun port because an earlier "
|
||||
"viewer still owns the preferred listener",
|
||||
extra={
|
||||
"event_code": "rerun_grpc_port_rotated",
|
||||
"preferred_port": grpc_port,
|
||||
"selected_port": selected_grpc_port,
|
||||
},
|
||||
)
|
||||
blueprint = _blueprint(self._settings)
|
||||
url = recording.serve_grpc(
|
||||
grpc_port=grpc_port,
|
||||
grpc_port=selected_grpc_port,
|
||||
default_blueprint=blueprint,
|
||||
# This is a reconnect cushion for the live preview, not the source
|
||||
# of record. Raw MQTT evidence is persisted independently. A large
|
||||
@@ -132,7 +176,7 @@ class RerunBridge:
|
||||
return self._url
|
||||
|
||||
def begin_session(self, metrics: BridgeMetrics | None = None) -> None:
|
||||
"""Reset session state while keeping the process-lifetime server alive."""
|
||||
"""Initialize the one acquisition owned by this recording server."""
|
||||
if self._closed:
|
||||
raise RuntimeError("Rerun bridge is already closed")
|
||||
if metrics is not None:
|
||||
|
||||
Reference in New Issue
Block a user