feat(k1): add local connection matrix
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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: "Название общей сети Wi‑Fi",
|
||||
ssidPlaceholder: "Сеть локального контура",
|
||||
buttonLabel: "Подключить K1 к общей сети",
|
||||
safetyNote: "K1 получит реквизиты существующей сети одним рассмотренным BLE-запросом без автоматического повтора.",
|
||||
},
|
||||
"quick-connect": {
|
||||
stepTitle: "Подключитесь к точке доступа K1",
|
||||
ssidLabel: "Название точки доступа K1",
|
||||
ssidPlaceholder: "SSID сканера, например XGR-…",
|
||||
buttonLabel: "Подключить этот Mac к K1",
|
||||
safetyNote: "Введите SSID и пароль точки доступа вашего K1. Mac сменит текущую Wi‑Fi сеть одним 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="Название сети Wi‑Fi" 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="Пароль Wi‑Fi" 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" ? "Подключаем…" : "Подключить устройство к Wi‑Fi"}
|
||||
{pendingAction === "connect" ? "Подключаем…" : modeCopy.buttonLabel}
|
||||
</Button>
|
||||
<p className="safety-note">Наличие адреса подтверждает результат предыдущей настройки, но не текущее соединение. Пароль передаётся только локальному сервису, не сохраняется в браузере и удаляется из формы после успеха.</p>
|
||||
<p className="safety-note">{modeCopy.safetyNote} Пароль передаётся только локальному сервису, не сохраняется в браузере и удаляется из формы после успеха.</p>
|
||||
</WizardStep>
|
||||
</div>
|
||||
</GlassSurface>
|
||||
|
||||
@@ -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 один раз подключается к Wi‑Fi сканера; K1 остаётся точкой доступа.",
|
||||
},
|
||||
{
|
||||
value: "direct-connect",
|
||||
label: "Хотспот контроллера · Direct Connect",
|
||||
description: "K1 подключается к сети управляющего устройства. Будет доступно после отдельной приёмки.",
|
||||
disabled: true,
|
||||
description: "Mission Core передаёт K1 реквизиты хотспота управляющего устройства.",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user