Fix onboard K1 enrollment continuity and share named acquisition preparation
This commit is contained in:
@@ -5,7 +5,7 @@ import math
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
from dataclasses import dataclass, replace
|
||||
from importlib.metadata import version
|
||||
from threading import Lock
|
||||
@@ -839,6 +839,35 @@ async def _retrieve_bluez_device(address: str, details: object = None) -> BLEDev
|
||||
return None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def hold_bluez_device_for_connect(device: BLEDevice):
|
||||
"""Keep discovery alive across the cache-check / D-Bus-connect gap.
|
||||
|
||||
BlueZ can discard an unconnected path after StopDiscovery. Holding one
|
||||
scanner reference until BleakClient connects avoids consuming a path just
|
||||
removed by that cleanup. This neither selects another device nor repeats
|
||||
Connect/GATT writes, and is released before any characteristic is read.
|
||||
"""
|
||||
details = getattr(device, "details", None)
|
||||
if not sys.platform.startswith("linux") or not isinstance(details, dict):
|
||||
yield
|
||||
return
|
||||
path = details.get("path", "")
|
||||
match = re.fullmatch(r"/org/bluez/(hci[0-9]+)/dev_[0-9A-F_]+", path)
|
||||
if (match is None
|
||||
or not re.fullmatch(r"(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}", device.address)
|
||||
or path.rsplit("/", 1)[-1] != "dev_" + device.address.upper().replace(":", "_")):
|
||||
raise BleakDeviceNotFoundError(device.address, "Invalid selected BlueZ transport")
|
||||
runtime = ble_runtime_snapshot()
|
||||
owner = ble_runtime_owner_epoch_for_current_loop()
|
||||
if (owner is None or runtime["owner_epoch"] != owner
|
||||
or runtime["poisoned"] or not runtime["owner_loop_bound"]
|
||||
or runtime["active_operation_kind"] not in {"status-read", "wifi-provision"}):
|
||||
raise BleakDeviceNotFoundError(device.address, "BLE operation owner changed")
|
||||
async with BleakScanner(bluez={"adapter": match[1]}):
|
||||
yield
|
||||
|
||||
|
||||
async def ensure_device_for_gatt(device: BLEDevice, *, timeout_seconds: float) -> None:
|
||||
"""Restore a vanished BlueZ path before the one admitted GATT connection.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
import ipaddress
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
from contextlib import AsyncExitStack
|
||||
from importlib.metadata import version
|
||||
from time import monotonic
|
||||
from typing import Literal, TypedDict
|
||||
@@ -24,6 +25,7 @@ from k1link.device_plugins.xgrids_k1.ble.scanner import (
|
||||
discover_known_device_capture_for_status_read,
|
||||
discovered_device_selection,
|
||||
ensure_device_for_gatt,
|
||||
hold_bluez_device_for_connect,
|
||||
mark_captured_device_gatt_validated,
|
||||
retrieve_connected_device_capture,
|
||||
retrieve_known_device_capture_for_status_read,
|
||||
@@ -391,12 +393,16 @@ async def _read_wifi_status_impl(
|
||||
"Exact BLE device is unavailable; run an explicit recovery or scan.",
|
||||
)
|
||||
|
||||
await ensure_device_for_gatt(device, timeout_seconds=timeout_seconds)
|
||||
if (active_captured_device is not None
|
||||
and captured_device_handle(active_captured_device) is not device):
|
||||
raise BleakDeviceNotFoundError(device_macos_uuid, "BLE selection invalidated")
|
||||
progress.operation_stage = "connect"
|
||||
async with BleakClient(device, timeout=timeout_seconds, pair=False) as client:
|
||||
async with AsyncExitStack() as gatt_session:
|
||||
async with hold_bluez_device_for_connect(device):
|
||||
await ensure_device_for_gatt(device, timeout_seconds=timeout_seconds)
|
||||
if (active_captured_device is not None
|
||||
and captured_device_handle(active_captured_device) is not device):
|
||||
raise BleakDeviceNotFoundError(device_macos_uuid, "BLE selection invalidated")
|
||||
progress.operation_stage = "connect"
|
||||
client = await gatt_session.enter_async_context(
|
||||
BleakClient(device, timeout=timeout_seconds, pair=False)
|
||||
)
|
||||
progress.operation_stage = "gatt-contract"
|
||||
service = client.services.get_service(SERVICE_UUID)
|
||||
write_characteristic = client.services.get_characteristic(
|
||||
@@ -619,13 +625,17 @@ async def _provision_wifi_impl(
|
||||
"Device was not rediscovered; keep the K1 powered and nearby.",
|
||||
)
|
||||
|
||||
await ensure_device_for_gatt(device, timeout_seconds=timeout_seconds)
|
||||
if (active_captured_device is not None
|
||||
and captured_device_handle(active_captured_device) is not device):
|
||||
raise BleakDeviceNotFoundError(device_macos_uuid, "BLE selection invalidated")
|
||||
operation_stage = "connect"
|
||||
progress.operation_stage = operation_stage
|
||||
async with BleakClient(device, timeout=timeout_seconds, pair=False) as client:
|
||||
async with AsyncExitStack() as gatt_session:
|
||||
async with hold_bluez_device_for_connect(device):
|
||||
await ensure_device_for_gatt(device, timeout_seconds=timeout_seconds)
|
||||
if (active_captured_device is not None
|
||||
and captured_device_handle(active_captured_device) is not device):
|
||||
raise BleakDeviceNotFoundError(device_macos_uuid, "BLE selection invalidated")
|
||||
operation_stage = "connect"
|
||||
progress.operation_stage = operation_stage
|
||||
client = await gatt_session.enter_async_context(
|
||||
BleakClient(device, timeout=timeout_seconds, pair=False)
|
||||
)
|
||||
device_name = client.name
|
||||
operation_stage = "gatt-contract"
|
||||
progress.operation_stage = operation_stage
|
||||
|
||||
@@ -10,6 +10,7 @@ from .facade import (
|
||||
XGRIDS_K1_PLUGIN_ID,
|
||||
XGRIDS_K1_PLUGIN_VERSION,
|
||||
ViewerSettingsRequest,
|
||||
normalize_project_name,
|
||||
)
|
||||
from .node_bridge import ATTESTATION, plugin_operation_id
|
||||
|
||||
@@ -106,6 +107,33 @@ def project_sensor(snapshot, node_id):
|
||||
}
|
||||
|
||||
|
||||
def verification_parameters(state, operation_id):
|
||||
"""Use the admitted persisted Bridge target before asking for BLE again.
|
||||
|
||||
The facade still checks its ledger, exact identity, route, DeviceInfo and
|
||||
physical reconciliation fences. A saved address alone never grants START.
|
||||
"""
|
||||
parameters = {
|
||||
"expected_snapshot_runtime_id": state["snapshot_runtime_id"],
|
||||
"operation_id": operation_id,
|
||||
}
|
||||
policy = (state.get("connection_policy") or {}).get("actions", {})
|
||||
decision = policy.get("observe-configured-device-network") or {}
|
||||
target = decision.get("required_transport_ref")
|
||||
selected = state.get("selected_device_id")
|
||||
if (decision.get("allowed") is True
|
||||
and decision.get("requires_live_gatt_validation") is False
|
||||
and decision.get("required_connection_mode") == "bridge"
|
||||
and isinstance(target, str) and isinstance(selected, str)
|
||||
and target.casefold() == selected.casefold()):
|
||||
parameters.update(
|
||||
device_id=target, source="durable-configured-state",
|
||||
compatibility_attestation=ATTESTATION,
|
||||
expected_mode_revision=state.get("desired_connection_mode_revision"),
|
||||
)
|
||||
return parameters
|
||||
|
||||
|
||||
class NodeK1Sensor:
|
||||
def __init__(self, bridge, peers):
|
||||
self.bridge, self.peers = bridge, peers
|
||||
@@ -194,7 +222,7 @@ class NodeK1Sensor:
|
||||
if action == "verify":
|
||||
result = await self.bridge.invoke(
|
||||
"connection.verify",
|
||||
{"expected_snapshot_runtime_id": runtime, "operation_id": plugin_identifier},
|
||||
verification_parameters(state, plugin_identifier),
|
||||
identifier,
|
||||
)
|
||||
return project_sensor(result, node_id)
|
||||
@@ -224,6 +252,12 @@ class NodeK1Sensor:
|
||||
)
|
||||
await self.peers.close_all()
|
||||
return project_sensor(result, node_id)
|
||||
# Validate the complete operator draft before workspace/project commands.
|
||||
if not isinstance(params.get("project_name"), str):
|
||||
raise ValueError("Введите название проекта")
|
||||
project_name = normalize_project_name(params["project_name"])
|
||||
if params.get("mount_type") != "handheld" or params.get("gnss_mode") != "none":
|
||||
raise ValueError("Unsupported acquisition configuration")
|
||||
deadline = min(
|
||||
datetime.fromisoformat(command["deadline_at"]).timestamp(), time.time() + 165
|
||||
)
|
||||
@@ -253,10 +287,13 @@ class NodeK1Sensor:
|
||||
self.cas(state),
|
||||
operation_id=prepare_identifier,
|
||||
idempotency_key=prepare_identifier,
|
||||
project_name="node-" + identifier[3:15],
|
||||
project_name=project_name,
|
||||
mount_type=params["mount_type"], gnss_mode=params["gnss_mode"],
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
elif phase == "project-ready" and acquisition.get("state") == "prepared":
|
||||
if acquisition.get("project_name") != project_name:
|
||||
raise ValueError("Prepared project changed")
|
||||
next_action = "acquisition.start"
|
||||
payload.update(
|
||||
self.cas(state),
|
||||
|
||||
Reference in New Issue
Block a user