Build Argo V2 control mappings and telemetry dashboard
Add configurable Xbox motion and action bindings, frame-aware six-axis MAVLink controls, sensor logging, camera viewing and recording, and a faster startup path. Synchronize both app copies and document companion hardware setup and verification limits.
This commit is contained in:
+107
-91
@@ -6,7 +6,7 @@ const STATUS_LABELS = {
|
||||
link_degraded: 'Signal lost — check tether',
|
||||
};
|
||||
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
|
||||
const control={forward:0,turn:0,vertical:0}, pressedKeys=new Set(); let bridgeReady=false, appState='disconnected', batteryConfig={}, linkConfig={baud:57600,control_hz:20,ping_hz:1,system_hz:1,battery_hz:1,servo_hz:2,rc_hz:2,esc_hz:1}, servoSignature='', lastAxisSend=0, axisTimer=0, demoActive=false, demoState=null;
|
||||
const control={forward:0,turn:0,vertical:0,lateral:0,pitch:0,roll:0}, pressedKeys=new Set(); let bridgeReady=false, appState='disconnected', batteryConfig={}, linkConfig={baud:57600,control_hz:20,ping_hz:1,system_hz:1,battery_hz:1,servo_hz:2,rc_hz:2,esc_hz:1}, servoSignature='', lastAxisSend=0, axisTimer=0, demoActive=false, demoState=null;
|
||||
let motorSettings={independent:false,customized:false,motors:Object.fromEntries(Array.from({length:12},(_,i)=>[String(i+1),{name:`Motor ${i+1}`,scale:20}]))},motorSettingsQueue=Promise.resolve();
|
||||
const api=(name,...args)=>window.pywebview.api[name](...args);
|
||||
const activity=[];
|
||||
@@ -25,126 +25,132 @@ let vehicleSnapshot={connected:false,armed:null}, view=ControlState.present(vehi
|
||||
const canDrive=()=>view.drive&&!document.querySelector('dialog[open]')&&performance.now()-lastSafetyTick<750;
|
||||
|
||||
window.canDrive=canDrive;
|
||||
let inputMode='keyboard',controllerKey=null,controllerConfig=null,controllerNeedCenter=true,controllerWasDriving=false;
|
||||
let inputMode='keyboard',controllerKey=null,controllerConfig=null,controllerDraft=null,controllerNeedCenter=true,controllerWasDriving=false;
|
||||
let controllerProfiles={};try{controllerProfiles=JSON.parse(localStorage.getItem('sub-v2-profiles'))||{}}catch(_){}
|
||||
const controllerMotion=new ControllerInput.Motion();
|
||||
let controllerActionState={},controllerActionsReady=false;
|
||||
const controllerId=pad=>pad.index+':'+pad.id;
|
||||
function getControllers(){try{return [...(navigator.getGamepads?.()||[])].filter(pad=>pad?.connected)}catch(_){return []}}
|
||||
function loadControllerConfig(pad){
|
||||
let saved=null;try{saved=JSON.parse(localStorage.getItem('sub-controller:'+pad.id))}catch(_){}
|
||||
const defaults=ControllerInput.defaults(pad),config={...defaults,...(saved||{})};
|
||||
return Number.isFinite(config.deadZone)&&config.deadZone>=0&&config.deadZone<=.3&&ControllerInput.read(pad,config)?config:defaults;
|
||||
let saved=controllerProfiles[pad?.id||'$xbox']||controllerProfiles.$xbox;
|
||||
if(!saved&&pad)try{saved=JSON.parse(localStorage.getItem('sub-controller:'+pad.id))}catch(_){}
|
||||
const config=ControllerInput.migrate(saved,pad);
|
||||
return ControllerInput.validProfile(config)?config:ControllerInput.preset();
|
||||
}
|
||||
function selectController(pad){
|
||||
resetInputs();controllerKey=controllerId(pad);controllerConfig=loadControllerConfig(pad);
|
||||
resetInputs();controllerKey=pad?controllerId(pad):null;controllerConfig=loadControllerConfig(pad);
|
||||
}
|
||||
function setInputMode(mode){
|
||||
if(mode===inputMode||inputConfigurationLocked())return false;
|
||||
resetInputs();inputMode=mode;document.body.dataset.inputMode=mode;
|
||||
$('#input-keyboard').setAttribute('aria-pressed',String(mode==='keyboard'));
|
||||
$('#input-joystick').setAttribute('aria-pressed',String(mode==='joystick'));
|
||||
$('#controller-settings').hidden=mode!=='joystick';
|
||||
$('#joystick-canvas').setAttribute('aria-label',mode==='joystick'?'Controller steering and throttle output; read only':'Steering and throttle joystick; W A S D keys');
|
||||
$('#heave').setAttribute('aria-label',mode==='joystick'?'Controller vertical output; read only':'Vertical thrust control');
|
||||
$('#joystick-canvas').setAttribute('aria-label',mode==='joystick'?'Controller output':'Steering and throttle joystick; W A S D keys');
|
||||
$('#heave').setAttribute('aria-label',mode==='joystick'?'Controller vertical output':'Vertical thrust control');
|
||||
return true;
|
||||
}
|
||||
function inputConfigurationLocked(){
|
||||
return !!(engageStart||localRequest||view.pending||vehicleSnapshot.armed===true||(vehicleSnapshot.connected&&!view.known));
|
||||
}
|
||||
function inputConfigurationLocked(){return !!(engageStart||localRequest||view.pending||vehicleSnapshot.armed===true||(vehicleSnapshot.connected&&!view.known))}
|
||||
function syncInputModeControls(){
|
||||
const locked=inputConfigurationLocked(),pads=getControllers(),pad=pads.find(pad=>controllerId(pad)===controllerKey);
|
||||
const locked=inputConfigurationLocked();
|
||||
$('.input-mode-switch').dataset.locked=String(locked);
|
||||
for(const button of [$('#input-keyboard'),$('#input-joystick')]){
|
||||
button.disabled=locked;button.title=locked?'Disengage before changing input mode':'Choose input mode';
|
||||
}
|
||||
$('#controller-settings').disabled=locked||!pads.length;
|
||||
$('#controller-settings').title=locked?'Disengage before changing joystick settings':'Joystick setup';
|
||||
if($('#controller-modal').open){
|
||||
const form=$('#controller-form');
|
||||
for(const field of form.querySelectorAll('input,select,button[type="submit"]'))field.disabled=locked||!pad;
|
||||
$('#controller-device').disabled=locked||!pads.length;
|
||||
const feedback=form.querySelector('.form-feedback'),message='Disengage before changing joystick settings.';
|
||||
if(locked)setFeedback(feedback,message);else if(feedback.textContent===message)setFeedback(feedback);
|
||||
}
|
||||
for(const button of [$('#input-keyboard'),$('#input-joystick'),$('#controller-settings')])button.disabled=locked;
|
||||
if($('#controller-modal').open)for(const field of $('#controller-form').querySelectorAll('input,select,button[type=submit],button[data-swap]'))field.disabled=locked;
|
||||
}
|
||||
window.canTouchControl=()=>inputMode==='keyboard'&&canDrive();
|
||||
$('#input-keyboard').onclick=()=>setInputMode('keyboard');
|
||||
$('#input-joystick').onclick=()=>setInputMode('joystick');
|
||||
function fillControllerForm(){
|
||||
const axisLabels={forward:'Forward',lateral:'Strafe',vertical:'Vertical',turn:'Turn',pitch:'Pitch',roll:'Roll'};
|
||||
function fillControllerForm(keepDraft=false){
|
||||
const pads=getControllers(),device=$('#controller-device'),form=$('#controller-form');
|
||||
device.replaceChildren(...pads.map(pad=>new Option(pad.id,controllerId(pad))));
|
||||
if(!pads.length)device.add(new Option('No controller detected',''));
|
||||
device.replaceChildren(new Option('Xbox · offline',''),...pads.map(pad=>new Option(pad.id,controllerId(pad))));
|
||||
device.value=controllerKey||'';
|
||||
const pad=pads.find(pad=>controllerId(pad)===controllerKey);
|
||||
const config=controllerConfig||ControllerInput.defaults(pad);
|
||||
for(const name of ['turn','forward','vertical']){
|
||||
const field=form.elements[name];field.replaceChildren(new Option('Off','-1'));
|
||||
for(let axis=0;axis<(pad?.axes.length||0);axis++)field.add(new Option('Axis '+axis,String(axis)));
|
||||
field.value=String(config[name]);field.disabled=inputConfigurationLocked()||!pad;
|
||||
const invert='invert'+name[0].toUpperCase()+name.slice(1);
|
||||
form.elements[invert].checked=!!config[invert];form.elements[invert].disabled=inputConfigurationLocked()||!pad;
|
||||
if(!keepDraft)controllerDraft=structuredClone(controllerConfig||loadControllerConfig(pad));
|
||||
const config=controllerDraft;
|
||||
$('#controller-preset').value=['triggers3','sixaxis','classic'].includes(config.preset)?config.preset:'custom';
|
||||
const rows=$('#controller-bindings');rows.replaceChildren();
|
||||
for(const name of ControllerInput.names){
|
||||
const row=document.createElement('div');row.className='binding-row';row.dataset.axis=name;
|
||||
const label=document.createElement('span');label.textContent=axisLabels[name];
|
||||
const meter=document.createElement('meter');meter.min=-1;meter.max=1;meter.value=0;meter.setAttribute('aria-label',axisLabels[name]+' demand');label.append(meter);row.append(label);
|
||||
for(const direction of ['negative','positive']){
|
||||
const select=document.createElement('select');select.name=name+'_'+direction;select.setAttribute('aria-label',axisLabels[name]+' '+direction);
|
||||
for(const [value,text] of ControllerInput.sources(pad))select.add(new Option(text,value));
|
||||
select.value=config.bindings[name][direction];
|
||||
select.onchange=()=>{config.bindings[name][direction]=select.value;config.preset='custom';$('#controller-preset').value='custom'};
|
||||
row.append(select);
|
||||
}
|
||||
const swap=document.createElement('button');swap.type='button';swap.dataset.swap=name;swap.className='binding-swap';swap.textContent='⇄';swap.title='Reverse '+axisLabels[name];swap.setAttribute('aria-label','Reverse '+axisLabels[name]);
|
||||
swap.onclick=()=>{const item=config.bindings[name];[item.negative,item.positive]=[item.positive,item.negative];config.preset='custom';fillControllerForm(true)};row.append(swap);
|
||||
const scale=document.createElement('input');scale.type='number';scale.min=0;scale.max=100;scale.step=1;scale.value=Math.round(config.bindings[name].scale*100);scale.setAttribute('aria-label',axisLabels[name]+' limit');
|
||||
scale.oninput=()=>{config.bindings[name].scale=Number(scale.value)/100;config.preset='custom';$('#controller-preset').value='custom'};row.append(scale);rows.append(row);
|
||||
}
|
||||
form.elements.deadZone.value=Math.round(config.deadZone*100);
|
||||
device.disabled=inputConfigurationLocked()||!pads.length;
|
||||
form.elements.deadZone.disabled=inputConfigurationLocked()||!pad;
|
||||
form.querySelector('[type=submit]').disabled=inputConfigurationLocked()||!pad;
|
||||
for(const action of ['lights','stream','record']){
|
||||
const select=form.elements[action];select.replaceChildren(...ControllerInput.sources(pad).filter(([value])=>!value.startsWith('axis:')).map(([value,text])=>new Option(text,value)));
|
||||
select.value=config.actions[action];select.onchange=()=>config.actions[action]=select.value;
|
||||
}
|
||||
form.elements.deadZone.value=Math.round(config.deadZone*100);form.elements.expo.value=Math.round(config.expo*100);
|
||||
form.elements.deadZone.oninput=()=>config.deadZone=Number(form.elements.deadZone.value)/100;
|
||||
form.elements.expo.oninput=()=>config.expo=Number(form.elements.expo.value)/100;
|
||||
syncInputModeControls();
|
||||
}
|
||||
$('#controller-device').onchange=e=>{
|
||||
if(inputConfigurationLocked())return;
|
||||
const pad=getControllers().find(pad=>controllerId(pad)===e.target.value);
|
||||
if(pad){selectController(pad);fillControllerForm()}
|
||||
selectController(getControllers().find(pad=>controllerId(pad)===e.target.value));fillControllerForm();
|
||||
};
|
||||
$('#controller-form').onsubmit=e=>{
|
||||
e.preventDefault();const form=e.currentTarget,pad=getControllers().find(pad=>controllerId(pad)===controllerKey);
|
||||
if(inputConfigurationLocked()){setFeedback(form.querySelector('.form-feedback'),'Disengage before changing joystick settings.');return}
|
||||
if(!pad){setFeedback(form.querySelector('.form-feedback'),'Controller disconnected. Reconnect it before saving.');return}
|
||||
const config={deadZone:Number(form.elements.deadZone.value)/100};
|
||||
for(const name of ['turn','forward','vertical']){
|
||||
config[name]=Number(form.elements[name].value);
|
||||
const invert='invert'+name[0].toUpperCase()+name.slice(1);config[invert]=form.elements[invert].checked;
|
||||
}
|
||||
if(!ControllerInput.read(pad,config)||!Number.isFinite(config.deadZone)||config.deadZone<0||config.deadZone>.3){
|
||||
setFeedback(form.querySelector('.form-feedback'),'Choose valid axes and a dead zone between 0 and 30%.');return;
|
||||
}
|
||||
controllerConfig=config;try{localStorage.setItem('sub-controller:'+pad.id,JSON.stringify(config))}catch(_){}
|
||||
resetInputs();closeModal($('#controller-modal'));
|
||||
$('#controller-preset').onchange=e=>{if(e.target.value==='custom')return;controllerDraft=ControllerInput.preset(e.target.value);fillControllerForm(true)};
|
||||
$('#controller-form').onsubmit=async e=>{
|
||||
e.preventDefault();const form=e.currentTarget,feedback=form.querySelector('.form-feedback');
|
||||
if(inputConfigurationLocked()){setFeedback(feedback,'Disengage first');return}
|
||||
if(!ControllerInput.validProfile(controllerDraft)){setFeedback(feedback,'Check inputs');return}
|
||||
const pad=getControllers().find(pad=>controllerId(pad)===controllerKey),device=pad?.id||'$xbox';
|
||||
try{
|
||||
const clean=bridgeReady?await api('save_controller_profile',device,controllerDraft):structuredClone(controllerDraft);
|
||||
controllerProfiles[device]=clean;localStorage.setItem('sub-v2-profiles',JSON.stringify(controllerProfiles));controllerConfig=clean;
|
||||
resetInputs();closeModal($('#controller-modal'));toast('Mapping saved');
|
||||
}catch(error){setFeedback(feedback,error.message||String(error))}
|
||||
};
|
||||
function pollController(now){
|
||||
try{
|
||||
if(inputMode==='joystick'){
|
||||
if(inputMode==='joystick'||$('#controller-modal').open){
|
||||
const pads=getControllers();
|
||||
if(controllerKey===null&&pads.length)selectController(pads[0]);
|
||||
const pad=pads.find(pad=>controllerId(pad)===controllerKey),target=pad&&ControllerInput.read(pad,controllerConfig);
|
||||
let status=!navigator.getGamepads?'Controller input unavailable in this webview':!pad?(pads.length?'Selected controller unavailable · choose Setup':'Waiting for controller · press a button to detect it'):!target?'Invalid axis mapping · open Setup':!canDrive()?'Controller ready · '+pad.id:controllerNeedCenter?'Center sticks to resume':pad.id;
|
||||
$('#controller-settings').disabled=inputConfigurationLocked()||!pads.length;
|
||||
if(!target||!canDrive()){
|
||||
if(controllerWasDriving)resetInputs();
|
||||
controllerMotion.reset();controllerNeedCenter=true;controllerWasDriving=false;
|
||||
}else{
|
||||
if(controllerNeedCenter&&!Object.values(target).some(value=>value!==0)){controllerNeedCenter=false;status=pad.id}
|
||||
if(!controllerNeedCenter){
|
||||
const demand=controllerMotion.step(target,now);controllerWasDriving=true;
|
||||
control.turn=demand.turn;control.forward=demand.forward;
|
||||
window.joystick3D?.setVisual(demand.turn,-demand.forward);
|
||||
setHeave(demand.vertical,false);updateReadouts();sendAxes();
|
||||
if(controllerKey===null&&pads.length&&!$('#controller-modal').open)selectController(pads[0]);
|
||||
const pad=pads.find(pad=>controllerId(pad)===controllerKey),raw=pad&&ControllerInput.read(pad,controllerConfig||loadControllerConfig(pad));
|
||||
const target=raw&&(window.V2?.filterDemand(raw)||raw);
|
||||
let status=!pad?'No controller':!raw?'Check mapping':controllerNeedCenter&&canDrive()?'Center controls':'Controller ready';
|
||||
if(inputMode==='joystick'){
|
||||
if(!target||!canDrive()){
|
||||
if(controllerWasDriving)resetInputs();controllerNeedCenter=true;controllerWasDriving=false;
|
||||
}else{
|
||||
if(controllerNeedCenter&&!Object.values(raw).some(value=>value!==0)){controllerNeedCenter=false;status='Controller ready'}
|
||||
if(!controllerNeedCenter){
|
||||
controllerWasDriving=true;
|
||||
Object.assign(control,target);window.joystick3D?.setVisual(target.turn,-target.forward);
|
||||
setHeave(target.vertical,false);updateReadouts();sendAxes();
|
||||
}
|
||||
}
|
||||
const active=ControllerInput.actions(pad,controllerConfig);
|
||||
if(!Object.values(active).some(Boolean))controllerActionsReady=true;
|
||||
if(controllerActionsReady&&inputFocused&&!document.hidden&&!document.querySelector('dialog[open]'))for(const [action,pressed] of Object.entries(active))
|
||||
if(pressed&&!controllerActionState[action])window.V2?.action(action);
|
||||
controllerActionState=active;
|
||||
}
|
||||
if($('#controller-modal').open){
|
||||
const demand=pad&&ControllerInput.read(pad,controllerDraft);
|
||||
for(const meter of $('#controller-bindings').querySelectorAll('meter'))meter.value=demand?.[meter.closest('[data-axis]').dataset.axis]||0;
|
||||
$('#controller-axes').textContent=pad?.mapping==='standard'?'Xbox layout':pad?'Raw inputs':'Xbox · offline';
|
||||
}
|
||||
if($('#controller-status').textContent!==status)$('#controller-status').textContent=status;
|
||||
$('#controller-status').dataset.quiet=String(status===pad?.id||status.startsWith('Controller ready'));
|
||||
if($('#controller-modal').open){
|
||||
const axes=pad?'Axes · '+pad.axes.map((value,index)=>index+': '+value.toFixed(2)).join(' '):'Controller disconnected';
|
||||
if($('#controller-axes').textContent!==axes)$('#controller-axes').textContent=axes;
|
||||
if($('#controller-device').options.length!==Math.max(1,pads.length)||[...$('#controller-device').options].some(option=>option.value&&!pads.some(pad=>controllerId(pad)===option.value)))fillControllerForm();
|
||||
}
|
||||
}else {$('#controller-status').dataset.quiet='true';if($('#controller-status').textContent!=='Keyboard + touch')$('#controller-status').textContent='Keyboard + touch'}
|
||||
}catch(error){
|
||||
if(controllerWasDriving)resetInputs();
|
||||
$('#controller-status').dataset.quiet='false';$('#controller-status').textContent='Controller unavailable · reconnect or use keyboard';
|
||||
}
|
||||
$('#controller-status').dataset.quiet=String(!!pad&&!!raw);
|
||||
}else{$('#controller-status').dataset.quiet='true';$('#controller-status').textContent='Keyboard + touch'}
|
||||
}catch(error){if(controllerWasDriving)resetInputs();$('#controller-status').dataset.quiet='false';$('#controller-status').textContent='Controller unavailable'}
|
||||
requestAnimationFrame(pollController);
|
||||
}
|
||||
document.body.dataset.inputMode=inputMode;
|
||||
requestAnimationFrame(pollController);
|
||||
|
||||
function makeDemoState(){const servos={};for(let i=1;i<=16;i++)servos[String(i)]={function:i<=3?32+i:0,label:i<=3?`Motor ${i}`:'Unassigned',motor:i<=3?i:null,direction:i<=3?1:null};return {connected:true,link:'LINK OK',vehicle:'DEMO • DISARMED',armed:false,armed_known:true,mode:'MANUAL',app_state:'connected_disarmed',status:'Demo mode ready — no hardware or spoofer.',battery:{voltage:15.8,per_cell:3.95,percent:72,cells:4,low:false},link_quality:null,servos}}
|
||||
|
||||
function makeDemoState(){const frame=window.V2?.station().frame??4,count=({0:6,1:6,2:8,3:8,4:3,5:4,6:5})[frame],servos={};for(let i=1;i<=16;i++)servos[String(i)]={function:i<=count?32+i:0,label:i<=count?`Motor ${i}`:'Unassigned',motor:i<=count?i:null,direction:i<=count?1:null};return {connected:true,link:'LINK OK',vehicle:'DEMO • DISARMED',armed:false,armed_known:true,mode:'MANUAL',app_state:'connected_disarmed',status:'Demo mode ready — no hardware or spoofer.',battery:{voltage:15.8,per_cell:3.95,percent:72,cells:4,low:false,current_a:3.2,consumed_mah:320,remaining_mah:5180,consumed_source:'Demo'},capabilities:{frame,firmware:[4,5,3]},accessories:{},link_quality:null,servos}}
|
||||
function updateDemo(patch){Object.assign(demoState,patch);render(demoState)}
|
||||
document.addEventListener('click',e=>{const btn=e.target.closest('.num-step');if(!btn)return;const pill=btn.closest('.number-pill');if(!pill)return;const input=pill.querySelector('input');if(!input)return;const step=+input.step||1,min=input.min!==''?+input.min:-Infinity,max=input.max!==''?+input.max:Infinity;let val=+input.value||0;if(btn.classList.contains('down'))val=Math.max(min,+(val-step).toFixed(4));else val=Math.min(max,+(val+step).toFixed(4));input.value=val;input.dispatchEvent(new Event('input',{bubbles:true}));input.dispatchEvent(new Event('change',{bubbles:true}))});
|
||||
function toast(message,error=false){const el=$('#toast');el.textContent=message;el.className=`toast show${error?' error':''}`;clearTimeout(toast.t);toast.t=setTimeout(()=>el.className='toast',2600)}
|
||||
@@ -156,7 +162,7 @@ function populatePorts(ports){
|
||||
const old=p.value||'';
|
||||
p.innerHTML='';
|
||||
|
||||
const allList=[];
|
||||
const allList=[{device:'udpin:0.0.0.0:14550',label:'Network · UDP 14550',type:'network'}];
|
||||
const seen=new Set();
|
||||
|
||||
const addPort=item=>{
|
||||
@@ -194,7 +200,7 @@ function sendAxes(){
|
||||
axisTimer=0;
|
||||
if(!canDrive()||demoActive||axisBusy)return;
|
||||
lastAxisSend=performance.now();axisBusy=true;
|
||||
withTimeout(api('set_axes',control.forward,control.turn,control.vertical,++inputSequence))
|
||||
withTimeout(api('set_axes',control.forward,control.turn,control.vertical,++inputSequence,control.lateral,control.pitch,control.roll))
|
||||
.catch(()=>markBridgeStale()).finally(()=>axisBusy=false);
|
||||
};
|
||||
const wait=50-(performance.now()-lastAxisSend);
|
||||
@@ -203,7 +209,7 @@ function sendAxes(){
|
||||
// This is a local bridge lease, not extra MAVLink traffic. The worker retains
|
||||
// the configured wire rate and neutralizes if the UI stops renewing its input.
|
||||
setInterval(()=>{if(canDrive())sendAxes()},250);
|
||||
function updateReadouts(){ if(!canDrive()){$('#surge-readout').textContent='—';$('#yaw-readout').textContent='—';$('#heave-readout').textContent='—';return;}$('#surge-readout').textContent=Math.round(control.forward*100);$('#yaw-readout').textContent=Math.round(control.turn*100);$('#heave-readout').textContent=Math.round(control.vertical*100);}
|
||||
function updateReadouts(){ window.V2?.updateDemand(control);if(!canDrive()){$('#surge-readout').textContent='—';$('#yaw-readout').textContent='—';$('#heave-readout').textContent='—';return;}$('#surge-readout').textContent=Math.round(control.forward*100);$('#yaw-readout').textContent=Math.round(control.turn*100);$('#heave-readout').textContent=Math.round(control.vertical*100);}
|
||||
|
||||
window.setJoyControl=(x,y)=>{if(!window.canTouchControl())return;control.turn=x;control.forward=-y;updateReadouts();sendAxes();};
|
||||
function releaseHandle(el){el.classList.remove('pressed');el.classList.add('released');setTimeout(()=>el.classList.remove('released'),520)}
|
||||
@@ -216,8 +222,8 @@ heave.addEventListener('pointerdown',e=>{if(!window.canTouchControl()||heavePoin
|
||||
function easeHeave(target=0){cancelAnimationFrame(heaveAnim);const sv=control.vertical,start=performance.now();function frame(now){const p=Math.min(1,(now-start)/400),smooth=p*p*(3-2*p);setHeave(sv+(target-sv)*smooth);if(p<1)heaveAnim=requestAnimationFrame(frame)}heaveAnim=requestAnimationFrame(frame)}
|
||||
window.verticalControl={setPos:value=>{cancelAnimationFrame(heaveAnim);setHeave(value)},easeBack:()=>{if(heavePointer===null)easeHeave()},getState:()=>({value:control.vertical,active:heavePointer!==null})};
|
||||
|
||||
function applyKeys(){if(inputMode!=='keyboard'||!canDrive()){pressedKeys.clear();return;}const turn=(pressedKeys.has('d')||pressedKeys.has('e')?1:0)-(pressedKeys.has('a')||pressedKeys.has('q')?1:0),forward=(pressedKeys.has('w')?1:0)-(pressedKeys.has('s')?1:0),vertical=(pressedKeys.has('r')?1:0)-(pressedKeys.has('f')?1:0);if(!window.joystick3D?.getState().active)window.joystick3D?.easeTo(turn,-forward);if(heavePointer===null)easeHeave(vertical)}
|
||||
addEventListener('keydown',e=>{if(e.key==='F3'){e.preventDefault();if(!e.repeat&&view.action==='disarm')requestControl('disarm');return;}if(document.querySelector('dialog[open]')||/^(INPUT|SELECT|TEXTAREA)$/.test(e.target.tagName))return;if(e.key==='F2'){e.preventDefault();if(!e.repeat)startEngageHold();return;}const k=e.key.toLowerCase();if('wasdqerf'.includes(k)&&k.length===1){if(inputMode!=='keyboard'||!canDrive())return;e.preventDefault();if(e.repeat)return;if(!pressedKeys.has(k)){pressedKeys.add(k);applyKeys();}}});addEventListener('keyup',e=>{if(e.key==='F2'){e.preventDefault();cancelEngageHold(true);return;}const k=e.key.toLowerCase();if(pressedKeys.delete(k))applyKeys();});
|
||||
function applyKeys(){if(inputMode!=='keyboard'||!canDrive()){pressedKeys.clear();return;}const extra=window.V2?.filterDemand({lateral:(pressedKeys.has('l')?1:0)-(pressedKeys.has('j')?1:0),pitch:(pressedKeys.has('k')?1:0)-(pressedKeys.has('i')?1:0),roll:(pressedKeys.has('o')?1:0)-(pressedKeys.has('u')?1:0)});Object.assign(control,extra);updateReadouts();sendAxes();const turn=(pressedKeys.has('d')||pressedKeys.has('e')?1:0)-(pressedKeys.has('a')||pressedKeys.has('q')?1:0),forward=(pressedKeys.has('w')?1:0)-(pressedKeys.has('s')?1:0),vertical=(pressedKeys.has('r')?1:0)-(pressedKeys.has('f')?1:0);if(!window.joystick3D?.getState().active)window.joystick3D?.easeTo(turn,-forward);if(heavePointer===null)easeHeave(vertical)}
|
||||
addEventListener('keydown',e=>{if(e.key==='F3'){e.preventDefault();if(!e.repeat&&view.action==='disarm')requestControl('disarm');return;}if(document.querySelector('dialog[open]')||/^(INPUT|SELECT|TEXTAREA)$/.test(e.target.tagName))return;if(e.key==='F2'){e.preventDefault();if(!e.repeat)startEngageHold();return;}const k=e.key.toLowerCase();if('wasdqerfjlikuo'.includes(k)&&k.length===1){if(inputMode!=='keyboard'||!canDrive())return;e.preventDefault();if(e.repeat)return;if(!pressedKeys.has(k)){pressedKeys.add(k);applyKeys();}}});addEventListener('keyup',e=>{if(e.key==='F2'){e.preventDefault();cancelEngageHold(true);return;}const k=e.key.toLowerCase();if(pressedKeys.delete(k))applyKeys();});
|
||||
function resetInputs(){
|
||||
controllerMotion.reset();controllerNeedCenter=true;controllerWasDriving=false;
|
||||
if(gainPointer!==null)releaseGain({pointerId:gainPointer});
|
||||
@@ -226,7 +232,7 @@ function resetInputs(){
|
||||
const pointer=heavePointer;heavePointer=null;cancelAnimationFrame(heaveAnim);
|
||||
if(pointer!==null)try{heave.releasePointerCapture(pointer)}catch(_){}
|
||||
hknob.classList.remove('pressed','released');
|
||||
control.forward=control.turn=control.vertical=0;setHeave(0,false);
|
||||
for(const axis of Object.keys(control))control[axis]=0;setHeave(0,false);window.V2?.updateDemand(control);controllerActionState={};controllerActionsReady=false;
|
||||
if(bridgeReady&&!demoActive)api('stop_controls',++inputSequence).catch(()=>{});
|
||||
}
|
||||
function focusChanged(focused){inputFocused=focused;if(!focused)resetInputs();render(vehicleSnapshot)}
|
||||
@@ -750,6 +756,7 @@ function render(s,confirmed=false){
|
||||
const previous=view;
|
||||
view=ControlState.present(s,{stale:bridgeStale,focused:inputFocused,pending:localRequest?.name,error:commandError});
|
||||
syncInputModeControls();
|
||||
window.V2?.render(s);
|
||||
if((previous.drive&&!view.drive)||(previous.action==='arm'&&view.action!=='arm'))resetInputs();
|
||||
appState=bridgeStale&&s.connected?'link_degraded':s.app_state||'disconnected';
|
||||
renderHeavyQuality(s.heavy_quality,s.connected,s.armed);
|
||||
@@ -964,6 +971,9 @@ async function boot(){
|
||||
batteryConfig=init.battery_config||{};
|
||||
linkConfig=init.link_config||{};
|
||||
motorSettings=init.motor_settings||motorSettings;
|
||||
controllerProfiles=init.controller_profiles||controllerProfiles;
|
||||
controllerConfig=null;
|
||||
window.V2?.setStation(init.station_settings||{});
|
||||
syncMotorScalingUI();
|
||||
fillBatteryForm();
|
||||
fillLinkForm();
|
||||
@@ -996,6 +1006,15 @@ function waitForBridge(){
|
||||
if(hasDesktopApi())boot();
|
||||
setTimeout(waitForBridge,250);
|
||||
}
|
||||
window.V2.init({
|
||||
api,bridge:()=>bridgeReady,demo:()=>demoActive,toast,open:openModal,close:closeModal,
|
||||
locked:inputConfigurationLocked,reset:resetInputs,touch:()=>window.canTouchControl(),demand:()=>control,
|
||||
axis:(name,value)=>{control[name]=value;updateReadouts();sendAxes()},
|
||||
scaling:allowed=>{const toggle=$('#motor-scale-independent');toggle.disabled=!allowed||inputConfigurationLocked();toggle.title=allowed?'Independent scales':'SimpleROV 3 only';if(!allowed&&motorSettings.independent){motorSettings.independent=false;syncMotorScalingUI();servoSignature=''}},
|
||||
demoAccessory:(name,enabled)=>{if(demoState){demoState.accessories??={};demoState.accessories[name]={requested:enabled,accepted:true,demo:true};render(demoState)}},
|
||||
previewFrame:(frame,count)=>{if(demoState){demoState.capabilities={frame,firmware:[4,5,3]};for(let i=1;i<=16;i++)demoState.servos[String(i)]={function:i<=count?32+i:0,label:i<=count?`Motor ${i}`:'Unassigned',motor:i<=count?i:null,direction:i<=count?1:null};servoSignature='';render(demoState)}}
|
||||
});
|
||||
|
||||
addEventListener('pywebviewready',boot);
|
||||
render(vehicleSnapshot);
|
||||
if(document.readyState==='loading'){
|
||||
@@ -1003,6 +1022,3 @@ if(document.readyState==='loading'){
|
||||
}else{
|
||||
waitForBridge();
|
||||
}
|
||||
|
||||
|
||||
requestAnimationFrame(pollController);
|
||||
|
||||
Reference in New Issue
Block a user