feat(k1): add local connection matrix
This commit is contained in:
@@ -14,7 +14,7 @@ from collections.abc import Callable, Mapping
|
||||
from datetime import UTC, datetime
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Any, Concatenate, Literal, Protocol, cast
|
||||
from typing import Any, Concatenate, Literal, Protocol, Self, cast
|
||||
|
||||
from bleak.exc import BleakError
|
||||
from missioncore_plugin_sdk.v0alpha2 import (
|
||||
@@ -28,6 +28,7 @@ from pydantic import (
|
||||
SecretStr,
|
||||
ValidationError,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from k1link.artifacts import write_json_atomic
|
||||
@@ -44,6 +45,7 @@ from k1link.device_plugins.xgrids_k1.camera import (
|
||||
XgridsK1CameraGateway,
|
||||
build_xgrids_k1_camera_router,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.macos_wifi import associate_with_wifi_once
|
||||
from k1link.device_plugins.xgrids_k1.mqtt import validate_private_ipv4
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import seal_capture_clock
|
||||
from k1link.device_plugins.xgrids_k1.protocol.application_authority import (
|
||||
@@ -53,6 +55,9 @@ from k1link.device_plugins.xgrids_k1.protocol.application_execution import (
|
||||
ApplicationAuthorityLoader,
|
||||
DormantApplicationControlCoordinator,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.protocol.application_mqtt import (
|
||||
ReviewedApplicationMqttTransport,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.protocol.application_publish import (
|
||||
UninstalledApplicationPublishSink,
|
||||
WriteDisabledOneShotPublisher,
|
||||
@@ -90,9 +95,19 @@ from k1link.web.plugin_runtime import (
|
||||
)
|
||||
|
||||
XGRIDS_K1_PLUGIN_ID = "nodedc.device.xgrids-lixelkity-k1"
|
||||
XGRIDS_K1_PLUGIN_VERSION = "0.5.0"
|
||||
XGRIDS_K1_PLUGIN_VERSION = "0.6.0"
|
||||
XGRIDS_K1_MODEL_ID = "xgrids.lixelkity-k1"
|
||||
XGRIDS_K1_COMPATIBILITY_PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
|
||||
XGRIDS_K1_COMPATIBILITY_PROFILE_ID = (
|
||||
"xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"
|
||||
)
|
||||
|
||||
ConnectionMode = Literal["bridge", "quick-connect", "direct-connect"]
|
||||
ConnectionTopology = Literal["direct-lan", "device-ap", "controller-hotspot"]
|
||||
CONNECTION_TOPOLOGY_BY_MODE: dict[ConnectionMode, ConnectionTopology] = {
|
||||
"bridge": "direct-lan",
|
||||
"quick-connect": "device-ap",
|
||||
"direct-connect": "controller-hotspot",
|
||||
}
|
||||
|
||||
ACTION_STATE_READ = "state.read"
|
||||
ACTION_DISCOVERY_SCAN = "discovery.scan"
|
||||
@@ -182,7 +197,7 @@ class CompatibilityAttestationRequest(StrictRequest):
|
||||
"""Selected profile whose facts must be verified from live DeviceInfo."""
|
||||
|
||||
firmware_version: Literal["3.0.2"]
|
||||
topology: Literal["direct-lan"]
|
||||
topology: ConnectionTopology
|
||||
verification: Literal["live-device-info"]
|
||||
|
||||
|
||||
@@ -190,11 +205,26 @@ class ConnectRequest(StrictRequest):
|
||||
device_id: str = Field(min_length=1, max_length=128)
|
||||
ssid: str = Field(min_length=1, max_length=128)
|
||||
password: SecretStr = Field(min_length=1, max_length=256)
|
||||
connection_mode: Literal["bridge"] = "bridge"
|
||||
connection_mode: ConnectionMode = "bridge"
|
||||
compatibility_attestation: CompatibilityAttestationRequest
|
||||
operation_id: str | None = Field(default=None, min_length=1, max_length=128)
|
||||
idempotency_key: str | None = Field(default=None, min_length=1, max_length=160)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_connection_topology(self) -> Self:
|
||||
if not 1 <= len(self.ssid.encode("utf-8")) <= 32:
|
||||
raise ValueError("SSID must contain between 1 and 32 UTF-8 bytes")
|
||||
if not 1 <= len(self.password.get_secret_value().encode("utf-8")) <= 64:
|
||||
raise ValueError(
|
||||
"Wi-Fi password must contain between 1 and 64 UTF-8 bytes"
|
||||
)
|
||||
expected = CONNECTION_TOPOLOGY_BY_MODE[self.connection_mode]
|
||||
if self.compatibility_attestation.topology != expected:
|
||||
raise ValueError(
|
||||
f"connection_mode={self.connection_mode} requires topology={expected}"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class LiveRequest(StrictRequest):
|
||||
project_name: str = Field(min_length=1, max_length=96)
|
||||
@@ -335,7 +365,7 @@ class XgridsK1CompatibilityService:
|
||||
self._devices: list[dict[str, Any]] = []
|
||||
self._selected_device_id: str | None = None
|
||||
self._k1_ip: str | None = None
|
||||
self._connection_mode: Literal["bridge"] | None = None
|
||||
self._connection_mode: ConnectionMode | None = None
|
||||
self._device_ids_by_transport_ref: dict[str, str] = {}
|
||||
self._device_id: str | None = None
|
||||
self._device_session_id: str | None = None
|
||||
@@ -369,7 +399,8 @@ class XgridsK1CompatibilityService:
|
||||
WriteDisabledOneShotPublisher(UninstalledApplicationPublishSink()),
|
||||
)
|
||||
self._application_control_session = InteractiveApplicationControlSession(
|
||||
authority_loader
|
||||
authority_loader,
|
||||
transport_factory=self._application_control_transport,
|
||||
)
|
||||
self.runtime = VisualizationRuntime(
|
||||
normalizer=normalize_k1_message,
|
||||
@@ -380,6 +411,14 @@ class XgridsK1CompatibilityService:
|
||||
XGRIDS_K1_PLUGIN_ID,
|
||||
)
|
||||
|
||||
def _application_control_transport(self, host: str) -> ReviewedApplicationMqttTransport:
|
||||
with self._lock:
|
||||
connection_mode = self._connection_mode
|
||||
return ReviewedApplicationMqttTransport(
|
||||
host,
|
||||
allow_device_ap=connection_mode == "quick-connect",
|
||||
)
|
||||
|
||||
@_serialized_acquisition_access
|
||||
def state(self) -> dict[str, Any]:
|
||||
application_control = self._application_control.snapshot().as_dict()
|
||||
@@ -600,9 +639,10 @@ class XgridsK1CompatibilityService:
|
||||
known_ids = {str(item["device_id"]) for item in self.state()["devices"]}
|
||||
if request.device_id not in known_ids:
|
||||
raise ValueError("сначала найдите и выберите устройство через Bluetooth")
|
||||
# Unwrap once at the provisioning service boundary. The plain value is
|
||||
# kept only in this stack frame, included in a keyed request digest, and
|
||||
# passed to the reviewed BLE write boundary; it is never journaled.
|
||||
# Unwrap once at the network service boundary. The plain value is kept
|
||||
# only in this stack frame, included in a keyed request digest, and
|
||||
# passed either to the reviewed BLE write or to the short-lived macOS
|
||||
# CoreWLAN helper over stdin; it is never journaled.
|
||||
password = request.password.get_secret_value()
|
||||
request_fingerprint = self._request_fingerprint(
|
||||
ACTION_NETWORK_PROVISION,
|
||||
@@ -647,7 +687,8 @@ class XgridsK1CompatibilityService:
|
||||
raise RuntimeError("другая операция настройки Wi-Fi уже выполняется")
|
||||
|
||||
session_dir: Path | None = None
|
||||
write_attempted = False
|
||||
network_change_attempted = False
|
||||
quick_connect = request.connection_mode == "quick-connect"
|
||||
try:
|
||||
with self._lock:
|
||||
active_acquisition = self._acquisition
|
||||
@@ -659,55 +700,109 @@ class XgridsK1CompatibilityService:
|
||||
"нельзя менять устройство или его сеть во время активной acquisition-сессии"
|
||||
)
|
||||
self._provisioning_active = True
|
||||
# Once a new network operation is admitted, the previous route
|
||||
# and device session can no longer be represented as current.
|
||||
# This is especially important when Quick Connect may move the
|
||||
# host off the prior LAN before CoreWLAN reports an outcome.
|
||||
self._selected_device_id = None
|
||||
self._k1_ip = None
|
||||
self._connection_mode = None
|
||||
self._compatibility_attestation = None
|
||||
self._device_session_id = None
|
||||
self._device_session_opened_at = None
|
||||
self._connection_verification = {
|
||||
"status": "not-probed",
|
||||
"endpoint_validation": "not-performed",
|
||||
"network_reachability": "unknown",
|
||||
"observed_at": None,
|
||||
}
|
||||
|
||||
# A reprovision can replace both the device session and its address.
|
||||
# A connection-mode change can replace both the device session and
|
||||
# its address.
|
||||
self._application_control.disarm()
|
||||
# Revoke preview/producer state only after the active-acquisition
|
||||
# guard; a rejected network write must never stop evidence capture.
|
||||
self.camera_preview.stop_current()
|
||||
|
||||
self._set_operation(
|
||||
"provisioning",
|
||||
"Передаём устройству настройки Wi-Fi одним подтверждённым запросом.",
|
||||
operation_message = (
|
||||
"Подключаем этот Mac к точке доступа выбранного K1 одним запросом."
|
||||
if quick_connect
|
||||
else "Передаём устройству настройки Wi-Fi одним подтверждённым запросом."
|
||||
)
|
||||
self._set_operation("provisioning", operation_message)
|
||||
session_dir = _new_operation_session_dir(
|
||||
self.evidence_root,
|
||||
"viewer_wifi_provisioning",
|
||||
"viewer_k1_ap_association"
|
||||
if quick_connect
|
||||
else "viewer_wifi_provisioning",
|
||||
)
|
||||
session_dir.mkdir(parents=True, exist_ok=False)
|
||||
self._operations.transition(
|
||||
operation.operation_id,
|
||||
"running",
|
||||
stage_code="ble-provisioning-write",
|
||||
stage_code=(
|
||||
"host-wifi-association" if quick_connect else "ble-provisioning-write"
|
||||
),
|
||||
message_code="network.provision.running",
|
||||
)
|
||||
write_attempted = True
|
||||
result = await provision_wifi_once(
|
||||
request.device_id,
|
||||
request.ssid,
|
||||
password,
|
||||
timeout_seconds=45.0,
|
||||
write_mode="auto",
|
||||
)
|
||||
write_json_atomic(session_dir / "provisioning.sensitive.json", result)
|
||||
ipv4 = _provisioned_ipv4(result)
|
||||
local_address_conflict = ipv4 is not None and _target_is_local_ipv4(ipv4)
|
||||
write_json_atomic(
|
||||
session_dir / "manifest.redacted.json",
|
||||
{
|
||||
network_change_attempted = True
|
||||
if quick_connect:
|
||||
started_at = _utc_now_iso()
|
||||
association = await asyncio.to_thread(
|
||||
associate_with_wifi_once,
|
||||
self.repository_root
|
||||
/ "plugins"
|
||||
/ "xgrids-k1"
|
||||
/ "macos"
|
||||
/ "associate_wifi.swift",
|
||||
request.ssid,
|
||||
password,
|
||||
timeout_seconds=45.0,
|
||||
)
|
||||
completed_at = _utc_now_iso()
|
||||
ipv4: str | None = AP_FALLBACK_IPV4
|
||||
connection_manifest: dict[str, Any] = {
|
||||
"schema_version": 1,
|
||||
"started_at_utc": started_at,
|
||||
"completed_at_utc": completed_at,
|
||||
"operation": "single_corewlan_k1_ap_association",
|
||||
"connection_mode": request.connection_mode,
|
||||
"topology": request.compatibility_attestation.topology,
|
||||
"outcome": association["outcome"],
|
||||
"credentials_persisted_by_connector": False,
|
||||
}
|
||||
else:
|
||||
result = await provision_wifi_once(
|
||||
request.device_id,
|
||||
request.ssid,
|
||||
password,
|
||||
timeout_seconds=45.0,
|
||||
write_mode="auto",
|
||||
)
|
||||
write_json_atomic(session_dir / "provisioning.sensitive.json", result)
|
||||
ipv4 = _provisioned_ipv4(result)
|
||||
connection_manifest = {
|
||||
"schema_version": 1,
|
||||
"started_at_utc": result["started_at_utc"],
|
||||
"completed_at_utc": result["completed_at_utc"],
|
||||
"operation": "single_reviewed_wifi_provisioning_write",
|
||||
"profile_id": result["profile_id"],
|
||||
"connection_mode": request.connection_mode,
|
||||
"topology": request.compatibility_attestation.topology,
|
||||
"outcome": result["outcome"],
|
||||
"k1_lan_address_observed": ipv4 is not None,
|
||||
"k1_lan_address_admitted": ipv4 is not None
|
||||
and not local_address_conflict,
|
||||
"local_address_conflict": local_address_conflict,
|
||||
"credentials_persisted_by_connector": False,
|
||||
},
|
||||
}
|
||||
local_address_conflict = ipv4 is not None and _target_is_local_ipv4(ipv4)
|
||||
connection_manifest.update(
|
||||
{
|
||||
"k1_lan_address_observed": ipv4 is not None,
|
||||
"k1_lan_address_admitted": (
|
||||
ipv4 is not None and not local_address_conflict
|
||||
),
|
||||
"local_address_conflict": local_address_conflict,
|
||||
}
|
||||
)
|
||||
write_json_atomic(session_dir / "manifest.redacted.json", connection_manifest)
|
||||
if ipv4 is None:
|
||||
raise RuntimeError(
|
||||
"Устройство не сообщило адрес в локальной сети; автоматического повтора не было"
|
||||
@@ -730,9 +825,21 @@ class XgridsK1CompatibilityService:
|
||||
self._compatibility_attestation = _attestation_snapshot(
|
||||
request.compatibility_attestation
|
||||
)
|
||||
self._operation_message = (
|
||||
"Устройство подключено к Wi-Fi и сообщило локальный адрес."
|
||||
)
|
||||
self._connection_verification = {
|
||||
"status": "not-probed",
|
||||
"endpoint_validation": "not-performed",
|
||||
"network_reachability": "unknown",
|
||||
"observed_at": None,
|
||||
}
|
||||
self._operation_message = {
|
||||
"bridge": "K1 подключён к общей сети и сообщил локальный адрес.",
|
||||
"direct-connect": (
|
||||
"K1 подключён к хотспоту контроллера и сообщил локальный адрес."
|
||||
),
|
||||
"quick-connect": (
|
||||
"Mission Core подключён к точке доступа K1; адрес K1 подтверждён."
|
||||
),
|
||||
}[request.connection_mode]
|
||||
self._operations.transition(
|
||||
operation.operation_id,
|
||||
"succeeded",
|
||||
@@ -742,6 +849,13 @@ class XgridsK1CompatibilityService:
|
||||
"device_id": self._device_id,
|
||||
"device_session_id": self._device_session_id,
|
||||
"lan_address_observed": True,
|
||||
"connection_mode": request.connection_mode,
|
||||
"topology": request.compatibility_attestation.topology,
|
||||
"address_source": (
|
||||
"reviewed-k1-ap-baseline"
|
||||
if quick_connect
|
||||
else "ble-wifi-status"
|
||||
),
|
||||
},
|
||||
evidence_refs=(f"evidence-session-{session_dir.name}",),
|
||||
)
|
||||
@@ -753,9 +867,11 @@ class XgridsK1CompatibilityService:
|
||||
message_code="network.provision.failed",
|
||||
error=_operation_error(
|
||||
exc,
|
||||
category="device",
|
||||
side_effect_status="unknown" if write_attempted else "none",
|
||||
safe_to_retry=not write_attempted,
|
||||
category="transport" if quick_connect else "device",
|
||||
side_effect_status=(
|
||||
"unknown" if network_change_attempted else "none"
|
||||
),
|
||||
safe_to_retry=not network_change_attempted,
|
||||
),
|
||||
evidence_refs=(
|
||||
(f"evidence-session-{session_dir.name}",) if session_dir is not None else ()
|
||||
@@ -853,7 +969,7 @@ class XgridsK1CompatibilityService:
|
||||
if self._k1_ip is None or self._selected_device_id is None:
|
||||
raise RuntimeError("сначала выберите и подключите K1 через BLE/Wi-Fi")
|
||||
if self._compatibility_attestation is None:
|
||||
raise RuntimeError("exact FW 3.0.2 direct-LAN profile не выбран")
|
||||
raise RuntimeError("exact FW 3.0.2 local-network profile не выбран")
|
||||
acquisition = self._acquisition
|
||||
if acquisition is not None and acquisition.state not in TERMINAL_ACQUISITION_STATES:
|
||||
raise RuntimeError(
|
||||
@@ -908,10 +1024,24 @@ class XgridsK1CompatibilityService:
|
||||
"сначала подключите устройство к Wi-Fi или укажите его локальный адрес"
|
||||
)
|
||||
target = validate_private_ipv4(target)
|
||||
if target == AP_FALLBACK_IPV4:
|
||||
with self._lock:
|
||||
connection_mode = self._connection_mode
|
||||
if connection_mode is None and request.compatibility_attestation.topology != "direct-lan":
|
||||
raise ValueError(
|
||||
"адрес точки доступа устройства нельзя использовать как direct-LAN target"
|
||||
"не-direct-LAN topology требует сначала завершить выбранный connection flow"
|
||||
)
|
||||
if connection_mode is not None:
|
||||
expected_topology = CONNECTION_TOPOLOGY_BY_MODE[connection_mode]
|
||||
if request.compatibility_attestation.topology != expected_topology:
|
||||
raise ValueError(
|
||||
"compatibility attestation не совпадает с активным способом подключения"
|
||||
)
|
||||
if target == AP_FALLBACK_IPV4 and connection_mode != "quick-connect":
|
||||
raise ValueError(
|
||||
"адрес точки доступа K1 разрешён только после Quick Connect"
|
||||
)
|
||||
if connection_mode == "quick-connect" and target != AP_FALLBACK_IPV4:
|
||||
raise ValueError("Quick Connect должен использовать подтверждённый AP-адрес K1")
|
||||
if control_mode == "plugin-commanded":
|
||||
with self._lock:
|
||||
control_target = self._k1_ip
|
||||
@@ -1830,6 +1960,7 @@ class XgridsK1CompatibilityService:
|
||||
current_session_id = self._device_session_id
|
||||
target = self._k1_ip
|
||||
attestation = self._compatibility_attestation
|
||||
connection_mode = self._connection_mode
|
||||
if current_session_id is None or device_session_id != current_session_id:
|
||||
raise ValueError("указана неактивная device-сессия")
|
||||
if attestation is None:
|
||||
@@ -1837,8 +1968,10 @@ class XgridsK1CompatibilityService:
|
||||
if target is None:
|
||||
raise ValueError("у плагина нет подтверждённого локального адреса K1")
|
||||
target = validate_private_ipv4(target)
|
||||
if target == AP_FALLBACK_IPV4:
|
||||
raise ValueError("адрес точки доступа K1 нельзя использовать как camera target")
|
||||
if target == AP_FALLBACK_IPV4 and connection_mode != "quick-connect":
|
||||
raise ValueError("адрес точки доступа K1 не принят для этой device-сессии")
|
||||
if connection_mode == "quick-connect" and target != AP_FALLBACK_IPV4:
|
||||
raise ValueError("Quick Connect camera target не совпадает с AP-адресом K1")
|
||||
return target
|
||||
|
||||
def _require_acquisition(self, acquisition_id: str | None) -> AcquisitionRecord:
|
||||
@@ -2647,7 +2780,7 @@ def _validate_installed_compatibility_profile(repository_root: Path) -> None:
|
||||
|
||||
plugin_root = repository_root.resolve() / "plugins" / "xgrids-k1"
|
||||
loader_path = plugin_root / "profile_loader.py"
|
||||
profile_path = plugin_root / "profiles" / "fw-3.0.2" / "direct-lan.v1.json"
|
||||
profile_path = plugin_root / "profiles" / "fw-3.0.2" / "local-network.v2.json"
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"missioncore_installed_xgrids_k1_profile_loader",
|
||||
loader_path,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, TypedDict
|
||||
|
||||
|
||||
class HostWifiAssociationResult(TypedDict):
|
||||
schema_version: int
|
||||
adapter: str
|
||||
outcome: str
|
||||
already_associated: bool
|
||||
|
||||
|
||||
class HostWifiAssociationError(RuntimeError):
|
||||
"""One bounded host-side Wi-Fi association attempt failed."""
|
||||
|
||||
def __init__(self, reason_code: str) -> None:
|
||||
self.reason_code = reason_code
|
||||
super().__init__(f"macOS Wi-Fi association failed: {reason_code}")
|
||||
|
||||
|
||||
RunProcess = Callable[..., subprocess.CompletedProcess[bytes]]
|
||||
|
||||
|
||||
def associate_with_wifi_once(
|
||||
helper_path: Path,
|
||||
ssid: str,
|
||||
password: str,
|
||||
*,
|
||||
timeout_seconds: float = 45.0,
|
||||
runner: RunProcess = subprocess.run,
|
||||
) -> HostWifiAssociationResult:
|
||||
"""Associate the Mac with one operator-selected Wi-Fi network exactly once.
|
||||
|
||||
The credential is sent to the short-lived CoreWLAN helper through stdin. It
|
||||
never appears in argv, the environment, stdout, stderr, or a persisted
|
||||
artifact. The helper performs at most one scan and one association call.
|
||||
"""
|
||||
|
||||
if sys.platform != "darwin":
|
||||
raise HostWifiAssociationError("unsupported-platform")
|
||||
if not helper_path.is_file():
|
||||
raise HostWifiAssociationError("corewlan-helper-missing")
|
||||
if not 1 <= len(ssid.encode("utf-8")) <= 32:
|
||||
raise ValueError("SSID must contain between 1 and 32 UTF-8 bytes")
|
||||
if not 1 <= len(password.encode("utf-8")) <= 64:
|
||||
raise ValueError("Wi-Fi password must contain between 1 and 64 UTF-8 bytes")
|
||||
if timeout_seconds <= 0:
|
||||
raise ValueError("timeout_seconds must be positive")
|
||||
|
||||
request_bytes = bytearray(
|
||||
json.dumps(
|
||||
{"ssid": ssid, "password": password},
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
)
|
||||
try:
|
||||
completed = runner(
|
||||
["/usr/bin/xcrun", "swift", str(helper_path.resolve())],
|
||||
input=request_bytes,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=timeout_seconds,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise HostWifiAssociationError("corewlan-helper-unavailable") from exc
|
||||
finally:
|
||||
request_bytes[:] = b"\x00" * len(request_bytes)
|
||||
|
||||
if len(completed.stdout) > 4096:
|
||||
raise HostWifiAssociationError("corewlan-response-too-large")
|
||||
try:
|
||||
response: Any = json.loads(completed.stdout.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise HostWifiAssociationError("corewlan-response-invalid") from exc
|
||||
if not isinstance(response, dict):
|
||||
raise HostWifiAssociationError("corewlan-response-invalid")
|
||||
|
||||
reason_code = response.get("reason_code")
|
||||
if completed.returncode != 0 or response.get("ok") is not True:
|
||||
if not isinstance(reason_code, str) or not reason_code:
|
||||
reason_code = "corewlan-association-failed"
|
||||
raise HostWifiAssociationError(reason_code)
|
||||
|
||||
already_associated = response.get("already_associated")
|
||||
if not isinstance(already_associated, bool):
|
||||
raise HostWifiAssociationError("corewlan-response-invalid")
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"adapter": "CoreWLAN",
|
||||
"outcome": "already-associated" if already_associated else "associated",
|
||||
"already_associated": already_associated,
|
||||
}
|
||||
@@ -19,7 +19,7 @@ MAX_APPLICATION_PAYLOAD_BYTES = 64 * 1024
|
||||
MAX_HEADER_BYTES = 4 * 1024
|
||||
MAX_TEXT_BYTES = 4 * 1024
|
||||
APPLICATION_AUTHORITY_SOURCE = "owner-captured-lixelgo-application"
|
||||
COMPATIBILITY_PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
|
||||
COMPATIBILITY_PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"
|
||||
REVIEWED_DEVICE_MODEL = "LixelKity K1"
|
||||
REVIEWED_DEVICE_TYPE = "A4"
|
||||
REVIEWED_PROFILE_ATTESTATION_FAILURE = (
|
||||
@@ -73,8 +73,10 @@ class ApplicationControlAuthority:
|
||||
|
||||
openapi_key: str = field(repr=False)
|
||||
source: Literal["owner-captured-lixelgo-application"] = "owner-captured-lixelgo-application"
|
||||
compatibility_profile_id: Literal["xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"] = (
|
||||
"xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
|
||||
compatibility_profile_id: Literal[
|
||||
"xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"
|
||||
] = (
|
||||
"xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"
|
||||
)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
|
||||
@@ -292,11 +292,12 @@ class ReviewedApplicationMqttTransport:
|
||||
port: int = 1883,
|
||||
connect_timeout_seconds: float = CONTROL_CONNECT_TIMEOUT_SECONDS,
|
||||
exchange_timeout_seconds: float = CONTROL_EXCHANGE_TIMEOUT_SECONDS,
|
||||
allow_device_ap: bool = False,
|
||||
client_factory: Callable[[], mqtt.Client] | None = None,
|
||||
monotonic: Callable[[], float] = time.monotonic,
|
||||
) -> None:
|
||||
self._target_ipv4 = validate_private_ipv4(host)
|
||||
if self._target_ipv4 == AP_FALLBACK_IPV4:
|
||||
if self._target_ipv4 == AP_FALLBACK_IPV4 and not allow_device_ap:
|
||||
raise ValueError("K1 access-point fallback address is not a direct-LAN control target")
|
||||
if not 1 <= port <= 65535:
|
||||
raise ValueError("port must be between 1 and 65535")
|
||||
|
||||
Reference in New Issue
Block a user