Unify K1 discovery ownership and verification across enrollment paths
This commit is contained in:
@@ -3,7 +3,7 @@ import { Button } from "@nodedc/ui-react";
|
||||
|
||||
import type { XgridsConnectionAttempt } from "../api";
|
||||
import { hostFailureDiagnosticPresentation } from "../hostDiagnosticPresentation";
|
||||
import { STATION_WIFI_FAILURE_MESSAGES } from "../networkFailurePresentation";
|
||||
import { BLE_DISCOVERY_FAILURE_MESSAGES, STATION_WIFI_FAILURE_MESSAGES } from "../networkFailurePresentation";
|
||||
|
||||
const connectionAttemptStageLabels: Record<string, string> = {
|
||||
accepted: "Запрос принят",
|
||||
@@ -97,6 +97,7 @@ function publicConnectionErrorLabel(
|
||||
structured: ReturnType<typeof hostFailureDiagnosticPresentation>,
|
||||
): string {
|
||||
const publicCode = attempt?.public_error_code?.trim();
|
||||
if (attempt?.side_effect_status === "none" && publicCode && BLE_DISCOVERY_FAILURE_MESSAGES[publicCode]) return BLE_DISCOVERY_FAILURE_MESSAGES[publicCode];
|
||||
if (publicCode && publicConnectionErrorLabels[publicCode]) {
|
||||
return publicConnectionErrorLabels[publicCode];
|
||||
}
|
||||
|
||||
@@ -13,13 +13,14 @@ import {
|
||||
Button,
|
||||
GlassSurface,
|
||||
Icon,
|
||||
IconButton,
|
||||
Select,
|
||||
StatusBadge,
|
||||
TextField,
|
||||
type StatusTone,
|
||||
} from "@nodedc/ui-react";
|
||||
|
||||
import {K1WifiPasswordField} from "./K1WifiPasswordField";
|
||||
|
||||
import type {
|
||||
BleDevice,
|
||||
ConnectionVerifyRequest,
|
||||
@@ -1291,12 +1292,6 @@ export function K1ProvisioningPipeline({
|
||||
const [selectedDeviceSnapshot, setSelectedDeviceSnapshot] = useState<BleDevice | null>(null);
|
||||
const [ssid, setSsid] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
useEffect(() => {
|
||||
if (password.length === 0 && passwordVisible) {
|
||||
setPasswordVisible(false);
|
||||
}
|
||||
}, [password, passwordVisible]);
|
||||
const connectionMode = desiredMode;
|
||||
const [successfulLocalConnect, setSuccessfulLocalConnect] =
|
||||
useState<CompletedLocalNetworkIntent | null>(null);
|
||||
@@ -2729,7 +2724,6 @@ export function K1ProvisioningPipeline({
|
||||
setExplicitProvisioningDraft(null);
|
||||
setSsid("");
|
||||
setPassword("");
|
||||
setPasswordVisible(false);
|
||||
setConnectionAttemptPresentation(null);
|
||||
setCandidateUnavailableMessage(null);
|
||||
setReadOnlyReconnectPresentation(null);
|
||||
@@ -3107,31 +3101,13 @@ export function K1ProvisioningPipeline({
|
||||
spellCheck={false}
|
||||
placeholder={modeCopy.ssidPlaceholder}
|
||||
/>
|
||||
<div className="password-field-row">
|
||||
<TextField
|
||||
label="Пароль Wi‑Fi"
|
||||
hint={connectionAttemptOwnsDraft
|
||||
? "Удалён после отправки"
|
||||
: "Только в оперативной памяти"}
|
||||
type={passwordVisible ? "text" : "password"}
|
||||
value={connectionAttemptOwnsDraft ? "" : password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
disabled={provisioningFieldsDisabled}
|
||||
autoComplete="off"
|
||||
placeholder={connectionAttemptOwnsDraft
|
||||
? "Пароль передан"
|
||||
: "Введите пароль"}
|
||||
/>
|
||||
<IconButton
|
||||
label={passwordVisible ? "Скрыть пароль" : "Показать пароль"}
|
||||
shape="rounded"
|
||||
disabled={provisioningFieldsDisabled || password.length === 0}
|
||||
aria-pressed={passwordVisible}
|
||||
onClick={() => setPasswordVisible((visible) => !visible)}
|
||||
>
|
||||
<Icon name={passwordVisible ? "eye-off" : "eye"} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<K1WifiPasswordField
|
||||
value={connectionAttemptOwnsDraft ? "" : password}
|
||||
onChange={setPassword}
|
||||
disabled={provisioningFieldsDisabled}
|
||||
hint={connectionAttemptOwnsDraft ? "Удалён после отправки" : "Только в оперативной памяти"}
|
||||
placeholder={connectionAttemptOwnsDraft ? "Пароль передан" : "Введите пароль"}
|
||||
/>
|
||||
{applyAction}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.password-field-row {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: end;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import {Icon, IconButton, TextField} from '@nodedc/ui-react';
|
||||
import './K1WifiPasswordField.css';
|
||||
|
||||
/** The same Wi-Fi entry composition for direct and onboard K1 enrollment. */
|
||||
export function K1WifiPasswordField({value, onChange, disabled, hint, placeholder}: {
|
||||
value:string; onChange:(value:string)=>void; disabled?:boolean;
|
||||
hint?:string; placeholder?:string;
|
||||
}) {
|
||||
const [visible,setVisible]=useState(false);
|
||||
useEffect(()=>{if(!value)setVisible(false);},[value]);
|
||||
return <div className="password-field-row">
|
||||
<TextField label="Пароль Wi‑Fi" type={visible?'text':'password'} value={value}
|
||||
onChange={event=>onChange(event.target.value)} disabled={disabled}
|
||||
hint={hint} placeholder={placeholder} autoComplete="off" spellCheck={false}/>
|
||||
<IconButton label={visible?'Скрыть пароль':'Показать пароль'} shape="rounded"
|
||||
disabled={disabled||!value} aria-pressed={visible} onClick={()=>setVisible(current=>!current)}>
|
||||
<Icon name={visible?'eye-off':'eye'}/>
|
||||
</IconButton>
|
||||
</div>;
|
||||
}
|
||||
@@ -5,3 +5,15 @@ export const STATION_WIFI_FAILURE_MESSAGES: Readonly<Record<string, string>> = {
|
||||
"k1-wifi-credentials-required":
|
||||
"K1 не смог подключиться к сети Wi‑Fi: устройство запросило учётные данные сети. Проверьте название сети и пароль, затем укажите сеть заново.",
|
||||
};
|
||||
|
||||
/** These failures precede Connect and the Wi-Fi write, on either host. */
|
||||
export const BLE_DISCOVERY_FAILURE_MESSAGES: Readonly<Record<string, string>> = {
|
||||
'ble-discovery-busy':
|
||||
'Bluetooth-поиск уже выполняется. Дождитесь его завершения и повторите подключение. Настройки Wi-Fi не были отправлены.',
|
||||
'ble-adapter-unavailable':
|
||||
'Bluetooth недоступен на компьютере, к которому подключается K1. Включите Bluetooth и повторите подключение. Настройки Wi-Fi не были отправлены.',
|
||||
'ble-discovery-failed':
|
||||
'Служба Bluetooth не смогла выполнить поиск K1. Повторите поиск устройства. Настройки Wi-Fi не были отправлены.',
|
||||
'ble-selected-device-unavailable':
|
||||
'Выбранный K1 не найден по Bluetooth. Убедитесь, что он включён рядом с компьютером, и найдите K1 ещё раз. Настройки Wi-Fi не были отправлены.',
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiError, type ConnectRequest, type XgridsK1State, type XgridsOperation } from "./api";
|
||||
import { operationByIdempotencyKey } from "./lifecycle";
|
||||
import { selectMonotonicXgridsState } from "./stateOrdering";
|
||||
import { STATION_WIFI_FAILURE_MESSAGES } from "./networkFailurePresentation";
|
||||
import { BLE_DISCOVERY_FAILURE_MESSAGES, STATION_WIFI_FAILURE_MESSAGES } from "./networkFailurePresentation";
|
||||
|
||||
const CONTROL_STATE_READ_INTERVAL_MS = 250;
|
||||
const NETWORK_PROVISION_SETTLEMENT_FALLBACK_MS = 30_000;
|
||||
@@ -165,6 +165,7 @@ export function networkProvisionFailureMessage(
|
||||
if (!operation || operation.status !== "failed") return null;
|
||||
const code = operation.error?.code;
|
||||
if (typeof code !== "string") return null;
|
||||
if (operation.error?.side_effect_status === "none" && BLE_DISCOVERY_FAILURE_MESSAGES[code]) return BLE_DISCOVERY_FAILURE_MESSAGES[code];
|
||||
if (STATION_WIFI_FAILURE_MESSAGES[code]) return STATION_WIFI_FAILURE_MESSAGES[code];
|
||||
|
||||
if (code === "BleakGATTProtocolError") {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {useEffect,useRef,useState} from 'react';
|
||||
import {ActivityIndicator,Button,ResourceList,ResourceRow,Select,SettingsCard,StatusBadge,TextField,ToastStack} from '@nodedc/ui-react';
|
||||
import type {SensorEnrollmentProps} from '@mission-core/sensor-sdk';
|
||||
import {bridgeFormValid,enroll,enrollmentAllowed,connectionAttempt,enrollmentNotice,enrollmentBluetoothFailure,mergeEnrollmentState,enrollmentProof,enrollmentProofCurrent,type EnrollmentProof,type EnrollmentState} from './enrollment';
|
||||
import {K1WifiPasswordField} from '../components/K1WifiPasswordField';
|
||||
import {revealEnrollment} from './revealEnrollment';
|
||||
|
||||
export function DeviceEnrollmentWindow({transport,onChange,onComplete,renderWindow}:SensorEnrollmentProps){
|
||||
@@ -157,7 +158,7 @@ export function DeviceEnrollmentWindow({transport,onChange,onComplete,renderWind
|
||||
onChange={value=>{setNetwork(value);if(value!=='manual')setSSID(networks[Number(value)].ssid);setPassword('');}} disabled={pending}/>}
|
||||
</div>
|
||||
<TextField label="Название сети Wi-Fi" value={ssid} onChange={event=>{setSSID(event.target.value);setNetwork('manual');}} disabled={pending} autoComplete="off"/>
|
||||
<TextField label="Пароль Wi-Fi" type="password" value={password} onChange={event=>setPassword(event.target.value)} disabled={pending} autoComplete="new-password"/>
|
||||
<K1WifiPasswordField value={password} onChange={setPassword} disabled={pending}/>
|
||||
<Button disabled={pending||!enrollmentAllowed(state,'connect')||!bridgeFormValid(ssid,password)} aria-busy={busy==='connect'}
|
||||
icon={busy==='connect'?<ActivityIndicator size="compact"/>:undefined}
|
||||
onClick={()=>void run('connect')}>{busy==='connect'?'Применяем Wi-Fi и проверяем':'Применить Wi-Fi и проверить'}</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {EnrollmentState,EnrollmentCommand,EnrollmentOperation,EnrollmentTransport} from '@mission-core/sensor-sdk';
|
||||
import type {XgridsConnectionAttemptSummary} from '../connectionAttempt';
|
||||
import {STATION_WIFI_FAILURE_MESSAGES} from '../networkFailurePresentation';
|
||||
import {BLE_DISCOVERY_FAILURE_MESSAGES,STATION_WIFI_FAILURE_MESSAGES} from '../networkFailurePresentation';
|
||||
export type {EnrollmentState,EnrollmentTransport} from '@mission-core/sensor-sdk';
|
||||
|
||||
export function connectionAttempt(state:EnrollmentState|null):XgridsConnectionAttemptSummary|null {
|
||||
@@ -115,6 +115,7 @@ export function enrollmentNotice(state:EnrollmentState):string {
|
||||
return ''; // The pending action is already shown inside its button.
|
||||
}
|
||||
const code=attempt?.public_error_code??state.command_result?.error_code;
|
||||
if(attempt?.side_effect_status==='none'&&code&&BLE_DISCOVERY_FAILURE_MESSAGES[code])return BLE_DISCOVERY_FAILURE_MESSAGES[code];
|
||||
if(code==='network-provision-candidate-not-fresh')return 'Результат Bluetooth-поиска больше недоступен. Найдите K1 ещё раз и выберите его из списка. Настройки Wi-Fi не были отправлены.';
|
||||
if(enrollmentBluetoothFailure(state))return 'Не удалось установить связь с K1 по Bluetooth. Настройки Wi-Fi не были отправлены. Найдите K1 ещё раз и выберите его из списка.';
|
||||
if(state.command_result?.status==='rejected')return 'Выбранное устройство или сеанс изменились. Обновите сведения и выберите K1 заново.';
|
||||
@@ -123,6 +124,7 @@ export function enrollmentNotice(state:EnrollmentState):string {
|
||||
if(state.connected)return 'K1 подключён к БК.';
|
||||
if(attempt?.phase==='network_applied')return 'K1 подключился к Wi-Fi. Связь с БК пока не подтверждена. Нажмите «Проверить состояние K1»; повторно вводить сеть и пароль не нужно.';
|
||||
if(attempt?.phase==='network_outcome_unknown')return unknownResult;
|
||||
if(attempt?.side_effect_status==='none'&&attempt.stage==='resolution-failed')return 'Не удалось подготовить Bluetooth-соединение с K1. Настройки Wi-Fi не были отправлены. Найдите K1 ещё раз и повторите подключение.';
|
||||
if(attempt?.status==='failed'||state.command_result?.status==='failed')return 'Подключение не завершено. Проверьте состояние K1 и выбранную сеть.';
|
||||
return '';
|
||||
}
|
||||
@@ -131,7 +133,8 @@ export function enrollmentBluetoothFailure(state:EnrollmentState|null):boolean {
|
||||
const attempt=connectionAttempt(state);
|
||||
return !!attempt&&attempt.status==='failed'&&attempt.side_effect_status==='none'&&
|
||||
(attempt.public_error_code==='network-provision-candidate-not-fresh'||
|
||||
(attempt.stage==='connect-failed'&&['BleakError','BleakDBusError','BleakDeviceNotFoundError','TimeoutError'].includes(attempt.public_error_code??'')));
|
||||
attempt.public_error_code==='ble-selected-device-unavailable'||
|
||||
(['resolution-failed','connect-failed','gatt-contract-failed','baseline-read-failed'].includes(attempt.stage)&&['BleakError','BleakDBusError','BleakDeviceNotFoundError','TimeoutError'].includes(attempt.public_error_code??'')));
|
||||
}
|
||||
|
||||
export interface EnrollmentProof {
|
||||
|
||||
@@ -502,14 +502,6 @@ box-sizing: border-box;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.password-field-row {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: end;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.connection-recovery-choice {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
|
||||
@@ -22,7 +22,7 @@ from credential_install import PROFILE_ID, validate # noqa: E402
|
||||
from debian import package # noqa: E402
|
||||
from runtime_payload import files as runtime_files # noqa: E402
|
||||
|
||||
VERSION = "0.1.11"
|
||||
VERSION = "0.1.12"
|
||||
RESOURCES = (
|
||||
"plugins/xgrids-k1/profile_loader.py",
|
||||
"plugins/xgrids-k1/plugin.manifest.json",
|
||||
@@ -135,7 +135,7 @@ Architecture: amd64
|
||||
Maintainer: NODE.DC local build <noreply@example.invalid>
|
||||
Section: admin
|
||||
Priority: optional
|
||||
Depends: mission-core-node (>= 0.8.11), mission-core-node (<< 0.9.0),
|
||||
Depends: mission-core-node (>= 0.8.12), mission-core-node (<< 0.9.0),
|
||||
systemd, python3, adduser, bluez, network-manager, iproute2, ffmpeg
|
||||
Breaks: mission-core-node (<< 0.8.0)
|
||||
Replaces: mission-core-node (<< 0.8.0)
|
||||
|
||||
Reference in New Issue
Block a user