feat(control-station): add rover scene telemetry and guarded keyboard control

This commit is contained in:
DCCONSTRUCTIONS
2026-09-25 16:37:57 +03:00
parent 63cbb08ea0
commit 6d772b29fe
36 changed files with 3536 additions and 21 deletions
+70
View File
@@ -0,0 +1,70 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import ts from 'typescript';
const source=readFileSync(new URL('../../../plugins/vesc/frontend/src/model.ts',import.meta.url),'utf8');
const code=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.ESNext}}).outputText;
const {vescLabel,testInput,speedInput,rotationResult,driveTestIds}=await import('data:text/javascript;base64,'+Buffer.from(code).toString('base64'));
const controller={online:true,prepared:true,verified:true,vesc_status:{identity:{uuid:'synthetic'},readable:true}};
test('speed mode requires board capability and reports measured rotation time',()=>{
assert.ok(speedInput('1200',undefined).error);
const bounds={min_erpm:300,max_erpm:3000,duration_basis:'measured_speed'};
assert.equal(speedInput('1200',bounds).error,null);
for(const value of ['', 'NaN','299','3001'])assert.ok(speedInput(value,bounds).error);
const text=rotationResult({outcome:'stopped',rotation_s:2.5,release_confirmed:true,limits_restored:true});
assert.match(text,/2,5 с/);
assert.match(text,/Проверка остановлена/);
assert.doesNotMatch(text,/время вращения набрано/);
});
test('USB discovery and model preparation never claim a verified VESC',()=>{
assert.equal(vescLabel({...controller,prepared:false},true).label,'Требуется подготовка');
assert.equal(vescLabel({...controller,vesc_status:{identity:null,readable:false}},true).tone,'warning');
});
test('fresh read capability is distinct from unsupported firmware and offline state',()=>{
assert.equal(vescLabel(controller,true).label,'Готов к чтению');
assert.equal(vescLabel(controller,false).label,'Нет связи');
assert.equal(vescLabel({...controller,online:false},true).label,'Нет связи');
assert.equal(vescLabel({...controller,vesc_status:{identity:{uuid:'synthetic'},readable:false}},true).label,'Прошивка не поддерживается');
});
test('test fields accept entered values only inside the board capability bounds',()=>{
const bounds={min_current_a:0.5,max_current_a:5,min_duration_s:0.5,max_duration_s:10};
assert.equal(testInput('3.7','7.2',bounds).valid,true);
assert.equal(testInput('5','10',bounds).valid,true);
for(const [amps,seconds] of [['','5'],['5',''],['60','10'],['5','11'],['NaN','1'],['Infinity','1']])assert.equal(testInput(amps,seconds,bounds).valid,false);
assert.equal(testInput('2','1.5',undefined).valid,false);
const overlong=testInput('5','15',bounds);
assert.equal(overlong.valid,false);
assert.equal(overlong.currentError,null);
assert.equal(overlong.durationError,'Длительность должна быть от 0,5 до 10 с.');
assert.equal(testInput('5','10',bounds).durationError,null);
});
// Capabilities come from the installed board: a newer Core must retain old bounds.
test('extended board accepts 30 A / 30 s without changing older board bounds',()=>{
const old={min_current_a:0.5,max_current_a:5,min_duration_s:0.5,max_duration_s:10};
const expanded={...old,max_current_a:30,max_duration_s:30,continuous_current:true};
assert.equal(testInput('30','30',expanded).valid,true);
assert.equal(testInput('30.1','30',expanded).valid,false);
assert.equal(testInput('30','30.1',expanded).valid,false);
assert.equal(testInput('30','30',old).valid,false);
});
test('group spin requires a complete unique profile containing the selected controller',()=>{
const profile={layout:'1x1',revision:3,bindings:{'left.1':{device_id:'left',uuid:'a'},'right.1':{device_id:'right',uuid:'b'}}};
assert.deepEqual(driveTestIds(profile,'left'),['left','right']);
assert.deepEqual(driveTestIds(profile,'unassigned'),[]);
assert.deepEqual(driveTestIds({...profile,bindings:{'left.1':profile.bindings['left.1']}},'left'),[]);
assert.deepEqual(driveTestIds({...profile,bindings:{...profile.bindings,'right.1':profile.bindings['left.1']}},'left'),[]);
assert.deepEqual(driveTestIds(undefined,'left'),[]);
});
test('four-wheel profile preserves front and rear membership for common testing',()=>{
const ids=['lf','lr','rf','rr'];
const bindings=Object.fromEntries(['left.1','left.2','right.1','right.2'].map((slot,i)=>[slot,{device_id:ids[i],uuid:ids[i]}]));
assert.deepEqual(driveTestIds({layout:'2x2',revision:4,bindings},'rf'),ids);
});