feat(k1): stabilize LAB bridge and isolate onboard device integration

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 10:31:33 +03:00
parent 020a878915
commit 63ea2bed67
115 changed files with 13700 additions and 988 deletions
@@ -2,7 +2,10 @@ from __future__ import annotations
import asyncio
import math
import re
import sys
from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass, replace
from importlib.metadata import version
from threading import Lock
@@ -33,6 +36,8 @@ BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS = BLE_DISCOVERY_CANDIDATE_LEASE_TTL_SECONDS
# low-level retained-handle lifetime.
BLE_DISCOVERY_LEASE_TTL_SECONDS = BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS
BLE_SCAN_HARD_TIMEOUT_GRACE_SECONDS = 5.0
BLE_SCAN_INITIAL_WINDOW_SECONDS = 6.0
BLE_SCAN_DEFAULT_TIMEOUT_SECONDS = 20.0
_runtime_handle_lock = Lock()
_runtime_handles: dict[str, BLEDevice] = {}
@@ -132,6 +137,14 @@ class BleDeviceRecord(TypedDict):
k1_name_candidate: bool
class BleDiscoveryTiming(TypedDict, total=False):
scan_elapsed_ms: int
scanner_start_ms: int
initial_window_ms: int
first_candidate_ms: int
scan_extended: bool
class BleScanResult(TypedDict):
schema_version: int
started_at_utc: str
@@ -141,6 +154,7 @@ class BleScanResult(TypedDict):
bleak_version: str
device_count: int
devices: list[BleDeviceRecord]
discovery_timing: BleDiscoveryTiming
def advertisement_record(device: BLEDevice, advertisement: AdvertisementData) -> BleDeviceRecord:
@@ -148,7 +162,7 @@ def advertisement_record(device: BLEDevice, advertisement: AdvertisementData) ->
normalized_name = (name or "").casefold()
return {
"macos_uuid": device.address,
"id_kind": "corebluetooth_uuid",
"id_kind": "bluez_address" if sys.platform.startswith("linux") else "corebluetooth_uuid",
"name": device.name,
"local_name": advertisement.local_name,
"rssi": advertisement.rssi,
@@ -803,11 +817,34 @@ def connected_device_recovery_name(
return normalized or None
async def _retrieve_bluez_device(address: str, details: object = None) -> BLEDevice | None:
"""Retrieve only the exact cached BlueZ object; no discovery or connection."""
if not re.fullmatch(r"(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}", address):
return None
try:
from bleak.backends.bluezdbus.manager import get_global_bluez_manager
manager = await asyncio.wait_for(get_global_bluez_manager(), timeout=3)
path = details.get("path") if isinstance(details, dict) else None
if not path:
path = manager.get_default_adapter() + "/dev_" + address.upper().replace(":", "_")
if manager.get_device_address(path).casefold() != address.casefold():
return None
name = manager.get_device_name(path)
return BLEDevice(address, name, details={"path": path, "props": {
"Address": address, "Alias": name, "Adapter": path.rsplit("/", 1)[0],
}})
except Exception:
return None
async def _retrieve_corebluetooth_device(
captured: CapturedDiscoveredDevice,
) -> BLEDevice | None:
"""Retrieve one UUID through the exact CoreBluetooth manager that observed it."""
if sys.platform.startswith("linux"):
return await _retrieve_bluez_device(captured.macos_uuid, captured.device.details)
details = captured.device.details
if not isinstance(details, tuple) or len(details) != 2:
return None
@@ -899,6 +936,20 @@ async def retrieve_known_device_capture_for_status_read(
or runtime["poisoned"]
):
return None
if sys.platform.startswith("linux"):
device = await _retrieve_bluez_device(macos_uuid)
after = ble_runtime_snapshot()
if (device is None or ble_runtime_owner_epoch_for_current_loop() != owner_epoch
or after["owner_epoch"] != owner_epoch
or not after["owner_loop_bound"] or after["poisoned"]
or after["active_operation_kind"] != "status-read"):
return None
now = _freshness_observed_at()
return CapturedDiscoveredDevice(
device=device, macos_uuid=macos_uuid, owner_epoch=owner_epoch,
scan_generation=0, captured_at_monotonic=now.monotonic,
captured_at_suspend_aware=now.suspend_aware, source="retrieved-durable",
)
context = _new_corebluetooth_retrieval_context(macos_uuid)
if context is None:
return None
@@ -977,10 +1028,14 @@ async def discover_known_device_capture_for_status_read(
or runtime["poisoned"]
):
return None
try:
UUID(macos_uuid)
except (TypeError, ValueError):
return None
if sys.platform.startswith("linux"):
if not re.fullmatch(r"(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}", macos_uuid):
return None
else:
try:
UUID(macos_uuid)
except (TypeError, ValueError):
return None
candidate = await BleakScanner.find_device_by_address(
macos_uuid,
@@ -1148,6 +1203,42 @@ def connected_device_recovery_snapshot(
}
async def _discover_k1_advertisements(
*, timeout: float, diagnostics: BleDiscoveryTiming
) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
"""Listen once, extending the initial window only while no K1 is visible.
Every result and native handle comes from this scanner's callbacks. A name
match only ends discovery; it grants no identity or GATT authority.
"""
loop = asyncio.get_running_loop()
started = loop.time()
initial_window = min(timeout, BLE_SCAN_INITIAL_WINDOW_SECONDS)
discovered: dict[str, tuple[BLEDevice, AdvertisementData]] = {}
candidate_seen = asyncio.Event()
def observe(device: BLEDevice, advertisement: AdvertisementData) -> None:
discovered[device.address] = (device, advertisement)
if advertisement_record(device, advertisement)["k1_name_candidate"]:
if not candidate_seen.is_set():
diagnostics["first_candidate_ms"] = round((loop.time() - started) * 1000)
candidate_seen.set()
diagnostics["initial_window_ms"] = round(initial_window * 1000)
diagnostics["scan_extended"] = False
async with BleakScanner(detection_callback=observe):
listening_started = loop.time()
diagnostics["scanner_start_ms"] = round((listening_started - started) * 1000)
await asyncio.sleep(initial_window)
remaining = max(0.0, timeout - (loop.time() - listening_started))
if not candidate_seen.is_set() and remaining > 0:
diagnostics["scan_extended"] = True
with suppress(TimeoutError):
await asyncio.wait_for(candidate_seen.wait(), timeout=remaining)
diagnostics["scan_elapsed_ms"] = round((loop.time() - started) * 1000)
return discovered
async def _scan_impl(
duration_seconds: float,
progress: BleOperationProgress,
@@ -1163,7 +1254,10 @@ async def _scan_impl(
progress.operation_stage = "discovery"
scan_generation = _begin_scan_generation(owner_epoch)
try:
discovered = await BleakScanner.discover(timeout=duration_seconds, return_adv=True)
discovery_timing: BleDiscoveryTiming = {}
discovered = await _discover_k1_advertisements(
timeout=duration_seconds, diagnostics=discovery_timing
)
handles = {device.address: device for device, _advertisement in discovered.values()}
devices = [
advertisement_record(device, advertisement)
@@ -1181,10 +1275,11 @@ async def _scan_impl(
"started_at_utc": started_at,
"completed_at_utc": utc_now_iso(),
"duration_seconds": duration_seconds,
"adapter": "CoreBluetooth",
"adapter": "BlueZ" if sys.platform.startswith("linux") else "CoreBluetooth",
"bleak_version": version("bleak"),
"device_count": len(devices),
"devices": devices,
"discovery_timing": discovery_timing,
}
except BaseException:
_finish_failed_scan(scan_generation)
+11 -4
View File
@@ -303,6 +303,7 @@ class XgridsK1CameraGateway:
repository_root: Path,
plugin_id: str,
*,
evidence_root: Path | None = None,
committed_segment_observer: CommittedCameraSegmentObserver | None = None,
process_fence_descriptor_factory: Callable[[], int] | None = None,
producer_stall_observer: CameraProducerStallObserver | None = None,
@@ -316,6 +317,12 @@ class XgridsK1CameraGateway:
if not 0.01 <= producer_watchdog_interval_seconds <= 60.0:
raise ValueError("camera watchdog interval must be within 0.01..60 seconds")
self._repository_root = repository_root.resolve()
# Runtime evidence may live outside a source checkout (worktrees/Node).
# The composition supplies this trusted root; request paths cannot widen it.
self._evidence_root = (
evidence_root.expanduser().resolve() if evidence_root is not None
else self._repository_root
)
self._plugin_id = plugin_id
self._lock = threading.RLock()
self._lifecycle_lock = threading.Lock()
@@ -674,8 +681,8 @@ class XgridsK1CameraGateway:
root = session_dir.expanduser().resolve()
if not root.is_dir():
raise ValueError("observation session directory does not exist")
if not root.is_relative_to(self._repository_root):
raise ValueError("camera recording root must stay inside the repository")
if not root.is_relative_to(self._evidence_root):
raise ValueError("camera recording root must stay inside the configured evidence root")
reserved_generation: list[int] = []
def reserve() -> bool:
@@ -805,8 +812,8 @@ class XgridsK1CameraGateway:
root = session_dir.expanduser().resolve()
if not root.is_dir():
raise ValueError("observation session directory does not exist")
if not root.is_relative_to(self._repository_root):
raise ValueError("camera recording root must stay inside the repository")
if not root.is_relative_to(self._evidence_root):
raise ValueError("camera recording root must stay inside the configured evidence root")
with self._lifecycle_lock:
with self._lock:
@@ -0,0 +1,495 @@
"""Pure K1 connection read model shared by operator and onboard hosts.
Journal and supervisor own state and authority. These functions only join their
existing evidence; they never dispatch commands or schedule recovery.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from typing import Any, Literal
from .connection_supervisor import ConnectionSupervisorSnapshot
ACTION_NETWORK_PROVISION = "network.provision"
ACTION_CONNECTION_VERIFY = "connection.verify"
ACTION_CONNECTION_CONTROL_BOOTSTRAP = "connection.control-bootstrap"
CONTROL_MQTT_PORT = 1883
def compact_connection_attempt(value: object) -> dict[str, Any] | None:
"""Expose the same attempt to Node without journal payloads or diagnostics.
Safe-next-action is already intersected with current authority by the
service. Do not rederive it from historical success in a second host.
"""
if not isinstance(value, Mapping):
return None
if value.get("schema_version") != "missioncore.xgrids-k1-connection-attempt/v1":
return None
keys = (
"schema_version",
"attempt_id",
"connection_mode",
"status",
"phase",
"control_state",
"stage",
"public_error_code",
"side_effect_status",
"safe_next_action",
"accepted_at",
"completed_at",
"recovery_operation_id",
)
result = {
key: item if isinstance(item := value.get(key), str) and len(item) <= 160 else None
for key in keys
}
result["automatic_retry"] = False
return result
def _network_attempt_matches_live_control_authority(
*,
operation_documents: Sequence[Mapping[str, Any]],
attempt_id: object,
snapshot_runtime_id: str,
supervisor: ConnectionSupervisorSnapshot,
active_binding: Mapping[str, Any] | None,
) -> bool:
"""Require historical bootstrap evidence and the exact live supervisor proof."""
if not isinstance(attempt_id, str) or not attempt_id:
return False
network_operation = next(
(
item
for item in reversed(operation_documents)
if item.get("operation_id") == attempt_id
and item.get("action") == ACTION_NETWORK_PROVISION
),
None,
)
if not isinstance(network_operation, Mapping):
return False
context = network_operation.get("context")
result = network_operation.get("result")
if not isinstance(context, Mapping) or not isinstance(result, Mapping):
return False
transport_ref = result.get("transport_ref")
connection_mode = result.get("connection_mode")
target_ipv4 = result.get("target_ipv4")
target_port = result.get("target_port")
expected_live_intent_id = attempt_id
recovery_verify = next(
(
item
for item in reversed(operation_documents)
if item.get("action") == ACTION_CONNECTION_VERIFY
and item.get("status") == "succeeded"
and _recovery_verify_matches_network_attempt(item, network_operation)
),
None,
)
if isinstance(recovery_verify, Mapping):
verify_result = recovery_verify.get("result")
verified_binding = (
verify_result.get("verified_binding") if isinstance(verify_result, Mapping) else None
)
recovered_intent_id = (
verified_binding.get("intent_id") if isinstance(verified_binding, Mapping) else None
)
if not isinstance(recovered_intent_id, str) or not recovered_intent_id:
return False
expected_live_intent_id = recovered_intent_id
return bool(
network_operation.get("status") == "succeeded"
and result.get("phase") == "network_applied"
and context.get("snapshot_runtime_id") == snapshot_runtime_id
and result.get("snapshot_runtime_id") == snapshot_runtime_id
and result.get("parent_intent_id") == attempt_id
and isinstance(transport_ref, str)
and transport_ref
and context.get("transport_ref") == transport_ref
and connection_mode in {"bridge", "quick-connect", "direct-connect"}
and context.get("connection_mode") == connection_mode
and isinstance(target_ipv4, str)
and target_ipv4
and target_port == CONTROL_MQTT_PORT
and isinstance(active_binding, Mapping)
and active_binding.get("intent_id") == expected_live_intent_id
and active_binding.get("transport_ref") == transport_ref
and active_binding.get("connection_mode") == connection_mode
and active_binding.get("target_ipv4") == target_ipv4
and active_binding.get("target_port") == target_port
and supervisor.authority.control_allowed
and supervisor.intent is not None
and supervisor.intent.intent_id == expected_live_intent_id
and supervisor.intent.requested_mode == connection_mode
and supervisor.device_network.state == "applied"
and supervisor.device_network.intent_id == expected_live_intent_id
and supervisor.device_network.transport_ref == transport_ref
and supervisor.device_network.connection_mode == connection_mode
and supervisor.device_network.target is not None
and supervisor.device_network.target.ipv4 == target_ipv4
and supervisor.device_network.target.port == target_port
and supervisor.host_path.available
and supervisor.host_path.epoch >= 1
and supervisor.endpoint.intent_id == expected_live_intent_id
and supervisor.endpoint.target == supervisor.device_network.target
and supervisor.endpoint.tcp_state == "reachable"
and supervisor.endpoint.host_path_epoch == supervisor.host_path.epoch
and supervisor.lease.state == "reachable"
and supervisor.lease.intent_id == expected_live_intent_id
and supervisor.lease.target == supervisor.device_network.target
and supervisor.lease.connection_mode == connection_mode
and supervisor.lease.host_path_epoch == supervisor.host_path.epoch
and supervisor.control_plane.state == "healthy"
and supervisor.control_plane.host_path_epoch == supervisor.host_path.epoch
and active_binding.get("host_path_epoch") == supervisor.host_path.epoch
and isinstance(active_binding.get("control_session_id"), str)
and bool(active_binding.get("control_session_id"))
)
def _owned_control_bootstrap_matches_network_attempt(
bootstrap_operation: Mapping[str, Any],
network_operation: Mapping[str, Any],
) -> bool:
"""Join only the service-owned child admitted for this exact Apply proof."""
bootstrap_context = bootstrap_operation.get("context")
network_context = network_operation.get("context")
network_result = network_operation.get("result")
if not (
isinstance(bootstrap_context, Mapping)
and isinstance(network_context, Mapping)
and isinstance(network_result, Mapping)
):
return False
network_operation_id = network_operation.get("operation_id")
runtime_id = network_result.get("snapshot_runtime_id")
transport_ref = network_result.get("transport_ref")
connection_mode = network_result.get("connection_mode")
target_ipv4 = network_result.get("target_ipv4")
target_port = network_result.get("target_port")
exact_parent = bool(
isinstance(network_operation_id, str)
and network_operation_id
and network_operation.get("status") == "succeeded"
and network_result.get("phase") == "network_applied"
and isinstance(runtime_id, str)
and runtime_id
and network_context.get("snapshot_runtime_id") == runtime_id
and network_context.get("transport_ref") == transport_ref
and network_context.get("connection_mode") == connection_mode
and network_result.get("parent_intent_id") == network_operation_id
and isinstance(transport_ref, str)
and transport_ref
and connection_mode in {"bridge", "quick-connect", "direct-connect"}
and isinstance(target_ipv4, str)
and target_ipv4
and target_port == CONTROL_MQTT_PORT
and bootstrap_context.get("ownership") == "service-owned-apply-continuation"
and bootstrap_context.get("snapshot_runtime_id") == runtime_id
and bootstrap_context.get("parent_operation_id") == network_operation_id
and bootstrap_context.get("parent_intent_id") == network_operation_id
and bootstrap_context.get("transport_ref") == transport_ref
and bootstrap_context.get("connection_mode") == connection_mode
and bootstrap_context.get("target_ipv4") == target_ipv4
and bootstrap_context.get("target_port") == target_port
and isinstance(bootstrap_context.get("device_session_id"), str)
and bool(bootstrap_context.get("device_session_id"))
and bootstrap_context.get("network_mutation_performed") is False
and bootstrap_context.get("ble_operation_performed") is False
and bootstrap_context.get("automatic_retry") is False
)
if not exact_parent:
return False
if bootstrap_operation.get("status") != "succeeded":
return True
bootstrap_result = bootstrap_operation.get("result")
return bool(
isinstance(bootstrap_result, Mapping)
and bootstrap_result.get("connection_mode") == connection_mode
and bootstrap_result.get("control_verified") is True
and bootstrap_result.get("network_mutation_performed") is False
and bootstrap_result.get("ble_operation_performed") is False
and bootstrap_result.get("automatic_retry") is False
)
def _recovery_verify_matches_network_attempt(
verify_operation: Mapping[str, Any],
network_operation: Mapping[str, Any],
) -> bool:
"""Join Verify only to the exact network parent it was admitted against."""
verify_context = verify_operation.get("context")
network_context = network_operation.get("context")
network_result = network_operation.get("result")
if not (
isinstance(verify_context, Mapping)
and isinstance(network_context, Mapping)
and isinstance(network_result, Mapping)
):
return False
network_operation_id = network_operation.get("operation_id")
runtime_id = network_result.get("snapshot_runtime_id")
transport_ref = network_result.get("transport_ref")
connection_mode = network_result.get("connection_mode")
target_ipv4 = network_result.get("target_ipv4")
target_port = network_result.get("target_port")
exact_parent = bool(
isinstance(network_operation_id, str)
and network_operation_id
and isinstance(runtime_id, str)
and runtime_id
and network_context.get("snapshot_runtime_id") == runtime_id
and network_context.get("transport_ref") == transport_ref
and network_context.get("connection_mode") == connection_mode
and network_result.get("parent_intent_id") == network_operation_id
and isinstance(transport_ref, str)
and transport_ref
and connection_mode in {"bridge", "quick-connect", "direct-connect"}
and isinstance(target_ipv4, str)
and target_ipv4
and target_port == CONTROL_MQTT_PORT
and verify_context.get("snapshot_runtime_id") == runtime_id
and verify_context.get("recovery_parent_operation_id") == network_operation_id
and verify_context.get("recovery_parent_intent_id") == network_operation_id
and verify_context.get("recovery_transport_ref") == transport_ref
and verify_context.get("recovery_connection_mode") == connection_mode
and verify_context.get("recovery_target_ipv4") == target_ipv4
and verify_context.get("recovery_target_port") == target_port
)
if not exact_parent:
return False
if verify_operation.get("status") != "succeeded":
return True
verify_result = verify_operation.get("result")
if not isinstance(verify_result, Mapping):
return False
binding = verify_result.get("verified_binding")
return bool(
isinstance(binding, Mapping)
and binding.get("snapshot_runtime_id") == runtime_id
and isinstance(binding.get("intent_id"), str)
and bool(binding.get("intent_id"))
and binding.get("transport_ref") == transport_ref
and binding.get("connection_mode") == connection_mode
and binding.get("target_ipv4") == target_ipv4
and binding.get("target_port") == target_port
and isinstance(binding.get("host_path_epoch"), int)
and not isinstance(binding.get("host_path_epoch"), bool)
and binding.get("host_path_epoch", 0) >= 1
and verify_result.get("write_performed") is False
and verify_result.get("control_verified") is True
)
def _connection_attempt_projection(
operation_documents: Sequence[Mapping[str, Any]],
) -> dict[str, Any] | None:
"""Project the existing operation journal as one K1 connection attempt.
This is deliberately a read model, not another lifecycle authority. The
operation journal, durable network ledger and connection supervisor remain
the writers of their respective facts; the projection only makes the
current stage and a safe next action visible to the operator.
"""
network_operation_index = next(
(
index
for index in range(len(operation_documents) - 1, -1, -1)
if operation_documents[index].get("action") == ACTION_NETWORK_PROVISION
),
None,
)
if network_operation_index is None:
return None
operation = operation_documents[network_operation_index]
network_context = operation.get("context")
network_context_mapping = network_context if isinstance(network_context, Mapping) else {}
control_bootstrap = next(
(
item
for item in reversed(operation_documents)
if item.get("action") == ACTION_CONNECTION_CONTROL_BOOTSTRAP
and _owned_control_bootstrap_matches_network_attempt(item, operation)
),
None,
)
error = operation.get("error")
result = operation.get("result")
error_mapping = error if isinstance(error, Mapping) else {}
result_mapping = result if isinstance(result, Mapping) else {}
context_mapping = network_context_mapping
network_status = str(operation.get("status") or "unknown")
status = network_status
stage = operation.get("stage_code")
control_state: Literal["ready", "control_not_ready", "unknown"] = "unknown"
public_error_code = (
error_mapping.get("code") if isinstance(error_mapping.get("code"), str) else None
)
physical_reconciliation: Mapping[str, Any] | None = None
physical_active_observed = False
if network_status == "succeeded" and control_bootstrap is not None:
bootstrap_status = str(control_bootstrap.get("status") or "unknown")
status = bootstrap_status
stage = control_bootstrap.get("stage_code")
bootstrap_result = control_bootstrap.get("result")
bootstrap_reconciliation = (
bootstrap_result.get("physical_reconciliation")
if isinstance(bootstrap_result, Mapping)
else None
)
if isinstance(bootstrap_reconciliation, Mapping):
physical_reconciliation = bootstrap_reconciliation
physical_active_observed = bool(
bootstrap_reconciliation.get("performed") is True
and bootstrap_reconciliation.get("resolution") == "physical-active-observed"
and bootstrap_reconciliation.get("observed_session_state") == "scanning"
and bootstrap_reconciliation.get("device_write_performed") is False
)
if bootstrap_status == "succeeded":
control_state = "unknown" if physical_active_observed else "ready"
elif bootstrap_status == "failed":
control_state = "control_not_ready"
bootstrap_error = control_bootstrap.get("error")
if isinstance(bootstrap_error, Mapping):
bootstrap_code = bootstrap_error.get("code")
public_error_code = (
bootstrap_code if isinstance(bootstrap_code, str) else public_error_code
)
recovery_verify = next(
(
item
for item in reversed(operation_documents[network_operation_index + 1 :])
if item.get("action") == ACTION_CONNECTION_VERIFY
and _recovery_verify_matches_network_attempt(item, operation)
),
None,
)
# A later explicit read-only Verify is the canonical recovery for an
# applied network edge whose first DeviceInfo bootstrap failed. It must
# update the visible attempt without rewriting or replaying that edge.
if network_status == "succeeded" and recovery_verify is not None:
status = str(recovery_verify.get("status") or "unknown")
stage = recovery_verify.get("stage_code")
verify_result = recovery_verify.get("result")
verify_reconciliation = (
verify_result.get("physical_reconciliation")
if isinstance(verify_result, Mapping)
else None
)
if isinstance(verify_reconciliation, Mapping):
physical_reconciliation = verify_reconciliation
physical_active_observed = bool(
verify_reconciliation.get("performed") is True
and verify_reconciliation.get("resolution") == "physical-active-observed"
and verify_reconciliation.get("observed_session_state") == "scanning"
and verify_reconciliation.get("device_write_performed") is False
)
if status == "succeeded":
control_state = "unknown" if physical_active_observed else "ready"
elif status == "failed":
control_state = "control_not_ready"
else:
control_state = "unknown"
recovery_error = recovery_verify.get("error")
public_error_code = (
recovery_error.get("code")
if isinstance(recovery_error, Mapping) and isinstance(recovery_error.get("code"), str)
else None
)
side_effect_status = error_mapping.get(
"side_effect_status",
result_mapping.get("side_effect_status"),
)
if not isinstance(side_effect_status, str):
side_effect_status = "none" if status in {"accepted", "running"} else "unknown"
phase: Literal[
"network_applied",
"network_not_applied",
"network_outcome_unknown",
]
if network_status == "succeeded":
phase = "network_applied"
elif side_effect_status == "unknown":
# Once a device write may have crossed the GATT boundary, a failed
# host request cannot truthfully claim that the network was not
# applied. This remains an audit/read-model distinction only: it does
# not authorize an automatic retry or replay of the old operation.
phase = "network_outcome_unknown"
else:
phase = "network_not_applied"
owned_bootstrap_pending = bool(
control_bootstrap is not None and status in {"accepted", "running"}
)
recovery_verify_pending = bool(
recovery_verify is not None and status in {"accepted", "running"}
)
if owned_bootstrap_pending or recovery_verify_pending:
safe_next_action = "wait-for-current-attempt"
elif status == "succeeded" and physical_active_observed:
safe_next_action = "stop-acquisition"
elif status == "succeeded" and control_state == "ready":
safe_next_action = "start-acquisition"
elif network_status == "succeeded":
safe_next_action = "verify-control-read-only"
else:
# Historical ambiguity blocks automatic replay of this attempt, not a
# later explicit scan-select-connect session.
safe_next_action = "scan-select-connect"
events = operation.get("events")
return {
"schema_version": "missioncore.xgrids-k1-connection-attempt/v1",
"attempt_id": operation.get("operation_id"),
"connection_mode": context_mapping.get("connection_mode"),
"status": status,
"phase": phase,
"control_state": control_state,
"stage": stage,
"public_error_code": public_error_code,
"side_effect_status": side_effect_status,
"safe_next_action": safe_next_action,
"physical_reconciliation": (
dict(physical_reconciliation) if physical_reconciliation is not None else None
),
"automatic_retry": False,
"accepted_at": operation.get("accepted_at"),
"completed_at": operation.get("completed_at"),
"recovery_operation_id": (
recovery_verify.get("operation_id") if isinstance(recovery_verify, Mapping) else None
),
"timeline": [
*(
[dict(item) for item in events if isinstance(item, Mapping)]
if isinstance(events, list)
else []
),
*(
[
{**dict(item), "source_action": ACTION_CONNECTION_CONTROL_BOOTSTRAP}
for item in control_bootstrap.get("events", [])
if isinstance(item, Mapping)
]
if isinstance(control_bootstrap, Mapping)
and isinstance(control_bootstrap.get("events"), list)
else []
),
*(
[
{**dict(item), "source_action": ACTION_CONNECTION_VERIFY}
for item in recovery_verify.get("events", [])
if isinstance(item, Mapping)
]
if isinstance(recovery_verify, Mapping)
and isinstance(recovery_verify.get("events"), list)
else []
),
],
}
+53 -464
View File
@@ -88,6 +88,7 @@ from k1link.device_plugins.xgrids_k1.ble.runtime_arbiter import (
wait_for_ble_runtime_idle,
)
from k1link.device_plugins.xgrids_k1.ble.scanner import (
BLE_SCAN_DEFAULT_TIMEOUT_SECONDS,
BLE_SCAN_HARD_TIMEOUT_GRACE_SECONDS,
CapturedDiscoveredDevice,
capture_discovered_device,
@@ -122,6 +123,14 @@ from k1link.device_plugins.xgrids_k1.camera import (
build_xgrids_k1_camera_router,
classify_camera_recording_health,
)
from k1link.device_plugins.xgrids_k1.connection_attempt import (
ACTION_CONNECTION_CONTROL_BOOTSTRAP,
ACTION_CONNECTION_VERIFY,
ACTION_NETWORK_PROVISION,
CONTROL_MQTT_PORT,
_connection_attempt_projection,
_network_attempt_matches_live_control_authority,
)
from k1link.device_plugins.xgrids_k1.connection_supervisor import (
ConnectionMonitorProbeSuperseded,
ConnectionSupervisor,
@@ -226,6 +235,7 @@ from k1link.device_plugins.xgrids_k1.viewer.runtime import (
VisualizationRuntime,
new_live_session_dir,
)
from k1link.device_plugins.xgrids_k1.wifi_failure import reviewed_station_failure_code
from k1link.host_network import (
HostWifiAssociationIdentityProbe,
HostWifiAssociationIdentityResult,
@@ -264,7 +274,6 @@ XGRIDS_K1_PLUGIN_VERSION = "0.7.5"
XGRIDS_K1_MODEL_ID = "xgrids.lixelkity-k1"
XGRIDS_K1_COMPATIBILITY_PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"
DEFAULT_ACQUISITION_CAMERA_SOURCE: CameraSourceId = "sensor.camera.right"
CONTROL_MQTT_PORT = 1883
CONTROL_ENDPOINT_PROBE_TIMEOUT_SECONDS = 1.5
CONTROL_ENDPOINT_ADMISSION_TIMEOUT_SECONDS = 8.0
CONTROL_ENDPOINT_ADMISSION_INTERVAL_SECONDS = 0.5
@@ -425,12 +434,9 @@ ACTION_DISCOVERY_SCAN = "discovery.scan"
ACTION_DEVICE_INSPECT = "device.inspect"
ACTION_SENSOR_CATALOG_READ = "sensor.catalog.read"
ACTION_DEVICE_CALIBRATION_SNAPSHOT_READ = "calibration.device-snapshot.read"
ACTION_NETWORK_PROVISION = "network.provision"
ACTION_CONNECTION_MODE_SELECT = "connection.mode.select"
ACTION_CONNECTION_RECONFIGURE_PREPARE = "connection.reconfigure.prepare"
ACTION_CONNECTION_VERIFY = "connection.verify"
ACTION_CONFIGURED_ENDPOINT_PROBE = "connection.endpoint-probe"
ACTION_CONNECTION_CONTROL_BOOTSTRAP = "connection.control-bootstrap"
ACTION_ACQUISITION_PREPARE = "acquisition.prepare"
ACTION_ACQUISITION_START = "acquisition.start"
ACTION_ACQUISITION_STOP = "acquisition.stop"
@@ -1014,7 +1020,7 @@ class _ConfiguredEndpointHostObservation:
class BleScanRequest(StrictRequest):
duration_seconds: float = Field(default=6.0, ge=1.0, le=60.0)
duration_seconds: float = Field(default=BLE_SCAN_DEFAULT_TIMEOUT_SECONDS, ge=1.0, le=60.0)
operation_id: str | None = Field(default=None, min_length=1, max_length=128)
@@ -1476,6 +1482,7 @@ class XgridsK1CompatibilityService:
application_authority_loader: ApplicationAuthorityLoader | None = None,
calibration_snapshot_reader: DeviceCalibrationSnapshotReader | None = None,
host_wifi_association_probe: HostWifiAssociationProbe | None = None,
visualization_bridge_factory: Callable[..., Any] | None = None,
) -> None:
self.repository_root = repository_root.resolve()
# Every CoreBluetooth entrypoint, including read-only calibration and
@@ -1876,6 +1883,7 @@ class XgridsK1CompatibilityService:
physical_command_coordinator=self._physical_command_coordinator,
)
self.runtime = VisualizationRuntime(
bridge_factory=visualization_bridge_factory,
normalizer=normalize_k1_message,
message_observer=self._observe_runtime_message,
published_envelope_observer=self._observe_published_runtime_envelope,
@@ -1886,6 +1894,7 @@ class XgridsK1CompatibilityService:
self.camera_preview = XgridsK1CameraGateway(
self.repository_root,
XGRIDS_K1_PLUGIN_ID,
evidence_root=self.evidence_root,
committed_segment_observer=self._observe_committed_camera_segment,
process_fence_descriptor_factory=(self._duplicate_camera_process_fence_descriptor),
producer_stall_observer=self._observe_camera_producer_stall,
@@ -10190,19 +10199,27 @@ class XgridsK1CompatibilityService:
f"Поиск завершён. Найдено BLE-устройств: {len(devices)}."
)
self._operation_phase = None
discovery_result = {
"candidate_count": len(devices),
"likely_k1_candidate_count": sum(item["likely_k1"] is True for item in devices),
"duration_seconds": duration_seconds,
"discovery_generation": scan_generation,
**result.get("discovery_timing", {}),
}
logger.info(
"K1 BLE discovery completed",
extra={
"event_code": "k1_ble_discovery_completed",
"operation_id": operation.operation_id,
**discovery_result,
},
)
self._operations.transition(
operation.operation_id,
"succeeded",
stage_code="completed",
message_code="discovery.scan.completed",
result={
"candidate_count": len(devices),
"likely_k1_candidate_count": sum(
item["likely_k1"] is True for item in devices
),
"duration_seconds": duration_seconds,
"discovery_generation": scan_generation,
},
result=discovery_result,
)
with self._lock:
if (
@@ -12143,6 +12160,14 @@ class XgridsK1CompatibilityService:
safe_to_retry=not device_write_attempted,
host_boundary="corebluetooth" if isinstance(exc, BleakError) else None,
)
station_failure_code = reviewed_station_failure_code(
operation_error,
firmware_version=request.compatibility_attestation.firmware_version,
connection_mode=request.connection_mode,
)
if station_failure_code is not None:
operation_error["transport_error_code"] = operation_error["code"]
operation_error["code"] = station_failure_code
if idempotency_record.stage == "prepared":
cancelled = isinstance(exc, asyncio.CancelledError)
idempotency_record = idempotency_journal.complete(
@@ -12222,6 +12247,7 @@ class XgridsK1CompatibilityService:
"connection_mode": request.connection_mode,
"error_category": operation_error["category"],
"error_code": operation_error["code"],
"transport_error_code": operation_error.get("transport_error_code"),
"safe_to_retry": operation_error["safe_to_retry"],
"side_effect_status": operation_error["side_effect_status"],
"network_change_attempted": device_write_attempted,
@@ -34801,457 +34827,6 @@ def _network_mutation_ledger_public_snapshot(
}
def _network_attempt_matches_live_control_authority(
*,
operation_documents: Sequence[Mapping[str, Any]],
attempt_id: object,
snapshot_runtime_id: str,
supervisor: ConnectionSupervisorSnapshot,
active_binding: Mapping[str, Any] | None,
) -> bool:
"""Require historical bootstrap evidence and the exact live supervisor proof."""
if not isinstance(attempt_id, str) or not attempt_id:
return False
network_operation = next(
(
item
for item in reversed(operation_documents)
if item.get("operation_id") == attempt_id
and item.get("action") == ACTION_NETWORK_PROVISION
),
None,
)
if not isinstance(network_operation, Mapping):
return False
context = network_operation.get("context")
result = network_operation.get("result")
if not isinstance(context, Mapping) or not isinstance(result, Mapping):
return False
transport_ref = result.get("transport_ref")
connection_mode = result.get("connection_mode")
target_ipv4 = result.get("target_ipv4")
target_port = result.get("target_port")
expected_live_intent_id = attempt_id
recovery_verify = next(
(
item
for item in reversed(operation_documents)
if item.get("action") == ACTION_CONNECTION_VERIFY
and item.get("status") == "succeeded"
and _recovery_verify_matches_network_attempt(item, network_operation)
),
None,
)
if isinstance(recovery_verify, Mapping):
verify_result = recovery_verify.get("result")
verified_binding = (
verify_result.get("verified_binding") if isinstance(verify_result, Mapping) else None
)
recovered_intent_id = (
verified_binding.get("intent_id") if isinstance(verified_binding, Mapping) else None
)
if not isinstance(recovered_intent_id, str) or not recovered_intent_id:
return False
expected_live_intent_id = recovered_intent_id
return bool(
network_operation.get("status") == "succeeded"
and result.get("phase") == "network_applied"
and context.get("snapshot_runtime_id") == snapshot_runtime_id
and result.get("snapshot_runtime_id") == snapshot_runtime_id
and result.get("parent_intent_id") == attempt_id
and isinstance(transport_ref, str)
and transport_ref
and context.get("transport_ref") == transport_ref
and connection_mode in {"bridge", "quick-connect", "direct-connect"}
and context.get("connection_mode") == connection_mode
and isinstance(target_ipv4, str)
and target_ipv4
and target_port == CONTROL_MQTT_PORT
and isinstance(active_binding, Mapping)
and active_binding.get("intent_id") == expected_live_intent_id
and active_binding.get("transport_ref") == transport_ref
and active_binding.get("connection_mode") == connection_mode
and active_binding.get("target_ipv4") == target_ipv4
and active_binding.get("target_port") == target_port
and supervisor.authority.control_allowed
and supervisor.intent is not None
and supervisor.intent.intent_id == expected_live_intent_id
and supervisor.intent.requested_mode == connection_mode
and supervisor.device_network.state == "applied"
and supervisor.device_network.intent_id == expected_live_intent_id
and supervisor.device_network.transport_ref == transport_ref
and supervisor.device_network.connection_mode == connection_mode
and supervisor.device_network.target is not None
and supervisor.device_network.target.ipv4 == target_ipv4
and supervisor.device_network.target.port == target_port
and supervisor.host_path.available
and supervisor.host_path.epoch >= 1
and supervisor.endpoint.intent_id == expected_live_intent_id
and supervisor.endpoint.target == supervisor.device_network.target
and supervisor.endpoint.tcp_state == "reachable"
and supervisor.endpoint.host_path_epoch == supervisor.host_path.epoch
and supervisor.lease.state == "reachable"
and supervisor.lease.intent_id == expected_live_intent_id
and supervisor.lease.target == supervisor.device_network.target
and supervisor.lease.connection_mode == connection_mode
and supervisor.lease.host_path_epoch == supervisor.host_path.epoch
and supervisor.control_plane.state == "healthy"
and supervisor.control_plane.host_path_epoch == supervisor.host_path.epoch
and active_binding.get("host_path_epoch") == supervisor.host_path.epoch
and isinstance(active_binding.get("control_session_id"), str)
and bool(active_binding.get("control_session_id"))
)
def _owned_control_bootstrap_matches_network_attempt(
bootstrap_operation: Mapping[str, Any],
network_operation: Mapping[str, Any],
) -> bool:
"""Join only the service-owned child admitted for this exact Apply proof."""
bootstrap_context = bootstrap_operation.get("context")
network_context = network_operation.get("context")
network_result = network_operation.get("result")
if not (
isinstance(bootstrap_context, Mapping)
and isinstance(network_context, Mapping)
and isinstance(network_result, Mapping)
):
return False
network_operation_id = network_operation.get("operation_id")
runtime_id = network_result.get("snapshot_runtime_id")
transport_ref = network_result.get("transport_ref")
connection_mode = network_result.get("connection_mode")
target_ipv4 = network_result.get("target_ipv4")
target_port = network_result.get("target_port")
exact_parent = bool(
isinstance(network_operation_id, str)
and network_operation_id
and network_operation.get("status") == "succeeded"
and network_result.get("phase") == "network_applied"
and isinstance(runtime_id, str)
and runtime_id
and network_context.get("snapshot_runtime_id") == runtime_id
and network_context.get("transport_ref") == transport_ref
and network_context.get("connection_mode") == connection_mode
and network_result.get("parent_intent_id") == network_operation_id
and isinstance(transport_ref, str)
and transport_ref
and connection_mode in {"bridge", "quick-connect", "direct-connect"}
and isinstance(target_ipv4, str)
and target_ipv4
and target_port == CONTROL_MQTT_PORT
and bootstrap_context.get("ownership") == "service-owned-apply-continuation"
and bootstrap_context.get("snapshot_runtime_id") == runtime_id
and bootstrap_context.get("parent_operation_id") == network_operation_id
and bootstrap_context.get("parent_intent_id") == network_operation_id
and bootstrap_context.get("transport_ref") == transport_ref
and bootstrap_context.get("connection_mode") == connection_mode
and bootstrap_context.get("target_ipv4") == target_ipv4
and bootstrap_context.get("target_port") == target_port
and isinstance(bootstrap_context.get("device_session_id"), str)
and bool(bootstrap_context.get("device_session_id"))
and bootstrap_context.get("network_mutation_performed") is False
and bootstrap_context.get("ble_operation_performed") is False
and bootstrap_context.get("automatic_retry") is False
)
if not exact_parent:
return False
if bootstrap_operation.get("status") != "succeeded":
return True
bootstrap_result = bootstrap_operation.get("result")
return bool(
isinstance(bootstrap_result, Mapping)
and bootstrap_result.get("connection_mode") == connection_mode
and bootstrap_result.get("control_verified") is True
and bootstrap_result.get("network_mutation_performed") is False
and bootstrap_result.get("ble_operation_performed") is False
and bootstrap_result.get("automatic_retry") is False
)
def _recovery_verify_matches_network_attempt(
verify_operation: Mapping[str, Any],
network_operation: Mapping[str, Any],
) -> bool:
"""Join Verify only to the exact network parent it was admitted against."""
verify_context = verify_operation.get("context")
network_context = network_operation.get("context")
network_result = network_operation.get("result")
if not (
isinstance(verify_context, Mapping)
and isinstance(network_context, Mapping)
and isinstance(network_result, Mapping)
):
return False
network_operation_id = network_operation.get("operation_id")
runtime_id = network_result.get("snapshot_runtime_id")
transport_ref = network_result.get("transport_ref")
connection_mode = network_result.get("connection_mode")
target_ipv4 = network_result.get("target_ipv4")
target_port = network_result.get("target_port")
exact_parent = bool(
isinstance(network_operation_id, str)
and network_operation_id
and isinstance(runtime_id, str)
and runtime_id
and network_context.get("snapshot_runtime_id") == runtime_id
and network_context.get("transport_ref") == transport_ref
and network_context.get("connection_mode") == connection_mode
and network_result.get("parent_intent_id") == network_operation_id
and isinstance(transport_ref, str)
and transport_ref
and connection_mode in {"bridge", "quick-connect", "direct-connect"}
and isinstance(target_ipv4, str)
and target_ipv4
and target_port == CONTROL_MQTT_PORT
and verify_context.get("snapshot_runtime_id") == runtime_id
and verify_context.get("recovery_parent_operation_id") == network_operation_id
and verify_context.get("recovery_parent_intent_id") == network_operation_id
and verify_context.get("recovery_transport_ref") == transport_ref
and verify_context.get("recovery_connection_mode") == connection_mode
and verify_context.get("recovery_target_ipv4") == target_ipv4
and verify_context.get("recovery_target_port") == target_port
)
if not exact_parent:
return False
if verify_operation.get("status") != "succeeded":
return True
verify_result = verify_operation.get("result")
if not isinstance(verify_result, Mapping):
return False
binding = verify_result.get("verified_binding")
return bool(
isinstance(binding, Mapping)
and binding.get("snapshot_runtime_id") == runtime_id
and isinstance(binding.get("intent_id"), str)
and bool(binding.get("intent_id"))
and binding.get("transport_ref") == transport_ref
and binding.get("connection_mode") == connection_mode
and binding.get("target_ipv4") == target_ipv4
and binding.get("target_port") == target_port
and isinstance(binding.get("host_path_epoch"), int)
and not isinstance(binding.get("host_path_epoch"), bool)
and binding.get("host_path_epoch", 0) >= 1
and verify_result.get("write_performed") is False
and verify_result.get("control_verified") is True
)
def _connection_attempt_projection(
operation_documents: Sequence[Mapping[str, Any]],
) -> dict[str, Any] | None:
"""Project the existing operation journal as one K1 connection attempt.
This is deliberately a read model, not another lifecycle authority. The
operation journal, durable network ledger and connection supervisor remain
the writers of their respective facts; the projection only makes the
current stage and a safe next action visible to the operator.
"""
network_operation_index = next(
(
index
for index in range(len(operation_documents) - 1, -1, -1)
if operation_documents[index].get("action") == ACTION_NETWORK_PROVISION
),
None,
)
if network_operation_index is None:
return None
operation = operation_documents[network_operation_index]
network_context = operation.get("context")
network_context_mapping = network_context if isinstance(network_context, Mapping) else {}
control_bootstrap = next(
(
item
for item in reversed(operation_documents)
if item.get("action") == ACTION_CONNECTION_CONTROL_BOOTSTRAP
and _owned_control_bootstrap_matches_network_attempt(item, operation)
),
None,
)
error = operation.get("error")
result = operation.get("result")
error_mapping = error if isinstance(error, Mapping) else {}
result_mapping = result if isinstance(result, Mapping) else {}
context_mapping = network_context_mapping
network_status = str(operation.get("status") or "unknown")
status = network_status
stage = operation.get("stage_code")
control_state: Literal["ready", "control_not_ready", "unknown"] = "unknown"
public_error_code = (
error_mapping.get("code") if isinstance(error_mapping.get("code"), str) else None
)
physical_reconciliation: Mapping[str, Any] | None = None
physical_active_observed = False
if network_status == "succeeded" and control_bootstrap is not None:
bootstrap_status = str(control_bootstrap.get("status") or "unknown")
status = bootstrap_status
stage = control_bootstrap.get("stage_code")
bootstrap_result = control_bootstrap.get("result")
bootstrap_reconciliation = (
bootstrap_result.get("physical_reconciliation")
if isinstance(bootstrap_result, Mapping)
else None
)
if isinstance(bootstrap_reconciliation, Mapping):
physical_reconciliation = bootstrap_reconciliation
physical_active_observed = bool(
bootstrap_reconciliation.get("performed") is True
and bootstrap_reconciliation.get("resolution")
== "physical-active-observed"
and bootstrap_reconciliation.get("observed_session_state")
== "scanning"
and bootstrap_reconciliation.get("device_write_performed") is False
)
if bootstrap_status == "succeeded":
control_state = "unknown" if physical_active_observed else "ready"
elif bootstrap_status == "failed":
control_state = "control_not_ready"
bootstrap_error = control_bootstrap.get("error")
if isinstance(bootstrap_error, Mapping):
bootstrap_code = bootstrap_error.get("code")
public_error_code = (
bootstrap_code if isinstance(bootstrap_code, str) else public_error_code
)
recovery_verify = next(
(
item
for item in reversed(operation_documents[network_operation_index + 1 :])
if item.get("action") == ACTION_CONNECTION_VERIFY
and _recovery_verify_matches_network_attempt(item, operation)
),
None,
)
# A later explicit read-only Verify is the canonical recovery for an
# applied network edge whose first DeviceInfo bootstrap failed. It must
# update the visible attempt without rewriting or replaying that edge.
if network_status == "succeeded" and recovery_verify is not None:
status = str(recovery_verify.get("status") or "unknown")
stage = recovery_verify.get("stage_code")
verify_result = recovery_verify.get("result")
verify_reconciliation = (
verify_result.get("physical_reconciliation")
if isinstance(verify_result, Mapping)
else None
)
if isinstance(verify_reconciliation, Mapping):
physical_reconciliation = verify_reconciliation
physical_active_observed = bool(
verify_reconciliation.get("performed") is True
and verify_reconciliation.get("resolution")
== "physical-active-observed"
and verify_reconciliation.get("observed_session_state")
== "scanning"
and verify_reconciliation.get("device_write_performed") is False
)
if status == "succeeded":
control_state = "unknown" if physical_active_observed else "ready"
elif status == "failed":
control_state = "control_not_ready"
else:
control_state = "unknown"
recovery_error = recovery_verify.get("error")
public_error_code = (
recovery_error.get("code")
if isinstance(recovery_error, Mapping) and isinstance(recovery_error.get("code"), str)
else None
)
side_effect_status = error_mapping.get(
"side_effect_status",
result_mapping.get("side_effect_status"),
)
if not isinstance(side_effect_status, str):
side_effect_status = "none" if status in {"accepted", "running"} else "unknown"
phase: Literal[
"network_applied",
"network_not_applied",
"network_outcome_unknown",
]
if network_status == "succeeded":
phase = "network_applied"
elif side_effect_status == "unknown":
# Once a device write may have crossed the GATT boundary, a failed
# host request cannot truthfully claim that the network was not
# applied. This remains an audit/read-model distinction only: it does
# not authorize an automatic retry or replay of the old operation.
phase = "network_outcome_unknown"
else:
phase = "network_not_applied"
owned_bootstrap_pending = bool(
control_bootstrap is not None and status in {"accepted", "running"}
)
recovery_verify_pending = bool(
recovery_verify is not None and status in {"accepted", "running"}
)
if owned_bootstrap_pending or recovery_verify_pending:
safe_next_action = "wait-for-current-attempt"
elif status == "succeeded" and physical_active_observed:
safe_next_action = "stop-acquisition"
elif status == "succeeded" and control_state == "ready":
safe_next_action = "start-acquisition"
elif network_status == "succeeded":
safe_next_action = "verify-control-read-only"
else:
# Historical ambiguity blocks automatic replay of this attempt, not a
# later explicit scan-select-connect session.
safe_next_action = "scan-select-connect"
events = operation.get("events")
return {
"schema_version": "missioncore.xgrids-k1-connection-attempt/v1",
"attempt_id": operation.get("operation_id"),
"connection_mode": context_mapping.get("connection_mode"),
"status": status,
"phase": phase,
"control_state": control_state,
"stage": stage,
"public_error_code": public_error_code,
"side_effect_status": side_effect_status,
"safe_next_action": safe_next_action,
"physical_reconciliation": (
dict(physical_reconciliation)
if physical_reconciliation is not None
else None
),
"automatic_retry": False,
"accepted_at": operation.get("accepted_at"),
"completed_at": operation.get("completed_at"),
"recovery_operation_id": (
recovery_verify.get("operation_id") if isinstance(recovery_verify, Mapping) else None
),
"timeline": [
*(
[dict(item) for item in events if isinstance(item, Mapping)]
if isinstance(events, list)
else []
),
*(
[
{**dict(item), "source_action": ACTION_CONNECTION_CONTROL_BOOTSTRAP}
for item in control_bootstrap.get("events", [])
if isinstance(item, Mapping)
]
if isinstance(control_bootstrap, Mapping)
and isinstance(control_bootstrap.get("events"), list)
else []
),
*(
[
{**dict(item), "source_action": ACTION_CONNECTION_VERIFY}
for item in recovery_verify.get("events", [])
if isinstance(item, Mapping)
]
if isinstance(recovery_verify, Mapping)
and isinstance(recovery_verify.get("events"), list)
else []
),
],
}
def _provisioned_ipv4(result: Mapping[str, Any]) -> str | None:
observations = result.get("observations")
if not isinstance(observations, list):
@@ -35803,6 +35378,14 @@ def _inspect_host_path(target: str) -> HostPathProbeResult:
if separator:
fields[key] = value.strip()
if sys.platform.startswith("linux"):
from .linux_host import route_fields
try:
fields = route_fields(target)
except (OSError, ValueError, subprocess.SubprocessError):
inspection_reason = "host-route-inspection-unavailable"
interface = fields.get("interface") or None
destination = fields.get("destination", "")
gateway = fields.get("gateway", "")
@@ -35825,6 +35408,10 @@ def _host_route_class(target: str) -> str:
"""Classify the host route without transmitting packets to the target."""
target = validate_private_ipv4(target)
if sys.platform.startswith("linux"):
path = _inspect_host_path(target)
return {"direct": "direct-or-routed", "default": "default-route",
"tunnel": "tunnel"}.get(path.route_class, "unknown")
if sys.platform != "darwin":
return "unknown"
try:
@@ -35874,6 +35461,7 @@ def _classify_host_route(
"wireguard",
"gif",
"stf",
"tailscale",
)
direct_lan_prefixes = (
"en",
@@ -35882,6 +35470,7 @@ def _classify_host_route(
"vlan",
"usb",
"p2p",
"wl",
)
if normalized_interface.startswith(tunnel_prefixes):
return "tunnel", "host-route-tunnel"
@@ -0,0 +1,214 @@
"""Read-only Linux host adapters for the reviewed K1 Bridge workflow.
No host association or subnet discovery is available through this module.
"""
from __future__ import annotations
import hashlib
import hmac
import json
import os
import re
import secrets
import stat
import subprocess
from pathlib import Path
from k1link.device_plugins.xgrids_k1.protocol.application_authority import (
ApplicationAuthorityLoadError,
ApplicationAuthoritySourceSnapshot,
)
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ApplicationControlAuthority,
)
def _run(args: list[str], timeout: float = 8) -> str:
result = subprocess.run(args, capture_output=True, text=True, timeout=timeout, check=False)
if result.returncode:
# Never forward subprocess diagnostics: SSIDs or other local data may occur.
raise OSError("Network observation unavailable")
if len(result.stdout) > 131072:
raise OSError("Network observation exceeded its bound")
return result.stdout
def nm_fields(line: str) -> list[str]:
"""Parse nmcli terse escaping, including SSIDs containing colons/backslashes."""
fields, value, escaped = [], "", False
for char in line:
if escaped:
value += char
escaped = False
elif char == "\\":
escaped = True
elif char == ":":
fields.append(value)
value = ""
else:
value += char
if escaped:
value += "\\"
return [*fields, value]
def wifi_networks() -> list[dict]:
text = _run(
[
"nmcli",
"-t",
"--escape",
"yes",
"-f",
"SSID,SIGNAL,SECURITY",
"device",
"wifi",
"list",
"--rescan",
"yes",
],
timeout=20,
)
networks = {}
for line in text.splitlines():
fields = nm_fields(line)
if len(fields) != 3:
continue
ssid, strength, security = fields
if not ssid or not 1 <= len(ssid.encode()) <= 32 or not strength.isdigit():
continue
signal = min(100, max(0, int(strength)))
if signal >= networks.get(ssid, {}).get("signal", -1):
networks[ssid] = {"ssid": ssid, "signal": signal, "security": security}
return sorted(networks.values(), key=lambda v: (-v["signal"], v["ssid"]))[:64]
def route_fields(target: str) -> dict[str, str]:
# fibmatch returns the actual matched route (including "default"), whereas
# an ordinary `route get` resolves dst to the host even for a default route.
routes = json.loads(_run(["ip", "-j", "route", "get", target, "fibmatch"]))
if not isinstance(routes, list) or len(routes) != 1:
raise OSError("Ambiguous kernel route")
route = routes[0]
if route.get("type", "unicast") != "unicast" or not route.get("dev"):
raise OSError("No unicast route")
return {
"interface": str(route["dev"]),
"destination": str(route.get("dst", "")),
"gateway": str(route.get("gateway", "")),
}
class LinuxWifiAssociationProbe:
def __init__(self, sys_net: Path = Path("/sys/class/net")):
self.sys_net = sys_net
self.key = secrets.token_bytes(32)
def observe(self, *, interface_name: str | None, timeout_seconds: float = 30) -> dict:
result = {
"schema_version": 1,
"adapter": "linux-networkmanager",
"wifi_interface": None,
"association_state": "unavailable",
"evidence_quality": "unavailable",
"continuity_proven": False,
"continuity_token": "",
"reason_code": "host-wifi-observation-unavailable",
}
if not interface_name or not re.fullmatch(r"[A-Za-z0-9_.:-]{1,64}", interface_name):
return result
interface = self.sys_net / interface_name
if not interface.exists():
return result
wireless = (interface / "wireless").exists() or (interface / "phy80211").exists()
result["wifi_interface"] = wireless
if not wireless:
result.update(
association_state="not-wifi",
evidence_quality="not-wifi",
continuity_proven=True,
reason_code=None,
)
material = "not-wifi:" + interface_name
else:
try:
lines = _run(
[
"nmcli",
"-t",
"--escape",
"yes",
"-f",
"ACTIVE,SSID,BSSID",
"device",
"wifi",
"list",
"ifname",
interface_name,
"--rescan",
"no",
],
timeout=min(timeout_seconds, 8),
)
active = [nm_fields(line) for line in lines.splitlines() if line.startswith("yes:")]
if len(active) != 1 or len(active[0]) != 3 or not active[0][2]:
return result
except (OSError, subprocess.SubprocessError):
return result
result.update(
association_state="associated",
evidence_quality="ssid+bssid",
continuity_proven=True,
reason_code=None,
)
material = json.dumps([interface_name, *active[0][1:]], ensure_ascii=False)
result["continuity_token"] = hmac.new(
self.key, material.encode(), hashlib.sha256
).hexdigest()
return result
class LinuxApplicationAuthorityLoader:
"""Read only a systemd-delivered credential, never an argv/env secret.
Provisioning of the encrypted credential is a separate privileged install
operation. Missing authority does not prevent discovery but prevents control.
"""
def __init__(self, directory: Path | None = None):
self.directory = directory
def snapshot(self) -> ApplicationAuthoritySourceSnapshot:
return ApplicationAuthoritySourceSnapshot(
provider="linux-systemd-credential",
service="mission-core-k1.service",
account="k1-application",
)
def load(self) -> ApplicationControlAuthority:
directory = self.directory or Path(
os.environ.get("CREDENTIALS_DIRECTORY", "/run/credentials/mission-core-k1.service")
)
buffer = bytearray()
try:
fd = os.open(directory / "k1-application", os.O_RDONLY | os.O_NOFOLLOW)
with os.fdopen(fd, "rb") as stream:
info = os.fstat(stream.fileno())
if (
not stat.S_ISREG(info.st_mode)
or info.st_uid != os.geteuid()
or info.st_mode & 0o077
):
raise ValueError("Invalid credential permissions")
buffer.extend(stream.read(1025))
if len(buffer) > 1024:
raise ValueError("Credential too large")
return ApplicationControlAuthority(openapi_key=buffer.decode("ascii").strip())
except (OSError, ValueError, UnicodeError):
raise ApplicationAuthorityLoadError(
"Служебный ключ K1 не установлен на БК.",
reason_code="application_authority_unavailable",
) from None
finally:
buffer[:] = b"\0" * len(buffer)
@@ -0,0 +1,319 @@
"""Node-owned, Bridge-only adapter around the admitted K1 plugin runtime.
The local Unix worker accepts neither host-Wi-Fi actions nor arbitrary plugin
invocations. Its public projection contains no credentials or raw evidence.
"""
from __future__ import annotations
import asyncio
from datetime import UTC, datetime
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from missioncore_plugin_sdk.v0alpha2.runtime import RuntimeActionInvocation
from k1link.viewer.node_rerun import NodeRerunHub
from .ble.scanner import BLE_SCAN_DEFAULT_TIMEOUT_SECONDS
from .connection_attempt import compact_connection_attempt
from .facade import (
XGRIDS_K1_PLUGIN_ID,
XgridsK1CompatibilityService,
XgridsK1PluginFacade,
_validate_installed_compatibility_profile,
)
from .linux_host import LinuxApplicationAuthorityLoader, LinuxWifiAssociationProbe, wifi_networks
ATTESTATION = {
"firmware_version": "3.0.2",
"topology": "direct-lan",
"verification": "live-device-info",
}
class NodeEnrollmentRejected(ValueError):
"""A host admission failure proven to precede any device invocation."""
def __init__(self, code: str):
self.code = code
super().__init__(code)
class NodeBridge:
def __init__(self, repository_root: Path, *, service=None):
self.rerun = NodeRerunHub()
if service is None:
_validate_installed_compatibility_profile(repository_root)
service = XgridsK1CompatibilityService(
repository_root,
application_authority_loader=LinuxApplicationAuthorityLoader(),
host_wifi_association_probe=LinuxWifiAssociationProbe(),
visualization_bridge_factory=self.rerun.create,
)
self.service = service
self.facade = XgridsK1PluginFacade(service)
self.lock = asyncio.Lock()
async def state(self) -> dict:
snapshot = await self.invoke("state.read", {}, "state-read")
return self.project(snapshot)
@staticmethod
def project(snapshot: dict) -> dict:
lifecycle = snapshot.get("connection_lifecycle", {})
return {
"available": True,
"model": "XGRIDS K1",
"mode": "bridge",
"runtime_id": snapshot["snapshot_runtime_id"],
"snapshot_revision": snapshot.get("snapshot_revision", 0),
"runtime_started_at": snapshot.get("snapshot_runtime_started_at_utc"),
"discovery_generation": snapshot.get("ble_discovery_generation", 0),
"mode_revision": snapshot.get("desired_connection_mode_revision", 0),
"candidates": [
{"id": v["device_id"], "name": v.get("name") or "K1", "rssi": v.get("rssi")}
for v in snapshot.get("devices", [])
if v.get("likely_k1")
][:32],
"connected": lifecycle.get("connection_ready") is True
and snapshot.get("active_connection_mode") == "bridge",
"ready_to_start": lifecycle.get("ready_to_start") is True,
"connection_attempt": compact_connection_attempt(snapshot.get("connection_attempt")),
"allowed_actions": [
action
for action in lifecycle.get("allowed_actions", [])
if action
in {
"scan-ble",
"provision-fresh-device",
"verify-control-read-only",
"observe-current-device-network",
"observe-configured-device-network",
"observe-fresh-device-network",
}
],
"selected_device_id": snapshot.get("selected_device_id"),
"device_session": snapshot.get("device_session"),
"ip": snapshot.get("k1_ip"),
"phase": snapshot.get("phase"),
"observed_at": datetime.now(UTC).isoformat(),
}
async def invoke(self, action: str, parameters: dict, identifier: str) -> dict:
return await self.facade.invoke(
RuntimeActionInvocation(
invocation_id=identifier,
plugin_id=XGRIDS_K1_PLUGIN_ID,
action_id=action,
requested_at=datetime.now(UTC),
parameters=parameters,
)
)
async def execute(self, command: dict) -> dict:
try:
return await self._execute(command)
finally:
parameters = command.get("parameters")
if isinstance(parameters, dict):
parameters.pop("password", None)
async def deliver(self, command: dict) -> dict:
try:
return await self.execute(command)
except NodeEnrollmentRejected as error:
result = await self.state()
result["command_result"] = {
"operation_id": command["operation_id"],
"status": "rejected",
"error_code": error.code,
}
return result
async def _execute(self, command: dict) -> dict:
action, identifier = command["action"], command["operation_id"]
parameters = command.get("parameters", {})
if action not in {"scan", "networks", "connect", "verify"}:
raise NodeEnrollmentRejected("unsupported-action")
async with self.lock:
if datetime.fromisoformat(command["deadline_at"]) <= datetime.now(UTC):
raise NodeEnrollmentRejected("command-expired")
state = await self.state()
if command.get("runtime_id") != state["runtime_id"]:
raise NodeEnrollmentRejected("runtime-changed")
if action == "networks":
return {**state, "networks": await asyncio.to_thread(wifi_networks)}
if action == "scan":
result = await self.invoke(
"discovery.scan",
{
"duration_seconds": BLE_SCAN_DEFAULT_TIMEOUT_SECONDS,
"operation_id": identifier,
},
identifier,
)
else:
# Do not derive fences at dispatch: they are the exact version
# shown to the operator before credentials were submitted.
if (
parameters.get("discovery_generation") != state["discovery_generation"]
or parameters.get("mode_revision") != state["mode_revision"]
or parameters.get("device_id") not in {v["id"] for v in state["candidates"]}
):
raise NodeEnrollmentRejected("device-selection-changed")
payload = {
"device_id": parameters["device_id"],
"compatibility_attestation": ATTESTATION,
"operation_id": identifier,
"expected_mode_revision": parameters["mode_revision"],
"expected_discovery_generation": parameters["discovery_generation"],
"expected_snapshot_runtime_id": state["runtime_id"],
}
if action == "connect":
payload.update(
connection_mode="bridge",
allow_host_wifi_switch=False,
idempotency_key=identifier,
ssid=parameters.get("ssid"),
password=parameters.get("password"),
)
try:
result = await self.invoke(
"network.provision" if action == "connect" else "connection.verify",
payload,
identifier,
)
except Exception:
# A rejected/failed invocation may already have a durable
# result. Read that exact journal row; never resend the
# network command and never serialize exception/payload text.
result = await self.invoke("state.read", {}, identifier + "-observe")
if not any(
item.get("operation_id") == identifier
for item in result.get("operations", [])
):
raise
finally:
payload.pop("password", None)
parameters.pop("password", None)
projected = self.project(result)
operation = next(
(
item
for item in reversed(result.get("operations", []))
if item.get("operation_id") == identifier
),
None,
)
if operation is not None:
error = operation.get("error") or {}
projected["command_result"] = {
"operation_id": identifier,
"status": operation.get("status"),
"error_code": error.get("code"),
}
return projected
def create_app(repository_root: Path):
from contextlib import asynccontextmanager
bridge = NodeBridge(repository_root)
from k1link.viewer.node_media import NodeMediaPeers
from .node_sensor import NodeK1Sensor
peers = NodeMediaPeers(bridge.rerun, bridge.service.camera_preview)
sensor = NodeK1Sensor(bridge, peers)
@asynccontextmanager
async def lifespan(_app):
yield
await peers.close_all()
await asyncio.to_thread(bridge.service.close)
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None, lifespan=lifespan)
@app.get("/status")
async def status():
return await bridge.state()
@app.get("/inventory")
async def inventory(request: Request):
return await sensor.inventory(request.headers["X-Node-Id"])
@app.get("/prepare-safe")
async def prepare_safe():
state = await sensor.raw_state()
acquisition = state.get("acquisition") or {}
control = state.get("application_control_session") or {}
physical = control.get("physical_command") or state.get("physical_command") or {}
return {
"safe": not bridge.lock.locked()
and state.get("source_mode") == "idle"
and acquisition.get("state") in {None, "completed", "failed", "aborted"}
and not physical.get("requires_reconciliation")
and control.get("state")
not in {"start-requested", "initializing", "scanning", "stop-requested"}
}
@app.post("/sensor-operation")
async def sensor_operation(request: Request):
try:
data = await request.body()
if len(data) > 65536:
raise ValueError("Request too large")
command = await request.json()
if datetime.fromisoformat(command["deadline_at"]) <= datetime.now(UTC):
raise ValueError("Command expired")
result = await sensor.execute(command, request.headers["X-Node-Id"])
return {"state": "complete", "result": result}
except Exception:
return {
"state": "unknown",
"error": "Действие K1 не подтверждено. Обновите состояние устройства.",
}
@app.post("/operation")
async def operation(request: Request):
# The Go broker admits size/action/binding/deadline and journals the
# operation before calling this private Unix socket. Never serialize
# Pydantic validation errors, exception text or the incoming payload.
body = {}
try:
data = await request.body()
if len(data) > 16384:
raise ValueError("Request too large")
body = await request.json()
return await bridge.deliver(body)
except Exception:
return JSONResponse(
{
"error": (
"Действие K1 не подтверждено. Обновите состояние; "
"проверьте питание, Bluetooth и сеть БК."
)
},
status_code=409,
)
finally:
if isinstance(body, dict) and isinstance(body.get("parameters"), dict):
body["parameters"].pop("password", None)
return app
def main():
import os
import uvicorn
os.umask(0o007)
app = create_app(Path("/usr/lib/mission-core-node/k1"))
uvicorn.run(app, uds="/run/mission-core-k1/driver.sock", access_log=False, log_level="warning")
if __name__ == "__main__":
main()
@@ -0,0 +1,255 @@
"""SDK projection and explicit acquisition actions for the Node-owned K1."""
import asyncio
import hashlib
import time
from datetime import UTC, datetime
from .facade import (
XGRIDS_K1_MODEL_ID,
XGRIDS_K1_PLUGIN_ID,
XGRIDS_K1_PLUGIN_VERSION,
ViewerSettingsRequest,
)
from .node_bridge import ATTESTATION
PHYSICAL_ACCEPTANCE_KEYS = (
"operator_present",
"owner_controlled_device",
"lixelgo_closed",
"battery_storage_confirmed",
"expected_physical_state_confirmed",
)
def project_sensor(snapshot, node_id):
session = snapshot.get("device_session")
if not session:
return None
identifier = "k1_" + hashlib.sha256(session["device_id"].encode()).hexdigest()[:32]
lifecycle = snapshot.get("connection_lifecycle", {})
connected = (
lifecycle.get("connection_ready") is True
and snapshot.get("active_connection_mode") == "bridge"
)
acquisition = snapshot.get("acquisition") or {}
acquisition_state = {
"running": "streaming",
"active": "streaming",
"prepared": "preparing",
"starting": "starting",
"stopping": "stopping",
"failed": "failed",
}.get(acquisition.get("state"), "idle")
if snapshot.get("source_mode") == "live":
acquisition_state = "streaming"
now = datetime.now(UTC).isoformat()
context = {
"session_id": session["device_session_id"],
"device": {
"device_id": identifier,
"model": {
"plugin_id": XGRIDS_K1_PLUGIN_ID,
"plugin_version": XGRIDS_K1_PLUGIN_VERSION,
"model_id": XGRIDS_K1_MODEL_ID,
},
"stability": "provisional",
"basis": "plugin-derived",
},
"execution": {
"node_id": node_id,
"agent_instance_id": snapshot["snapshot_runtime_id"],
"platform": "linux",
},
"opened_at": session["opened_at"],
}
control = snapshot.get("application_control_session") or {}
return {
"id": identifier,
"name": "XGRIDS K1",
"model": "XGRIDS K1",
"kind": "k1",
"prepared": True,
"configured": True,
"verified": connected,
"online": connected,
"usb": "",
"connection_label": "Wi-Fi · " + (snapshot.get("k1_ip") or "—"),
"layers": ["points", "camera"],
"snapshot": {
"context": context,
"revision": snapshot["snapshot_revision"],
"enrollment": "enrolled",
"connectivity": "connected" if connected else "offline",
"acquisition": acquisition_state,
"observed_at": now,
},
"control": {
"generation": control.get("session_generation"),
"revision": control.get("state_revision"),
"phase": control.get("state"),
"can_start": lifecycle.get("ready_to_start", False),
"can_stop": control.get("state") in {"start-requested", "initializing", "scanning"},
"acquisition_id": acquisition.get("acquisition_id"),
},
"live_settings": snapshot.get("viewer_settings", {}),
"frames": snapshot.get("metrics", {}),
"recordings": [],
}
class NodeK1Sensor:
def __init__(self, bridge, peers):
self.bridge, self.peers = bridge, peers
async def raw_state(self):
return await self.bridge.invoke("state.read", {}, "sensor-state")
async def inventory(self, node_id):
state = await self.raw_state()
item = project_sensor(state, node_id)
return {"items": [item] if item else []}
@staticmethod
def cas(state, *, acquisition=True):
control = state["application_control_session"]
if acquisition:
return {
"expected_control_session_generation": control["session_generation"],
"expected_control_state_revision": control["state_revision"],
}
return {
"expected_session_generation": control["session_generation"],
"expected_state_revision": control["state_revision"],
}
async def execute(self, command, node_id):
state = await self.raw_state()
item = project_sensor(state, node_id)
if (
not item
or command["session"]["device_id"] != item["id"]
or command["session"]["session_id"] != item["snapshot"]["context"]["session_id"]
):
raise ValueError("Device session changed")
action, params, identifier = (
command["action_id"],
command.get("parameters", {}),
command["operation_id"],
)
if action == "details":
return item
if action == "close-peer":
await self.peers.close(params.get("peer_id"))
return {"ok": True}
if action == "offer":
if item["snapshot"]["acquisition"] != "streaming":
raise ValueError("Acquisition is not active")
return await self.peers.offer(params)
async with self.bridge.lock:
if datetime.fromisoformat(command["deadline_at"]) <= datetime.now(UTC):
raise ValueError("Command expired before dispatch")
state = await self.raw_state()
current = project_sensor(state, node_id)
if (
current is None
or current["snapshot"]["context"]["session_id"] != command["session"]["session_id"]
):
raise ValueError("Device session changed")
runtime = state["snapshot_runtime_id"]
if action == "option":
if (
set(params) != {"profile", "settings"}
or params["profile"] != "live-acquisition"
):
raise ValueError("Only the live profile belongs to this device")
settings = ViewerSettingsRequest.model_validate(params["settings"])
result = await self.bridge.invoke(
"viewer.settings.update", settings.model_dump(), identifier
)
return project_sensor(result, node_id)
if action == "verify":
result = await self.bridge.invoke(
"connection.verify", {"expected_snapshot_runtime_id": runtime}, identifier
)
return project_sensor(result, node_id)
if action not in {"start", "stop"} or params.get("operator_confirmed") is not True:
raise ValueError("Unsupported device action")
if (
params.get("control_generation") != current["control"]["generation"]
or params.get("acquisition_id") != current["control"]["acquisition_id"]
):
raise ValueError("Control session changed")
# Same explicit START/STOP acceptance as the admitted local plugin.
# No confirmation is inferred from polling, discovery or a preview offer.
acceptance = dict.fromkeys(PHYSICAL_ACCEPTANCE_KEYS, True)
if action == "stop":
result = await self.bridge.invoke(
"acquisition.stop",
{
"operation_id": identifier,
"idempotency_key": identifier,
"acquisition_id": params.get("acquisition_id"),
"mode": "graceful",
"physical_acceptance": acceptance,
"expected_snapshot_runtime_id": runtime,
**self.cas(state),
},
identifier,
)
await self.peers.close_all()
return project_sensor(result, node_id)
deadline = min(
datetime.fromisoformat(command["deadline_at"]).timestamp(), time.time() + 165
)
dispatched = set()
while time.time() < deadline:
if state["snapshot_runtime_id"] != runtime:
raise ValueError("Runtime changed during START")
control = state.get("application_control_session") or {}
phase = control.get("state")
physical = control.get("physical_command") or state.get("physical_command") or {}
if physical.get("requires_reconciliation"):
raise ValueError("Physical state requires explicit reconciliation")
acquisition = state.get("acquisition") or {}
payload = {"expected_snapshot_runtime_id": runtime}
next_action = None
if phase == "connection-ready":
if control.get("inspection_only"):
next_action = "application-control.session.open"
payload.update(acceptance, timezone_name="UTC")
else:
next_action = "application-control.workspace.enter"
payload.update(self.cas(state, acquisition=False), operator_confirmed=True)
elif phase == "workspace-ready" and acquisition.get("state") != "prepared":
next_action = "acquisition.prepare"
payload.update(
self.cas(state),
operation_id=identifier + "_prepare",
idempotency_key=identifier + "_prepare",
project_name="node-" + identifier[3:15],
compatibility_attestation=ATTESTATION,
)
elif phase == "project-ready" and acquisition.get("state") == "prepared":
next_action = "acquisition.start"
payload.update(
self.cas(state),
operation_id=identifier,
idempotency_key=identifier,
acquisition_id=acquisition["acquisition_id"],
expected_state_revision=acquisition["state_revision"],
physical_acceptance=acceptance,
)
elif phase in {"failed", "idle", "closed", "completed"}:
raise ValueError("K1 control not ready")
elif phase in {"start-requested", "initializing", "scanning"}:
return project_sensor(state, node_id)
if next_action and next_action not in dispatched:
dispatched.add(next_action)
state = await self.bridge.invoke(
next_action, payload, identifier + "_" + str(len(dispatched))
)
else:
await asyncio.sleep(0.5)
state = await self.raw_state()
raise TimeoutError("K1 did not confirm START")
@@ -0,0 +1,33 @@
from __future__ import annotations
from collections.abc import Mapping
def reviewed_station_failure_code(
error: Mapping[str, object], *, firmware_version: str, connection_mode: str
) -> str | None:
"""Interpret the reviewed FW 3.0.2 station callback, retaining ATT evidence.
lixel_nman's wifi_connect return value is passed to the GATT write result.
Its 4 (SSID not found) and 6 (credentials required) collide with standard
ATT names. This is a profile-scoped diagnosis, not network-state proof or
retry authority. See the 2026-09-06 firmware callback audit.
"""
if not (
firmware_version == "3.0.2"
and connection_mode in {"bridge", "direct-connect"}
and error.get("code") == "BleakGATTProtocolError"
and error.get("operation_stage") == "gatt-write"
and error.get("device_write_attempted") is True
and error.get("device_write_confirmed") is False
and error.get("resolved_write_mode") == "with_response"
and error.get("frame_length") == 99
):
return None
code = error.get("ble_att_error_code")
if not isinstance(code, int) or isinstance(code, bool):
return None
return {
4: "k1-wifi-network-not-found",
6: "k1-wifi-credentials-required",
}.get(code)
+173
View File
@@ -0,0 +1,173 @@
"""Bounded, transient delivery of Node-owned wireless device operations.
Unlike ordinary sensor operations these can contain a WLAN password. The
pending payload never enters Fleet's database, listing, event stream or result.
The Node owns the durable, redacted operation journal and at-most-once dispatch.
"""
import copy
import json
import re
import time
from datetime import UTC, datetime
from .trust import PairingError
ACTIONS = {"scan", "networks", "connect", "verify"}
def validate(value):
if not isinstance(value, dict) or set(value) != {
"operation_id",
"node_id",
"runtime_id",
"action",
"deadline_at",
"parameters",
}:
raise PairingError("Неверный запрос подключения устройства.")
if (
not re.fullmatch(r"op_[0-9a-f]{32}", str(value["operation_id"]))
or value["action"] not in ACTIONS
or len(json.dumps(value)) > 16384
or not isinstance(value["runtime_id"], str)
or not value["runtime_id"]
or len(value["runtime_id"]) > 128
):
raise PairingError("Неподдерживаемая операция подключения.")
try:
delta = (datetime.fromisoformat(value["deadline_at"]) - datetime.now(UTC)).total_seconds()
if not 0 < delta <= 180:
raise ValueError
except (ValueError, TypeError):
raise PairingError("Срок запроса истёк. Обновите устройства.") from None
parameters = value["parameters"]
expected = {"device_id", "discovery_generation", "mode_revision"}
if value["action"] == "connect":
expected |= {"ssid", "password"}
elif value["action"] in {"scan", "networks"}:
expected = set()
if not isinstance(parameters, dict) or set(parameters) != expected:
raise PairingError("Неверные параметры подключения.")
if "device_id" in expected and (
not isinstance(parameters["device_id"], str)
or not 1 <= len(parameters["device_id"]) <= 128
or type(parameters["discovery_generation"]) is not int
or type(parameters["mode_revision"]) is not int
or min(parameters["discovery_generation"], parameters["mode_revision"]) < 0
):
raise PairingError("Обновите выбранное устройство.")
if value["action"] == "connect" and any(
not isinstance(parameters[key], str) or not 1 <= len(parameters[key].encode()) <= limit
for key, limit in (("ssid", 32), ("password", 64))
):
raise PairingError("Проверьте название сети и пароль.")
class DeviceEnrollment:
def __init__(self):
# Access only under the enclosing FleetRegistry.lock.
self.pending = {}
def prune(self):
now = time.time()
for key, entry in list(self.pending.items()):
if now - entry["created"] > 600:
del self.pending[key]
elif entry["deadline"] <= now and entry["public"]["state"] in {"queued", "running"}:
entry["payload"] = None
entry["public"] = {
"operation_id": key[1],
"state": "unknown",
"error": "Подтверждение с БК не получено. Обновите состояние K1.",
}
def submit(self, fleet, vehicle_id, value):
validate(value)
with fleet.lock:
self.prune()
row = fleet.find(vehicle_id)
state = row.get("device_enrollment", {})
if (
fleet.public(row)["connectivity"] != "online"
or value["node_id"] != row["node_id"]
or value["runtime_id"] != state.get("runtime_id")
or state.get("available") is not True
):
raise PairingError("БК или служба K1 недоступны. Обновите устройства.")
key = (vehicle_id, value["operation_id"])
existing = self.pending.get(key)
if existing:
# First intent owns this ID; never accept replacement secrets.
return copy.deepcopy(existing["public"])
if len(self.pending) >= 32:
raise PairingError("Слишком много запросов. Повторите позже.")
public = {"operation_id": value["operation_id"], "state": "queued"}
self.pending[key] = {
"public": public,
"payload": copy.deepcopy(value),
"binding": row["binding"]["binding_id"],
"node_id": row["node_id"],
"created": time.time(),
"deadline": datetime.fromisoformat(value["deadline_at"]).timestamp(),
}
return dict(public)
def operation(self, fleet, vehicle_id, identifier):
with fleet.lock:
self.prune()
row = fleet.find(vehicle_id)
entry = self.pending.get((vehicle_id, identifier))
if not entry or entry["binding"] != (row.get("binding") or {}).get("binding_id"):
# A Core restart loses transient delivery; never recreate it
# from browser credentials or imply that a physical write failed.
return {
"operation_id": identifier,
"state": "unknown",
"error": "Результат запроса недоступен. Обновите состояние устройства.",
}
return copy.deepcopy(entry["public"])
def heartbeat(self, row, value):
self.prune()
state = value.get("device_enrollment", {"available": False})
if not isinstance(state, dict) or len(json.dumps(state)) > 32768:
raise ValueError("Invalid device enrollment state")
row["device_enrollment"] = state
acknowledgements = []
results = value.get("enrollment_results", [])
if not isinstance(results, list) or len(results) > 32:
raise ValueError("Invalid enrollment results")
for result in results:
if not isinstance(result, dict) or len(json.dumps(result)) > 65536:
raise ValueError("Invalid enrollment result")
key = (row["id"], result.get("operation_id"))
entry = self.pending.get(key)
if (
entry
and entry["binding"] == row["binding"]["binding_id"]
and result.get("state") in {"running", "complete", "error", "unknown"}
):
entry["payload"] = None # Node's journal now owns the intent.
entry["public"] = {
k: result[k]
for k in ("operation_id", "state", "error", "result")
if k in result
}
if result["state"] != "running":
acknowledgements.append(result["operation_id"])
elif result.get("state") != "running":
acknowledgements.append(result.get("operation_id"))
commands = []
for (vehicle_id, _), entry in self.pending.items():
if vehicle_id != row["id"]:
continue
if entry["binding"] != row["binding"]["binding_id"]:
entry["payload"] = None
entry["public"]["state"] = "unknown"
elif entry["payload"] is not None:
commands.append(entry["payload"])
return {
"enrollment_commands": commands[:2],
"enrollment_acknowledgements": acknowledgements,
}
+5 -1
View File
@@ -30,6 +30,9 @@ class FleetRegistry:
from .events import FleetEvents
self.events = FleetEvents()
from .device_enrollment import DeviceEnrollment
self.device_enrollment = DeviceEnrollment()
self.trust = CoreTrust(root)
path = root / "fleet.sqlite3"
if path.is_symlink():
@@ -378,6 +381,7 @@ class FleetRegistry:
return 400, {"error": "Invalid Node inventory"}
sensor_state = sensors.validate_inventory(value, node_id)
sensor_response = sensors.heartbeat(row, value)
enrollment_response = self.device_enrollment.heartbeat(row, value)
host = value.get("host")
if (
not isinstance(host, dict)
@@ -403,4 +407,4 @@ class FleetRegistry:
}
row["binding"]["client_pem"] = self.trust.leaf(node_id, key)
self.save(row)
return 200, {"ok": True, "client_pem": row["binding"]["client_pem"], **sensor_response}
return 200, {"ok": True, "client_pem": row["binding"]["client_pem"], **sensor_response, **enrollment_response}
+170
View File
@@ -0,0 +1,170 @@
"""Paired signalling, private ICE candidates and bounded live data channels."""
import asyncio
import ipaddress
import queue
import time
from contextlib import suppress
from uuid import uuid4
import aioice.ice
from aiortc import RTCConfiguration, RTCPeerConnection, RTCSessionDescription
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")
)
def private(address):
try:
value = ipaddress.ip_address(address)
return value.version == 4 and any(value in network for network in PRIVATE_NETWORKS)
except ValueError:
return False
def admit_sdp(sdp):
if not isinstance(sdp, str) or not 1 <= len(sdp) <= 32768:
raise ValueError("Invalid media invitation")
media = [line for line in sdp.splitlines() if line.startswith("m=")]
if not sdp.startswith("v=0") or len(media) != 1 or not media[0].startswith("m=application "):
raise ValueError("A single data-channel media section is required")
for line in sdp.splitlines():
if line.startswith("a=candidate:"):
parts = line.split()
if (
len(parts) < 8
or parts[7] != "host"
or not (private(parts[4]) or parts[4].endswith(".local"))
):
raise ValueError("Only private host ICE candidates are admitted")
class NodeMediaPeers:
def __init__(self, hub, camera):
self.hub, self.camera, self.items = hub, camera, {}
# Process-local adapter in this dedicated worker; no public/STUN/TURN ICE.
original = aioice.ice.get_host_addresses
aioice.ice.get_host_addresses = lambda use_ipv4, use_ipv6: [
value for value in original(use_ipv4=True, use_ipv6=False) if private(value)
]
async def offer(self, parameters):
admit_sdp(parameters.get("sdp"))
if len(self.items) >= 2:
raise ValueError("Close another live viewer")
pc = RTCPeerConnection(RTCConfiguration(iceServers=[]))
identifier = "peer_" + uuid4().hex
entry = {"pc": pc, "seen": time.monotonic(), "tasks": [], "labels": set()}
self.items[identifier] = entry
@pc.on("datachannel")
def datachannel(channel):
if channel.label not in {"rrd", "camera"} or channel.label in entry["labels"]:
channel.close()
return
entry["labels"].add(channel.label)
@channel.on("message")
def message(value):
if value == "keepalive":
entry["seen"] = time.monotonic()
entry["tasks"].append(asyncio.create_task(self.deliver(identifier, channel)))
@pc.on("connectionstatechange")
async def changed():
if pc.connectionState in {"closed", "failed"}:
await self.close(identifier)
try:
await pc.setRemoteDescription(
RTCSessionDescription(sdp=parameters["sdp"], type="offer")
)
await pc.setLocalDescription(await pc.createAnswer())
async def expiry():
await asyncio.sleep(25)
if pc.connectionState != "connected":
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"),
"profile": "live-acquisition",
"transport": "webrtc-rrd-fmp4",
}
except BaseException:
await self.close(identifier)
raise
async def send(self, channel, payload):
if len(payload) > 8 * 1024 * 1024:
raise RuntimeError("Preview fragment exceeds bound")
for offset in range(0, len(payload), 16384):
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])
await asyncio.sleep(0)
async def deliver(self, identifier, channel):
subscriber = lease = None
try:
entry = self.items[identifier]
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()
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)
else:
try:
segment = await asyncio.to_thread(lease.segments.get, 0.5)
except queue.Empty:
continue
if segment is None:
break
kind, payload = segment
if kind == "media":
self.camera.mark_streaming(lease)
if payload is None:
break
if payload:
await self.send(channel, payload)
except (Exception, asyncio.CancelledError):
pass
finally:
if subscriber:
subscriber.close()
if lease:
self.camera.release_delivery(lease, client_closed=True)
if channel.label == "camera":
channel.close()
else:
await self.close(identifier)
async def close(self, identifier):
entry = self.items.pop(identifier, None)
if entry:
for task in entry["tasks"]:
if task is not asyncio.current_task():
task.cancel()
with suppress(Exception):
await entry["pc"].close()
async def close_all(self):
for identifier in list(self.items):
await self.close(identifier)
+144
View File
@@ -0,0 +1,144 @@
"""Bounded live RRD publication for paired Node viewers, without a TCP listener.
Every viewer receives a fresh native recording including StoreInfo/blueprint.
Latest-value queues discard decoded preview frames before encoding; encoded
RRD bytes are never dropped inside a stream. Slow viewers are closed instead.
"""
import queue
import threading
from contextlib import suppress
from uuid import uuid4
from k1link.viewer.rerun_bridge import RerunBridge
MAX_ENCODED_CHUNK = 8 * 1024 * 1024
class RrdSubscriber:
def __init__(self, settings_provider):
self.closed = threading.Event()
self.inputs = queue.Queue(maxsize=2)
self.output = queue.Queue(maxsize=2)
self.settings_provider = settings_provider
self.thread = threading.Thread(target=self.run, name="node-rerun-view", daemon=True)
self.thread.start()
def offer(self, envelope):
if self.closed.is_set():
return
with suppress(queue.Full):
if self.inputs.full():
with suppress(queue.Empty):
self.inputs.get_nowait()
self.inputs.put_nowait(envelope)
def read(self):
if self.closed.is_set():
return None
try:
return self.output.get(timeout=0.5)
except queue.Empty:
return b""
def close(self):
self.closed.set()
def run(self):
binary = None
bridge = None
def output(recording):
nonlocal binary
binary = recording.binary_stream()
return "webrtc+rrd://" + str(uuid4())
try:
bridge = RerunBridge(settings_provider=self.settings_provider, recording_output=output)
bridge.begin_session()
while not self.closed.is_set():
payload = binary.read()
if len(payload) > MAX_ENCODED_CHUNK:
break
if payload:
# Bound both bytes and waiting time. The archive/producer
# never waits for this disposable preview subscription.
self.output.put(payload, timeout=0.5)
try:
envelope = self.inputs.get(timeout=0.1)
except queue.Empty:
continue
bridge.process(envelope)
except Exception:
pass
finally:
self.closed.set()
if bridge is not None:
with suppress(Exception):
bridge.close()
binary.read()
class NodeRerunBridge(RerunBridge):
def __init__(self, **kwargs):
self.lock = threading.Lock()
self.subscribers = []
self.latest = {}
def output(recording):
self.binary = recording.binary_stream()
return "webrtc+rrd://" + str(uuid4())
super().__init__(**kwargs, recording_output=output)
self.binary.read()
def process(self, envelope):
super().process(envelope)
# The primary recording proves native publication and owns metrics.
# It is continuously drained even when no viewer is attached.
self.binary.read()
with self.lock:
self.latest[type(envelope)] = envelope
self.subscribers = [v for v in self.subscribers if not v.closed.is_set()]
for subscriber in self.subscribers:
subscriber.offer(envelope)
def process_perception(self, frame):
super().process_perception(frame)
self.binary.read()
def subscribe(self):
with self.lock:
self.subscribers = [v for v in self.subscribers if not v.closed.is_set()]
if self._closed or len(self.subscribers) >= 2:
raise RuntimeError("Live viewer unavailable")
subscriber = RrdSubscriber(self._settings_provider)
for envelope in self.latest.values():
subscriber.offer(envelope)
self.subscribers.append(subscriber)
return subscriber
def close(self):
with self.lock:
for subscriber in self.subscribers:
subscriber.close()
self.subscribers.clear()
self.latest.clear()
super().close()
self.binary.read()
class NodeRerunHub:
def __init__(self):
self.bridge = None
def create(self, **kwargs):
bridge = NodeRerunBridge(**kwargs)
self.bridge = bridge
return bridge
def subscribe(self):
bridge = self.bridge
if bridge is None:
raise RuntimeError("Live acquisition is not active")
return bridge.subscribe()
+23 -24
View File
@@ -118,6 +118,7 @@ class RerunBridge:
settings_provider: SettingsProvider | None = None,
cors_allow_origin: tuple[str, ...] = DEFAULT_CORS_ORIGINS,
recording_factory: Callable[[str], rr.RecordingStream] | None = None,
recording_output: Callable[[rr.RecordingStream], str] | None = None,
) -> None:
self.metrics = metrics or BridgeMetrics()
self._settings_provider = settings_provider or RerunSceneSettings
@@ -130,31 +131,29 @@ 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=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
# late-client backlog can block the native SDK and freeze preview.
server_memory_limit=LIVE_GRPC_BUFFER_LIMIT,
# Rerun transport can replay ActivateStore before StoreInfo when an
# evicted buffer is served newest-first, leaving late viewers on the
# welcome screen. Preserve protocol order within the bounded cache.
newest_first=False,
cors_allow_origin=list(cors_allow_origin),
)
if recording_output is not None:
# Node's paired WebRTC delivery uses an RRD sink and never opens
# an unauthenticated gRPC listener on an onboard interface.
url = recording_output(recording)
else:
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},
)
url = recording.serve_grpc(
grpc_port=selected_grpc_port,
default_blueprint=blueprint,
# Bounded reconnect cushion, not the source of record.
server_memory_limit=LIVE_GRPC_BUFFER_LIMIT,
# StoreInfo must precede ActivateStore for late viewers.
newest_first=False,
cors_allow_origin=list(cors_allow_origin),
)
recording.send_blueprint(
blueprint,
make_active=True,
+20 -6
View File
@@ -215,8 +215,8 @@ def default_environment_settings() -> EnvironmentSettingsDocument:
description=(
"Свести все сенсоры в синхронную и управляемую операторскую картину."
),
primary_workspace_id="spatial-scene",
secondary_workspace_id="cameras",
primary_workspace_id="cameras",
secondary_workspace_id="world-map",
background=background.model_copy(),
),
missions=EnvironmentPage(
@@ -372,10 +372,24 @@ def _upgrade_environment(payload: object) -> EnvironmentSettingsDocument:
raise ValueError("environment document must be an object")
schema_version = payload.get("schema_version")
if schema_version == "missioncore.operator-environment/v1":
return _upgrade_v1_environment(payload)
if schema_version == "missioncore.operator-environment/v2":
return _upgrade_v2_environment(payload)
return EnvironmentSettingsDocument.model_validate(payload)
document = _upgrade_v1_environment(payload)
elif schema_version == "missioncore.operator-environment/v2":
document = _upgrade_v2_environment(payload)
else:
document = EnvironmentSettingsDocument.model_validate(payload)
# The owner moved the local live scene to LAB. Retire only its old
# Control shortcuts, preserving custom copy, backgrounds and other links.
page = document.pages.observation
if page.primary_workspace_id == "spatial-scene":
page.primary_workspace_id = (
page.secondary_workspace_id
if page.secondary_workspace_id not in (None, "spatial-scene")
else "cameras"
)
page.secondary_workspace_id = None
elif page.secondary_workspace_id == "spatial-scene":
page.secondary_workspace_id = None
return document
class EnvironmentSettingsStore:
+49
View File
@@ -136,3 +136,52 @@ def sensor_operation(
return operation(fleet, vehicle_id, operation_id)
except PairingError as error:
raise HTTPException(404, str(error)) from None
@router.get("/{vehicle_id}/devices/enrollment")
def enrollment_state(
vehicle_id: str, response: Response, fleet: Annotated[FleetRegistry, Depends(local_operator)]
):
response.headers["Cache-Control"] = "no-store"
try:
with fleet.lock:
row = fleet.find(vehicle_id)
return {
**row.get("device_enrollment", {"available": False}),
"node_id": row["node_id"],
"name": row["name"],
"fresh": fleet.public(row)["connectivity"] == "online",
}
except PairingError as error:
raise HTTPException(404, str(error)) from None
@router.post("/{vehicle_id}/devices/enrollment/operations")
def enrollment_submit(
vehicle_id: str,
body: dict,
response: Response,
fleet: Annotated[FleetRegistry, Depends(local_operator)],
):
response.headers["Cache-Control"] = "no-store"
try:
return fleet.device_enrollment.submit(fleet, vehicle_id, body)
except (PairingError, ValueError, TypeError):
# Validation diagnostics must never echo a supplied credential.
raise HTTPException(
409, "Запрос подключения не принят. Обновите БК и проверьте параметры сети."
) from None
@router.get("/{vehicle_id}/devices/enrollment/operations/{operation_id}")
def enrollment_operation(
vehicle_id: str,
operation_id: str,
response: Response,
fleet: Annotated[FleetRegistry, Depends(local_operator)],
):
response.headers["Cache-Control"] = "no-store"
try:
return fleet.device_enrollment.operation(fleet, vehicle_id, operation_id)
except PairingError as error:
raise HTTPException(404, str(error)) from None
+35
View File
@@ -18,6 +18,7 @@ _EXTRA_FIELDS: Final = (
"connection_mode",
"error_category",
"error_code",
"transport_error_code",
"reason_code",
"failed_phase",
"dialogue_stage",
@@ -32,6 +33,20 @@ _EXTRA_FIELDS: Final = (
"device_write_confirmed",
"ble_att_error_code",
"ble_att_error_name",
"resolved_write_mode",
"max_write_without_response_size",
"mtu_size",
"frame_length",
"bridge_frame_length",
"quick_connect_frame_length",
"scan_elapsed_ms",
"scanner_start_ms",
"initial_window_ms",
"first_candidate_ms",
"scan_extended",
"candidate_count",
"likely_k1_candidate_count",
"discovery_generation",
"helper_stage",
"helper_elapsed_ms",
"status_reconciliation",
@@ -102,6 +117,26 @@ class ScannerDiagnosticJsonFormatter(logging.Formatter):
value = getattr(record, field, None)
if value is not None and isinstance(value, (str, int, bool)):
document[field] = value
if record.exc_info is not None:
exception_type, _exception, traceback = record.exc_info
if exception_type is not None:
document["exception_type"] = exception_type.__name__[:128]
if traceback is not None:
while traceback.tb_next is not None:
traceback = traceback.tb_next
code = traceback.tb_frame.f_code
# Preserve the failure location, never exception text, locals,
# source lines or absolute paths that may contain private data.
document["exception_site"] = (
f"{Path(code.co_filename).name}:{traceback.tb_lineno}:{code.co_name}"
)[:256]
properties = getattr(record, "write_characteristic_properties", None)
if isinstance(properties, (list, tuple)) and all(
isinstance(value, str)
and value in {"read", "write", "write-without-response", "notify", "indicate"}
for value in properties
):
document["write_characteristic_properties"] = sorted(set(properties))
return json.dumps(document, ensure_ascii=False, separators=(",", ":"))