Files
SC-F001/main/webpage_minified.html
2026-04-28 12:43:43 -05:00

10 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html><meta charset=utf-8><title>Control Panel</title><meta content="width=device-width,initial-scale=1.0" name=viewport><style>:root{--green:#38b000;--red:#c1121f;--yellow:#fd0;--bg:#1a1a1a;--surface:#262626;--surface-2:#333;--text:#f0f0f0;--text-dim:#a0a0a0;--border:#444;--accent:#ba965b}html,body{background-color:var(--bg);color:var(--text)}body{text-align:center;margin:0;padding:0}#wrapper{text-align:center;box-sizing:border-box}#content{max-width:500px;margin:auto;padding:0 10px}*{color:var(--text);background-color:#0000;font-family:system-ui,sans-serif;font-size:1.2rem}input,button{background-color:var(--surface);width:100%;color:var(--text);text-align:right;box-sizing:border-box;border:1px solid var(--border);border-radius:5px;margin:5px}input[type=text],input[type=number],input[type=time]{border:1px solid var(--accent);font-family:monospace}input[readonly]{background-color:var(--surface-2);color:var(--text-dim)}button{text-align:center;border:1px solid var(--accent);cursor:pointer;border-radius:5px}button:hover{background-color:var(--surface-2)}.changed,#commit_btn{background-color:var(--green)!important;color:#fff!important;border-color:var(--green)!important}#cancel_btn{background-color:var(--red)!important;color:#fff!important;border-color:var(--red)!important}#commit_btn,#cancel_btn{width:45%;margin-top:10px;padding:10px;font-weight:700}#commit_btn[disabled],#cancel_btn[disabled]{cursor:not-allowed;background-color:var(--surface-2)!important;color:var(--text-dim)!important;border-color:var(--border)!important}table{border-collapse:collapse;text-align:left;width:100%}td{border-bottom:1px solid var(--border);padding:8px}#log_viewer_table td{padding:2px 6px;font-size:.65rem}#log_viewer_table td:first-child{white-space:nowrap}summary{text-align:left;color:#fff;background-color:var(--red);cursor:pointer;border-radius:5px;padding:.3rem;font-weight:700}.action_row{align-items:stretch;gap:10px;padding:5px 0;display:flex}.action_row>button{flex:1 1 0;min-height:64px;margin:0}.cmd{white-space:normal;word-break:break-word;border:none;font-size:1.5rem;font-weight:700;line-height:1.15}.cmd_action{background-color:var(--green);color:#fff}.cmd_action.moving{background-color:var(--yellow);color:#1a1a1a}.cmd_estop{background-color:var(--red);color:#fff}#msg{text-align:center}#status_box{text-align:center;padding:6px 0}#status_state{color:var(--text);padding:4px 0;font-size:1.3rem;font-weight:700}#status_indicators{flex-wrap:wrap;justify-content:center;gap:6px;margin-top:4px;display:flex}#status_indicators:empty{display:none}.status_pill{color:#fff;background-color:var(--surface-2);white-space:nowrap;border-radius:12px;padding:3px 10px;font-size:.85rem;font-weight:700}.status_pill.error{background-color:var(--red)}.status_pill.warn{background-color:var(--yellow);color:#1a1a1a}.status_pill.info{background-color:var(--accent)}h1{color:var(--text);font-size:2.5rem}#popup-overlay{z-index:1000;background-color:#000000bf;justify-content:center;align-items:center;width:100%;height:100%;display:none;position:fixed;top:0;left:0}#popup-content{background-color:var(--surface);color:var(--text);border:1px solid var(--border);text-align:center;border-radius:10px;max-width:400px;padding:30px;box-shadow:0 4px 12px #00000080}#popup-content h2,#popup-content p{background-color:var(--surface);color:var(--text)}#popup-content h2{margin-top:0}#popup-content p{font-size:1.1rem}@media screen and (width<=350px){#content{max-width:100%;padding:0 5px}table:not(#log_viewer_table) tr td{box-sizing:border-box;width:100%;display:block}table:not(#log_viewer_table) tr{margin-bottom:10px;display:block}}#popup-buttons{background-color:var(--surface);flex-wrap:wrap;justify-content:center;gap:10px;margin-top:20px;display:flex}#popup-buttons button{background-color:var(--surface-2);color:var(--text);border:1px solid var(--accent);cursor:pointer;border-radius:5px;min-width:100px;padding:10px 20px;font-weight:700}#popup-buttons button:hover,#popup-buttons button.primary{background-color:var(--accent);color:#1a1a1a}#popup-buttons button.primary:hover{background-color:#d4b27e}#popup-input-container{background-color:var(--surface);margin:20px 0}#popup-input{border:1px solid var(--accent);background-color:var(--surface-2);width:100%;color:var(--text);text-align:center;border-radius:5px;padding:10px;font-size:1.1rem}.sqbtn{width:35%;padding:30px;font-weight:700}</style></head><body><div id=wrapper><div id=content><h1>CluckCommand</h1><div class=action_row><button class="cmd cmd_action" id=action_btn onclick=actionBtnClick()>START MOVE</button><button class="cmd cmd_estop" onclick="sendCommand('stop')">E-STOP</button></div><table><tr><td colspan=3><div id=status_box><div id=status_state></div><div id=status_indicators></div></div></td></tr><tr><td colspan=3><input id=UX_TIME readonly step=1 type=datetime-local> <button id=now_btn onclick=setTimeToNow()>Sync Time</button></td></tr><tr><td>Schedule Start</td><td><input id=UX_MOVE_START onchange=changeSchedule(this) type=time></td></tr><tr><td>Schedule End</td><td><input id=UX_MOVE_END onchange=changeSchedule(this) type=time></td></tr><tr><td># Moves/Day</td><td><input id=UX_NUM_MOVES min=0 onchange=changeSchedule(this) type=number></td></tr><tr><td>Next Move At</td><td><input id=UX_NEXT_ALARM readonly type=datetime-local></td></tr><tr><td>Leash (ft)</td><td><input id=UX_REM_DIST onchange=handleRemainingDistChange(this) type=number></td></tr><tr><td>Move Dist. (ft)</td><td><input id=UX_DRIVE_DIST min=0 onchange=changeSchedule(this) type=number></td></tr><tr><td>Jack Ht. (in)</td><td><input id=UX_JACK_DIST min=0 onchange=changeSchedule(this) type=number></td></tr><tr><td>Battery (V)</td><td><input id=voltage readonly></td></tr><tr><td>Program RF Remote</td><td><button onclick=programRFSequence()>Program All Buttons</button></td></tr><tr><td>Deep Sleep</td><td><button onclick="sendCommand('hibernate')">Enter Deep Sleep</button></td></tr></table><button disabled id=cancel_btn onclick=location.reload();>Discard</button><button disabled id=commit_btn onclick=commitParams()>Save Changes</button><br><br><details open><summary>REMOTE CONTROL</summary> <br> <button onmousedown="startRemote('fwd', event)" ontouchstart="startRemote('fwd', event)" class=sqbtn onmouseleave=stopRemote() onmouseup=stopRemote() ontouchend=stopRemote()>FWD</button> <button onmousedown="startRemote('rev', event)" ontouchstart="startRemote('rev', event)" class=sqbtn onmouseleave=stopRemote() onmouseup=stopRemote() ontouchend=stopRemote()>REV</button> <button onmousedown="startRemote('up', event)" ontouchstart="startRemote('up', event)" class=sqbtn onmouseleave=stopRemote() onmouseup=stopRemote() ontouchend=stopRemote()>UP</button> <button onmousedown="startRemote('down', event)" ontouchstart="startRemote('down', event)" class=sqbtn onmouseleave=stopRemote() onmouseup=stopRemote() ontouchend=stopRemote()>DOWN</button> <button onmousedown="startRemote('aux', event)" ontouchstart="startRemote('aux', event)" class=sqbtn onmouseleave=stopRemote() onmouseup=stopRemote() ontouchend=stopRemote()>AUX</button></details><br><details><summary>WiFi Settings</summary> <table><tr><td>AP SSID</td><td><input id=PARAM_WIFI_SSID onchange=markChanged(this)></td></tr><tr><td>AP Password</td><td><input id=PARAM_WIFI_PASS onchange=markChanged(this)></td></tr><tr><td></td><td><button onclick=applyWifiSettings()>Apply WiFi Settings</button></td></tr></table></details><br><details id=log_viewer_details><summary style=background-color:var(--surface-2)>EVENT LOG</summary> <div style="box-sizing:border-box;width:100vw;margin-left:-50vw;padding:0 8px;position:relative;left:50%" id=log_viewer_body><button style="width:auto;padding:4px 12px" onclick=loadLogViewer()>Refresh</button><div id=log_viewer_msg style=padding:8px;font-size:.75rem></div><div style=overflow-x:auto><table id=log_viewer_table style=white-space:nowrap;font-family:monospace;font-size:.65rem></table></div></div></details><br><details><summary>DANGER ZONE</summary> <table><tr><td>Calibration</td><td><button onclick="calibrate('jack')">Jack Calibration</button> <button onclick="calibrate('drive')">Drive Calibration</button></td></tr><tr><td>Current Version</td><td><input id=version readonly></td></tr><tr><td>Firmware</td><td><input accept=.bin id=firmware_file style=display:none type=file> <button id=upload_btn onclick=uploadFirmware()>Upload Firmware</button></td></tr><tr><td>Log File</td><td><button id=log_btn onclick=downloadLogFile()>Download Log</button></td></tr></table> <table id=table></table> <button onclick="sendCommand('reboot')" class=cmd style=background-color:var(--red);color:#fff>REBOOT</button> <button onclick="sendCommand('sleep')" class=cmd style=background-color:var(--surface-2);color:var(--text)>SLEEP</button></details></div></div><div id=popup-overlay><div id=popup-content><h2 id=popup-title></h2><p id=popup-message></p><div id=popup-input-container style=display:none><input id=popup-input></div><div id=popup-buttons></div></div></div><script>let data={},paramTableCreated=!1,pollInterval=null,modalResolve=null;const ge=id=>document.getElementById(id);function showModal(title,message,buttons=[],options={}){let overlay=document.getElementById(`popup-overlay`),titleEl=document.getElementById(`popup-title`),messageEl=document.getElementById(`popup-message`),buttonsEl=document.getElementById(`popup-buttons`),inputContainer=document.getElementById(`popup-input-container`),inputEl=document.getElementById(`popup-input`);titleEl.textContent=title,messageEl.innerHTML=message,options.showInput?(inputContainer.style.display=`block`,inputEl.type=options.inputType||`text`,inputEl.value=options.inputValue||``,inputEl.placeholder=options.inputPlaceholder||``,setTimeout(()=>inputEl.focus(),100)):inputContainer.style.display=`none`,buttonsEl.innerHTML=``;let buttonElements=[];buttons.forEach(btn=>{let button=document.createElement(`button`);button.textContent=btn.text,btn.primary&&button.classList.add(`primary`),button.onclick=()=>{let result=options.showInput?{button:btn.value,input:inputEl.value}:btn.value;modalResolve&&=(modalResolve(result),null),hideModal(),btn.callback&&btn.callback(result)},buttonsEl.appendChild(button),buttonElements.push({button,btn})});let keyHandler=e=>{if(e.key===`Enter`){e.preventDefault();let primaryBtn=buttonElements.find(b=>b.btn.primary);primaryBtn?primaryBtn.button.click():buttonElements.length>0&&buttonElements[buttonElements.length-1].button.click()}else if(e.key===`Escape`){e.preventDefault();let cancelBtn=buttonElements.find(b=>!b.btn.primary);cancelBtn?cancelBtn.button.click():buttonElements.length>0&&buttonElements[0].button.click()}};window.modalKeyHandler&&document.removeEventListener(`keydown`,window.modalKeyHandler),window.modalKeyHandler=keyHandler,document.addEventListener(`keydown`,keyHandler),overlay.style.display=`flex`}function hideModal(){let overlay=document.getElementById(`popup-overlay`);overlay.style.display=`none`,modalResolve&&=(modalResolve(!1),null),window.modalKeyHandler&&(document.removeEventListener(`keydown`,window.modalKeyHandler),window.modalKeyHandler=null)}function modalConfirm(message,title=`Confirm`){return new Promise(resolve=>{modalResolve=resolve,showModal(title,message,[{text:`Cancel`,value:!1},{text:`OK`,value:!0,primary:!0}])})}function modalAlert(message,title=`Notice`){return new Promise(resolve=>{modalResolve=resolve,showModal(title,message,[{text:`OK`,value:!0,primary:!0}])})}function modalPrompt(message,title=`Input`,defaultValue=``){return new Promise(resolve=>{modalResolve=result=>{result&&result.button?resolve(result.input):resolve(null)},showModal(title,message,[{text:`Cancel`,value:!1},{text:`OK`,value:!0,primary:!0}],{showInput:!0,inputType:`text`,inputValue:defaultValue,inputPlaceholder:``})})}function showRebootModal(){showModal(`Device Rebooting`,`Page will refresh in <span id="popup-countdown">5</span> seconds...`,[]);let countdown=5,countdownInterval=setInterval(()=>{countdown--;let countdownEl=document.getElementById(`popup-countdown`);countdownEl&&(countdownEl.textContent=countdown),countdown<=0&&(clearInterval(countdownInterval),location.reload())},1e3)}function showRTCSyncModal(){showModal(`Time Not Set`,`The device clock needs to be synchronized.`,[{text:`Sync Time`,value:!0,primary:!0,callback:async()=>{setTimeToNow(),await commitParams()}}])}let warnedOfDesync=!1;function showDesyncModal(driftMinutes){warnedOfDesync||(warnedOfDesync=!0,showModal(`Clock De-Synced`,`The device clock is off by ${driftMinutes} minute${driftMinutes===1?``:`s`}. Sync it now?`,[{text:`Sync Time`,value:!0,primary:!0,callback:async()=>{setTimeToNow(),await commitParams()}},{text:`Dismiss`,value:!1}]))}const warnedOfEOT=!1;function showEOTModal(){warnedOfEOT||(warnedOfEOT=!0,showModal(`Out of Travel`,`Device cannot move until more travel distance is allowed.`,[{text:`OK`,value:!0,primary:!0}]))}let intervalId=null;function remote(command){fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:command})}).catch(()=>{});try{navigator.vibrate(200)}catch{}}function startRemote(command,event){event&&event.preventDefault(),intervalId&&clearInterval(intervalId),remote(command),intervalId=setInterval(()=>{remote(command)},150)}function stopRemote(){intervalId&&=(clearInterval(intervalId),null)}function actionBtnClick(){sendCommand(!data.state||data.state===0?`start`:`undo`)}async function sendCommand(cmdName){if(!(cmdName===`start`&&!await modalConfirm(`Will begin moving - please confirm.`))&&!(cmdName===`reboot`&&!await modalConfirm(`Device will reboot - clearing clock and distance. Are you sure?`))&&!(cmdName===`sleep`&&!await modalConfirm(`Device will sleep. Are you sure?`))&&!(cmdName===`hibernate`&&!await modalConfirm(`Device will enter deep sleep — clock will be cleared, only the physical button can wake it. Are you sure?`)))try{let response=await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:cmdName})});if(!response.ok){await modalAlert(`Command '${cmdName}' failed: ${response.status} ${response.statusText}`);return}cmdName===`reboot`?showRebootModal():setTimeout(fetchStatus,200)}catch(e){console.log(e),await modalAlert(`Network error: ${e.message}`)}}async function calibrate(type){let cmdName=type===`jack`?`cal_jack`:`cal_drive`;if(await modalConfirm(`This will calibrate the ${type}. Continue?`))try{let response=await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:cmdName})});if(!response.ok){await modalAlert(`Calibration failed: ${response.status} ${response.statusText}`);return}await modalAlert(`${type.charAt(0).toUpperCase()+type.slice(1)} calibration started.`),setTimeout(fetchStatus,200)}catch(e){await modalAlert(`Network error: ${e.message}`)}}function setTimeToNow(){let now=/* @__PURE__ */ new Date,input=ge(`UX_TIME`);input.value=`${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,`0`)}-${String(now.getDate()).padStart(2,`0`)}T${String(now.getHours()).padStart(2,`0`)}:${String(now.getMinutes()).padStart(2,`0`)}:${String(now.getSeconds()).padStart(2,`0`)}`,markChanged(input)}function markChanged(el){el.classList.add(`changed`),ge(`commit_btn`).disabled=!1,ge(`cancel_btn`).disabled=!1}function handleRemainingDistChange(input){markChanged(input)}const scheduleInputs=[`MOVE_START`,`MOVE_END`,`NUM_MOVES`,`DRIVE_DIST`,`JACK_DIST`];function changeSchedule(ux_input){if(param_input=ge(`PARAM_${ux_input.id.substring(3)}`),markChanged(ux_input),markChanged(param_input),ux_input.type===`time`){let[hours,minutes]=ux_input.value.split(`:`).map(Number);param_input.value=(hours*60+minutes)*60}else param_input.value=parseFloat(ux_input.value)||0}function updateScheduleInputs(){for(let i=0;i<scheduleInputs.length;i++)if(ux_input=ge(`UX_${scheduleInputs[i]}`),param_input=ge(`PARAM_${scheduleInputs[i]}`),!(!ux_input||!param_input)&&!(ux_input.classList.contains(`changed`)||document.activeElement===ux_input))if(ux_input.type===`time`){let hours=Math.floor(param_input.value/3600),minutes=Math.floor(param_input.value%3600/60);ux_input.value=`${String(hours).padStart(2,`0`)}:${String(minutes).padStart(2,`0`)}`}else ux_input.value=param_input.value}async function commitParams(){let changedInputs=document.querySelectorAll(`input.changed`);if(changedInputs.length===0)return;let payload={};for(let input of changedInputs){let id=input.id;if(id===`UX_TIME`){let dt=new Date(input.value),year=dt.getFullYear(),month=dt.getMonth(),day=dt.getDate(),hours=dt.getHours(),minutes=dt.getMinutes(),seconds=dt.getSeconds();payload.time=Math.floor(Date.UTC(year,month,day,hours,minutes,seconds)/1e3)}else if(id===`UX_REM_DIST`)payload.remaining_dist=parseFloat(input.value)||0;else if(id.startsWith(`PARAM_`)){let paramName=id.substring(6),value=input.value;input.type===`number`&&(value=parseFloat(input.value)||0),payload.parameters||={},payload.parameters[paramName]=value}}try{console.log(`Sending data:`,payload);let response=await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(payload)});if(!response.ok){await modalAlert(`Failed to save changes: ${response.status} ${response.statusText}`);return}changedInputs.forEach(input=>input.classList.remove(`changed`)),ge(`commit_btn`).disabled=!0,ge(`cancel_btn`).disabled=!0,setTimeout(fetchStatus,200)}catch(e){await modalAlert(`Network error: ${e.message}`)}}async function fetchStatus(){try{let response=await fetch(`./get`);if(!response.ok){console.error(`Failed to fetch status:`,response.status);return}data=await response.json(),console.log(`Got data:`,data),updateUI()}catch(e){console.error(`Error fetching status:`,e)}}function _safeSet(el,value){el&&document.activeElement!==el&&(el.classList&&el.classList.contains(`changed`)||(el.value=value))}function updateUI(){if(!data.rtc_set)showRTCSyncModal();else if(data.time!==void 0){let n=/* @__PURE__ */ new Date,localAsUtc=Math.floor(Date.UTC(n.getFullYear(),n.getMonth(),n.getDate(),n.getHours(),n.getMinutes(),n.getSeconds())/1e3),driftS=Math.abs(localAsUtc-data.time);driftS>300&&showDesyncModal(Math.round(driftS/60))}{let state=Array.isArray(data.msg)?data.msg[0]||``:data.msg||``,INDICATORS=[[`safety_trip`,`SAFETY BREAK`,`error`],[`efuse_drive`,`DRIVE EFUSE`,`error`],[`efuse_jack`,`JACK EFUSE`,`error`],[`efuse_aux`,`AUX EFUSE`,`error`],[`low_battery`,`LOW BATTERY`,`warn`],[`leash_hit`,`LEASH LIMIT`,`warn`],[`rtc_not_set`,`CLOCK NOT SET`,`warn`]],errs=data.errors||{},activePills=INDICATORS.filter(([k])=>errs[k]),stateEl=ge(`status_state`);stateEl.textContent=state||``,stateEl.classList.toggle(`error`,activePills.length>0);let actionBtn=ge(`action_btn`);if(actionBtn){let isIdle=!data.state||data.state===0;actionBtn.textContent=isIdle?`START MOVE`:`UNDO`,actionBtn.classList.toggle(`moving`,!isIdle)}let box=ge(`status_indicators`);box.innerHTML=``;for(let[,label,kind]of activePills){let pill=document.createElement(`span`);pill.className=`status_pill `+kind,pill.textContent=label,box.appendChild(pill)}}data.voltage!==void 0&&_safeSet(ge(`voltage`),data.voltage.toFixed(2)),(data.build_version||data.build_date)&&_safeSet(ge(`version`),data.build_version+` (`+data.build_date+`)`);let timeInput=ge(`UX_TIME`);if(data.time!==void 0){let dt=/* @__PURE__ */ new Date(data.time*1e3);_safeSet(timeInput,`${dt.getUTCFullYear()}-${String(dt.getUTCMonth()+1).padStart(2,`0`)}-${String(dt.getUTCDate()).padStart(2,`0`)}T${String(dt.getUTCHours()).padStart(2,`0`)}:${String(dt.getUTCMinutes()).padStart(2,`0`)}:${String(dt.getUTCSeconds()).padStart(2,`0`)}`)}let timeOutput=ge(`UX_NEXT_ALARM`);if(data.next_alarm!==void 0&&document.activeElement!==timeOutput)if(data.next_alarm>0){timeOutput.type!==`datetime-local`&&(timeOutput.type=`datetime-local`);let dt=/* @__PURE__ */ new Date(data.next_alarm*1e3);timeOutput.value=`${dt.getUTCFullYear()}-${String(dt.getUTCMonth()+1).padStart(2,`0`)}-${String(dt.getUTCDate()).padStart(2,`0`)}T${String(dt.getUTCHours()).padStart(2,`0`)}:${String(dt.getUTCMinutes()).padStart(2,`0`)}:${String(dt.getUTCSeconds()).padStart(2,`0`)}`}else timeOutput.type!==`text`&&(timeOutput.type=`text`),timeOutput.value=`DISABLED`;data.parameters&&(updateParamTable(),updateScheduleInputs()),data.remaining_dist!==void 0&&_safeSet(ge(`UX_REM_DIST`),data.remaining_dist.toFixed(1))}const PARAM_TABLE_SKIP=new Set([`NET_SSID`,`NET_PASS`,`WIFI_SSID`,`WIFI_PASS`]);function updateParamTable(){let table=ge(`table`),sortedParams=Object.entries(data.parameters).filter(([k])=>!PARAM_TABLE_SKIP.has(k)).sort((a,b)=>a[0].localeCompare(b[0]));if(paramTableCreated)for(let[key,value]of sortedParams){let input=ge(`PARAM_${key}`);if(input)_safeSet(input,value);else{paramTableCreated=!1,updateParamTable();return}}else{table.innerHTML=``;for(let[key,value]of sortedParams){let row=table.insertRow(),cell1=row.insertCell(0),cell2=row.insertCell(1);cell1.textContent=key;let input=document.createElement(`input`);input.type=typeof value==`string`?`text`:`number`,input.id=`PARAM_${key}`,input.value=value,input.onchange=function(){markChanged(this)},cell2.appendChild(input)}document.querySelectorAll(`input`).forEach(input=>{input.addEventListener(`click`,function(){this.readOnly!=1&&this.select()})}),paramTableCreated=!0}}async function uploadFirmware(){let fileInput=ge(`firmware_file`);fileInput.click(),await new Promise(resolve=>{fileInput.onchange=()=>resolve()});let file=fileInput.files[0];if(file){if(!file.name.endsWith(`.bin`)){await modalAlert(`Please select a .bin file`);return}if(await modalConfirm(`Upload firmware file "${file.name}"?\n\nDevice will reboot after upload.`))try{let arrayBuffer=await file.arrayBuffer();showModal(`Uploading Firmware`,`<div style="background-color: #2a493d;">
<div style="background-color: #2a493d; margin-bottom: 10px;">Uploading ${file.name}...</div>
<div style="background-color: #2a493d; font-size: 2rem; font-weight: bold;" id="upload-progress">0%</div>
</div>`,[]);let xhr=new XMLHttpRequest;xhr.upload.addEventListener(`progress`,e=>{if(e.lengthComputable){let percentComplete=Math.round(e.loaded/e.total*100),progressEl=document.getElementById(`upload-progress`);progressEl&&(progressEl.textContent=percentComplete+`%`)}});let uploadPromise=new Promise((resolve,reject)=>{xhr.addEventListener(`load`,()=>{xhr.status>=200&&xhr.status<300?resolve():reject(/* @__PURE__ */ Error(`Upload failed: ${xhr.status} ${xhr.statusText}`))}),xhr.addEventListener(`error`,()=>{reject(/* @__PURE__ */ Error(`Network error during upload`))}),xhr.addEventListener(`abort`,()=>{reject(/* @__PURE__ */ Error(`Upload cancelled`))})});xhr.open(`POST`,`./ota`),xhr.setRequestHeader(`Content-Type`,`application/octet-stream`),xhr.send(arrayBuffer),await uploadPromise,showModal(`Firmware Update`,`Firmware uploaded successfully! Device is rebooting...<br>Page will refresh in <span id="popup-countdown">5</span> seconds...`,[]);let countdown=5,countdownInterval=setInterval(()=>{countdown--;let countdownEl=document.getElementById(`popup-countdown`);countdownEl&&(countdownEl.textContent=countdown),countdown<=0&&(clearInterval(countdownInterval),location.reload())},1e3)}catch(e){hideModal(),await modalAlert(`Error: ${e.message}`)}}}function programRF(i){fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_learn`,channel:i})})}async function programRFSequence(){let buttonNames=[`Forward`,`Reverse`,`Up`,`Down`],learnedCodes=[null,null,null,null];if(await modalConfirm(`This will program all 4 RF remote buttons in sequence.
Press OK to begin, then follow the prompts.`))try{let response=await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_clear_temp`})});if(!response.ok){await modalAlert(`Failed to clear RF temp storage`);return}if(response=await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_disable`})}),!response.ok){await modalAlert(`Failed to disable RF controls`);return}for(let i=0;i<4;i++){await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_learn`,channel:i})}),await new Promise(resolve=>setTimeout(resolve,100)),await modalAlert(`Button ${i+1}/4: ${buttonNames[i]}\n\nPress the ${buttonNames[i]} button on your remote once now.\n\nPress OK when done (or just press OK to leave unprogrammed).`,`Program Button`),await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_learn`,channel:-1})});try{let response=await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_status`})});if(!response.ok)console.error(`Error checking RF status:`,response.status),learnedCodes[i]=0;else{let data=await response.json();learnedCodes[i]=data.codes?data.codes[i]:0}}catch(e){console.error(`Error checking RF status:`,e),learnedCodes[i]=0}learnedCodes[i]===0&&await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_set_temp`,index:i,code:0})})}for(let i=0;i<4;i++){let input=ge(`PARAM_KEYCODE_${i}`);input&&(input.value=learnedCodes[i],markChanged(input))}let parameters={KEYCODE_0:learnedCodes[0],KEYCODE_1:learnedCodes[1],KEYCODE_2:learnedCodes[2],KEYCODE_3:learnedCodes[3]};if(response=await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({parameters})}),response.ok){for(let i=0;i<4;i++){let input=ge(`PARAM_KEYCODE_${i}`);input&&input.classList.remove(`changed`)}document.querySelectorAll(`input.changed`).length===0&&(ge(`commit_btn`).disabled=!0,ge(`cancel_btn`).disabled=!0)}else await modalAlert(`Failed to save RF codes: ${response.status} ${response.statusText}`);await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_enable`})});let summary=`RF Remote Programming Complete!
`;for(let i=0;i<4;i++)learnedCodes[i]===0?summary+=`${buttonNames[i]}: - <br/>`:summary+=`${buttonNames[i]}: ${learnedCodes[i]}<br/>`;await modalAlert(summary),await fetchStatus()}catch(e){await modalAlert(`RF programming error: ${e.message}`)}}async function calibrate(axis){try{let response=await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`cal_${axis}_start`})});if(!response.ok){await modalAlert(`Failed to start ${axis} calibration: ${response.status}`);return}let amt=await modalPrompt(`Press button on mover. Press button again to stop it.
Then, type the actual travelled distance in inches:`,`Calibration`);if(amt===null||amt===``){await modalAlert(`Calibration cancelled`);return}let distance=parseFloat(amt);if(isNaN(distance)||distance<=0){await modalAlert(`Invalid distance entered. Please enter a positive number.`);return}if(response=await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`cal_get`})}),!response.ok){await modalAlert(`Failed to get calibration data: ${response.status}`);return}let calData=await response.json();if(axis===`drive`){let ke=calData.e/(distance/12),kt=calData.t/(distance/12)*1.2,keInput=ge(`PARAM_DRIVE_KE`),ktInput=ge(`PARAM_DRIVE_KT`);keInput?(keInput.value=ke.toFixed(2),markChanged(keInput)):console.error(`Could not find PARAM_DRIVE_KE`),ktInput?(ktInput.value=Math.round(kt),markChanged(ktInput)):console.error(`Could not find PARAM_DRIVE_KT`),keInput||ktInput?await modalAlert(`Drive calibration complete!\n\n${keInput?`KE(steps/ft):${ke.toFixed(2)}\n`:``}${ktInput?`KT(timeout ms/ft):${Math.round(kt)}\n`:``}\nPlease save changes.`):await modalAlert(`Drive calibration failed: Could not find DRIVE_KE or DRIVE_KT parameters.`)}else if(axis===`jack`){let kt=calData.t/distance,ktInput=ge(`PARAM_JACK_KT`);ktInput?(ktInput.value=Math.round(kt),markChanged(ktInput),await modalAlert(`Jack calibration complete!\n\nKT (timeout ms/in): ${Math.round(kt)}\n\nPlease save changes.`)):(console.error(`Could not find PARAM_JACK_KT`),await modalAlert(`Jack calibration failed: Could not find JACK_KT parameter.`))}}catch(e){await modalAlert(`Calibration error: ${e.message}`)}}async function applyWifiSettings(){if(!await modalConfirm(`WiFi will restart. In STA mode, reconnect at http://sc.local on your network. In AP mode, reconnect to the device's AP.`,`Apply WiFi Settings`))return;let wifiKeys=[`NET_SSID`,`NET_PASS`,`WIFI_SSID`,`WIFI_PASS`],params={};for(let key of wifiKeys){let el=ge(`PARAM_`+key);el&&(params[key]=el.value)}try{let response=await fetch(`./post`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({parameters:params})});if(!response.ok){await modalAlert(`Failed to apply WiFi settings: `+response.status);return}for(let key of wifiKeys){let el=ge(`PARAM_`+key);el&&el.classList.remove(`changed`)}document.querySelectorAll(`input.changed`).length===0&&(ge(`commit_btn`).disabled=!0,ge(`cancel_btn`).disabled=!0),await modalAlert(`WiFi settings applied. Reconnect in a moment.`)}catch(e){await modalAlert(`Network error: `+e.message)}}async function downloadLogFile(){try{let response=await fetch(`./log`);if(!response.ok){await modalAlert(`Failed to download log file: ${response.status} ${response.statusText}`);return}let blob=await response.blob(),now=/* @__PURE__ */ new Date,formattedDate=`${String(now.getDate()).padStart(2,`0`)}${[`JAN`,`FEB`,`MAR`,`APR`,`MAY`,`JUN`,`JUL`,`AUG`,`SEP`,`OCT`,`NOV`,`DEC`][now.getMonth()]}${now.getFullYear()}-${String(now.getHours()).padStart(2,`0`)}${String(now.getMinutes()).padStart(2,`0`)}`,url=URL.createObjectURL(blob),a=document.createElement(`a`);a.href=url,a.download=`storage-${formattedDate}.bin`,document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(url)}catch(error){await modalAlert(`Error downloading log file: ${error.message}`)}}const LOG_FSM_NAMES=[`IDLE`,`MOVE_START_DELAY`,`JACK_UP_START`,`JACK_UP`,`DRIVE_START_DELAY`,`DRIVE`,`DRIVE_END_DELAY`,`JACK_DOWN`,`UNDO_JACK_START`,`CAL_JACK_DELAY`,`CAL_JACK_MOVE`,`CAL_DRIVE_DELAY`,`CAL_DRIVE_MOVE`,`DRIVE_FLUFF_START`],LOG_TYPE_BAT=100,LOG_TYPE_CRASH=101,LOG_TYPE_BOOT=102,LOG_TYPE_TIME_SET=103,LOG_RESET_REASONS={0:`UNKNOWN`,1:`POWERON`,2:`EXT`,3:`SW`,4:`PANIC`,5:`INT_WDT`,6:`TASK_WDT`,7:`WDT`,8:`DEEPSLEEP`,9:`BROWNOUT`,10:`SDIO`};let logViewerLoaded=!1;const LOG_MONTHS=[`JAN`,`FEB`,`MAR`,`APR`,`MAY`,`JUN`,`JUL`,`AUG`,`SEP`,`OCT`,`NOV`,`DEC`];function _logFormatTs(tsMs){if(!tsMs||tsMs<=0)return`-`;let d=new Date(tsMs),pad=(n,w=2)=>String(n).padStart(w,`0`);return`${d.getUTCFullYear()}${LOG_MONTHS[d.getUTCMonth()]}${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}.${pad(d.getUTCMilliseconds(),3)}`}const LOG_SENSOR_NAMES=[`SAFETY`,`DRIVE`,`JACK`,`AUX`];function _decodeSensors(b){let stable=b&15,raw=b>>4&15,parts=[];for(let i=0;i<4;i++){let s=stable>>i&1,r=raw>>i&1;parts.push(`${LOG_SENSOR_NAMES[i]}=${s}${s===r?``:`(raw ${r})`}`)}return parts.join(`, `)}function _parseLogEntries(bytes){let entries=[],n=bytes.length,dv=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),i=0;for(;i<n;){let b=bytes[i];if(b===255||b===0){i=(Math.floor(i/4096)+1)*4096;continue}let entryLen=b,payloadSize=entryLen-1,endOffset=i+entryLen;if(endOffset>=n)break;let type=bytes[endOffset],payloadStart=i+1,entry={type};try{if(type>=0&&type<=13&&payloadSize>=19)entry.ts_ms=Number(dv.getBigUint64(payloadStart+0,!0)),entry.bat_V=dv.getFloat32(payloadStart+8,!0),entry.current_A=dv.getFloat32(payloadStart+12,!0),entry.counter=dv.getInt16(payloadStart+16,!0),entry.sensors=bytes[payloadStart+18],payloadSize>=23&&(entry.heat=dv.getFloat32(payloadStart+19,!0)),payloadSize>=25&&(entry.i2c_out=dv.getUint16(payloadStart+23,!0)),entry.name=LOG_FSM_NAMES[type]||`STATE_${type}`;else if(type===100&&payloadSize>=12)entry.ts_ms=Number(dv.getBigUint64(payloadStart,!0)),entry.bat_V=dv.getFloat32(payloadStart+8,!0),entry.name=`BAT`;else if((type===101||type===102)&&payloadSize>=9){entry.ts_ms=Number(dv.getBigUint64(payloadStart,!0));let info=bytes[payloadStart+8];entry.reason=LOG_RESET_REASONS[info&15]||`UNKNOWN(${info&15})`,entry.name=type===102?`BOOT`:`CRASH`}else type===103&&payloadSize>=8?(entry.ts_ms=Number(dv.getBigUint64(payloadStart,!0)),entry.name=`TIME_SET`):entry.name=`UNK(0x${type.toString(16).padStart(2,`0`)})`}catch{entry.name=`PARSE_ERR`}entries.push(entry),i=endOffset+1}return entries}function _entryCells(e){let sensorHex=e.sensors===void 0?``:`0x`+e.sensors.toString(16).padStart(2,`0`),i2cHex=e.i2c_out===void 0?``:`0x`+e.i2c_out.toString(16).padStart(4,`0`);return[{text:_logFormatTs(e.ts_ms)},{text:e.name||``},{text:e.bat_V===void 0?``:e.bat_V.toFixed(2)},{text:e.current_A===void 0?``:e.current_A.toFixed(2)},{text:e.counter===void 0?``:String(e.counter)},{text:sensorHex,title:e.sensors===void 0?void 0:_decodeSensors(e.sensors)},{text:e.heat===void 0?``:e.heat.toFixed(2)},{text:i2cHex,title:e.i2c_out===void 0?void 0:`0b`+e.i2c_out.toString(2).padStart(16,`0`)},{text:e.reason||``}]}function _appendRow(table,cells,opts={}){let row=table.insertRow();return opts.className&&(row.className=opts.className),opts.hidden&&(row.style.display=`none`),opts.onclick&&row.addEventListener(`click`,opts.onclick),opts.style&&Object.assign(row.style,opts.style),cells.forEach(c=>{let td=document.createElement(`td`);typeof c==`string`?td.textContent=c:(td.textContent=c.text??``,c.title&&(td.title=c.title),c.colSpan&&(td.colSpan=c.colSpan),c.style&&Object.assign(td.style,c.style),c.bold&&(td.style.fontWeight=`bold`)),row.appendChild(td)}),row}function _renderLogEntries(entries){let table=ge(`log_viewer_table`);table.innerHTML=``;let cols=[`Time`,`Type`,`Battery V`,`Current A`,`Counter`,`Sensors`,`Heat`,`I2C Out`,`Extra`];_appendRow(table,cols.map(h=>({text:h,bold:!0})));let groupId=0,i=entries.length-1;for(;i>=0;){let name=entries[i].name||``,j=i;for(;j>=0&&(entries[j].name||``)===name;)j--;let group=entries.slice(j+1,i+1).reverse();if(i=j,group.length===1){_appendRow(table,_entryCells(group[0]));continue}groupId++;let rowClass=`log-group-${groupId}`,collapsed=!0,firstTs=_logFormatTs(group[group.length-1].ts_ms),lastTs=_logFormatTs(group[0].ts_ms),header=_appendRow(table,[{text:`[+] ${name} × ${group.length} ${firstTs}${lastTs}`,colSpan:cols.length,style:{cursor:`pointer`,background:`var(--surface-2)`,color:`var(--text)`,fontWeight:`bold`,fontSize:`0.7rem`}}],{style:{background:`var(--surface-2)`}}),childRows=group.map(e=>_appendRow(table,_entryCells(e),{className:rowClass,hidden:!0}));header.addEventListener(`click`,()=>{collapsed=!collapsed,childRows.forEach(r=>r.style.display=collapsed?`none`:``);let cell=header.cells[0];cell.textContent=`[${collapsed?`+`:`-`}] ${name} × ${group.length} ${firstTs}${lastTs}`})}}async function loadLogViewer(){let msg=ge(`log_viewer_msg`);msg.textContent=`Connecting...`;try{let resp=await fetch(`./log`);if(!resp.ok){msg.textContent=`Failed: ${resp.status} ${resp.statusText}`;return}let total=parseInt(resp.headers.get(`Content-Length`)||`0`,10),reader=resp.body.getReader(),chunks=[],received=0;for(;;){let{done,value}=await reader.read();if(done)break;chunks.push(value),received+=value.length;let kb=(received/1024).toFixed(1);total?msg.textContent=`Downloading... ${kb} / ${(total/1024).toFixed(1)} KB (${Math.round(received/total*100)}%)`:msg.textContent=`Downloading... ${kb} KB`}let buf=new Uint8Array(received);{let off=0;for(let c of chunks)buf.set(c,off),off+=c.length}if(buf.length<12){msg.textContent=`Log response too short.`;return}msg.textContent=`Parsing ${received} bytes...`;let jsonLen=new DataView(buf.buffer,buf.byteOffset,buf.byteLength).getUint32(0,!1);if(jsonLen>65536||4+jsonLen+8>buf.length){msg.textContent=`Log response malformed.`;return}let binStart=4+jsonLen+8,logBytes=buf.subarray(binStart),entries=_parseLogEntries(logBytes);_renderLogEntries(entries),msg.textContent=`${entries.length} entries (${logBytes.length} bytes)`,logViewerLoaded=!0}catch(err){msg.textContent=`Error: ${err.message}`}}function _attachLogViewerToggle(){let d=ge(`log_viewer_details`);d&&d.addEventListener(`toggle`,()=>{d.open&&!logViewerLoaded&&loadLogViewer()})}function startPolling(){fetchStatus(),pollInterval=setInterval(fetchStatus,3e3)}function stopPolling(){pollInterval&&=(clearInterval(pollInterval),null)}document.addEventListener(`visibilitychange`,function(){document.hidden?stopPolling():(fetchStatus(),pollInterval=setInterval(fetchStatus,3e3))}),window.onload=function(){ge(`commit_btn`).disabled=!0,ge(`cancel_btn`).disabled=!0,_attachLogViewerToggle(),startPolling()};</script></body></html>