Refresh selected BlueZ path before K1 GATT operations

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 15:50:06 +03:00
parent 7ce09eabcd
commit ad24518b71
8 changed files with 303 additions and 55 deletions
@@ -17,6 +17,7 @@ from uuid import UUID
from bleak import BleakScanner
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from bleak.exc import BleakDeviceNotFoundError
from k1link.artifacts import utc_now_iso
from k1link.device_plugins.xgrids_k1.ble.runtime_arbiter import (
@@ -838,16 +839,60 @@ async def _retrieve_bluez_device(address: str, details: object = None) -> BLEDev
return None
async def status_read_device_is_current(device: BLEDevice) -> bool:
"""Check a BlueZ handle before the one explicit status-read connection.
async def ensure_device_for_gatt(device: BLEDevice, *, timeout_seconds: float) -> None:
"""Restore a vanished BlueZ path before the one admitted GATT connection.
BlueZ may remove an unpaired object after a completed scan session. This
cache check neither scans nor connects and never changes macOS selection.
A BLEDevice stores a D-Bus path, not a native object lease. After the form
has been filled in, BlueZ may have removed that path. Observe the same
address on the same adapter once, then require that exact path to exist.
Keep the original selection/capture; no session pin, public scan generation,
GATT connection or write is created here. CoreBluetooth needs no refresh.
"""
details = getattr(device, "details", None)
if not sys.platform.startswith("linux") or not isinstance(details, dict):
return True
return await _retrieve_bluez_device(device.address, details) is not None
return
path = details.get("path", "")
address = device.address
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}", address)
or path.rsplit("/", 1)[-1] != "dev_" + address.upper().replace(":", "_")
or not math.isfinite(timeout_seconds) or timeout_seconds <= 0):
raise BleakDeviceNotFoundError(address, "Invalid selected BlueZ transport")
owner_epoch = ble_runtime_owner_epoch_for_current_loop()
operation_kind = ble_runtime_snapshot()["active_operation_kind"]
def require_owner() -> None:
runtime = ble_runtime_snapshot()
if (owner_epoch is None
or ble_runtime_owner_epoch_for_current_loop() != owner_epoch
or runtime["owner_epoch"] != owner_epoch
or not runtime["owner_loop_bound"] or runtime["poisoned"]
or operation_kind not in {"status-read", "wifi-provision"}
or runtime["active_operation_kind"] != operation_kind):
raise BleakDeviceNotFoundError(address, "BLE operation owner changed")
require_owner()
current = await _retrieve_bluez_device(address, details)
require_owner()
if current is not None:
return
candidate = await BleakScanner.find_device_by_address(
address,
timeout=min(timeout_seconds, 8.0),
bluez={"adapter": match[1]},
)
require_owner()
if (candidate is None or candidate.address.casefold() != address.casefold()
or not isinstance(candidate.details, dict)
or candidate.details.get("path") != path):
raise BleakDeviceNotFoundError(address, "Selected BlueZ transport unavailable")
current = await _retrieve_bluez_device(address, details)
require_owner()
if current is None:
raise BleakDeviceNotFoundError(address, "Selected BlueZ transport disappeared")
async def _retrieve_corebluetooth_device(
@@ -23,10 +23,10 @@ from k1link.device_plugins.xgrids_k1.ble.scanner import (
demote_connected_device_handle_after_gatt_failure,
discover_known_device_capture_for_status_read,
discovered_device_selection,
ensure_device_for_gatt,
mark_captured_device_gatt_validated,
retrieve_connected_device_capture,
retrieve_known_device_capture_for_status_read,
status_read_device_is_current,
)
PROFILE_ID = "xgrids-k1-fw3-wifi-v1"
@@ -391,18 +391,10 @@ async def _read_wifi_status_impl(
"Exact BLE device is unavailable; run an explicit recovery or scan.",
)
if not await status_read_device_is_current(device):
# One explicit read may refresh the exact vanished BlueZ object
# before GATT. No failed connect/write is retried; public discovery
# generations, the pinned target and macOS behavior are unchanged.
progress.operation_stage = "exact-uuid-scan"
active_captured_device = await discover_known_device_capture_for_status_read(
device_macos_uuid, timeout_seconds=min(timeout_seconds, 8.0),
)
device = (captured_device_handle(active_captured_device)
if active_captured_device is not None else None)
if device is None:
raise BleakDeviceNotFoundError(device_macos_uuid, "Exact BLE device unavailable")
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:
progress.operation_stage = "gatt-contract"
@@ -627,6 +619,10 @@ 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: