From bd3e9b20d14798bb05b102b7a188d6ef4381b596 Mon Sep 17 00:00:00 2001 From: PitresAli Date: Wed, 26 Aug 2026 11:48:16 +0200 Subject: [PATCH] Make demo mode fully frontend-only --- SUB Control/web/app.js | 25 ++++++++++++++++++------- SUB Control/web/index.html | 8 ++++---- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/SUB Control/web/app.js b/SUB Control/web/app.js index 810e853..f9a8bd7 100644 --- a/SUB Control/web/app.js +++ b/SUB Control/web/app.js @@ -6,8 +6,10 @@ 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}, keys=new Set(); let bridgeReady=false, appState='disconnected', batteryConfig={}, servoSignature='', lastAxisSend=0, axisTimer=0; +const control={forward:0,turn:0,vertical:0}, keys=new Set(); let bridgeReady=false, appState='disconnected', batteryConfig={}, servoSignature='', lastAxisSend=0, axisTimer=0, demoActive=false, demoState=null; const api=(name,...args)=>window.pywebview.api[name](...args); +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,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},servos}} +function updateDemo(patch){Object.assign(demoState,patch);render(demoState)} addEventListener('pointermove',e=>{const button=e.target.closest?.('button:not(.battery-card), .stick');if(!button)return;const rect=button.getBoundingClientRect();button.style.setProperty('--glow-x',`${e.clientX-rect.left}px`);button.style.setProperty('--glow-y',`${e.clientY-rect.top}px`)}); 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)} @@ -49,7 +51,7 @@ function populatePorts(ports){ p.value=allList[0].device; } } -function sendAxes(){if(!bridgeReady||appState!=='armed')return;const transmit=()=>{lastAxisSend=performance.now();axisTimer=0;api('set_axes',control.forward,control.turn,control.vertical).catch(e=>toast(e.message,true))},wait=50-(performance.now()-lastAxisSend);if(wait<=0)transmit();else{clearTimeout(axisTimer);axisTimer=setTimeout(transmit,wait)}} +function sendAxes(){if(appState!=='armed')return;if(demoActive){demoState.status=`Demo controls • surge ${control.forward.toFixed(2)} • yaw ${control.turn.toFixed(2)} • heave ${control.vertical.toFixed(2)}`;$('#status-text').textContent=demoState.status;return}if(!bridgeReady)return;const transmit=()=>{lastAxisSend=performance.now();axisTimer=0;api('set_axes',control.forward,control.turn,control.vertical).catch(e=>toast(e.message,true))},wait=50-(performance.now()-lastAxisSend);if(wait<=0)transmit();else{clearTimeout(axisTimer);axisTimer=setTimeout(transmit,wait)}} function updateReadouts(){ if(appState!=='armed'){$('#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(appState!=='armed')return;control.turn=x;control.forward=-y;updateReadouts();sendAxes();}; @@ -169,6 +171,12 @@ $('#connect').onclick=async()=>{ const p=$('#port'); const port=p?p.value:''; + if(port==='demo'||demoActive){ + if(demoActive){demoActive=false;demoState=null;render({app_state:'disconnected',connected:false,link:'DISCONNECTED',vehicle:'No heartbeat',armed:false,mode:null,status:'Demo mode disconnected',battery:null,servos:{}});toast('Demo disconnected')} + else{demoActive=true;demoState=makeDemoState();render(demoState);toast('Demo connected — no hardware required')} + return; + } + if(appState==='connecting'){ label.textContent='Canceling…'; try{ await api('disconnect'); }catch(_){} @@ -247,7 +255,8 @@ function updateEngageProgress(){ stopEngageHold(false); engageButton.classList.add('charged'); setTimeout(()=>engageButton.classList.remove('charged'),600); - api('command','arm').then(()=>toast('Vehicle armed')).catch(e=>toast(e.message,true)); + if(demoActive){updateDemo({armed:true,app_state:'armed',vehicle:'DEMO • ARMED',status:'Demo: Arm confirmed'});toast('Demo vehicle armed')} + else api('command','arm').then(()=>toast('Vehicle armed')).catch(e=>toast(e.message,true)); return; } engageHoldTimer=requestAnimationFrame(updateEngageProgress); @@ -288,7 +297,8 @@ if(engageButton){ engageButton.onclick=async()=>{ if(appState==='armed'){ try{ - await api('command','disarm'); + if(demoActive)updateDemo({armed:false,app_state:'connected_disarmed',vehicle:'DEMO • DISARMED',status:'Demo: Disarm confirmed'}); + else await api('command','disarm'); toast('Disarm command sent'); }catch(e){ toast(e.message,true); @@ -298,7 +308,8 @@ if(engageButton){ settingMode=true; const lbl=$('#engage-label'); if(lbl)lbl.textContent='Setting mode…'; - await api('command','manual'); + if(demoActive)updateDemo({mode:'MANUAL',status:'Demo: Manual mode confirmed'}); + else await api('command','manual'); toast('Manual mode requested'); }catch(e){ settingMode=false; @@ -313,7 +324,7 @@ function setGain(value){const gain=$('#gain');gain.value=Math.max(+gain.min,Math $('#gain').oninput=e=>setGain(+e.target.value);$('#gain-down').onclick=()=>setGain(+$('#gain').value-5);$('#gain-up').onclick=()=>setGain(+$('#gain').value+5); $('#battery-form').onsubmit=async e=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.target));try{batteryConfig=await api('save_battery',data);closeModal($('#battery-modal'));toast('Battery settings saved')}catch(err){toast(err.message,true)}}; function fillBatteryForm(){for(const [k,v] of Object.entries(batteryConfig)){const el=$(`#battery-form [name=${k}]`);if(el)el.value=v}} -function renderMotors(servos){const signature=JSON.stringify(servos);if(signature===servoSignature)return;servoSignature=signature;const list=$('#motor-list');list.innerHTML='';for(let i=1;i<=16;i++){const s=servos[String(i)]||{},row=document.createElement('div');row.className='motor-row';const opts=['',...Array.from({length:12},(_,n)=>``)].join('');row.innerHTML=`SERVO${i}${s.direction==null?'—':s.direction===-1?'Reversed':'Normal'}`;row.querySelector('select').onchange=e=>api('set_servo_function',i,+e.target.value).catch(x=>toast(x.message,true));row.querySelector('button').onclick=()=>api('reverse_motor',i).catch(x=>toast(x.message,true));list.append(row)}} +function renderMotors(servos){const signature=JSON.stringify(servos);if(signature===servoSignature)return;servoSignature=signature;const list=$('#motor-list');list.innerHTML='';for(let i=1;i<=16;i++){const s=servos[String(i)]||{},row=document.createElement('div');row.className='motor-row';const opts=['',...Array.from({length:12},(_,n)=>``)].join('');row.innerHTML=`SERVO${i}${s.direction==null?'—':s.direction===-1?'Reversed':'Normal'}`;row.querySelector('select').onchange=e=>{if(demoActive){const motor=+e.target.value;demoState.servos[String(i)]={function:motor?32+motor:0,label:motor?`Motor ${motor}`:'Unassigned',motor:motor||null,direction:motor?1:null};servoSignature='';renderMotors(demoState.servos);toast(`Demo SERVO${i} updated`)}else api('set_servo_function',i,+e.target.value).catch(x=>toast(x.message,true))};row.querySelector('button').onclick=()=>{if(demoActive){if(s.motor){s.direction=s.direction===-1?1:-1;servoSignature='';renderMotors(demoState.servos);toast(`Demo Motor ${s.motor} reversed`)}else toast(`SERVO${i} is unassigned`,true)}else api('reverse_motor',i).catch(x=>toast(x.message,true))};list.append(row)}} function render(s){ appState=s.app_state||'disconnected'; document.body.dataset.appState=appState; @@ -394,7 +405,7 @@ function render(s){ renderMotors(s.servos); } -async function poll(){if(!bridgeReady)return;try{render(await api('poll'))}catch(e){}setTimeout(poll,250)} +async function poll(){if(!bridgeReady)return;if(!demoActive)try{render(await api('poll'))}catch(e){}setTimeout(poll,250)} let modelLoadPromise=null; async function loadDefaultModel(){ if(modelLoadPromise)return modelLoadPromise; diff --git a/SUB Control/web/index.html b/SUB Control/web/index.html index 5e0d258..3245025 100644 --- a/SUB Control/web/index.html +++ b/SUB Control/web/index.html @@ -4,7 +4,7 @@ Sub Control - +
@@ -88,8 +88,8 @@ Power

Battery Settings

configuration

Motor Outputs

Function and direction are read one parameter at a time to protect the low-bandwidth link.

OutputFunctionDirection
- - - + + +