DNS, web ui nearly done, great log streaming, attempted https (abandoned that though)

This commit is contained in:
Thaddeus Hughes
2025-12-30 18:51:11 -06:00
parent 012d28ae14
commit d46cb252fb
134 changed files with 19226 additions and 740 deletions

View File

@@ -1,22 +1,52 @@
<!doctype html><title>Control Panel</title><style>*{color:#eee;background-color:#111;font-family:sans-serif}input,button{text-align:right;box-sizing:border-box;background-color:#333;border:1px solid #666;width:100%;font-family:monospace}button{text-align:center}.changed{color:#111!important;background-color:#3d3!important}#commit_btn{color:#111;cursor:pointer;background-color:#3d3;border:none;width:100%;margin-top:10px;padding:10px;font-weight:700}#commit_btn[disabled]{color:#888;cursor:not-allowed;background-color:#444}table{border-collapse:collapse;width:100%}td{border-bottom:1px solid #222;padding:8px}tr:hover{background-color:#1a1a1a}summary{text-align:left;color:#ccc;background-color:#723;padding:.3rem;font-weight:700}</style></head><body><button disabled id=commit_btn onclick=commit_params()>Save Changes</button><table><tr><td>Schedule Start</td><td><input id=move_start onchange=changeSchedule(this) type=time></td></tr><tr><td>Schedule End</td><td><input id=move_end onchange=changeSchedule(this) type=time></td></tr><tr><td># Moves/Day</td><td><input id=num_moves min=0 onchange=changeSchedule(this) type=number></td></tr><tr><td>Move Distance</td><td><input id=drive_dist min=0 onchange=changeSchedule(this) type=number></td><td>ft</td></tr><tr><td>Jack Height</td><td><input id=jack_dist min=0 onchange=changeSchedule(this) type=number></td><td>in</td></tr><tr><td>Time</td><td><input id=in_time onchange=markChanged(this) step=1 type=datetime-local></td><td><button id=now_btn onclick=setTimeToNow()>&lt; NOW</button></td></tr><tr><td>Battery</td><td><input id=voltage readonly></td><td>V</td></tr><tr><td></td><td><button onclick="cmd('start')">START MOVE</button></td></tr><tr><td></td><td><button onclick="cmd('undo')">UNDO MOVE</button></td></tr><tr><td></td><td><button onclick="cmd('stop')" style=background-color:#800>STOP MOVE</button></td></tr></table><br><details><summary>DANGER ZONE</summary> <table><tr><td>Program RF Remote</td><td><button onclick=programRF(0) style=width:40%>Fwd</button> <button onclick=programRF(1) style=width:40%>Rev</button> <button onclick=programRF(2) style=width:40%>Up</button> <button onclick=programRF(3) style=width:40%>Down</button> <button onclick=programRF(-1)>Cancel Learning</button></td></tr><tr><td>Calibration</td><td><button id=cal_jack_btn>Jack Calibration</button> <button id=cal_drive_btn>Drive Calibration</button></td></tr><tr><td>Firmware</td><td><input accept=.bin id=firmware_file 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></details><script>let param_values=[],param_names=[],param_units=[];function cmd(x){if(x===`start`&&!confirm(`Will begin moving - please confirm.`))return;let xhr=new XMLHttpRequest;xhr.open(`POST`,`./cmd`,!0),xhr.setRequestHeader(`Content-Type`,`application/json`),xhr.send(JSON.stringify({cmd:x})),xhr.onload=function(){console.log(xhr)}}function ge(x){return document.getElementById(x)}
<!doctype html><title>Control Panel</title><meta content="width=device-width,initial-scale=1.0" name=viewport><style>#wrapper{text-align:center;box-sizing:border-box}#content{max-width:500px;margin:auto;padding:0 10px}body{text-align:center;margin:0;padding:0}*{color:#2f2f2f;background-color:#fff;font-family:Noto Sans,Verdana,sans-serif;font-size:1.2rem}input,button{text-align:right;box-sizing:border-box;background-color:#efede9;border:1px solid #ba965b;border-radius:5px;width:100%}input[type=text],input[type=number]{font-family:monospace}button{text-align:center}.changed,#commit_btn{color:#fff!important;background-color:#2a493d!important}#commit_btn{cursor:pointer;border:none;width:100%;margin-top:10px;padding:10px;font-weight:700}#commit_btn[disabled]{color:#888;cursor:not-allowed;background-color:#444!important}table{border-collapse:collapse;text-align:left;width:100%}td{border-bottom:1px solid #efede9;padding:8px}summary{text-align:left;color:#fff;background-color:#723;border-radius:5px;padding:.3rem;font-weight:700}.cmd{border:none;font-size:1.5rem}#msg{text-align:center}h1{font-size:2.5rem}@media screen and (width<=350px){#content{max-width:100%;padding:0 5px}table tr td{box-sizing:border-box;width:100%;display:block}table tr{margin-bottom:10px;display:block}}</style></head><body><div id=wrapper><div id=content><h1>ClusterCommand</h1><table><tr><td><button onclick="sendCommand('start')" class=cmd>START</button></td><td><button onclick="sendCommand('stop')" class=cmd style=color:#fff;background-color:#723>STOP</button></td><td><button onclick="sendCommand('undo')" class=cmd>UNDO</button></td></tr></table><table><tr><td colspan=3><input id=msg readonly></td></tr><tr><td colspan=3><input id=in_time onchange=markChanged(this) step=1 type=datetime-local> <button id=now_btn onclick=setTimeToNow()>Sync Time</button></td></tr><tr><td>Schedule Start</td><td><input id=move_start onchange=changeSchedule(this) type=time></td></tr><tr><td>Schedule End</td><td><input id=move_end onchange=changeSchedule(this) type=time></td></tr><tr><td># Moves/Day</td><td><input id=num_moves min=0 onchange=changeSchedule(this) type=number></td></tr><tr><td>Remain. Distance (ft)</td><td><input id=remaining_dist onchange=markChanged(this) type=number></td></tr><tr><td>Move Distance (ft)</td><td><input id=drive_dist min=0 onchange=changeSchedule(this) type=number></td></tr><tr><td>Jack Height (in)</td><td><input id=jack_dist min=0 onchange=changeSchedule(this) type=number></td></tr><tr><td>Battery (V)</td><td><input id=voltage readonly></td></tr></table><button disabled id=commit_btn onclick=commitParams()>Save Changes</button><br><br><details><summary>DANGER ZONE</summary> <table><tr><td>Program RF Remote</td><td><button onclick=programRFSequence()>Program All Buttons</button></td></tr><tr><td>Calibration</td><td><button onclick="calibrate('jack')">Jack Calibration</button> <button onclick="calibrate('drive')">Drive Calibration</button></td></tr><tr><td>Firmware</td><td><input accept=.bin id=firmware_file 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> <td><button onclick="sendCommand('reboot')" class=cmd style=color:#fff;background-color:#723>REBOOT</button></td></details></div></div><script>let paramValues=[],paramNames=[],paramUnits=[];const ge=id=>document.getElementById(id);async function sendCommand(cmdName){cmdName===`start`&&!confirm(`Will begin moving - please confirm.`)||cmdName===`reboot`&&!confirm(`Device will reboot - clearing clock and distance. Are you sure?`)||await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:cmdName})})}async function calibrate(axis){await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`cal_${axis}_start`})});let amt=prompt(`Press button on mover. Press button again to stop it. Then, type in actual travelled distance here in inches:`);if(!isNaN(parseFloat(amt))){let response=await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`cal_get`})});if(response.ok){let data=await response.json();axis===`drive`?(markChanged(ge(`in_6`)).value=data.e/amt,markChanged(ge(`in_7`)).value=data.t/amt*1.2):markChanged(ge(`in_8`)).value=data.t/amt}}}
// Highlight changed inputs and enable the save button
function markChanged(el){el.classList.add(`changed`),ge(`commit_btn`).disabled=!1}function toFixed(input,n){let num=parseFloat(input);return isNaN(num)?null:Number(num.toFixed(n))}
function markChanged(el){return el.classList.add(`changed`),ge(`commit_btn`).disabled=!1,el}function toFixed(input,n){let num=parseFloat(input);return isNaN(num)?null:Number(num.toFixed(n))}
// --- 1. GET DATA ---
function fetchStatus(){let xhr=new XMLHttpRequest;xhr.open(`GET`,`./status`,!0),xhr.onload=function(){if(xhr.status===200)try{console.log(xhr.responseText);let data=JSON.parse(xhr.responseText);
async function fetchStatus(){try{let response=await fetch(`./status`);if(!response.ok)return;let data=await response.json();
// Update time field if available
if(data.time){let date=(/* @__PURE__ */ new Date(data.time*1e3)).toISOString().slice(0,19);ge(`in_time`).value=date}ge(`voltage`).value=toFixed(data.battery,2),param_values=data.values||[],param_names=data.names||[],param_units=data.units||[],data.rtc_set||(ge(`in_time`).classList.add(`error`),confirm(`Clock not set. Sync with this device's clock?`)&&(setTimeToNow(),commit_time()))}catch(e){console.error(`Error parsing JSON`,e)}
if(console.log(data),data.time){let date=(/* @__PURE__ */ new Date(data.time*1e3)).toISOString().slice(0,19);ge(`in_time`).value=date}ge(`voltage`).value=toFixed(data.battery,2),ge(`remaining_dist`).value=toFixed(data.remaining_dist,2),ge(`msg`).value=data.msg,paramValues=data.values||[],paramNames=data.names||[],paramUnits=data.units||[],ge(`commit_btn`).disabled=!0,data.rtc_set||(ge(`in_time`).classList.add(`error`),confirm(`Clock not set. Sync with this device's clock?`)&&(setTimeToNow(),commitTime()))}catch(e){console.error(`Error parsing JSON`,e)}
// Always render table even if request fails or data is empty
renderTable()},xhr.onerror=function(e){console.error(`Network error`,e),renderTable()},xhr.send()}function s_to_hhmm(s){return`${String(Math.floor(s/3600)).padStart(2,`0`)}:${String(Math.floor(s%3600/60)).padStart(2,`0`)}`}function renderTable(){let table=ge(`table`);
// Clear existing parameter rows (rows between index 0 and the last row)
for(;table.rows.length>0;)table.deleteRow(0);param_names.forEach((name,i)=>{let row=table.insertRow(table.rows.length);row.innerHTML=`
renderTable()}function secondsToHHMM(seconds){return`${String(Math.floor(seconds/3600)).padStart(2,`0`)}:${String(Math.floor(seconds%3600/60)).padStart(2,`0`)}`}function renderTable(){let table=ge(`table`);
// Clear existing parameter rows
for(;table.rows.length>0;)table.deleteRow(0);
// Loop through the NAMES array to ensure every input is shown
paramNames.forEach((name,i)=>{let row=table.insertRow(table.rows.length),pname=paramNames[i]!==void 0&&paramNames[i]!==null?paramNames[i]:`null`,pval=paramValues[i]!==void 0&&paramValues[i]!==null?paramValues[i]:`null`;row.innerHTML=`
<td>${i}</td>
<td>${param_names[i]!==void 0&&param_names[i]!==null?param_names[i]:`null`}</td>
<td><input type="number" id="in_${i}" value="${param_values[i]!==void 0&&param_values[i]!==null?param_values[i]:`null`}" oninput="markChanged(this)"></td>
<td>${param_units[i]||``}</td>
`}),ge(`num_moves`).value=param_values[1],ge(`move_start`).value=s_to_hhmm(param_values[2]),ge(`move_end`).value=s_to_hhmm(param_values[3]),ge(`drive_dist`).value=param_values[4],ge(`jack_dist`).value=param_values[5]}function changeSchedule(e){if(markChanged(e),e.id==`num_moves`&&(ge(`in_1`).value=e.value,markChanged(ge(`in_1`))),e.id==`move_start`){let[hours,minutes]=e.value.split(`:`).map(Number);ge(`in_2`).value=hours*3600+minutes*60,markChanged(ge(`in_2`))}if(e.id==`move_end`){let[hours,minutes]=e.value.split(`:`).map(Number);ge(`in_3`).value=hours*3600+minutes*60,markChanged(ge(`in_3`))}e.id==`drive_dist`&&(ge(`in_4`).value=e.value,markChanged(ge(`in_4`))),e.id==`jack_dist`&&(ge(`in_5`).value=e.value,markChanged(ge(`in_5`)))}function setTimeToNow(){e=ge(`in_time`),markChanged(e),e.value=(/* @__PURE__ */ new Date()).toLocaleString(`sv-SE`)}function commit_time(){let xhr=new XMLHttpRequest,[datePart,timePart]=ge(`in_time`).value.split(`T`),[year,month,day]=datePart.split(`-`).map(Number),[hour,minute,second=0]=timePart.split(`:`).map(Number),epoch=Math.floor(Date.UTC(year,month-1,day,hour,minute,second)/1e3);xhr.open(`POST`,`./st`,!0),xhr.setRequestHeader(`Content-Type`,`application/json`),xhr.onload=function(){xhr.status===200&&ge(`in_time`).classList.remove(`changed`),document.querySelectorAll(`input.changed`).length===0?ge(`commit_btn`).disabled=!0:ge(`commit_btn`).disabled=!1},xhr.send(epoch.toString())}
<td>${pname}</td>
<td><input type="${typeof pval==`number`?`number`:`text`}" id="in_${i}" value="${pval}" oninput="markChanged(this)"></td>
<td>${paramUnits[i]||``}</td>
`});for(let input of document.getElementsByTagName(`input`))input.addEventListener(`click`,e=>{e.target.select()});ge(`num_moves`).value=paramValues[1],ge(`move_start`).value=secondsToHHMM(paramValues[2]),ge(`move_end`).value=secondsToHHMM(paramValues[3]),ge(`drive_dist`).value=paramValues[4],ge(`jack_dist`).value=paramValues[5]}function changeSchedule(e){if(markChanged(e),e.id===`num_moves`&&(ge(`in_1`).value=e.value,markChanged(ge(`in_1`))),e.id===`move_start`){let[hours,minutes]=e.value.split(`:`).map(Number);ge(`in_2`).value=hours*3600+minutes*60,markChanged(ge(`in_2`))}if(e.id===`move_end`){let[hours,minutes]=e.value.split(`:`).map(Number);ge(`in_3`).value=hours*3600+minutes*60,markChanged(ge(`in_3`))}e.id===`drive_dist`&&(ge(`in_4`).value=e.value,markChanged(ge(`in_4`))),e.id===`jack_dist`&&(ge(`in_5`).value=e.value,markChanged(ge(`in_5`)))}function setTimeToNow(){let e=ge(`in_time`);markChanged(e),e.value=(/* @__PURE__ */ new Date()).toLocaleString(`sv-SE`)}async function commitTime(){
// Parse the components
let[datePart,timePart]=ge(`in_time`).value.split(`T`),[year,month,day]=datePart.split(`-`).map(Number),[hour,minute,second=0]=timePart.split(`:`).map(Number),epoch=Math.floor(Date.UTC(year,month-1,day,hour,minute,second)/1e3);(await fetch(`./st`,{method:`POST`,headers:{"Content-Type":`application/json`},body:epoch.toString()})).ok&&ge(`in_time`).classList.remove(`changed`),document.querySelectorAll(`input.changed`).length===0?ge(`commit_btn`).disabled=!0:ge(`commit_btn`).disabled=!1}
// --- 2. POST DATA ---
function commit_params(){ge(`commit_btn`).disabled=!0;let changedInputs=document.querySelectorAll(`input.changed`);if(sp={},changedInputs.forEach(input=>{if(input.id===`in_time`)commit_time();else if(input.id.startsWith(`in_`)){
async function commitParams(){ge(`commit_btn`).disabled=!0;let changedInputs=document.querySelectorAll(`input.changed`),sp={};for(let input of changedInputs)if(input.id===`in_time`)await commitTime();else if(input.id===`remaining_dist`){let x=parseFloat(ge(`remaining_dist`).value);(await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`remaining_dist`,amt:x})})).ok&&ge(`remaining_dist`).classList.remove(`changed`)}else if(input.id.startsWith(`in_`)){
// Parameter handling
let id=input.id.split(`_`)[1],val=input.value.toLowerCase()===`null`?null:parseFloat(input.value);sp[id]=val}}),Object.keys(sp).length!==0){let xhr2=new XMLHttpRequest;xhr2.open(`POST`,`./sp`,!0),xhr2.setRequestHeader(`Content-Type`,`application/json`),xhr2.onload=function(){xhr2.status===200?changedInputs.forEach(input=>{input.id!==`in_time`&&input.classList.remove(`changed`)}):ge(`commit_btn`).disabled=!1},xhr2.send(JSON.stringify(sp))}fetchStatus()}function uploadFirmware(){let fileInput=ge(`firmware_file`);if(!fileInput.files.length){alert(`No file selected`);return}let file=fileInput.files[0],xhr=new XMLHttpRequest;xhr.open(`POST`,`./ota`,!0),xhr.setRequestHeader(`Content-Type`,`application/octet-stream`),xhr.onload=function(){xhr.status===200?alert(`Upload successful. Device may reboot.`):alert(`Upload failed: `+xhr.status)},xhr.onerror=function(){alert(`Network error during upload`)},xhr.send(file)}function programRF(i){let xhr=new XMLHttpRequest;xhr.open(`POST`,`./prf`,!0),xhr.setRequestHeader(`Content-Type`,`application/json`),xhr.send(i.toString()),xhr.status}async function downloadLogFile(){try{let response=await fetch(`./log`);if(!response.ok)throw Error(`Network response was not ok`);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){console.error(`Download failed:`,error)}}
let id=input.id.split(`_`)[1];sp[id]=input.value,input.type===`number`&&(sp[id]=input.value.toLowerCase()===`null`?null:parseFloat(input.value))}Object.keys(sp).length!==0&&((await fetch(`./sp`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(sp)})).ok?changedInputs.forEach(input=>{input.id!==`in_time`&&input.classList.remove(`changed`)}):ge(`commit_btn`).disabled=!1),await fetchStatus()}async function uploadFirmware(){let fileInput=ge(`firmware_file`);if(!fileInput.files.length){alert(`No file selected`);return}let file=fileInput.files[0];try{let response=await fetch(`./ota`,{method:`POST`,headers:{"Content-Type":`application/octet-stream`},body:file});response.ok?alert(`Upload successful. Device may reboot.`):alert(`Upload failed: `+response.status)}catch{alert(`Network error during upload`)}}function programRF(i){fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rfp`,channel:i})})}async function programRFSequence(){let buttonNames=[`Forward`,`Reverse`,`Up`,`Down`],learnedCodes=[null,null,null,null];if(!confirm(`This will program all 4 RF remote buttons in sequence.
Press OK to begin, then follow the prompts.`))return;
// Disable RF controls during programming
await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_disable`})});for(let i=0;i<4;i++){if(programRF(i),await new Promise(resolve=>setTimeout(resolve,100)),!confirm(`Button ${i+1}/4: ${buttonNames[i]}\n\nPress the ${buttonNames[i]} button on your remote now, then press OK.\n\nPress Cancel to skip this button.`)){programRF(-1),learnedCodes[i]=-1;continue}
// Wait for code to be learned (poll for a bit)
let learned=!1,startCode=learnedCodes[i];for(let attempt=0;attempt<50;attempt++){await new Promise(resolve=>setTimeout(resolve,100));
// Check if code was learned by polling RF status via /cmd
try{let data=await(await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_status`})})).json();
// Check if this specific index changed from what we started with
if(data.codes[i]!==-1&&data.codes[i]!==null&&data.codes[i]!==startCode){learnedCodes[i]=data.codes[i],learned=!0;break}}catch(e){console.error(`Error checking RF status:`,e)}}if(!learned)if(confirm(`Timeout waiting for ${buttonNames[i]} button.\n\nRetry this button?`)){i--;continue}else
// Skip this button
learnedCodes[i]=-1}
// Update input fields for parameters 9-12 (PARAM_KEYCODE_0 through PARAM_KEYCODE_3)
for(let i=0;i<4;i++){let input=ge(`in_${9+i}`);input&&(input.value=learnedCodes[i],markChanged(input))}
// Commit just the RF keycodes (params 9-12)
let sp={};for(let i=0;i<4;i++)sp[9+i]=learnedCodes[i];if((await fetch(`./sp`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(sp)})).ok){
// Unhighlight the keycode inputs
for(let i=0;i<4;i++){let input=ge(`in_${9+i}`);input&&input.classList.remove(`changed`)}
// Check if commit button should stay enabled
document.querySelectorAll(`input.changed`).length===0&&(ge(`commit_btn`).disabled=!0)}
// Re-enable RF controls after programming
await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_enable`})});
// Show summary
let summary=`RF Remote Programming Complete!
`;for(let i=0;i<4;i++)learnedCodes[i]===-1?summary+=`${buttonNames[i]}: Not programmed\n`:summary+=`${buttonNames[i]}: ${learnedCodes[i]}\n`;alert(summary),await fetchStatus()}async function downloadLogFile(){try{let response=await fetch(`./log`);if(!response.ok)throw Error(`Network response was not ok`);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){console.error(`Download failed:`,error)}}
// Initial Load
window.onload=fetchStatus;</script></body></html>