Add packaged per-camera X4 wake and preview recovery

This commit is contained in:
DCCONSTRUCTIONS
2026-09-10 14:20:52 +03:00
parent f164606828
commit d45b329de8
22 changed files with 1045 additions and 16 deletions
@@ -0,0 +1,17 @@
import {Button,SettingsCard,StatusBadge} from '@nodedc/ui-react';
import type {RecoveryStatus} from './model';
export function RecoverySettings({value,online,enabled,pending,change}:{
value:RecoveryStatus; online:boolean; enabled:boolean; pending:boolean; change:(enabled:boolean)=>void;
}) {
const failed=['exhausted','unavailable','preview_failed'].includes(value.phase);
const label=!value.enabled?'Выключено':failed?'Требуется проверка камеры':
['waiting','waking','restoring_preview'].includes(value.phase)?'Восстанавливаем подключение':'Включено';
return <SettingsCard title="Восстановление подключения" description="После потери связи камера сможет включаться по Bluetooth. Ранее запущенный просмотр возобновится, запись на карту повторно не запускается."
actions={<StatusBadge tone={failed&&value.enabled?'warning':'neutral'}>{label}</StatusBadge>}>
<p>На X4 включите «Пробуждение по Bluetooth» и оставьте USB подключённым. Перед ручным выключением камеры отключите восстановление здесь.</p>
<Button disabled={!enabled||(!online&&!value.enabled)} loading={pending} onClick={()=>change(!value.enabled)}>
{value.enabled?'Отключить восстановление':'Включить восстановление'}
</Button>
</SettingsCard>;
}
@@ -4,6 +4,7 @@ import type {SensorDetailProps} from '../../../../packages/sensor-ui/src/extensi
import {perform} from '../../../../packages/sensor-ui/src/contracts';
import {LiveViewport} from '../../../../packages/sensor-ui/src/LiveViewport';
import {CameraSettings} from './CameraSettings';
import {RecoverySettings} from './RecoverySettings';
import {cameraStatus,fileName,type CameraFiles,type CameraSettings as Settings} from './model';
export function X4Detail({device,transport,enabled,back,refresh,failure}:SensorDetailProps){
@@ -22,7 +23,7 @@ export function X4Detail({device,transport,enabled,back,refresh,failure}:SensorD
},[transport,device.id,device.snapshot.context.session_id,status.function_mode]);
useEffect(()=>{if(available)void load().catch(error=>{if(mounted.current)failure(error);});},[available,load]);
async function action(name:string,parameters:Record<string,unknown>={},key=name){
if(running.current||!enabled||(!available&&name!=='refresh'))return;
if(running.current||!enabled||(!available&&!['refresh','recovery.configure','preview.stop'].includes(name)))return;
running.current=true;setPending(key);failure(null);
try{
const result=name==='refresh'?(available?await load():undefined):await perform<unknown>(transport,device,name,parameters);
@@ -40,11 +41,12 @@ export function X4Detail({device,transport,enabled,back,refresh,failure}:SensorD
return <div className="sensor-content"><div><Button onClick={back}>К устройствам</Button></div>
<SettingsCard title={device.name} description={`${device.model}${device.firmware?' · '+device.firmware:''}`} actions={<IconButton label="Обновить состояние камеры" loading={pending==='refresh'} disabled={!enabled||busy} onClick={()=>void action('refresh')}><Icon name="refresh"/></IconButton>}>
{!available&&<p>Нет свежей связи с камерой.</p>}
<div className="sensor-actions"><Button disabled={!available||busy||status.preview!==0} loading={pending==='preview.start'} onClick={()=>void action('preview.start')}>Начать просмотр</Button><Button disabled={!available||busy||status.preview===0} loading={pending==='preview.stop'} onClick={()=>void action('preview.stop')}>Остановить просмотр</Button><Button disabled={!available||busy||status.recording!==0} loading={pending==='verify'} onClick={()=>void action('verify')}>Проверить изображение</Button></div>
<div className="sensor-actions"><Button disabled={!available||busy||status.preview!==0} loading={pending==='preview.start'} onClick={()=>void action('preview.start')}>Начать просмотр</Button><Button disabled={!enabled||busy||(!available&&!status.recovery?.preview_wanted)||(available&&status.preview===0&&!status.recovery?.preview_wanted)} loading={pending==='preview.stop'} onClick={()=>void action('preview.stop')}>Остановить просмотр</Button><Button disabled={!available||busy||status.recording!==0} loading={pending==='verify'} onClick={()=>void action('verify')}>Проверить изображение</Button></div>
{status.alerts?.temperature_high&&<p>Камера сообщает о перегреве.</p>}
{status.alerts?.storage_full&&<p>На карте камеры закончилось место.</p>}
{status.alerts?.battery_low&&<p>Камера сообщает о низком заряде.</p>}
</SettingsCard>
{status.recovery?.supported&&<RecoverySettings value={status.recovery} online={available} enabled={enabled&&!busy} pending={pending==='recovery.configure'} change={value=>void action('recovery.configure',{enabled:value,wakeup_confirmed:value})}/>}
<LiveViewport device={viewDevice} layer="preview" title="Изображение X4" note="Запись сохраняется на карте памяти камеры." inactiveMessage="Нажмите «Начать просмотр», чтобы получить изображение." transport={transport} failure={failure}/>
<SettingsCard title="Съёмка" description="Фото и видеозапись сохраняются на карте памяти X4." actions={<StatusBadge tone={available&&status.recording===1?'success':status.recording===0?'neutral':'warning'}>{available?recordLabel:'Состояние недоступно'}</StatusBadge>}>
<div className="sensor-actions"><Button disabled={!available||busy||status.recording!==0||mode!==7} loading={pending==='record.start'} onClick={()=>void action('record.start')}>Начать запись</Button><Button disabled={!available||busy||status.recording===0} loading={pending==='record.stop'} onClick={()=>void action('record.stop')}>Остановить запись</Button><Button disabled={!available||busy||status.recording!==0||mode!==6} loading={pending==='photo.capture'} onClick={()=>void action('photo.capture')}>Сделать фото</Button></div>
@@ -1,9 +1,14 @@
import type {Sensor} from '../../../../packages/sensor-ui/src/contracts';
export interface RecoveryStatus {
supported:boolean; enabled:boolean; preview_wanted:boolean; phase:string;
}
export interface CameraStatus {
connected:boolean; preview:number|null; recording:number|null; function_mode:number|null;
firmware?:string; battery?:{level:number}; storage?:{free_bytes:number};
alerts?:{battery_low?:boolean;storage_full?:boolean;temperature_high?:boolean};
recovery?:RecoveryStatus;
}
export interface CameraSettings {
mode:number; photo_modes:number[]; video_modes:number[]; photo_sizes:number[];