feat(k1): add local connection matrix

This commit is contained in:
DCCONSTRUCTIONS 2026-07-19 09:52:22 +03:00
parent fecb5885d0
commit 57b2208cae
20 changed files with 1405 additions and 115 deletions

View File

@ -18,6 +18,7 @@ let automaticSourceStart;
let presentation;
let operatorIntentGeneration;
let configuration;
let compatibility;
before(async () => {
server = await createServer({
@ -52,6 +53,9 @@ before(async () => {
configuration = await server.ssrLoadModule(
"@xgrids-k1/frontend/configuration.ts",
);
compatibility = await server.ssrLoadModule(
"@xgrids-k1/frontend/compatibility.ts",
);
({ xgridsK1Api, ApiError } = await server.ssrLoadModule(
"@xgrids-k1/frontend/api.ts",
));
@ -204,8 +208,8 @@ test("installed XGRIDS frontend manifest exposes the semantic v1alpha2 actions",
assert.equal(xgridsK1Manifest.apiVersion, "missioncore.nodedc/v1alpha2");
assert.deepEqual(xgridsK1Manifest.spec.compatibilityProfiles, [
{
profileId: "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
path: "profiles/fw-3.0.2/direct-lan.v1.json",
profileId: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
path: "profiles/fw-3.0.2/local-network.v2.json",
modelId: "xgrids.lixelkity-k1",
},
]);
@ -563,16 +567,16 @@ test("replay ignores a stale failed live acquisition", () => {
);
});
test("K1 configuration anchors expose future modes without enabling them", () => {
assert.equal(configuration.SUPPORTED_CONNECTION_MODE, "bridge");
test("K1 configuration exposes all reviewed local connection directions", () => {
assert.equal(configuration.DEFAULT_CONNECTION_MODE, "bridge");
assert.equal(configuration.SUPPORTED_MOUNT_TYPE, "handheld");
assert.equal(configuration.SUPPORTED_GNSS_MODE, "none");
assert.deepEqual(
configuration.connectionModeOptions.map(({ value, disabled = false }) => ({ value, disabled })),
[
{ value: "bridge", disabled: false },
{ value: "quick-connect", disabled: true },
{ value: "direct-connect", disabled: true },
{ value: "quick-connect", disabled: false },
{ value: "direct-connect", disabled: false },
],
);
assert.deepEqual(
@ -594,6 +598,22 @@ test("K1 configuration anchors expose future modes without enabling them", () =>
);
});
test("connection directions select distinct fail-closed topologies", () => {
assert.deepEqual(compatibility.profileSelectionForConnectionMode("bridge"), {
firmware_version: "3.0.2",
topology: "direct-lan",
verification: "live-device-info",
});
assert.equal(
compatibility.profileSelectionForConnectionMode("quick-connect").topology,
"device-ap",
);
assert.equal(
compatibility.profileSelectionForConnectionMode("direct-connect").topology,
"controller-hotspot",
);
});
test("a provisioned address is not presented as a verified device connection", () => {
assert.equal(lifecycle.normalizeRuntimePhase({
phase: "connected",

View File

@ -401,7 +401,7 @@ function declaredState(cameraRows = [
foxglove_ws_url: "ws://192.168.7.10:8765",
foxglove_viewer_url: "http://192.168.7.10:8765/vendor-viewer",
compatibility: {
profile_id: "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
camera_preview: "rtsp://192.168.7.10:8554/vendor-preview",
},
device_ref: {
@ -413,7 +413,7 @@ function declaredState(cameraRows = [
device_session: {
device_session_id: "device-session-001",
device_id: "device-k1-001",
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",
connectivity: "connected",
},
sensor_catalog: {
@ -444,7 +444,7 @@ function pointCloudStreamingState() {
acquisition_id: "acquisition-001",
device_id: "device-k1-001",
device_session_id: "device-session-001",
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",
control_mode: "operator-manual",
requested_streams: ["spatial.point-cloud.live"],
target_host: "127.0.0.1",

View File

@ -65,6 +65,7 @@ export interface XgridsCompatibilityState {
firmware_claim?: string | null;
vendor_writes_enabled?: boolean;
camera_preview?: string | null;
attestation?: CompatibilityAttestation | null;
}
export interface XgridsModelingControlSafety {
@ -289,7 +290,7 @@ export interface XgridsK1State {
devices?: BleDevice[];
selected_device_id?: string | null;
k1_ip?: string | null;
connection_mode?: "bridge" | null;
connection_mode?: "bridge" | "quick-connect" | "direct-connect" | null;
foxglove_ws_url?: string | null;
foxglove_viewer_url?: string | null;
rerun_grpc_url?: string | null;
@ -322,7 +323,7 @@ export interface ScanRequest {
export interface CompatibilityAttestation {
firmware_version: "3.0.2";
topology: "direct-lan";
topology: "direct-lan" | "device-ap" | "controller-hotspot";
verification: "live-device-info";
}
@ -330,7 +331,7 @@ export interface ConnectRequest {
device_id: string;
ssid: string;
password: string;
connection_mode: "bridge";
connection_mode: "bridge" | "quick-connect" | "direct-connect";
compatibility_attestation: CompatibilityAttestation;
operation_id?: string;
idempotency_key?: string;

View File

@ -1,7 +1,24 @@
import type { CompatibilityAttestation } from "./api";
import type { ConnectionMode } from "./configuration";
export const EXACT_PROFILE_SELECTION: CompatibilityAttestation = Object.freeze({
firmware_version: "3.0.2",
topology: "direct-lan",
verification: "live-device-info",
});
const topologyByConnectionMode = {
bridge: "direct-lan",
"quick-connect": "device-ap",
"direct-connect": "controller-hotspot",
} as const satisfies Record<ConnectionMode, CompatibilityAttestation["topology"]>;
export function profileSelectionForConnectionMode(
connectionMode: ConnectionMode,
): CompatibilityAttestation {
return {
firmware_version: "3.0.2",
topology: topologyByConnectionMode[connectionMode],
verification: "live-device-info",
};
}

View File

@ -11,7 +11,7 @@ import {
type StatusTone,
} from "@nodedc/ui-react";
import { EXACT_PROFILE_SELECTION } from "../compatibility";
import { profileSelectionForConnectionMode } from "../compatibility";
import {
SUPPORTED_GNSS_MODE,
SUPPORTED_MOUNT_TYPE,
@ -149,7 +149,9 @@ export function K1AcquisitionPipeline({
project_name: projectNameValidation.value,
mount_type: SUPPORTED_MOUNT_TYPE,
gnss_mode: SUPPORTED_GNSS_MODE,
compatibility_attestation: EXACT_PROFILE_SELECTION,
compatibility_attestation: profileSelectionForConnectionMode(
state.connection_mode ?? "bridge",
),
},
physicalAcceptance: PHYSICAL_ACCEPTANCE,
}),

View File

@ -11,9 +11,9 @@ import {
} from "@nodedc/ui-react";
import type { BleDevice } from "../api";
import { EXACT_PROFILE_SELECTION } from "../compatibility";
import { profileSelectionForConnectionMode } from "../compatibility";
import {
SUPPORTED_CONNECTION_MODE,
DEFAULT_CONNECTION_MODE,
connectionModeOptions,
type ConnectionMode,
} from "../configuration";
@ -21,6 +21,36 @@ import { provisioningIntentKey } from "../lifecycle";
import { finiteMetric } from "../presentation";
import type { XgridsK1Controller } from "../runtimeContext";
const connectionCopy: Record<ConnectionMode, {
stepTitle: string;
ssidLabel: string;
ssidPlaceholder: string;
buttonLabel: string;
safetyNote: string;
}> = {
bridge: {
stepTitle: "Передайте настройки общей сети",
ssidLabel: "Название общей сети WiFi",
ssidPlaceholder: "Сеть локального контура",
buttonLabel: "Подключить K1 к общей сети",
safetyNote: "K1 получит реквизиты существующей сети одним рассмотренным BLE-запросом без автоматического повтора.",
},
"quick-connect": {
stepTitle: "Подключитесь к точке доступа K1",
ssidLabel: "Название точки доступа K1",
ssidPlaceholder: "SSID сканера, например XGR-…",
buttonLabel: "Подключить этот Mac к K1",
safetyNote: "Введите SSID и пароль точки доступа вашего K1. Mac сменит текущую WiFi сеть одним CoreWLAN-запросом; недокументированный BLE-секрет не читается и K1 не получает BLE-запись.",
},
"direct-connect": {
stepTitle: "Подключите K1 к хотспоту контроллера",
ssidLabel: "Название хотспота контроллера",
ssidPlaceholder: "SSID управляющего устройства",
buttonLabel: "Подключить K1 к хотспоту",
safetyNote: "Хотспот должен быть уже включён, а этот Mac — иметь к нему маршрут. K1 получит его реквизиты одним рассмотренным BLE-запросом.",
},
};
function WizardStep({
number,
title,
@ -96,13 +126,17 @@ export function K1ProvisioningPipeline({
const [ssid, setSsid] = useState("");
const [password, setPassword] = useState("");
const [connectionMode, setConnectionMode] = useState<ConnectionMode>(
SUPPORTED_CONNECTION_MODE,
DEFAULT_CONNECTION_MODE,
);
const provisioningIntentRef = useRef<string | null>(null);
const devices = state?.devices ?? [];
const isBusy = pendingAction !== null;
const credentialsReady = ssid.trim().length > 0 && password.length > 0;
const canConnect = powerConfirmed && selectedDeviceId.length > 0 && credentialsReady && !isBusy;
const modeCopy = connectionCopy[connectionMode];
const selectedModeConnected = Boolean(
state?.k1_ip && state.connection_mode === connectionMode,
);
useEffect(() => {
if (state?.selected_device_id) {
@ -117,6 +151,12 @@ export function K1ProvisioningPipeline({
}
}, [selectedDeviceId, state?.devices, state?.selected_device_id]);
useEffect(() => {
if (state?.connection_mode) {
setConnectionMode(state.connection_mode);
}
}, [state?.connection_mode]);
const deviceSummary = useMemo(
() => devices.find((device) => device.device_id === selectedDeviceId),
[devices, selectedDeviceId],
@ -134,8 +174,8 @@ export function K1ProvisioningPipeline({
device_id: selectedDeviceId,
ssid: ssid.trim(),
password,
connection_mode: SUPPORTED_CONNECTION_MODE,
compatibility_attestation: EXACT_PROFILE_SELECTION,
connection_mode: connectionMode,
compatibility_attestation: profileSelectionForConnectionMode(connectionMode),
idempotency_key: idempotencyKey,
});
if (succeeded) {
@ -152,7 +192,7 @@ export function K1ProvisioningPipeline({
</header>
<div className="configuration-anchor">
<span className="nodedc-field__description">
Неподтверждённые сетевые топологии уже отражены в интерфейсе, но не могут быть выбраны до отдельной приёмки.
Выберите направление связи. Каждый путь выполняет не более одного сетевого изменения и не повторяет его автоматически.
</span>
<Select
label="Способ подключения"
@ -160,6 +200,8 @@ export function K1ProvisioningPipeline({
options={connectionModeOptions}
onChange={(value) => {
setConnectionMode(value);
setSsid("");
setPassword("");
resetProvisioningIntent();
}}
disabled={isBusy}
@ -206,19 +248,19 @@ export function K1ProvisioningPipeline({
</WizardStep>
<WizardStep
number="03"
title="Передайте настройки общей сети"
status={state?.k1_ip ? "Адрес ранее получен" : "Настройки не переданы"}
tone={state?.k1_ip ? "warning" : "neutral"}
title={modeCopy.stepTitle}
status={selectedModeConnected ? "Адрес получен" : "Ожидает подключения"}
tone={selectedModeConnected ? "success" : "neutral"}
>
<div className="field-stack">
<TextField label="Название сети WiFi" hint="SSID" value={ssid} onChange={(event) => { setSsid(event.target.value); provisioningIntentRef.current = null; }} autoComplete="off" spellCheck={false} placeholder="Сеть локального контура" />
<TextField label={modeCopy.ssidLabel} hint="SSID" value={ssid} onChange={(event) => { setSsid(event.target.value); provisioningIntentRef.current = null; }} autoComplete="off" spellCheck={false} placeholder={modeCopy.ssidPlaceholder} />
<TextField label="Пароль WiFi" hint="Только в оперативной памяти" type="password" value={password} onChange={(event) => { setPassword(event.target.value); provisioningIntentRef.current = null; }} autoComplete="off" placeholder="Введите пароль" />
</div>
<div className="connection-summary"><span>Устройство</span><strong>{deviceSummary?.name || selectedDeviceId || "Сначала выберите устройство"}</strong></div>
<Button width="full" variant="primary" icon={<Icon name="network" />} disabled={!canConnect} onClick={() => void submitConnect()}>
{pendingAction === "connect" ? "Подключаем…" : "Подключить устройство к WiFi"}
{pendingAction === "connect" ? "Подключаем…" : modeCopy.buttonLabel}
</Button>
<p className="safety-note">Наличие адреса подтверждает результат предыдущей настройки, но не текущее соединение. Пароль передаётся только локальному сервису, не сохраняется в браузере и удаляется из формы после успеха.</p>
<p className="safety-note">{modeCopy.safetyNote} Пароль передаётся только локальному сервису, не сохраняется в браузере и удаляется из формы после успеха.</p>
</WizardStep>
</div>
</GlassSurface>

View File

@ -4,7 +4,7 @@ export type ConnectionMode = "bridge" | "quick-connect" | "direct-connect";
export type MountType = "handheld" | "vehicle-mounted" | "uav" | "backpack";
export type GnssMode = "none" | "rtk" | "ppk";
export const SUPPORTED_CONNECTION_MODE = "bridge" as const satisfies ConnectionMode;
export const DEFAULT_CONNECTION_MODE = "bridge" as const satisfies ConnectionMode;
export const SUPPORTED_MOUNT_TYPE = "handheld" as const satisfies MountType;
export const SUPPORTED_GNSS_MODE = "none" as const satisfies GnssMode;
@ -12,19 +12,17 @@ export const connectionModeOptions: Array<SelectOption<ConnectionMode>> = [
{
value: "bridge",
label: "Общая сеть · Bridge",
description: "K1 и Mission Core работают в одной локальной сети. Подтверждённый путь.",
description: "Mission Core передаёт K1 реквизиты существующей общей сети.",
},
{
value: "quick-connect",
label: "Точка доступа K1 · Quick Connect",
description: "Mission Core подключается к сети сканера. Будет доступно после отдельной приёмки.",
disabled: true,
description: "Этот Mac один раз подключается к WiFi сканера; K1 остаётся точкой доступа.",
},
{
value: "direct-connect",
label: "Хотспот контроллера · Direct Connect",
description: "K1 подключается к сети управляющего устройства. Будет доступно после отдельной приёмки.",
disabled: true,
description: "Mission Core передаёт K1 реквизиты хотспота управляющего устройства.",
},
];

View File

@ -0,0 +1,79 @@
import CoreWLAN
import Foundation
private struct AssociationRequest: Decodable {
let ssid: String
let password: String
}
private struct AssociationResponse: Encodable {
let ok: Bool
let alreadyAssociated: Bool?
let reasonCode: String?
enum CodingKeys: String, CodingKey {
case ok
case alreadyAssociated = "already_associated"
case reasonCode = "reason_code"
}
}
private func emit(_ response: AssociationResponse, exitCode: Int32) -> Never {
let encoder = JSONEncoder()
if let data = try? encoder.encode(response) {
FileHandle.standardOutput.write(data)
}
exit(exitCode)
}
private let input = FileHandle.standardInput.readDataToEndOfFile()
guard input.count <= 1024 else {
emit(
AssociationResponse(ok: false, alreadyAssociated: nil, reasonCode: "request-too-large"),
exitCode: 1
)
}
do {
let request = try JSONDecoder().decode(AssociationRequest.self, from: input)
guard let ssidData = request.ssid.data(using: .utf8),
(1 ... 32).contains(ssidData.count),
let passwordData = request.password.data(using: .utf8),
(1 ... 64).contains(passwordData.count)
else {
emit(
AssociationResponse(ok: false, alreadyAssociated: nil, reasonCode: "credential-bounds"),
exitCode: 1
)
}
guard let interface = CWWiFiClient.shared().interface() else {
emit(
AssociationResponse(ok: false, alreadyAssociated: nil, reasonCode: "wifi-interface-unavailable"),
exitCode: 1
)
}
if interface.ssid() == request.ssid {
emit(AssociationResponse(ok: true, alreadyAssociated: true, reasonCode: nil), exitCode: 0)
}
let networks = try interface.scanForNetworks(withSSID: ssidData)
guard let network = networks.first(where: { $0.ssid == request.ssid }) else {
emit(
AssociationResponse(ok: false, alreadyAssociated: nil, reasonCode: "network-not-found"),
exitCode: 1
)
}
try interface.associate(to: network, password: request.password)
// CoreWLAN's synchronous association call throws on failure. Reading the
// current SSID again would require Location authorization on recent macOS
// versions and could turn a successful association into a false negative.
emit(AssociationResponse(ok: true, alreadyAssociated: false, reasonCode: nil), exitCode: 0)
} catch {
emit(
AssociationResponse(ok: false, alreadyAssociated: nil, reasonCode: "corewlan-error"),
exitCode: 1
)
}

View File

@ -3,7 +3,7 @@
"kind": "DevicePlugin",
"metadata": {
"id": "nodedc.device.xgrids-lixelkity-k1",
"version": "0.5.0",
"version": "0.6.0",
"displayName": "XGRIDS K1 Integration"
},
"spec": {
@ -14,14 +14,15 @@
},
"compatibilityProfiles": [
{
"profileId": "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
"path": "profiles/fw-3.0.2/direct-lan.v1.json",
"profileId": "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
"path": "profiles/fw-3.0.2/local-network.v2.json",
"modelId": "xgrids.lixelkity-k1"
}
],
"permissions": [
"device.discovery.ble",
"device.provisioning.wifi-over-ble",
"host.network.wifi-associate-local",
"network.mqtt.subscribe-private-lan",
"network.mqtt.publish-private-lan",
"network.rtsp.read-private-lan",
@ -61,11 +62,12 @@
"vendor": "XGRIDS",
"displayName": "XGRIDS LixelKity K1",
"category": "Мобильный лидарный сканер",
"description": "Проверенный локальный профиль: BLE-настройка Wi-Fi, MQTT-приём, облако точек, поза и raw-first запись.",
"description": "Локальный профиль Bridge, Quick Connect и Direct Connect: BLE/CoreWLAN, MQTT, камеры и raw-first запись.",
"verified": true,
"capabilities": [
{ "id": "device.discovery.ble", "label": "Поиск BLE" },
{ "id": "device.provisioning.wifi-over-ble", "label": "Wi-Fi через BLE" },
{ "id": "host.network.wifi-associate-local", "label": "Подключение к точке доступа K1" },
{ "id": "spatial.point-cloud.live", "label": "Облако точек" },
{ "id": "spatial.pose.live", "label": "Траектория" },
{ "id": "device.modeling.live", "label": "Метрики маршрута" },

View File

@ -12,8 +12,10 @@ from pathlib import Path
from typing import Any
PROFILE_SCHEMA_VERSION = 1
DEFAULT_PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
DEFAULT_PROFILE_PATH = Path(__file__).parent / "profiles" / "fw-3.0.2" / "direct-lan.v1.json"
DEFAULT_PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"
DEFAULT_PROFILE_PATH = (
Path(__file__).parent / "profiles" / "fw-3.0.2" / "local-network.v2.json"
)
EVIDENCE_FLAGS = (
"observed",
"decoded",
@ -129,6 +131,49 @@ def _validate_sources(profile: dict[str, Any]) -> set[str]:
return set(sources)
def _validate_connection_modes(scope: dict[str, Any]) -> None:
modes = _unique_index(
_array(scope.get("connection_modes"), "$.scope.connection_modes"),
"$.scope.connection_modes",
)
expected = {
"bridge": {
"topology": "direct-lan",
"direction": "k1-joins-existing-network",
"device_network_action": "single-reviewed-ble-provisioning-write",
"host_network_action": "none",
"target_address_policy": "ble-status-non-ap-private-ipv4",
"acceptance": "mission-core-physical-accepted",
},
"direct-connect": {
"topology": "controller-hotspot",
"direction": "k1-joins-controller-network",
"device_network_action": "single-reviewed-ble-provisioning-write",
"host_network_action": "operator-prepared-hotspot",
"target_address_policy": "ble-status-non-ap-private-ipv4",
"acceptance": "implementation-ready-physical-acceptance-pending",
},
"quick-connect": {
"topology": "device-ap",
"direction": "controller-joins-k1-network",
"device_network_action": "none",
"host_network_action": "single-corewlan-association",
"target_address_policy": "reviewed-fixed-k1-ap-private-ipv4",
"acceptance": (
"lixelgo-data-plane-observed-mission-core-physical-acceptance-pending"
),
},
}
if set(modes) != set(expected):
raise CompatibilityProfileError("connection mode set differs from reviewed evidence")
for mode_id, expected_fields in expected.items():
actual = {key: value for key, value in modes[mode_id].items() if key != "id"}
if actual != expected_fields:
raise CompatibilityProfileError(
f"connection mode {mode_id!r} differs from reviewed evidence"
)
def _validate_source_references(value: Any, source_ids: set[str], path: str = "$") -> None:
if isinstance(value, dict):
if "source_ids" in value:
@ -155,7 +200,7 @@ def _validate_transports(profile: dict[str, Any]) -> None:
)
if set(transports) != {
"ble.wifi-bootstrap.fw3.v1",
"mqtt.direct-lan.fw3.v1",
"mqtt.local-ipv4.fw3.v1",
"rtsp.camera-preview.fw3.v1",
}:
raise CompatibilityProfileError("$.transports must contain only the reviewed v1 transports")
@ -180,14 +225,14 @@ def _validate_transports(profile: dict[str, Any]) -> None:
physical_verified=True,
)
mqtt = transports["mqtt.direct-lan.fw3.v1"]
mqtt = transports["mqtt.local-ipv4.fw3.v1"]
if mqtt.get("protocol") != "MQTT 3.1.1":
raise CompatibilityProfileError("direct-LAN application protocol must remain MQTT 3.1.1")
raise CompatibilityProfileError("local application protocol must remain MQTT 3.1.1")
network = _object(mqtt.get("network"), "$.transports[mqtt].network")
if network.get("transport") != "TCP" or network.get("port") != 1883:
raise CompatibilityProfileError("direct-LAN MQTT endpoint must remain TCP 1883")
raise CompatibilityProfileError("local MQTT endpoint must remain TCP 1883")
if network.get("tls") is not False or network.get("authentication") != "none-observed":
raise CompatibilityProfileError("direct-LAN MQTT security claim differs from observation")
raise CompatibilityProfileError("local MQTT security claim differs from observation")
allowlist = _array(
mqtt.get("subscription_allowlist"),
"$.transports[mqtt].subscription_allowlist",
@ -202,7 +247,7 @@ def _validate_transports(profile: dict[str, Any]) -> None:
)
_expect_evidence(
mqtt,
"$.transports[mqtt.direct-lan.fw3.v1]",
"$.transports[mqtt.local-ipv4.fw3.v1]",
observed=True,
decoded=False,
replay_verified=False,
@ -504,7 +549,7 @@ def _validate_acquisition_control(profile: dict[str, Any]) -> None:
def validate_compatibility_profile(profile: Any) -> dict[str, Any]:
"""Validate and return one read-only firmware-3/direct-LAN profile object."""
"""Validate the read-only firmware-3 local connection matrix."""
root = _object(profile, "$")
if root.get("schema_version") != PROFILE_SCHEMA_VERSION:
raise CompatibilityProfileError("unsupported compatibility profile schema_version")
@ -519,8 +564,9 @@ def validate_compatibility_profile(profile: Any) -> dict[str, Any]:
firmware = _object(scope.get("firmware"), "$.scope.firmware")
if firmware != {"match": "exact", "version": "3.0.2"}:
raise CompatibilityProfileError("profile must match firmware 3.0.2 exactly")
if scope.get("topology") != "direct-lan":
raise CompatibilityProfileError("profile topology must remain direct-lan")
if scope.get("topology") != "local-network-matrix":
raise CompatibilityProfileError("profile topology must remain local-network-matrix")
_validate_connection_modes(scope)
vocabulary = _object(root.get("evidence_vocabulary"), "$.evidence_vocabulary")
if set(vocabulary) != set(EVIDENCE_FLAGS):
@ -580,7 +626,10 @@ def matches_target(
validated = validate_compatibility_profile(profile)
scope = validated["scope"]
firmware_scope = _object(scope.get("firmware"), "$.scope.firmware")
return firmware_scope.get("version") == firmware and scope.get("topology") == topology
modes = _array(scope.get("connection_modes"), "$.scope.connection_modes")
return firmware_scope.get("version") == firmware and any(
isinstance(mode, dict) and mode.get("topology") == topology for mode in modes
)
def _main() -> int:

View File

@ -0,0 +1,496 @@
{
"schema_version": 1,
"profile_id": "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
"profile_status": "experimental-evidence-backed",
"scope": {
"vendor": "XGRIDS",
"model": "LixelKity K1",
"platform_type": "A4",
"firmware": {
"match": "exact",
"version": "3.0.2"
},
"topology": "local-network-matrix",
"claim_limit": "One owner-controlled K1 on exact firmware 3.0.2 across controlled Bridge/Mission Core and owner-operated Quick Connect LixelGO/iPhone runs. Direct Connect reuses the physically accepted 99-byte BLE Wi-Fi frame but still requires its own Mission Core physical acceptance. This is not a vendor API or a cross-firmware compatibility claim.",
"connection_modes": [
{
"id": "bridge",
"topology": "direct-lan",
"direction": "k1-joins-existing-network",
"device_network_action": "single-reviewed-ble-provisioning-write",
"host_network_action": "none",
"target_address_policy": "ble-status-non-ap-private-ipv4",
"acceptance": "mission-core-physical-accepted"
},
{
"id": "direct-connect",
"topology": "controller-hotspot",
"direction": "k1-joins-controller-network",
"device_network_action": "single-reviewed-ble-provisioning-write",
"host_network_action": "operator-prepared-hotspot",
"target_address_policy": "ble-status-non-ap-private-ipv4",
"acceptance": "implementation-ready-physical-acceptance-pending"
},
{
"id": "quick-connect",
"topology": "device-ap",
"direction": "controller-joins-k1-network",
"device_network_action": "none",
"host_network_action": "single-corewlan-association",
"target_address_policy": "reviewed-fixed-k1-ap-private-ipv4",
"acceptance": "lixelgo-data-plane-observed-mission-core-physical-acceptance-pending"
}
]
},
"evidence_vocabulary": {
"observed": "The channel, transport, or physical behavior was directly seen on the owner-controlled K1.",
"decoded": "A bounded decoder produces the stated semantic payload from observed bytes; transport framing alone is not a semantic decode.",
"replay_verified": "A captured payload of this semantic channel has passed the repository replay-to-view path.",
"physical_verified": "The result was correlated with a controlled physical K1 state or operator action.",
"write_enabled": "This profile authorizes software to emit the state-changing vendor request. False never grants runtime write authority."
},
"safety": {
"default_mode": "read-only",
"vendor_writes_enabled": false,
"unknown_firmware_policy": "reject-profile",
"request_topic_subscription_enabled": false,
"notes": [
"Loading this descriptive profile does not authorize a BLE or MQTT write.",
"The reviewed Wi-Fi provisioning procedure remains a separate explicit operator action and is not activated by loading this profile.",
"Observed LixelGO modeling requests remain write-disabled in this descriptive profile; the separately installed acceptance transport requires a live DeviceInfo match and one operator-present action permit.",
"Unknown firmware, transport, topics, fields, and action responses fail closed."
]
},
"evidence_sources": [
{
"id": "wifi-provisioning-profile",
"kind": "reviewed-profile",
"path": "docs/04_K1_WIFI_PROVISIONING_PROFILE.md",
"scope": "Observed firmware, GATT UUIDs, 99-byte provisioning frame, status read and physical LAN association."
},
{
"id": "mqtt-stream-profile",
"kind": "reviewed-profile",
"path": "docs/05_K1_MQTT_STREAM_PROFILE.md",
"scope": "Local MQTT transport, report topics, bounded point/pose codecs and static modeling-request mapping."
},
{
"id": "lab-001",
"kind": "redacted-physical-lab-report",
"path": "docs/lab/001_K1_LIVE_MQTT_20260715.redacted.md",
"scope": "Physical BLE-to-Wi-Fi result, MQTT message counts, point/pose decode totals and negative camera observation."
},
{
"id": "live-viewer-profile",
"kind": "implemented-path-description",
"path": "docs/06_K1_LIVE_VIEWER.md",
"scope": "Raw-first live/replay path for point cloud and pose."
},
{
"id": "lab-002",
"kind": "redacted-physical-protocol-report",
"path": "docs/lab/002_LIXELGO_IPHONE_LOCAL_PROTOCOL_20260716.redacted.md",
"scope": "Owner-operated LixelGO start/stop mapping, local-only bounded traffic result and left/right RTSP/H.264 camera-preview discovery."
}
],
"transports": [
{
"id": "ble.wifi-bootstrap.fw3.v1",
"role": "bootstrap-and-status",
"protocol": "BLE GATT",
"service_uuid": "00007f00-0000-1000-8000-00805f9b34fb",
"characteristics": {
"wifi_request": "00007f01-0000-1000-8000-00805f9b34fb",
"wifi_status": "00007f02-0000-1000-8000-00805f9b34fb"
},
"reviewed_profile_id": "xgrids-k1-fw3-wifi-v1",
"request_frame_bytes": 99,
"status_semantics": {
"ap_baseline_ipv4": "192.168.56.1",
"lan_acceptance": "A non-AP private IPv4 must be observed and independently confirmed on the intended LAN."
},
"evidence": {
"observed": true,
"decoded": true,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"wifi-provisioning-profile",
"lab-001"
]
},
{
"id": "mqtt.local-ipv4.fw3.v1",
"role": "report-data-plane",
"protocol": "MQTT 3.1.1",
"network": {
"transport": "TCP",
"port": 1883,
"tls": false,
"authentication": "none-observed",
"addressing": "confirmed-device-private-ipv4-only"
},
"subscription_allowlist": [
"lixel/application/report/#",
"RealtimePointcloud",
"RealtimePath",
"DeviceStatus"
],
"evidence": {
"observed": true,
"decoded": false,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"mqtt-stream-profile",
"lab-001",
"live-viewer-profile",
"lab-002"
]
},
{
"id": "rtsp.camera-preview.fw3.v1",
"role": "camera-preview-data-plane",
"protocol": "RTSP 1.0 with interleaved RTP over TCP",
"network": {
"transport": "TCP",
"port": 8554,
"tls": false,
"authentication": "none-observed",
"addressing": "confirmed-device-private-ipv4-only"
},
"media": {
"codec": "H.264",
"rtp_payload_type": 96,
"clock_hz": 90000,
"framing": "RTP/AVP/TCP interleaved channels 0-1"
},
"evidence": {
"observed": true,
"decoded": false,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"lab-002"
]
}
],
"channels": [
{
"id": "spatial.point-cloud.live",
"kind": "point-cloud",
"direction": "device-report",
"topic": "lixel/application/report/lio_pcl",
"wire_format": "protobuf MqttCompressMsg containing raw-LZ4 LioPclReport",
"semantic_payload": "metric XYZ, complete uint32 rgbi and verified low-byte intensity",
"bounds": {
"max_mqtt_payload_bytes": 2097152,
"max_compressed_bytes": 1048576,
"max_decoded_bytes": 8388608,
"max_expansion_ratio": 64,
"max_points_per_frame": 250000
},
"unverified_fields": [
"upper 24 bits of rgbi as RGB",
"sensor timestamp epoch",
"sensor-to-vehicle extrinsics"
],
"evidence": {
"observed": true,
"decoded": true,
"replay_verified": true,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"mqtt-stream-profile",
"lab-001",
"live-viewer-profile"
]
},
{
"id": "spatial.pose.live",
"kind": "pose",
"direction": "device-report",
"topic": "lixel/application/report/lio_pose",
"wire_format": "protobuf LioPoseReport",
"semantic_payload": "position XYZ, quaternion XYZW, distance and pose accuracy",
"unverified_fields": [
"sensor timestamp epoch",
"coordinate-frame convention",
"sensor-to-vehicle extrinsics"
],
"evidence": {
"observed": true,
"decoded": true,
"replay_verified": true,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"mqtt-stream-profile",
"lab-001",
"live-viewer-profile"
]
},
{
"id": "device.modeling.live",
"kind": "acquisition-telemetry",
"direction": "device-report",
"topic": "lixel/application/report/modeling",
"wire_format": "protobuf ModelingReport acquisition telemetry subset",
"semantic_payload": "nonnegative MoveDistance metres, MoveSpeed metres per second, int64 ScanTime at two ticks per second, and int32 PgoProgress",
"bounds": {
"max_mqtt_payload_bytes": 65536,
"max_fields": 64,
"max_nested_fields": 16
},
"limitations": [
"ScanTime scale is exact-profile evidence and must not be generalized to other firmware.",
"Replay-to-view acceptance remains open even though retained physical runs establish units and live decoding."
],
"evidence": {
"observed": true,
"decoded": true,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"mqtt-stream-profile",
"lab-001",
"live-viewer-profile"
]
},
{
"id": "device.status.live",
"kind": "device-status",
"direction": "device-report",
"topic": "lixel/application/report/device_status",
"wire_format": "protobuf DeviceStatusReport acquisition lifecycle subset",
"semantic_payload": "bounded modeling-state base-offset mapping, init-ready flag, project presence and redacted identity fields",
"limitations": [
"Nested system and RTK status payloads are presence-checked but not semantically decoded.",
"ScanOver and Ready observations do not prove durable artifact save completion."
],
"evidence": {
"observed": true,
"decoded": true,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"mqtt-stream-profile",
"lab-001"
]
},
{
"id": "device.heartbeat.live",
"kind": "heartbeat",
"direction": "device-report",
"topic": "lixel/application/report/heartbeat",
"wire_format": "opaque bytes",
"semantic_payload": null,
"limitations": [
"Only channel presence and approximate report cadence are verified.",
"Payload semantics are not decoded."
],
"evidence": {
"observed": true,
"decoded": false,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"mqtt-stream-profile",
"lab-001"
]
},
{
"id": "camera.preview.live",
"kind": "camera-preview",
"direction": "device-report",
"discovery_status": "observed",
"topic": null,
"endpoint_templates": [
"rtsp://{confirmed-device-private-ipv4}:8554/live/chn_left_main",
"rtsp://{confirmed-device-private-ipv4}:8554/live/chn_right_main"
],
"wire_format": "RTSP 1.0, interleaved RTP/TCP, H.264 PT96 at 90000 Hz",
"semantic_payload": "compressed live left/right camera preview selected by endpoint",
"limitations": [
"No full-resolution raw frame, camera calibration or panorama-stitching contract is verified.",
"Left/right optical identity is supported by endpoint labels and operator-selected application views, not an independent image-content fixture.",
"Mission Core implements bounded copy-remux, archival and replay admission, but no newly archived physical K1 camera session has passed shared-timeline playback acceptance."
],
"evidence": {
"observed": true,
"decoded": false,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"lab-002"
]
}
],
"acquisition_control": {
"mode": "operator-manual",
"write_enabled": false,
"software_acceptance_transport": {
"status": "installed-operator-present",
"default_authority": "disabled",
"profile_gate": "live-device-info-exact-match",
"dialogue": "single-socket-canonical-start-to-stop",
"automatic_retry": false,
"supported_mount_type": "handheld",
"supported_gnss_mode": "none"
},
"verified_device_control": {
"gesture": "physical-double-click",
"state_dependent_result": "start from steady-green standby; stop during active scanning",
"single_click_result": "not a verified scan-start action",
"evidence": {
"observed": true,
"decoded": false,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"lab-001"
]
},
"semantic_actions": [
{
"id": "acquisition.start",
"execution": "operator-manual",
"operator_control": "physical-double-click from steady-green standby",
"observed_application_control": "owner-operated LixelGO project confirmation",
"vendor_request_mapping": {
"evidence_kind": "owner-controlled-wire-observation",
"transport": "MQTT 3.1.1",
"topic": "lixel/application/request/modeling",
"qos": 2,
"retain": false,
"message_type": "ModelingRequest",
"action_field_value": 1,
"header_contract": {
"device_id": "explicit-observed-identity",
"session_id": "{device_id}:ModelingRequest",
"openapi_key": "private-application-level-runtime-authority"
},
"request_fields": {
"project_name": "required-operator-value",
"record_mode": 2,
"scan_mode": 1,
"mount_type": 0,
"pre_project_id": "omitted-in-retained-request"
},
"success_result_code": 302252033,
"required_unresolved_context": [
"operator-owned Keychain authority provisioning and operator-present physical acceptance",
"authorization policy for any setting outside the retained request",
"timeout, rejection and rollback contract"
],
"evidence": {
"observed": true,
"decoded": true,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"write_enabled": false
},
"evidence": {
"observed": true,
"decoded": true,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"mqtt-stream-profile",
"lab-001",
"lab-002"
]
},
{
"id": "acquisition.stop",
"execution": "operator-manual",
"operator_control": "physical-double-click during active scanning",
"observed_application_control": "owner-operated LixelGO stop confirmation",
"vendor_request_mapping": {
"evidence_kind": "owner-controlled-wire-observation",
"transport": "MQTT 3.1.1",
"topic": "lixel/application/request/modeling",
"qos": 2,
"retain": false,
"message_type": "ModelingRequest",
"action_field_value": 2,
"header_contract": {
"device_id": "explicit-observed-identity",
"session_id": "{device_id}:ModelingRequest",
"openapi_key": "private-application-level-runtime-authority"
},
"request_fields": {},
"success_result_code": 302252033,
"required_unresolved_context": [
"operator-owned Keychain authority provisioning and operator-present physical acceptance",
"save-completion and final-standby state mapping",
"timeout and rollback contract"
],
"evidence": {
"observed": true,
"decoded": true,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"write_enabled": false
},
"evidence": {
"observed": true,
"decoded": true,
"replay_verified": false,
"physical_verified": true,
"write_enabled": false
},
"source_ids": [
"mqtt-stream-profile",
"lab-001",
"lab-002"
]
},
{
"id": "calibration.device.start",
"execution": "unavailable",
"operator_control": null,
"vendor_request_mapping": null,
"observed_behavior": "Static initialization follows acquisition.start; no independent calibration action was observed.",
"limitations": [
"No standalone calibration command topic, request schema, acknowledgment or state transition is verified."
],
"evidence": {
"observed": false,
"decoded": false,
"replay_verified": false,
"physical_verified": false,
"write_enabled": false
},
"source_ids": [
"lab-002"
]
}
]
}
}

View File

@ -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,

View File

@ -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,
}

View File

@ -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:

View File

@ -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")

View File

@ -88,12 +88,12 @@ def test_repository_catalog_exposes_xgrids_model() -> None:
item for item in plugins if item["metadata"]["id"] == "nodedc.device.xgrids-lixelkity-k1"
)
assert plugin["apiVersion"] == "missioncore.nodedc/v1alpha2"
assert plugin["metadata"]["version"] == "0.5.0"
assert plugin["metadata"]["version"] == "0.6.0"
assert plugin["spec"]["hostApiRange"] == "v1alpha2"
assert plugin["spec"]["compatibilityProfiles"] == [
{
"profileId": "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
"path": "profiles/fw-3.0.2/direct-lan.v1.json",
"profileId": "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
"path": "profiles/fw-3.0.2/local-network.v2.json",
"modelId": "xgrids.lixelkity-k1",
}
]
@ -125,14 +125,14 @@ def test_repository_catalog_exposes_xgrids_model() -> None:
} <= action_ids
assert next(item for item in models if item["id"] == "xgrids.lixelkity-k1") == {
"pluginId": "nodedc.device.xgrids-lixelkity-k1",
"pluginVersion": "0.5.0",
"pluginVersion": "0.6.0",
"id": "xgrids.lixelkity-k1",
"vendor": "XGRIDS",
"displayName": "XGRIDS LixelKity K1",
"category": "Мобильный лидарный сканер",
"description": (
"Проверенный локальный профиль: BLE-настройка Wi-Fi, MQTT-приём, "
"облако точек, поза и raw-first запись."
"Локальный профиль Bridge, Quick Connect и Direct Connect: "
"BLE/CoreWLAN, MQTT, камеры и raw-first запись."
),
"verified": True,
"capabilities": [
@ -141,6 +141,10 @@ def test_repository_catalog_exposes_xgrids_model() -> None:
"id": "device.provisioning.wifi-over-ble",
"label": "Wi-Fi через BLE",
},
{
"id": "host.network.wifi-associate-local",
"label": "Подключение к точке доступа K1",
},
{"id": "spatial.point-cloud.live", "label": "Облако точек"},
{"id": "spatial.pose.live", "label": "Траектория"},
{"id": "device.modeling.live", "label": "Метрики маршрута"},

View File

@ -33,6 +33,16 @@ ATTESTATION = CompatibilityAttestationRequest(
topology="direct-lan",
verification="live-device-info",
)
QUICK_CONNECT_ATTESTATION = CompatibilityAttestationRequest(
firmware_version="3.0.2",
topology="device-ap",
verification="live-device-info",
)
DIRECT_CONNECT_ATTESTATION = CompatibilityAttestationRequest(
firmware_version="3.0.2",
topology="controller-hotspot",
verification="live-device-info",
)
PRIMARY_TEST_CREDENTIAL = "x" * 24
SECONDARY_TEST_CREDENTIAL = "y" * 24
PROJECT_NAME = "K1 lifecycle test"
@ -225,15 +235,48 @@ def test_project_name_is_normalized_and_control_characters_are_rejected() -> Non
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
def test_only_physically_accepted_configuration_values_are_admitted() -> None:
def test_connection_modes_require_their_exact_topology_attestation() -> None:
assert ConnectRequest(
device_id="synthetic-device",
ssid="synthetic-network",
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
connection_mode="quick-connect",
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
).connection_mode == "quick-connect"
assert ConnectRequest(
device_id="synthetic-device",
ssid="synthetic-network",
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
connection_mode="direct-connect",
compatibility_attestation=DIRECT_CONNECT_ATTESTATION,
).connection_mode == "direct-connect"
with pytest.raises(ValidationError):
ConnectRequest(
device_id="synthetic-device",
ssid="synthetic-network",
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
connection_mode="quick-connect", # type: ignore[arg-type]
connection_mode="quick-connect",
compatibility_attestation=ATTESTATION,
)
with pytest.raises(ValidationError, match="32 UTF-8 bytes"):
ConnectRequest(
device_id="synthetic-device",
ssid="🛰️" * 9,
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
compatibility_attestation=ATTESTATION,
)
with pytest.raises(ValidationError, match="64 UTF-8 bytes"):
ConnectRequest(
device_id="synthetic-device",
ssid="synthetic-network",
password=SecretStr("🔒" * 17),
compatibility_attestation=ATTESTATION,
)
def test_only_physically_accepted_mount_and_gnss_values_are_admitted() -> None:
with pytest.raises(ValidationError):
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
@ -1372,15 +1415,15 @@ def test_exact_profile_is_inactive_until_selected_for_live_device_info_verificat
)
def test_prepare_rejects_device_ap_fallback_as_direct_lan_target(tmp_path: Path) -> None:
def test_prepare_rejects_device_ap_without_completed_quick_connect(tmp_path: Path) -> None:
service, _ = service_with_fake_runtime(tmp_path)
with pytest.raises(ValueError, match="точки доступа"):
with pytest.raises(ValueError, match="connection flow"):
service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.56.1",
compatibility_attestation=ATTESTATION,
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
)
)
@ -1812,6 +1855,165 @@ def test_network_provisioning_is_single_flight_and_secret_is_unwrapped_only_at_b
assert {item["status"] for item in provision_operations} == {"succeeded", "failed"}
def test_quick_connect_associates_the_host_without_a_ble_write(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
service, _ = service_with_fake_runtime(tmp_path)
service._devices = [{"device_id": "k1-a"}] # noqa: SLF001
association_calls: list[tuple[Path, str, str]] = []
def fake_associate(
helper_path: Path,
ssid: str,
password: str,
**_: object,
) -> dict[str, Any]:
association_calls.append((helper_path, ssid, password))
return {
"schema_version": 1,
"adapter": "CoreWLAN",
"outcome": "associated",
"already_associated": False,
}
async def forbidden_ble_write(*_: object, **__: object) -> dict[str, Any]:
raise AssertionError("Quick Connect must not provision the K1 over BLE")
monkeypatch.setattr(facade_module, "associate_with_wifi_once", fake_associate)
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_ble_write)
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
state = asyncio.run(
service.connect(
ConnectRequest(
device_id="k1-a",
ssid="XGR-TEST",
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
connection_mode="quick-connect",
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
)
)
)
assert len(association_calls) == 1
assert association_calls[0][0].name == "associate_wifi.swift"
assert association_calls[0][1:] == ("XGR-TEST", PRIMARY_TEST_CREDENTIAL)
assert state["connection_mode"] == "quick-connect"
assert state["k1_ip"] == "192.168.56.1"
assert state["compatibility"]["attestation"]["topology"] == "device-ap"
quick_sessions = sorted(service.evidence_root.glob("*viewer_k1_ap_association*"))
assert len(quick_sessions) == 1
assert not (quick_sessions[0] / "provisioning.sensitive.json").exists()
redacted_manifest = (quick_sessions[0] / "manifest.redacted.json").read_text(
encoding="utf-8"
)
assert PRIMARY_TEST_CREDENTIAL not in redacted_manifest
assert "XGR-TEST" not in redacted_manifest
assert service._camera_target_for_session( # noqa: SLF001
state["device_session"]["device_session_id"]
) == "192.168.56.1"
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
)
)
assert prepared["acquisition"]["target_host"] == "192.168.56.1"
def test_direct_connect_reuses_the_reviewed_ble_provisioning_frame(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
service, _ = service_with_fake_runtime(tmp_path)
service._devices = [{"device_id": "k1-a"}] # noqa: SLF001
provisioning_calls: list[tuple[str, str, str]] = []
async def fake_provision(
device_id: str,
ssid: str,
password: str,
**_: object,
) -> dict[str, Any]:
provisioning_calls.append((device_id, ssid, password))
return {
"started_at_utc": "2026-07-19T01:00:00Z",
"completed_at_utc": "2026-07-19T01:00:01Z",
"profile_id": "xgrids-k1-fw3-wifi-v1",
"outcome": "lan_address_observed",
"observations": [{"status": {"ipv4": "172.20.10.2"}}],
}
def forbidden_host_association(*_: object, **__: object) -> dict[str, Any]:
raise AssertionError("Direct Connect must not switch the host Wi-Fi network")
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_provision)
monkeypatch.setattr(
facade_module,
"associate_with_wifi_once",
forbidden_host_association,
)
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
state = asyncio.run(
service.connect(
ConnectRequest(
device_id="k1-a",
ssid="controller-hotspot",
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
connection_mode="direct-connect",
compatibility_attestation=DIRECT_CONNECT_ATTESTATION,
)
)
)
assert provisioning_calls == [
("k1-a", "controller-hotspot", PRIMARY_TEST_CREDENTIAL)
]
assert state["connection_mode"] == "direct-connect"
assert state["k1_ip"] == "172.20.10.2"
assert state["compatibility"]["attestation"]["topology"] == (
"controller-hotspot"
)
def test_failed_connection_change_revokes_the_previous_route(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
service, _ = service_with_fake_runtime(tmp_path)
service._devices = [{"device_id": "k1-a"}] # noqa: SLF001
service._selected_device_id = "previous-k1" # noqa: SLF001
service._k1_ip = "192.168.1.20" # noqa: SLF001
service._connection_mode = "bridge" # noqa: SLF001
def failed_association(*_: object, **__: object) -> dict[str, Any]:
raise RuntimeError("offline association fixture failed")
monkeypatch.setattr(facade_module, "associate_with_wifi_once", failed_association)
with pytest.raises(RuntimeError, match="offline association fixture failed"):
asyncio.run(
service.connect(
ConnectRequest(
device_id="k1-a",
ssid="XGR-OFFLINE",
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
connection_mode="quick-connect",
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
)
)
)
state = service.state()
assert state["selected_device_id"] is None
assert state["k1_ip"] is None
assert state["connection_mode"] is None
assert state["compatibility"]["attestation"] is None
def test_network_provisioning_rejects_an_ipv4_owned_by_the_local_host(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,

View File

@ -275,6 +275,15 @@ def test_acceptance_transport_rejects_the_k1_access_point_fallback() -> None:
ReviewedApplicationMqttTransport("192.168.56.1")
def test_acceptance_transport_admits_the_k1_ap_only_with_an_explicit_gate() -> None:
transport = ReviewedApplicationMqttTransport(
"192.168.56.1",
allow_device_ap=True,
)
assert transport.snapshot().state == "new"
def test_retained_control_subscription_batches_remain_exact_and_separate_from_points() -> None:
assert [len(group) for group in CONTROL_SUBSCRIPTION_GROUPS] == [9, 5, 42]
assert CONTROL_SUBSCRIPTION_GROUPS[0][-1] == (DEVICE_INFO_RESPONSE_TOPIC, 0)

View File

@ -45,15 +45,29 @@ def _boolean_write_flags(value: Any) -> list[bool]:
def test_xgrids_compatibility_profile_loads_exact_firmware_and_sources() -> None:
profile = LOADER.load_compatibility_profile()
assert profile["profile_id"] == "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
assert profile["profile_id"] == "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"
assert profile["scope"]["vendor"] == "XGRIDS"
assert profile["scope"]["model"] == "LixelKity K1"
assert profile["scope"]["platform_type"] == "A4"
assert profile["scope"]["firmware"] == {"match": "exact", "version": "3.0.2"}
assert profile["scope"]["topology"] == "direct-lan"
assert profile["scope"]["topology"] == "local-network-matrix"
modes = _by_id(profile["scope"]["connection_modes"])
assert {
mode_id: mode["topology"] for mode_id, mode in modes.items()
} == {
"bridge": "direct-lan",
"direct-connect": "controller-hotspot",
"quick-connect": "device-ap",
}
assert LOADER.matches_target(profile, firmware="3.0.2", topology="direct-lan")
assert LOADER.matches_target(profile, firmware="3.0.2", topology="device-ap")
assert LOADER.matches_target(
profile,
firmware="3.0.2",
topology="controller-hotspot",
)
assert not LOADER.matches_target(profile, firmware="3.0.3", topology="direct-lan")
assert not LOADER.matches_target(profile, firmware="3.0.2", topology="device-ap")
assert not LOADER.matches_target(profile, firmware="3.0.2", topology="usb")
for source in profile["evidence_sources"]:
assert (REPOSITORY_ROOT / source["path"]).is_file()
@ -119,7 +133,7 @@ def test_xgrids_compatibility_profile_declares_only_reviewed_transports() -> Non
assert ble["evidence"]["physical_verified"] is True
assert ble["evidence"]["write_enabled"] is False
mqtt = transports["mqtt.direct-lan.fw3.v1"]
mqtt = transports["mqtt.local-ipv4.fw3.v1"]
assert mqtt["protocol"] == "MQTT 3.1.1"
assert mqtt["network"]["transport"] == "TCP"
assert mqtt["network"]["port"] == 1883
@ -205,6 +219,18 @@ def test_xgrids_compatibility_profile_rejects_vendor_write_promotion() -> None:
LOADER.validate_compatibility_profile(modified)
def test_xgrids_compatibility_profile_rejects_connection_matrix_drift() -> None:
profile = LOADER.load_compatibility_profile()
modified = copy.deepcopy(profile)
modes = _by_id(modified["scope"]["connection_modes"])
modes["quick-connect"]["device_network_action"] = (
"unreviewed-ble-credential-read"
)
with pytest.raises(LOADER.CompatibilityProfileError, match="differs"):
LOADER.validate_compatibility_profile(modified)
def test_xgrids_compatibility_profile_rejects_noncanonical_modeling_header() -> None:
profile = LOADER.load_compatibility_profile()
modified = copy.deepcopy(profile)

View File

@ -0,0 +1,108 @@
from __future__ import annotations
import json
import subprocess
from pathlib import Path
import pytest
from k1link.device_plugins.xgrids_k1 import macos_wifi
TEST_PASSWORD = "fixture-only-network-secret"
def _helper(tmp_path: Path) -> Path:
helper = tmp_path / "associate_wifi.swift"
helper.write_text("// offline fixture\n", encoding="utf-8")
return helper
def test_association_passes_secret_only_through_stdin(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.setattr(macos_wifi.sys, "platform", "darwin")
calls: list[dict[str, object]] = []
live_inputs: list[bytearray] = []
def fake_runner(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[bytes]:
assert isinstance(kwargs["input"], bytearray)
live_inputs.append(kwargs["input"])
calls.append(
{
"argv": argv,
**kwargs,
"input": bytes(kwargs["input"]),
}
)
return subprocess.CompletedProcess(
argv,
0,
stdout=b'{"ok":true,"already_associated":false}',
stderr=b"",
)
result = macos_wifi.associate_with_wifi_once(
_helper(tmp_path),
"XGR-OFFLINE",
TEST_PASSWORD,
runner=fake_runner,
)
assert result == {
"schema_version": 1,
"adapter": "CoreWLAN",
"outcome": "associated",
"already_associated": False,
}
assert len(calls) == 1
call = calls[0]
assert call["argv"][:2] == ["/usr/bin/xcrun", "swift"]
assert TEST_PASSWORD not in " ".join(call["argv"])
request = json.loads(bytes(call["input"]).decode("utf-8"))
assert request == {"ssid": "XGR-OFFLINE", "password": TEST_PASSWORD}
assert call["check"] is False
assert call["timeout"] == 45.0
assert live_inputs and not any(live_inputs[0])
def test_association_reports_only_sanitized_helper_reason(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.setattr(macos_wifi.sys, "platform", "darwin")
def fake_runner(argv: list[str], **_: object) -> subprocess.CompletedProcess[bytes]:
return subprocess.CompletedProcess(
argv,
1,
stdout=b'{"ok":false,"reason_code":"network-not-found"}',
stderr=f"private diagnostic {TEST_PASSWORD}".encode(),
)
with pytest.raises(
macos_wifi.HostWifiAssociationError,
match="network-not-found",
) as raised:
macos_wifi.associate_with_wifi_once(
_helper(tmp_path),
"XGR-OFFLINE",
TEST_PASSWORD,
runner=fake_runner,
)
assert TEST_PASSWORD not in str(raised.value)
def test_association_is_macos_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(macos_wifi.sys, "platform", "linux")
with pytest.raises(
macos_wifi.HostWifiAssociationError,
match="unsupported-platform",
):
macos_wifi.associate_with_wifi_once(
_helper(tmp_path),
"XGR-OFFLINE",
TEST_PASSWORD,
)