515 lines
14 KiB
HTML
515 lines
14 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 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 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){
|
|
alert("invalid config for "+v.getAttribute('name')+"("+v.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)
|
|
if (configItem.type === 'password'){
|
|
el.setAttribute('type','password');
|
|
}
|
|
else if (configItem.type === 'number'){
|
|
el.setAttribute('type','number');
|
|
}
|
|
else{
|
|
el.setAttribute('type','text');
|
|
}
|
|
frame.appendChild(el);
|
|
return el;
|
|
}
|
|
let configDefinitions;
|
|
function loadConfigDefinitions() {
|
|
getJson("api/capabilities")
|
|
.then(function (capabilities) {
|
|
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;
|
|
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 = 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 valueFrame=document.createElement("div");
|
|
valueFrame.classList.add("value");
|
|
row.appendChild(valueFrame);
|
|
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=document.createElement("div");
|
|
btContainer.classList.add("buttonContainer");
|
|
row.appendChild(btContainer);
|
|
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');
|
|
let changeEvent=new Event('change');
|
|
valueEl.dispatchEvent(changeEvent);
|
|
})
|
|
bt.textContent = "X";
|
|
btContainer.appendChild(bt);
|
|
bt = document.createElement('button');
|
|
bt.classList.add('infoButton');
|
|
bt.addEventListener('click', function (ev) {
|
|
showOverlay(item.description);
|
|
});
|
|
bt.textContent = "?";
|
|
btContainer.appendChild(bt);
|
|
frame.appendChild(row);
|
|
})
|
|
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);
|
|
});
|
|
}
|
|
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 input{
|
|
color: green
|
|
}
|
|
.changed select{
|
|
color: green
|
|
}
|
|
span.label {
|
|
width: 10em;
|
|
display: inline-block;
|
|
opacity: 0.6;
|
|
}
|
|
.value {
|
|
width: 19em;
|
|
display: flex;
|
|
flex-direction: row;
|
|
}
|
|
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;
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
}
|
|
.filter {
|
|
display: inline-block;
|
|
}
|
|
.buttons {
|
|
padding-left: 1em;
|
|
}
|
|
button.infoButton {
|
|
margin-left: 1em;
|
|
vertical-align: bottom;
|
|
}
|
|
#canDetails{
|
|
display:none;
|
|
}
|
|
#canDetails.visible{
|
|
display: block;
|
|
}
|
|
|
|
.overlayContainer {
|
|
position: fixed;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background-color: #80808070;
|
|
display: flex;
|
|
}
|
|
.overlayContainer.hidden{
|
|
display: none;
|
|
}
|
|
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;
|
|
}
|
|
.overlayButtons {
|
|
border-top: 1px solid grey;
|
|
padding-top: 0.5em;
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: end;
|
|
}
|
|
</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>
|
|
<button class="infoButton" id="converterInfo">?</button>
|
|
</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>
|
|
<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>
|
|
|
|
|