Files
NODEDC_MISSION_CORE/plugins/vesc/frontend/src/VescDetail.tsx
T

81 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {useEffect,useRef,useState} from 'react';
import {Button,LoadingRegion,ResourceList,ResourceRow,SettingsCard,StatusBadge} from '@nodedc/ui-react';
import type {SensorDetailProps} from '../../../../packages/sensor-ui/src/extensions';
import {perform} from '../../../../packages/sensor-ui/src/contracts';
import {vescLabel,vescStatus,type VescTelemetry} from './model';
import {VescBackups} from './VescBackups';
import {VescMotor} from './VescMotor';
import {VescHall} from './VescHall';
import {VescCalibration} from './VescCalibration';
import {VescLink} from './VescLink';
const fields=[['input_voltage_v','Напряжение питания','В'],['input_current_a','Ток питания','А'],
['motor_current_a','Ток мотора','А'],['erpm','Электрические обороты','ERPM'],['duty','Заполнение PWM',''],
['mos_temperature_c','Температура контроллера','°C'],['motor_temperature_c','Температура мотора','°C'],
['fault_code','Код ошибки',''],['can_id','CAN ID',''],['timeout','Тайм-аут управления',''],
['kill_switch','Вход аварийного останова','']] as const;
export function VescDetail(props:SensorDetailProps){
const {device,transport,enabled,back,refresh,failure}=props;
const status=vescStatus(device);const label=vescLabel(device,enabled);
const [telemetry,setTelemetry]=useState<VescTelemetry|null>(status.telemetry);
const [pending,setPending]=useState<string|null>(null);
const [saved,setSaved]=useState<string|null>(null);
const [motorBusy,setMotorBusy]=useState(false);
const [hallBusy,setHallBusy]=useState(false);
const [calibrationBusy,setCalibrationBusy]=useState(false);
const [linkBusy,setLinkBusy]=useState(false);
const poweredBusy=motorBusy||hallBusy||calibrationBusy||linkBusy;
const running=useRef(false);const mounted=useRef(true);
const available=enabled&&device.online&&device.verified&&status.readable;
useEffect(()=>{mounted.current=true;return()=>{mounted.current=false;};},[]);
async function read(action:'vesc.telemetry.read'|'vesc.config.backup'){
if(running.current||poweredBusy||!available)return;
running.current=true;setPending(action);failure(null);
try{
const result=await perform<Record<string,unknown>>(transport,device,action);
if(!mounted.current)return;
if(action==='vesc.telemetry.read')setTelemetry(result as unknown as VescTelemetry);
else {
setSaved(String(result.observed_at));
}
await refresh();
}catch(error){if(mounted.current)failure(error);}
finally{running.current=false;if(mounted.current)setPending(null);}
}
const backupAt=saved??status.backup?.observed_at;
return <div className="sensor-content">
<div><Button onClick={back}>К устройствам</Button></div>
<SettingsCard title={`${device.name} · Настройка VESC`} description={`${device.model} · ${device.connection_label??'USB'}`}
actions={<StatusBadge tone={label.tone}>{label.label}</StatusBadge>}>
{!enabled||!device.online?<p>Нет свежей связи с контроллером.</p>:status.message?<p>{status.message}</p>:null}
{status.identity?<ResourceList aria-label="Контроллер VESC">
<li><ResourceRow title="Прошивка" description={status.identity.version}/></li>
<li><ResourceRow title="Аппаратная версия" description={status.identity.hardware}/></li>
<li><ResourceRow title="UUID" description={status.identity.uuid.toUpperCase()}/></li>
{status.identity.test_firmware!==null&&status.identity.test_firmware>0&&<li><ResourceRow title="Тестовая прошивка" description={String(status.identity.test_firmware)}/></li>}
</ResourceList>:<p>Аппаратный идентификатор ещё не подтверждён.</p>}
</SettingsCard>
<SettingsCard title="Показания контроллера" description={telemetry?`Снимок: ${new Date(telemetry.observed_at).toLocaleString()}`:'Получите текущие значения и код ошибки.'}
actions={<Button disabled={!available||pending!==null||poweredBusy} loading={pending==='vesc.telemetry.read'} onClick={()=>void read('vesc.telemetry.read')}>Обновить показания</Button>}>
<LoadingRegion loading={pending==='vesc.telemetry.read'&&!telemetry} label="Чтение показаний VESC">
{telemetry?<ResourceList aria-label="Показания VESC">{fields.map(([key,title,unit])=>{
const value=telemetry.values[key];if(value===undefined)return null;
return <li key={key}><ResourceRow title={title} description={typeof value==='boolean'?(value?'Активен':'Не активен'):`${Number(value).toLocaleString(undefined,{maximumFractionDigits:3})}${unit?' '+unit:''}`}/></li>;
})}</ResourceList>:<p>Показания ещё не прочитаны.</p>}
</LoadingRegion>
<p>ERPM — электрические обороты. Обороты вала зависят от числа пар полюсов мотора.</p>
</SettingsCard>
<SettingsCard title="Резервная копия конфигурации" description="Сохраните текущие параметры мотора и входов новой версией."
actions={<Button disabled={!available||pending!==null||poweredBusy} loading={pending==='vesc.config.backup'} onClick={()=>void read('vesc.config.backup')}>Сохранить версию</Button>}>
{backupAt&&<p>Последняя копия: {new Date(backupAt).toLocaleString()}</p>}
<p>Копия привязана к UUID и прошивке. Версии до и после калибровки остаются в истории.</p>
</SettingsCard>
<VescLink {...props} blocked={motorBusy||hallBusy||calibrationBusy||pending!==null} onBusyChange={setLinkBusy}/>
<VescCalibration {...props} blocked={linkBusy||motorBusy||hallBusy||pending!==null} onBusyChange={setCalibrationBusy}/>
<VescHall {...props} blocked={linkBusy||motorBusy||calibrationBusy||pending!==null} onBusyChange={setHallBusy}/>
<VescMotor {...props} blocked={linkBusy||hallBusy||calibrationBusy||pending!==null} onBusyChange={setMotorBusy}/>
<VescBackups deviceId={device.id} archive={transport.configurationArchive} revision={backupAt??null}/>
</div>;
}