feat(vesc): integrate native calibration diagnostics and configuration archives

This commit is contained in:
DCCONSTRUCTIONS
2026-09-25 16:37:56 +03:00
parent 24bbaefb00
commit 45fb14b206
85 changed files with 17968 additions and 28 deletions
+80
View File
@@ -0,0 +1,80 @@
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>;
}