Save pre-V2 motor controls and UI checkpoint
This commit is contained in:
+104
-55
@@ -6,7 +6,8 @@ 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,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}, 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=[];
|
||||
let lastReportedStatus='';
|
||||
@@ -448,31 +449,50 @@ if(customPortBtn&&customPortInput){
|
||||
}
|
||||
const engageButton=$('#engage');
|
||||
const engageProgressLabel=engageButton?.querySelector('.engage-progress-label');
|
||||
let engageHoldTimer=null,engageStart=0,engagePointer=null,completedEngagePointer=null,suppressEngageClick=false,engageVisualState='',engageTransitionTimer=null;
|
||||
const ENGAGE_HOLD_MS=1400;
|
||||
let engageLabelValue=$('#engage-label')?.textContent||'engage';
|
||||
let engageHoldTimer=null,engageStart=0,engagePointer=null,completedEngagePointer=null,suppressEngageClick=false,engageVisualState='',engageTransitionTimer=null,engageAppendFrame=null;
|
||||
const ENGAGE_HOLD_MS=600;
|
||||
|
||||
function applyEngageVisual(next){
|
||||
if(!engageButton)return;
|
||||
const label=({'ENGAGE':'engage','DISARM':'disengage','ENGAGING…':'engaging…','DISARMING…':'disengaging…','DISARM · ?':'disengage · ?','STATE UNKNOWN':'state unknown'})[next.label]||next.label.toLowerCase();
|
||||
// Interaction updates immediately; the short fade never gates emergency access.
|
||||
const label=({'ENGAGE':'engage','DISARM':'disengage','ENGAGING…':'engaging','DISARMING…':'disengaging','DISARM · ?':'disengage · ?','STATE UNKNOWN':'state unknown'})[next.label]||next.label.toLowerCase();
|
||||
// Update the state label without delaying the requested control action.
|
||||
engageButton.disabled=!next.action;
|
||||
engageButton.setAttribute('aria-busy',String(next.pending));
|
||||
engageButton.setAttribute('aria-label',label);
|
||||
engageButton.title=next.action==='disarm'?'Request disarm immediately (F3)':next.action==='arm'?'Hold 1.4 seconds to engage (F2)':next.message;
|
||||
engageButton.title=next.action==='disarm'?'Request disarm immediately (F3)':next.action==='arm'?'Hold 0.6 seconds to engage (F2)':next.message;
|
||||
const key=`${next.label}/${next.tone}`;
|
||||
if(engageVisualState===key)return;
|
||||
const previous=engageVisualState;engageVisualState=key;
|
||||
if(engageAppendFrame)cancelAnimationFrame(engageAppendFrame);
|
||||
clearTimeout(engageTransitionTimer);
|
||||
engageButton.classList.remove('arm','danger','subtle','charged','pending');
|
||||
engageButton.classList.add(next.tone==='danger'?'danger':next.tone==='pending'?'pending':'arm');
|
||||
engageButton.style.setProperty('--engage-progress',next.pending?'100%':'0%');
|
||||
$('#engage-label').textContent=label;
|
||||
const mainLabel=$('#engage-label'),previousLabel=engageLabelValue;
|
||||
const appended=previousLabel==='engage'&&label==='engaging'||previousLabel==='disengage'&&label==='disengaging';
|
||||
let suffix=null;
|
||||
if(appended){
|
||||
const previousWidth=mainLabel.getBoundingClientRect().width;
|
||||
const stem=previousLabel.slice(0,-1);
|
||||
mainLabel.replaceChildren(document.createTextNode(stem));
|
||||
suffix=document.createElement('span');suffix.className='engage-suffix';suffix.textContent=label.slice(stem.length);mainLabel.append(suffix);
|
||||
const appendedWidth=mainLabel.getBoundingClientRect().width;
|
||||
mainLabel.style.setProperty('--engage-append-shift',`${(appendedWidth-previousWidth)/2}px`);
|
||||
mainLabel.classList.add('is-appending');
|
||||
}else{
|
||||
mainLabel.classList.remove('is-appending');
|
||||
mainLabel.style.removeProperty('--engage-append-shift');
|
||||
mainLabel.textContent=label;
|
||||
}
|
||||
engageLabelValue=label;
|
||||
if(engageProgressLabel)engageProgressLabel.textContent=label;
|
||||
const sheen=engageButton.querySelector('.engage-sheen-label');if(sheen)sheen.textContent=label;
|
||||
if(previous){
|
||||
engageButton.classList.remove('state-transition');void engageButton.offsetWidth;
|
||||
engageButton.classList.add('state-transition');
|
||||
engageTransitionTimer=setTimeout(()=>engageButton.classList.remove('state-transition'),240);
|
||||
if(suffix)engageAppendFrame=requestAnimationFrame(()=>suffix.isConnected&&suffix.classList.add('is-visible'));
|
||||
engageTransitionTimer=setTimeout(()=>engageButton.classList.remove('state-transition'),200);
|
||||
}
|
||||
}
|
||||
function completeEngage(){
|
||||
@@ -592,7 +612,22 @@ async function refreshConsole(){
|
||||
finally{consoleRefreshBusy=false}
|
||||
}
|
||||
setInterval(refreshConsole,750);
|
||||
function setGain(value){const gain=$('#gain');gain.value=Math.max(+gain.min,Math.min(+gain.max,value));$('#gain-value').textContent=`${gain.value}%`;if(bridgeReady)api('set_gain',+gain.value)}
|
||||
function syncMotorScalingUI(){
|
||||
const independent=!!motorSettings.independent;
|
||||
document.body.dataset.motorScaling=independent?'independent':'shared';
|
||||
const toggle=$('#motor-scale-independent');if(toggle)toggle.checked=independent;
|
||||
const gain=$('#gain'),down=$('#gain-down'),up=$('#gain-up');
|
||||
if(gain){gain.disabled=independent;gain.title=independent?'Set motor scales in Motor setup':'Shared limit for every motor';}
|
||||
if(down)down.disabled=independent;if(up)up.disabled=independent;
|
||||
}
|
||||
function pushMotorSettings(persist=true){
|
||||
if(!bridgeReady)return Promise.resolve(motorSettings);
|
||||
const payload=JSON.parse(JSON.stringify(motorSettings));
|
||||
motorSettingsQueue=motorSettingsQueue.catch(()=>{}).then(()=>api('save_motor_settings',payload,persist)).then(saved=>{motorSettings=saved;syncMotorScalingUI();if(persist){servoSignature='';renderMotors(vehicleSnapshot.servos||{});toast('Motor settings saved')}return saved}).catch(error=>{setFeedback($('#motor-feedback'),error.message||'Could not save motor settings.');throw error});
|
||||
return motorSettingsQueue;
|
||||
}
|
||||
function motorScale(motor){return motorSettings.independent?(motorSettings.motors[String(motor)]?.scale??100):+$('#gain').value}
|
||||
function setGain(value){if(motorSettings.independent)return;const gain=$('#gain');gain.value=Math.max(+gain.min,Math.min(+gain.max,value));$('#gain-value').textContent=`${gain.value}%`;servoSignature='';renderMotors(vehicleSnapshot.servos||{});if(bridgeReady)api('set_gain',+gain.value)}
|
||||
$('#gain').oninput=e=>setGain(+e.target.value);$('#gain-down').onclick=()=>setGain(+$('#gain').value-5);$('#gain-up').onclick=()=>setGain(+$('#gain').value+5);
|
||||
// Explicit pointer ownership makes the full range rail a reliable touch target
|
||||
// in the embedded window, without depending on native range drag defaults.
|
||||
@@ -623,6 +658,10 @@ function fillBatteryForm(){for(const [k,v] of Object.entries(batteryConfig)){con
|
||||
$('#link-form').onsubmit=e=>{e.preventDefault();saveSettings(e.target,'save_link_settings',value=>linkConfig=value,'Link settings applied')};
|
||||
function fillLinkForm(){for(const [k,v] of Object.entries(linkConfig)){const el=$(`#link-form [name=${k}]`);if(el)el.value=v}}
|
||||
const motorEdits=new Map();
|
||||
$('#motor-scale-independent').onchange=e=>{
|
||||
if(e.target.checked&&!motorSettings.customized){for(let motor=1;motor<=12;motor++)motorSettings.motors[String(motor)].scale=+$('#gain').value;motorSettings.customized=true}
|
||||
motorSettings={...motorSettings,independent:e.target.checked};syncMotorScalingUI();servoSignature='';renderMotors(vehicleSnapshot.servos||{});pushMotorSettings(true).catch(()=>{});
|
||||
};
|
||||
async function requestMotorEdit(servo,kind,value){
|
||||
if(motorEdits.has(servo))return;
|
||||
const s=vehicleSnapshot.servos?.[String(servo)]||{};
|
||||
@@ -645,41 +684,40 @@ async function requestMotorEdit(servo,kind,value){
|
||||
}
|
||||
function renderMotors(servos){
|
||||
for(const [servo,edit] of motorEdits){
|
||||
const s=servos[String(servo)]||{};
|
||||
if((edit.kind==='function'?s.function:s.direction)===edit.expected){
|
||||
motorEdits.delete(servo);toast(`SERVO${servo} change confirmed`);
|
||||
}else if(!vehicleSnapshot.connected||performance.now()-edit.started>8000){
|
||||
motorEdits.delete(servo);
|
||||
setFeedback($('#motor-feedback'),`SERVO${servo}: confirmation not received. Check the reported assignment before retrying.`);
|
||||
}
|
||||
const state=servos[String(servo)]||{};
|
||||
if((edit.kind==='function'?state.function:state.direction)===edit.expected){motorEdits.delete(servo);toast(`SERVO${servo} change confirmed`)}
|
||||
else if(!vehicleSnapshot.connected||performance.now()-edit.started>8000){motorEdits.delete(servo);setFeedback($('#motor-feedback'),`SERVO${servo}: confirmation not received.`)}
|
||||
}
|
||||
const editable=!!vehicleSnapshot.connected&&!vehicleSnapshot.armed&&(demoActive||view.known);
|
||||
const assigned=Object.values(servos).filter(s=>s.motor).length;
|
||||
$('#motor-status').textContent=vehicleSnapshot.armed?'Disarm to change assignments or direction.':!vehicleSnapshot.connected?'Connect to read motor outputs.':assigned?`${assigned} motor${assigned===1?'':'s'} assigned · changes wait for vehicle confirmation.`:'Reading motor assignments…';
|
||||
const signature=JSON.stringify([servos,editable,[...motorEdits]]);
|
||||
if(signature===servoSignature)return;
|
||||
servoSignature=signature;
|
||||
const list=$('#motor-list'),expanded=list.querySelector('details')?.open;
|
||||
list.replaceChildren();
|
||||
const other=document.createElement('details');other.className='motor-other';other.open=!!expanded;
|
||||
const summary=document.createElement('summary');summary.textContent=`Other outputs (${16-assigned})`;other.append(summary);
|
||||
for(let i=1;i<=16;i++){
|
||||
const s=servos[String(i)]||{},pending=motorEdits.get(i),row=document.createElement('div');row.className='motor-row';
|
||||
row.innerHTML=`<strong>SERVO${i}</strong><select aria-label="SERVO${i} assignment"></select><span></span><button type="button" class="button subtle">Reverse</button>`;
|
||||
const select=row.querySelector('select');
|
||||
select.add(new Option(s.function==null?'Reading…':'Unassigned','0'));
|
||||
for(let motor=1;motor<=12;motor++)select.add(new Option(`Motor ${motor}`,String(motor)));
|
||||
select.value=String(s.motor||0);
|
||||
select.disabled=!editable||!!pending||s.function==null;
|
||||
select.onchange=e=>requestMotorEdit(i,'function',+e.target.value);
|
||||
row.querySelector('span').textContent=pending?'Confirming…':s.direction==null?'—':s.direction===-1?'Reversed':'Normal';
|
||||
const reverse=row.querySelector('button');reverse.disabled=!editable||!!pending||!s.motor||s.direction==null;
|
||||
reverse.setAttribute('aria-label',`Reverse SERVO${i} motor direction`);
|
||||
reverse.onclick=()=>requestMotorEdit(i,'direction');
|
||||
(s.motor?list:other).append(row);
|
||||
const assignments=Object.entries(servos).filter(([,state])=>state.motor).sort((a,b)=>a[1].motor-b[1].motor);
|
||||
const servoByMotor=new Map(assignments.map(([servo,state])=>[String(state.motor),{servo,...state}]));
|
||||
$('#motor-status').textContent=!vehicleSnapshot.connected?'Offline':vehicleSnapshot.armed?'Armed':`${assignments.length} outputs`;
|
||||
const shownMotors=[...new Set(assignments.map(([,state])=>state.motor))].sort((a,b)=>a-b);
|
||||
const structure=JSON.stringify([assignments.map(([port,state])=>[port,state.function,state.motor,state.direction]),editable,[...motorEdits],motorSettings.independent,shownMotors,Object.values(motorSettings.motors).map(m=>m.name)]);
|
||||
if(structure!==servoSignature){
|
||||
servoSignature=structure;const list=$('#motor-list');list.replaceChildren();
|
||||
for(const motor of shownMotors){
|
||||
const row=document.createElement('article');row.className='motor-row';row.dataset.motor=String(motor);
|
||||
row.innerHTML='<input class="motor-name" type="text" maxlength="24" autocomplete="off" aria-label="Motor name"><input class="motor-scale" type="range" min="0" max="100" step="1" aria-label="Motor output scale"><output class="motor-scale-value"></output><button type="button" class="button subtle motor-reverse">Reverse</button><span class="motor-channel"></span>';
|
||||
const name=row.querySelector('.motor-name');name.onchange=()=>{motorSettings.motors[String(motor)].name=name.value.trim().slice(0,24)||`Motor ${motor}`;name.value=motorSettings.motors[String(motor)].name;pushMotorSettings(true).catch(()=>{})};
|
||||
const scale=row.querySelector('.motor-scale');scale.oninput=()=>{motorSettings.customized=true;motorSettings.motors[String(motor)].scale=+scale.value;row.querySelector('output').textContent=`${scale.value}%`;pushMotorSettings(false).catch(()=>{})};scale.onchange=()=>pushMotorSettings(true).catch(()=>{});
|
||||
row.querySelector('.motor-reverse').onclick=()=>{const state=servoByMotor.get(String(motor));if(state?.servo)requestMotorEdit(state.servo,'direction')};list.append(row);
|
||||
}
|
||||
const details=document.createElement('details');details.className='motor-mapping';const summary=document.createElement('summary');summary.textContent='Assignments';details.append(summary);
|
||||
for(let i=1;i<=16;i++){
|
||||
const state=servos[String(i)]||{},pending=motorEdits.get(i),row=document.createElement('div');row.className='motor-map-row';row.innerHTML='<span class="motor-map-port"></span><select aria-label="Servo output motor assignment"></select><span class="motor-map-direction"></span>';
|
||||
row.querySelector('.motor-map-port').textContent=`SERVO${i}`;const select=row.querySelector('select');select.add(new Option(state.function==null?'Reading…':'Unassigned','0'));for(let motor=1;motor<=12;motor++)select.add(new Option(motorSettings.motors[String(motor)]?.name||`Motor ${motor}`,String(motor)));select.value=String(state.motor||0);select.disabled=!editable||!!pending||state.function==null;select.onchange=e=>requestMotorEdit(i,'function',+e.target.value);
|
||||
row.querySelector('.motor-map-direction').textContent=pending?'Saving…':state.direction==null?'—':state.direction===-1?'Reversed':'Normal';details.append(row);
|
||||
}
|
||||
list.append(details);
|
||||
}
|
||||
for(const motor of shownMotors){
|
||||
const row=$(`#motor-list .motor-row[data-motor="${motor}"]`),state=servoByMotor.get(String(motor))||{},settings=motorSettings.motors[String(motor)]||{name:`Motor ${motor}`,scale:20};if(!row)continue;
|
||||
const servo=state.servo||null,name=row.querySelector('.motor-name');if(document.activeElement!==name)name.value=settings.name;
|
||||
const scale=row.querySelector('.motor-scale');scale.disabled=!motorSettings.independent;if(document.activeElement!==scale)scale.value=motorScale(motor);
|
||||
row.querySelector('.motor-scale-value').textContent=`${motorScale(motor)}%`;row.querySelector('.motor-channel').textContent=servo?`SERVO${servo} · ${state.output_us||'—'} µs`:'Unassigned';
|
||||
const reverse=row.querySelector('.motor-reverse');reverse.disabled=!editable||!servo||state.direction==null||motorEdits.has(servo);reverse.textContent=state.direction===-1?'Restore':'Reverse';reverse.setAttribute('aria-label',`${reverse.textContent} ${settings.name}`);
|
||||
}
|
||||
if(!assigned){const empty=document.createElement('p');empty.className='note';empty.textContent='Assigned motors will appear here.';list.append(empty)}
|
||||
if(assigned<16)list.append(other);
|
||||
}
|
||||
function renderHeavyQuality(test,connected,armed){
|
||||
const status=$('#heavy-quality-status'),results=$('#heavy-quality-results'),run=$('#heavy-quality-run'),stop=$('#heavy-quality-cancel');
|
||||
@@ -716,7 +754,8 @@ function render(s,confirmed=false){
|
||||
appState=bridgeStale&&s.connected?'link_degraded':s.app_state||'disconnected';
|
||||
renderHeavyQuality(s.heavy_quality,s.connected,s.armed);
|
||||
document.body.dataset.appState=appState;
|
||||
document.body.dataset.controlsEnabled=String(view.drive);
|
||||
const driveEnabled=view.drive;
|
||||
document.body.dataset.controlsEnabled=String(driveEnabled);
|
||||
document.body.dataset.telemetryStale=String(!view.known||!!s.battery_stale);
|
||||
$('#port').disabled=!!s.connected;
|
||||
$('#custom-port-input').disabled=!!s.connected||$('#link-form').dataset.busy==='true';
|
||||
@@ -751,7 +790,7 @@ function render(s,confirmed=false){
|
||||
controlStatus.classList.toggle('is-error',view.error);
|
||||
controlStatus.hidden=!(view.pending||view.error||(view.known&&s.armed&&!inputFocused));
|
||||
const motorBtn=$('[data-open="motors-modal"]');
|
||||
if(motorBtn)motorBtn.disabled=(!isOnline);
|
||||
if(motorBtn)motorBtn.disabled=false;
|
||||
|
||||
const confirmedCommand=previous.pending&&!view.pending&&view.known&&!view.error;
|
||||
const reported=commandError||s.command_error||((!view.known&&s.connected&&appState!=='connecting')?'Telemetry stale — vehicle state unknown':s.status);
|
||||
@@ -762,16 +801,25 @@ function render(s,confirmed=false){
|
||||
updateReadouts();
|
||||
const b=s.battery;
|
||||
if(b){
|
||||
$('#battery-title').textContent=`${b.voltage.toFixed(2)} V`;
|
||||
$('#battery-percent').textContent=`${Math.round(b.percent)}%`;
|
||||
$('#battery-title').textContent=Number.isFinite(b.voltage)?`${b.voltage.toFixed(2)} V`:'Voltage unavailable';
|
||||
$('#battery-percent').textContent=Number.isFinite(b.percent)?`${Math.round(b.percent)}%`:'—';
|
||||
$('#battery-live-voltage').textContent=Number.isFinite(b.voltage)?`${b.voltage.toFixed(2)} V`:'—';
|
||||
$('#battery-live-current').textContent=Number.isFinite(b.current_a)?`${b.current_a.toFixed(1)} A`:'Unavailable';
|
||||
$('#battery-live-consumed').textContent=Number.isFinite(b.consumed_mah)?`${Math.round(b.consumed_mah).toLocaleString()} mAh`:'Unavailable';
|
||||
$('#battery-live-consumed-source').textContent=b.consumed_source||'';
|
||||
$('#battery-live-remaining').textContent=Number.isFinite(b.remaining_mah)?`${Math.round(b.remaining_mah).toLocaleString()} mAh`:Number.isFinite(b.remaining_percent)?`${Math.round(b.remaining_percent)}%`:'Unavailable';
|
||||
$('#battery-live-note').textContent=s.battery_stale?'Battery telemetry is stale. Check the telemetry rate and battery monitor.':b.charge_source==='Voltage estimate'?'Charge percentage is estimated from voltage. Add a calibrated current sensor for consumption readings.':'Live values come from the flight controller; accuracy depends on its battery monitor setup.';
|
||||
$('.battery-card').dataset.stale=String(!view.known||!!s.battery_stale);
|
||||
$('.battery-card').title=`Estimated charge from voltage · ${b.cells}S${!view.known||s.battery_stale?' · stale reading':''} · Battery settings`;
|
||||
$('#battery-fill').style.width=`${b.percent}%`;
|
||||
$('.battery-card').title=`${b.charge_source||'Battery status'} · ${b.cells}S${!view.known||s.battery_stale?' · stale reading':''} · Battery settings`;
|
||||
$('#battery-fill').style.width=`${Number.isFinite(b.percent)?b.percent:0}%`;
|
||||
$('#battery-fill').style.background=b.low?'var(--battery-low)':'var(--battery-charge)';
|
||||
$('.battery-card').dataset.low=String(!!b.low);
|
||||
}else{
|
||||
$('#battery-title').textContent='No voltage';
|
||||
$('#battery-percent').textContent='—';
|
||||
$('#battery-live-voltage').textContent='—';$('#battery-live-current').textContent='—';
|
||||
$('#battery-live-consumed').textContent='—';$('#battery-live-consumed-source').textContent='';
|
||||
$('#battery-live-remaining').textContent='—';$('#battery-live-note').textContent='Waiting for battery telemetry.';
|
||||
$('.battery-card').dataset.stale='false';
|
||||
$('.battery-card').title='Waiting for battery voltage · Battery settings';
|
||||
$('#battery-fill').style.width='0';
|
||||
@@ -912,20 +960,22 @@ async function boot(){
|
||||
if(booted||booting||!hasDesktopApi())return;
|
||||
booting=true;clearTimeout(bootRetry);
|
||||
try{
|
||||
// OS port discovery must not depend on model/config initialization succeeding.
|
||||
populatePorts(await withTimeout(api('refresh_ports'),4000));
|
||||
const init=await withTimeout(api('initial_state'),4000);
|
||||
batteryConfig=init.battery_config||{};
|
||||
linkConfig=init.link_config||{};
|
||||
populatePorts(init.ports);
|
||||
motorSettings=init.motor_settings||motorSettings;
|
||||
syncMotorScalingUI();
|
||||
fillBatteryForm();
|
||||
fillLinkForm();
|
||||
bridgeReady=true;booted=true;lastBridgeAt=performance.now();
|
||||
if(!demoActive)render(init.state);
|
||||
poll();
|
||||
// Populate the serial selector before starting the optional model load.
|
||||
// A large cached mesh must never delay an operator seeing USB COM ports.
|
||||
loadDefaultModel();
|
||||
// COM discovery can block on Windows drivers; finish the first paint first.
|
||||
setTimeout(()=>withTimeout(api('refresh_ports'),4000).then(populatePorts).catch(()=>{}),0);
|
||||
// Let the controls paint before fetching and parsing the large STL mesh.
|
||||
const startModelLoad=()=>{void loadDefaultModel()};
|
||||
if('requestIdleCallback' in window)requestIdleCallback(startModelLoad,{timeout:2500});
|
||||
else setTimeout(startModelLoad,1500);
|
||||
if(!portRefreshTimer)portRefreshTimer=setInterval(async()=>{
|
||||
if(appState==='disconnected'||demoActive){
|
||||
try{populatePorts(await withTimeout(api('refresh_ports'),1500))}
|
||||
@@ -949,9 +999,8 @@ function waitForBridge(){
|
||||
addEventListener('pywebviewready',boot);
|
||||
render(vehicleSnapshot);
|
||||
if(document.readyState==='loading'){
|
||||
addEventListener('DOMContentLoaded',()=>{loadDefaultModel();waitForBridge()});
|
||||
addEventListener('DOMContentLoaded',waitForBridge);
|
||||
}else{
|
||||
loadDefaultModel();
|
||||
waitForBridge();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user