esp32-nmea2000-obp60/web/index.html

724 lines
21 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();
let reloadConfig=false;
function addEl(type,clazz,parent,text){
let el=document.createElement(type);
el.classList.add(clazz);
if (text) el.textContent=text;
if (parent)parent.appendChild(el);
return el;
}
function alertRestart(){
reloadConfig=true;
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();
if (reloadConfig){
reloadConfig=false;
resetForm();
}
})
}
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);
let changeEvent=new Event('change');
el.dispatchEvent(changeEvent);
}
}
});
}
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 checkApPass(v){
//min 8 characters
if (v.length < 8){
return "password must be at least 8 characters";
}
}
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 name=v.getAttribute('name');
if (! name) continue;
if (name.indexOf("_") >= 0) continue;
let check=v.getAttribute('data-check');
if (check){
if (typeof(self[check]) === 'function'){
let res=self[check](v.value);
if (res){
let value=v.value;
if (v.type === 'password') value="******";
alert("invalid config for "+v.getAttribute('name')+"("+value+"):\n"+res);
return;
}
}
}
url+=name+"="+encodeURIComponent(v.value)+"&";
}
getJson(url)
.then(function(status){
if(status.status == 'OK'){
alertRestart();
}
else{
alert("unable to set config: "+status.status);
}
})
}
function factoryReset(){
if (! confirm("Really delete all configuration?\n"+
"This will reset all your Wifi settings and disconnect you.")){
return;
}
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 showOverlay(text,isHtml){
let el=document.getElementById('overlayContent');
if (isHtml){
el.innerHTML=text;
el.classList.remove("text");
}
else{
el.textContent=text;
el.classList.add("text");
}
let container=document.getElementById('overlayContainer');
container.classList.remove('hidden');
}
function hideOverlay(){
let container=document.getElementById('overlayContainer');
container.classList.add('hidden');
}
function checkChange(el,row) {
let loaded = el.getAttribute('data-loaded');
if (loaded !== undefined) {
if (loaded != el.value) {
row.classList.add('changed');
}
else {
row.classList.remove("changed");
}
}
}
function createInput(configItem,frame){
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);
})
frame.appendChild(el);
return el;
}
if (configItem.type === 'filter'){
el=document.createElement('div');
el.classList.add('filter');
let ais=createInput({
type:'list',
name:configItem.name+"_ais",
list:['aison','aisoff']
},el);
let mode=createInput({
type:'list',
name:configItem.name+"_mode",
list: ['whitelist','blacklist']
},el);
let sentences=createInput({
type:'text',
name:configItem.name+"_sentences",
},el);
let data=document.createElement('input');
data.setAttribute('type','hidden');
el.appendChild(data);
let changeFunction=function(){
let cv=data.value||"";
let parts=cv.split(":");
ais.value=(parts[0]=='0')?"aisoff":"aison";
mode.value=(parts[1]=='0')?"whitelist":"blacklist";
sentences.value=parts[2]||"";
}
let updateFunction=function(){
let nv=(ais.value == 'aison')?"1":"0";
nv+=":";
nv+=(mode.value == 'blacklist')?"1":"0";
nv+=":";
nv+=sentences.value;
data.value=nv;
let chev=new Event('change');
data.dispatchEvent(chev);
}
mode.addEventListener('change',updateFunction);
ais.addEventListener("change",updateFunction);
sentences.addEventListener("change",updateFunction);
data.addEventListener('change',function(ev){
changeFunction();
});
data.setAttribute('name',configItem.name);
frame.appendChild(el);
return data;
}
el=document.createElement('input');
el.setAttribute('name',configItem.name)
frame.appendChild(el);
if (configItem.type === 'password'){
el.setAttribute('type','password');
let vis=addEl('span','icon-eye',frame);
vis.addEventListener('click',function(v){
if (vis.classList.toggle('active')){
el.setAttribute('type','text');
}
else{
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("api/capabilities")
.then(function (capabilities) {
getJson("config.json")
.then(function (defs) {
let category;
let categoryEl;
let frame = document.querySelector('.configFormRows');
if (!frame) throw Error("no config form");
frame.innerHTML = '';
configDefinitions = defs;
defs.forEach(function (item) {
if (!item.type) return;
if (item.category != category || ! categoryEl){
let categoryFrame=addEl('div','category',frame);
let categoryTitle=addEl('div','title',categoryFrame);
let categoryButton=addEl('button','categoryButton',categoryTitle,'v');
addEl('span','label',categoryTitle,item.category);
categoryEl=addEl('div','content',categoryFrame);
categoryEl.classList.add('hidden');
let currentEl=categoryEl;
categoryTitle.addEventListener('click',function(ev){
let rs=currentEl.classList.toggle('hidden');
if (rs) categoryButton.textContent="v";
else categoryButton.textContent="^";
})
category=item.category;
}
if (item.capabilities !== undefined){
for (let capability in item.capabilities){
let values=item.capabilities[capability];
if ( !capabilities[capability]) return;
let found =false;
values.forEach(function(v){
if (capabilities[capability] == v) found=true;
});
if (! found) return;
}
}
let row = addEl('div','row',categoryEl);
let label = item.label || item.name;
addEl('span','label',row,label);
let valueFrame=addEl('div','value',row);
let valueEl = createInput(item,valueFrame);
if (!valueEl) return;
valueEl.setAttribute('data-default', item.default);
valueEl.addEventListener('change', function (ev) {
let el = ev.target;
checkChange(el,row);
})
if (item.check) valueEl.setAttribute('data-check', item.check);
let btContainer=addEl('div','buttonContainer',row);
let bt = addEl('button','defaultButton',btContainer,'X');
bt.setAttribute('data-default', item.default);
bt.addEventListener('click', function (ev) {
valueEl.value = valueEl.getAttribute('data-default');
let changeEvent=new Event('change');
valueEl.dispatchEvent(changeEvent);
})
bt = addEl('button','infoButton',btContainer,'?');
bt.addEventListener('click', function (ev) {
showOverlay(item.description);
});
})
resetForm();
})
})
.catch(function (err) { alert("unable to load config: " + err) })
}
function converterInfo(){
getJson("api/converterInfo").then(function(json){
let text="<h3>Converted entities</h3>";
text+="<p><b>NMEA0183 to NMEA2000:</b><br/>";
text+=" "+(json.nmea0183||"").replace(/,/g,", ");
text+="</p>";
text+="<p><b>NMEA2000 to NMEA0183:</b><br/>";
text+=" "+(json.nmea2000||"").replace(/,/g,", ");
text+="</p>";
showOverlay(text,true);
});
}
function handleTab(el){
let activeName=el.getAttribute('data-page');
if (! activeName) return;
let activeTab=document.getElementById(activeName);
if (!activeTab) return;
let all=document.querySelectorAll('.tabPage');
for (let i=0;i<all.length;i++){
all[i].classList.add('hidden');
}
let tabs=document.querySelectorAll('.tab');
for (let i=0;i<all.length;i++){
tabs[i].classList.remove('active');
}
el.classList.add('active');
activeTab.classList.remove('hidden');
}
function createDashboardItem(name,def,parent){
let frame=addEl('div','dash',parent);
let title=addEl('span','dashTitle',frame,name);
let value=addEl('span','dashValue',frame);
value.setAttribute('id','data_'+name);
return value;
}
function createDashboard(){
let frame=document.getElementById('dashboardPage');
if (! frame) return;
getJson("api/boatData").then(function(json){
frame.innerHTML='';
for (let n in json){
createDashboardItem(n,json[n],frame);
}
updateDashboard(json);
});
}
let valueFormatters={
formatCourse: function(v){let x=parseFloat(v); return x.toFixed(0);},
formatKnots: function(v){let x=parseFloat(v); return x.toFixed(2);},
formatWind: function(v){let x=parseFloat(v); return x.toFixed(0);},
mtr2nm: function(v){let x=parseFloat(v); return x.toFixed(2);},
kelvinToC: function(v){let x=parseFloat(v); return x.toFixed(0);},
formatFixed0: function(v){let x=parseFloat(v); return x.toFixed(0);},
formatDepth: function(v){let x=parseFloat(v); return x.toFixed(1);},
}
function updateDashboard(data){
for (let n in data){
let de=document.getElementById('data_'+n);
if (de){
if (data[n].valid){
let formatter;
if (data[n].format && data[n].format != "NULL"){
let key=data[n].format.replace(/^\&/,'');
formatter=valueFormatters[key];
}
if (formatter){
de.textContent=formatter(data[n].value);
}
else {
let v = parseFloat(data[n].value);
if (!isNaN(v)) {
v = v.toFixed(3)
de.textContent = v;
}
else {
de.textContent = data[n].value;
}
}
}
else de.textContent="---";
}
}
}
window.setInterval(update,1000);
window.setInterval(function(){
let dp=document.getElementById('dashboardPage');
if (dp.classList.contains('hidden')) return;
getJson('api/boatData').then(function(data){
updateDashboard(data);
});
},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);
});
let tabs=document.querySelectorAll('.tab');
for (let i=0;i<tabs.length;i++){
tabs[i].addEventListener('click',function(ev){
handleTab(ev.target);
});
}
loadConfigDefinitions();
createDashboard();
});
</script>
<style type="text/css">
.configForm {
padding-bottom: 0.5em;
}
.configForm .buttons {
margin-top: 0.5em;
}
.configForm .content>div:nth-child(even) {
background-color: rgb(211 211 211 / 43%);
}
#statusPage .even {
background-color: rgb(211 211 211 / 43%);
}
.category .title .label {
opacity: 1;
margin-left: 1em;
}
.changed input{
color: green
}
.changed select{
color: green
}
span.label {
width: 10em;
display: inline-block;
opacity: 0.6;
}
.configForm .value {
width: 21em;
display: flex;
flex-direction: row;
margin-bottom: 0.2em;
}
span#connected {
display: inline-block;
background-color: red;
width: 1em;
height: 1em;
border-radius: 50%;
}
span#connected.ok{
background-color: #13ac13;
}
.row {
padding: 0.5em;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
input,select {
border: 1px solid #808080a1;
font-size: 0.9em;
padding: 0.2em;
}
.filter {
display: inline-block;
}
.buttons {
padding-left: 1em;
}
button.infoButton {
margin-left: 1em;
vertical-align: bottom;
}
.category .title {
padding-left: 0.5em;
background-color: lightgray;
padding-top: 0.3em;
padding-bottom: 0.5em;
border-bottom: 1px solid darkgray;
}
.hidden{
display: none !important;
}
#canDetails{
display:none;
}
#canDetails.visible{
display: block;
}
.overlayContainer {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #80808070;
display: flex;
}
div#overlay {
margin: auto;
background-color: white;
padding: 0.5em;
max-width: 100%;
box-sizing: border-box;
}
div#overlayContent {
padding: 0.5em;
}
div#overlayContent.text{
white-space: pre-line;
}
.overlayButtons {
border-top: 1px solid grey;
padding-top: 0.5em;
display: flex;
flex-direction: row;
justify-content: end;
}
#tabs {
display: flex;
border-bottom: 1px solid grey;
margin-bottom: 0.5em;
}
#tabs .tab {
background-color: lightgray;
padding: 0.5em;
border-left: 1px;
border-right: 1px;
border-top: 1px;
border-bottom: 1px;
border-color: grey;
border-style: solid;
opacity: 0.6;
}
#tabs .tab.active{
opacity: 1;
}
.buttons button{
padding: 0.5em;
}
button#reset{
padding: 0.5em;
}
h1{
margin-bottom: 0;
}
.icon-eye{
background-image: url("data:image/svg+xml;utf-8,<svg width='24' height='24' xmlns='http://www.w3.org/2000/svg' xmlns:svg='http://www.w3.org/2000/svg'> <g class='layer'> <title>Layer 1</title> <path d='m0,0l24,0l0,24l-24,0l0,-24z' fill='none' id='svg_1'/> <path d='m12,6c3.79,0 7.17,2.13 8.82,5.5c-1.65,3.37 -5.03,5.5 -8.82,5.5s-7.17,-2.13 -8.82,-5.5c1.65,-3.37 5.03,-5.5 8.82,-5.5m0,-2c-5,0 -9.27,3.11 -11,7.5c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zm0,5c1.38,0 2.5,1.12 2.5,2.5s-1.12,2.5 -2.5,2.5s-2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5m0,-2c-2.48,0 -4.5,2.02 -4.5,4.5s2.02,4.5 4.5,4.5s4.5,-2.02 4.5,-4.5s-2.02,-4.5 -4.5,-4.5z' id='svg_2'/> </g> </svg>");
height: 1.5em;
width: 1.5em;
margin-left: 0.5em;
opacity: 0.3;
}
.icon-eye.active{
opacity: 1;
}
.dash {
width: 6em;
height: 4em;
display: flex;
flex-direction: column;
border: 1px solid grey;
overflow: hidden;
margin: 0;
box-sizing: border-box;
font-size: 1.2em;
}
div#dashboardPage {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.dashTitle {
font-size: 0.8em;
background-color: lightgray;
}
.dashValue{
font-size: 1.8em;
text-align: center;
}
</style>
</head>
<body>
<div class="main">
<h1>NMEA 2000 Gateway </h1>
<div class="row">
<span class="label">connected</span>
<span class="value" id="connected"></span>
</div>
<div id="tabs">
<div class="tab active" data-page="statusPage">Status</div>
<div class="tab" data-page="configPage">Config</div>
<div class="tab" data-page="dashboardPage">Data</div>
</div>
<div id="statusPage" class="tabPage">
<div class="row">
<span class="label">VERSION</span>
<span class="value" id="version">---</span>
<button class="infoButton" id="converterInfo">?</button>
</div>
<div class="row even">
<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 even">
<span class="label">NMEA2000 details</span>
<input type="checkbox" id="showCanDetails"></span>
</div>
<div class="row even" id="canDetails">
</div>
<div class="row">
<span class="label"># TCP clients</span>
<span class="value" id="numClients">---</span>
</div>
<div class="row even">
<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>
<div class="configForm tabPage hidden" id="configPage" >
<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 class="tabPage hidden" id="dashboardPage">
</div>
</div>
<div class="overlayContainer hidden" id="overlayContainer">
<div id="overlay">
<div id="overlayContent">
AHA
</div>
<div class="overlayButtons">
<button id="hideOverlay">Close</button>
</div>
</div>
</div>
</body>
</html>