esp32-nmea2000-obp60/web/index.html

346 lines
9.3 KiB
HTML

<!DOCTYPE html>
<html><head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<title>NMEA 2000 Gateway</title>
<script type="text/javascript">
let self=this;
let lastUpdate=(new Date()).getTime();
function alertRestart(){
alert("Board reset triggered, reconnect WLAN if necessary");
}
function getJson(url){
return fetch(url)
.then(function(r){return r.json()});
}
function reset(){
fetch('/api/reset');
alertRestart();
}
function update(){
let now=(new Date()).getTime();
let ce=document.getElementById('connected');
if (ce){
if ((lastUpdate+3000) > now){
ce.classList.add('ok');
}
else{
ce.classList.remove('ok');
}
}
getJson('/api/status')
.then(function(jsonData){
for (let k in jsonData){
if (k == 'cnv'){
updateCanDetails(jsonData[k]);
}
else{
let el=document.getElementById(k);
if (el) el.textContent=jsonData[k];
}
}
lastUpdate=(new Date()).getTime();
})
}
function resetForm(ev){
getJson("/api/config")
.then(function(jsonData){
for (let k in jsonData){
let el=document.querySelector("[name='"+k+"']");
if (el){
let v=jsonData[k];
el.value=v;
el.setAttribute('data-loaded',v);
checkChange(el);
}
}
});
}
function checkMaxClients(v){
let parsed=parseInt(v);
if (isNaN(parsed)) return "not a valid number";
if (parsed < 0) return "must be >= 0";
if (parsed > 10) return "max is 10";
}
function checkSystemName(v){
//2...32 characters for ssid
let allowed=v.replace(/[^a-zA-Z0-9]*/g,'');
if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9";
if (v.length < 2 || v.length > 32) return "invalid length (2...32)";
}
function changeConfig(){
let url="/api/setConfig?";
let values=document.querySelectorAll('.configForm select , .configForm input');
for (let i=0;i<values.length;i++){
let v=values[i];
let check=v.getAttribute('data-check');
if (check){
if (typeof(self[check]) === 'function'){
let res=self[check](v.value);
if (res){
alert("invalid config for "+v.getAttribute('name')+"("+v.value+"):\n"+res);
return;
}
}
}
url+=v.getAttribute('name')+"="+encodeURIComponent(v.value)+"&";
}
getJson(url)
.then(function(status){
if(status.status == 'OK'){
alertRestart();
}
else{
alert("unable to set config: "+status.status);
}
})
}
function factoryReset(){
getJson("/api/resetConfig")
.then(function(status){
alertRestart();
})
}
function showCanDetails(on){
let el=document.getElementById('canDetails');
if (!el) return;
if (on) el.classList.add('visible');
else(el.classList).remove('visible');
}
function updateCanDetails(details){
let frame=document.getElementById('canDetails');
if (! frame) return;
for (let k in details){
let el=frame.querySelector("[data-id=\""+k+"\"] ");
if (!el){
el=document.createElement('div');
el.classList.add('row');
let cv=document.createElement('span');
cv.classList.add('label');
cv.textContent="PGN"+k;
el.appendChild(cv);
cv=document.createElement('span');
cv.classList.add('value');
cv.setAttribute('data-id',k);
cv.textContent=details[k];
el.appendChild(cv);
frame.appendChild(el);
}
else{
el.textContent=details[k];
}
}
}
function checkChange(el) {
let loaded = el.getAttribute('data-loaded');
if (loaded !== undefined) {
if (loaded != el.value) {
el.classList.add('changed');
}
else {
el.classList.remove("changed");
}
}
}
function createInput(configItem){
let el;
if (configItem.type === 'boolean' || configItem.type === 'list'){
el=document.createElement('select')
el.setAttribute('name',configItem.name)
let slist=[];
if (configItem.list){
configItem.list.forEach(function(v){
slist.push({l:v,v:v});
})
}
else{
slist.push({l:'on',v:'true'})
slist.push({l:'off',v:'false'})
}
slist.forEach(function(sitem){
let sitemEl=document.createElement('option');
sitemEl.setAttribute('value',sitem.v);
sitemEl.textContent=sitem.l;
el.appendChild(sitemEl);
})
return el;
}
el=document.createElement('input');
el.setAttribute('name',configItem.name)
if (configItem.type === 'password'){
el.setAttribute('type','password');
}
else if (configItem.type === 'number'){
el.setAttribute('type','number');
}
else{
el.setAttribute('type','text');
}
return el;
}
let configDefinitions;
function loadConfigDefinitions(){
getJson("config.json")
.then(function(defs){
let frame=document.querySelector('.configFormRows');
if (! frame) throw Error("no config form");
frame.innerHTML='';
configDefinitions=defs;
defs.forEach(function(item){
if (! item.type) return;
let row=document.createElement('div');
row.classList.add('row');
let label=item.label || item.name;
let labelEl=document.createElement('span');
labelEl.classList.add('label');
labelEl.textContent=label;
row.appendChild(labelEl);
let valueEl=createInput(item);
if (!valueEl) return;
valueEl.setAttribute('data-default',item.default);
valueEl.addEventListener('change',function(ev){
let el=ev.target;
checkChange(el);
})
if (item.check) valueEl.setAttribute('data-check',item.check);
row.appendChild(valueEl);
let bt=document.createElement('button');
bt.classList.add('defaultButton');
bt.setAttribute('data-default',item.default);
bt.addEventListener('click',function(ev){
valueEl.value=valueEl.getAttribute('data-default');
checkChange(valueEl);
})
bt.textContent="X";
row.appendChild(bt);
bt=document.createElement('button');
bt.classList.add('infoButton');
bt.addEventListener('click',function(ev){
alert(item.description);
});
bt.textContent="?";
row.appendChild(bt);
frame.appendChild(row);
})
resetForm();
})
.catch(function(err){alert("unable to load config: "+err)})
}
window.setInterval(update,1000);
window.addEventListener('load',function(){
let buttons=document.querySelectorAll('button');
for (let i=0;i<buttons.length;i++){
let be=buttons[i];
be.onclick=window[be.id]; //assume a function with the button id
}
let cd=document.getElementById("showCanDetails");
cd.addEventListener('change',function(ev){
showCanDetails(ev.target.checked);
});
loadConfigDefinitions();
});
</script>
<style type="text/css">
.configForm {
margin-top: 1em;
margin-bottom: 1em;
padding-top: 0.5em;
padding-bottom: 0.5em;
border: 1px solid grey;
max-width: 40em;
}
.changed {
color: green
}
span.label {
width: 10em;
display: inline-block;
opacity: 0.6;
}
span#connected {
display: inline-block;
background-color: red;
width: 1em;
height: 1em;
border-radius: 50%;
}
span#connected.ok{
background-color: #13ac13;
}
.row {
margin: 0.5em;
}
.buttons {
padding-left: 1em;
}
button.infoButton {
margin-left: 1em;
vertical-align: bottom;
}
#canDetails{
display:none;
}
#canDetails.visible{
display: block;
}
</style>
</head>
<body>
<div class="main">
<h1>NMEA 2000 Gateway </h1>
<div class="row">
<span class="label">VERSION</span>
<span class="value" id="version">---</span>
</div>
<div class="row">
<span class="label">connected</span>
<span class="value" id="connected"></span>
</div>
<div class="row">
<span class="label">Access Point IP</span>
<span class="value" id="apIp">---</span>
</div>
<div class="row">
<span class="label"># NMEA2000 messages</span>
<span class="value" id="numcan">---</span>
</div>
<div class="row">
<span class="label">NMEA2000 details</span>
<input type="checkbox" id="showCanDetails"></span>
</div>
<div class="row" id="canDetails">
</div>
<div class="row">
<span class="label"># TCP clients</span>
<span class="value" id="numClients">---</span>
</div>
<div class="row">
<span class="label">wifi client connected</span>
<span class="value" id="wifiConnected">---</span>
</div>
<div class="row">
<span class="label">wifi client IP</span>
<span class="value" id="clientIP">---</span>
</div>
<button id="reset" >Reset</button>
<div class="configForm">
<div class="configFormRows">
</div>
<div class="buttons">
<button id="resetForm">ReloadConfig</button>
<button id="changeConfig">Save&Restart</button>
<button id="factoryReset">FactoryReset</button>
</div>
</div>
</div>
</body>
</html>