Accept systemd credential delivery and clarify manual K1 control
This commit is contained in:
@@ -4,7 +4,7 @@ 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 {revealEnrollment} from './revealEnrollment';
|
||||
|
||||
export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow}:SensorEnrollmentProps){
|
||||
export function DeviceEnrollmentWindow({transport,onChange,onComplete,renderWindow}:SensorEnrollmentProps){
|
||||
const [state,setState]=useState<EnrollmentState|null>(null);
|
||||
const [device,setDevice]=useState('');
|
||||
const [ssid,setSSID]=useState('');
|
||||
@@ -106,9 +106,19 @@ export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow}
|
||||
}
|
||||
}
|
||||
|
||||
async function configure(){
|
||||
if(pending||!enrollmentProofCurrent(verified,state,device)||!verified)return;
|
||||
setBusy('opening');setError('');
|
||||
const signal=lifetime.current?.signal;
|
||||
try{await onComplete(verified.session,signal);}
|
||||
catch(cause){if(!signal?.aborted)setError(cause instanceof Error?cause.message:'Не удалось открыть устройство.');}
|
||||
finally{if(!signal?.aborted)setBusy('');}
|
||||
}
|
||||
|
||||
return renderWindow({busy:pending,
|
||||
actions:confirmed?<Button variant="primary" disabled={pending}
|
||||
onClick={()=>{if(enrollmentProofCurrent(verified,state,device)){onChange();onClose();}}}>Подключить</Button>:undefined,
|
||||
aria-busy={busy==='opening'} icon={busy==='opening'?<ActivityIndicator size="compact"/>:undefined}
|
||||
onClick={()=>void configure()}>Настроить устройство</Button>:undefined,
|
||||
content:<div className="sensor-content" ref={content}>
|
||||
<SettingsCard title={state?.name||'Бортовой компьютер'}
|
||||
description="K1 подключится к выбранной сети Wi-Fi рядом с БК. БК может работать через Ethernet или Wi-Fi; сеть компьютера оператора может отличаться.">
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
import {useEffect,useState} from 'react';
|
||||
import {Button,Icon,IconButton,SettingsCard,StatusBadge} from '@nodedc/ui-react';
|
||||
import {useEffect,useRef,useState} from 'react';
|
||||
import {ActivityIndicator,Button,Icon,IconButton,SettingsCard,StatusBadge} from '@nodedc/ui-react';
|
||||
import {perform,type Sensor,type SensorTransport} from './runtime';
|
||||
import {K1LiveView} from './K1LiveView';
|
||||
import type {RerunHostFactory} from '@mission-core/sensor-sdk';
|
||||
import {K1LiveSettings} from './K1LiveSettings';
|
||||
import {k1ConnectionNotice,k1ManualState,k1Status} from './presentation';
|
||||
|
||||
export function K1Detail({device,transport,enabled,back,refresh,failure,createRerunHost}:{device:Sensor;transport:SensorTransport;enabled:boolean;back:()=>void;refresh:()=>Promise<void>;failure:(error:unknown)=>void;createRerunHost?:RerunHostFactory}){
|
||||
const [busy,setBusy]=useState(false),[expanded,setExpanded]=useState(false),[generation,setGeneration]=useState(0),[settings,setSettings]=useState(false);
|
||||
const [busy,setBusy]=useState(''),[expanded,setExpanded]=useState(false),[generation,setGeneration]=useState(0),[settings,setSettings]=useState(false);
|
||||
const running=useRef(false);
|
||||
const streaming=device.snapshot.acquisition==='streaming';
|
||||
const manual=k1ManualState(device,enabled),status=k1Status(device,enabled);
|
||||
useEffect(()=>{const key=(event:KeyboardEvent)=>{if(event.key==='Escape')setExpanded(false);};window.addEventListener('keydown',key);return()=>window.removeEventListener('keydown',key);},[]);
|
||||
async function act(action:'start'|'stop'|'verify'){
|
||||
if(busy)return;setBusy(true);failure(null);
|
||||
if(running.current||!enabled)return;
|
||||
running.current=true;setBusy(action);failure(null);
|
||||
try{await perform(transport,device,action,action==='verify'?{}:{operator_confirmed:true,control_generation:device.control?.generation,acquisition_id:device.control?.acquisition_id??null});await refresh();}
|
||||
catch(e){failure(e);}finally{setBusy(false);}
|
||||
catch(e){failure(e);}finally{running.current=false;setBusy('');}
|
||||
}
|
||||
return <div className={expanded?'sensor-content sensor-viewer-expanded':'sensor-content'}>
|
||||
<div className="sensor-actions sensor-inventory-toolbar"><Button onClick={back}>К устройствам</Button><div className="sensor-actions"><StatusBadge tone={streaming&&enabled?'success':'neutral'}>{streaming?'Живой просмотр':'K1 · Bridge'}</StatusBadge><IconButton label={expanded?'Свернуть просмотр':'Развернуть просмотр'} onClick={()=>setExpanded(value=>!value)}><Icon name={expanded?'minimize':'expand'}/></IconButton></div></div>
|
||||
<SettingsCard title={device.name} description="Камера и лидар работают на бортовом компьютере. Исходные данные записи сохраняются на БК.">
|
||||
<div className="sensor-actions"><Button disabled={!enabled||busy||streaming||!device.control?.can_start} onClick={()=>void act('start')}>Начать просмотр</Button><Button disabled={!enabled||busy||(!streaming&&!device.control?.can_stop)} onClick={()=>void act('stop')}>Остановить</Button><Button disabled={!enabled||busy} onClick={()=>{void refresh();setGeneration(v=>v+1);}}>Обновить просмотр</Button><IconButton label="Настройки живого просмотра" onClick={()=>setSettings(v=>!v)}><Icon name="settings"/></IconButton></div>
|
||||
<div className="sensor-actions sensor-inventory-toolbar"><Button onClick={back}>К устройствам</Button><div className="sensor-actions"><StatusBadge tone={status.tone}>{status.label}</StatusBadge>{streaming&&<IconButton label={expanded?'Свернуть просмотр':'Развернуть просмотр'} onClick={()=>setExpanded(value=>!value)}><Icon name={expanded?'minimize':'expand'}/></IconButton>}</div></div>
|
||||
<SettingsCard title={device.name} description="Ручной запуск камеры и лидара на бортовом компьютере. Исходные данные записи сохраняются на БК."
|
||||
actions={manual.connected&&<IconButton label="Настройки устройства" disabled={!!busy} onClick={()=>setSettings(value=>!value)}><Icon name="settings"/></IconButton>}>
|
||||
{manual.connected&&!manual.showStop&&<Button variant="primary" disabled={!!busy||!manual.canStart} aria-busy={busy==='start'}
|
||||
icon={busy==='start'?<ActivityIndicator size="compact"/>:undefined} onClick={()=>void act('start')}>{busy==='start'?'Запускаем устройство':'Инициировать запуск'}</Button>}
|
||||
{manual.showStop&&<Button disabled={!manual.connected||!!busy||(!streaming&&!device.control?.can_stop)} aria-busy={busy==='stop'}
|
||||
icon={busy==='stop'?<ActivityIndicator size="compact"/>:undefined} onClick={()=>void act('stop')}>{busy==='stop'?'Останавливаем устройство':'Остановить устройство'}</Button>}
|
||||
{!manual.connected&&<SettingsCard align="center" title={k1ConnectionNotice(device,enabled)}>
|
||||
{device.control?.can_verify&&<Button disabled={!enabled||!!busy} aria-busy={busy==='verify'} icon={busy==='verify'?<ActivityIndicator size="compact"/>:undefined} onClick={()=>void act('verify')}>Проверить состояние K1</Button>}
|
||||
</SettingsCard>}
|
||||
</SettingsCard>
|
||||
{settings&&<K1LiveSettings device={device} transport={transport} enabled={enabled&&!busy} refresh={refresh} failure={failure}/>}
|
||||
{streaming&&createRerunHost&&<K1LiveView key={generation} device={device} transport={transport} createRerunHost={createRerunHost}/>}
|
||||
{manual.connected&&settings&&<K1LiveSettings device={device} transport={transport} enabled={enabled&&!busy} refresh={refresh} failure={failure}/>}
|
||||
{!streaming&&(busy==='start'||manual.active)&&<ActivityIndicator label="Запускаем K1 и ожидаем живые данные"/>}
|
||||
{streaming&&createRerunHost&&<K1LiveView key={generation} device={device} transport={transport} createRerunHost={createRerunHost} onReconnect={()=>setGeneration(value=>value+1)}/>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useEffect,useRef,useState} from 'react';
|
||||
import {ActivityIndicator,SettingsCard} from '@nodedc/ui-react';
|
||||
import {ActivityIndicator,Button,SettingsCard} from '@nodedc/ui-react';
|
||||
import {perform,type Sensor,type SensorTransport} from './runtime';
|
||||
import type {RerunHostFactory} from '@mission-core/sensor-sdk';
|
||||
|
||||
@@ -12,7 +12,7 @@ function privateCandidate(sdp:string):string{
|
||||
}).join('\r\n');
|
||||
}
|
||||
|
||||
export function K1LiveView({device,transport,createRerunHost}:{device:Sensor;transport:SensorTransport;createRerunHost:RerunHostFactory}){
|
||||
export function K1LiveView({device,transport,createRerunHost,onReconnect}:{device:Sensor;transport:SensorTransport;createRerunHost:RerunHostFactory;onReconnect:()=>void}){
|
||||
const spatial=useRef<HTMLDivElement>(null),video=useRef<HTMLVideoElement>(null);
|
||||
const [error,setError]=useState(''),[cameraError,setCameraError]=useState(''),[ready,setReady]=useState(false);
|
||||
useEffect(()=>{
|
||||
@@ -59,5 +59,5 @@ export function K1LiveView({device,transport,createRerunHost}:{device:Sensor;tra
|
||||
void run().catch(()=>fail('Не удалось открыть живой просмотр K1. Обновите состояние устройства.'));
|
||||
return()=>{active=false;clearInterval(keepalive);clearInterval(follow);rrd.onmessage=null;camera.onmessage=null;pc.onconnectionstatechange=null;pc.close();try{closeChannel?.();}finally{host.dispose();}if(peerID)void perform(transport,device,'close-peer',{peer_id:peerID}).catch(()=>{});if(mediaURL)URL.revokeObjectURL(mediaURL);};
|
||||
},[device.snapshot.context.session_id,transport,createRerunHost]);
|
||||
return <div className="sensor-content">{error?<SettingsCard title={error}/>:!ready&&<ActivityIndicator label="Получаем живые данные K1"/>}<div className="sensor-live-layout"><div className="sensor-live-spatial" ref={spatial}/><div className="sensor-content">{cameraError&&<SettingsCard title={cameraError}/>}<video className="sensor-media" ref={video} muted autoPlay playsInline aria-label="Камера K1"/></div></div></div>;
|
||||
return <div className="sensor-content">{error?<SettingsCard title={error}/>:!ready&&<ActivityIndicator label="Получаем живые данные K1"/>}{(error||cameraError)&&<Button onClick={onReconnect}>Восстановить просмотр</Button>}<div className="sensor-live-layout"><div className="sensor-live-spatial" ref={spatial}/><div className="sensor-content">{cameraError&&<SettingsCard title={cameraError}/>}<video className="sensor-media" ref={video} muted autoPlay playsInline aria-label="Камера K1"/></div></div></div>;
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ export function enrollmentNotice(state:EnrollmentState):string {
|
||||
if(enrollmentBluetoothFailure(state))return 'Не удалось установить связь с K1 по Bluetooth. Настройки Wi-Fi не были отправлены. Найдите K1 ещё раз и выберите его из списка.';
|
||||
if(state.command_result?.status==='rejected')return 'Выбранное устройство или сеанс изменились. Обновите сведения и выберите K1 заново.';
|
||||
if(code&&STATION_WIFI_FAILURE_MESSAGES[code])return STATION_WIFI_FAILURE_MESSAGES[code];
|
||||
if(code==='application_authority_unavailable')return 'Wi-Fi настроен. Служба K1 на БК не смогла авторизовать подключение. Обновите интеграцию K1 на БК и проверьте состояние устройства.';
|
||||
if(state.connected)return 'K1 подключён к БК.';
|
||||
if(attempt?.phase==='network_applied')return 'K1 подключился к Wi-Fi. Связь с БК пока не подтверждена. Нажмите «Проверить состояние K1»; повторно вводить сеть и пароль не нужно.';
|
||||
if(attempt?.phase==='network_outcome_unknown')return unknownResult;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type {SensorUiContribution} from '@mission-core/sensor-sdk';
|
||||
import {K1Detail} from './K1Detail';
|
||||
import {DeviceEnrollmentWindow} from './DeviceEnrollmentWindow';
|
||||
import {k1Status} from './presentation';
|
||||
|
||||
export const xgridsK1SensorUi:SensorUiContribution={
|
||||
kind:'k1', Detail:K1Detail, icon:'network', retainOffline:true, supportsPreparation:false,
|
||||
supportsRenaming:false,status:k1Status,
|
||||
wirelessEnrollment:{label:'XGRIDS LixelKity K1',View:DeviceEnrollmentWindow},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type {Sensor} from './runtime';
|
||||
|
||||
export function k1Status(device:Sensor,fresh:boolean):{label:string;tone:'neutral'|'success'|'warning'|'danger'} {
|
||||
if(!fresh)return {label:'Нет свежих сведений с БК',tone:'neutral'};
|
||||
if(!device.online||!device.verified)return device.control?.network_applied
|
||||
?{label:'Wi-Fi настроен · нет управления',tone:'warning'}:{label:'Связь не подтверждена',tone:'neutral'};
|
||||
if(device.snapshot.acquisition==='failed')return {label:'Ошибка захвата',tone:'danger'};
|
||||
if(device.snapshot.acquisition==='streaming')return {label:'Идёт захват',tone:'success'};
|
||||
if(['preparing','starting'].includes(device.snapshot.acquisition))return {label:'Запускается',tone:'neutral'};
|
||||
if(device.snapshot.acquisition==='stopping')return {label:'Останавливается',tone:'neutral'};
|
||||
return {label:device.control?.can_start?'Готов к запуску':'Подключён',tone:'success'};
|
||||
}
|
||||
|
||||
export function k1ConnectionNotice(device:Sensor,fresh:boolean):string {
|
||||
if(!fresh)return 'Нет свежих сведений с БК. Ожидаем восстановления связи.';
|
||||
if(device.control?.reason_code==='application_authority_unavailable')
|
||||
return 'Wi-Fi настроен. Служба K1 на БК не смогла авторизовать подключение. Обновите интеграцию K1 на БК и проверьте состояние устройства.';
|
||||
return device.control?.network_applied
|
||||
?'Wi-Fi настроен. Канал управления K1 пока не подтверждён. Проверьте состояние устройства.'
|
||||
:'Связь с K1 пока не подтверждена. Проверьте питание устройства и подключение к сети.';
|
||||
}
|
||||
|
||||
export function k1ManualState(device:Sensor,fresh:boolean) {
|
||||
const active=['preparing','starting','streaming','stopping'].includes(device.snapshot.acquisition);
|
||||
const connected=fresh&&device.online&&device.verified;
|
||||
return {active,connected,canStart:connected&&!active&&device.control?.can_start===true,
|
||||
showStop:active||device.control?.can_stop===true};
|
||||
}
|
||||
@@ -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.5"
|
||||
VERSION = "0.1.6"
|
||||
RESOURCES = (
|
||||
"plugins/xgrids-k1/profile_loader.py",
|
||||
"plugins/xgrids-k1/plugin.manifest.json",
|
||||
|
||||
Reference in New Issue
Block a user