1643 lines
		
	
	
		
			53 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			1643 lines
		
	
	
		
			53 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| let self = this;
 | |
| let lastUpdate = (new Date()).getTime();
 | |
| let reloadConfig = false;
 | |
| let needAdminPass=true;
 | |
| let lastSalt="";
 | |
| function addEl(type, clazz, parent, text) {
 | |
|     let el = document.createElement(type);
 | |
|     if (clazz) {
 | |
|         if (!(clazz instanceof Array)) {
 | |
|             clazz = clazz.split(/  */);
 | |
|         }
 | |
|         clazz.forEach(function (ce) {
 | |
|             el.classList.add(ce);
 | |
|         });
 | |
|     }
 | |
|     if (text) el.textContent = text;
 | |
|     if (parent) parent.appendChild(el);
 | |
|     return el;
 | |
| }
 | |
| function forEl(query, callback,base) {
 | |
|     if (! base) base=document;
 | |
|     let all = base.querySelectorAll(query);
 | |
|     for (let i = 0; i < all.length; i++) {
 | |
|         callback(all[i]);
 | |
|     }
 | |
| }
 | |
| function closestParent(element,clazz){
 | |
|     while (true){
 | |
|         let parent=element.parentElement;
 | |
|         if (! parent) return;
 | |
|         if (parent.classList.contains(clazz)) return parent;
 | |
|         element=parent;
 | |
|     }
 | |
| }
 | |
| 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 getText(url){
 | |
|     return fetch(url)
 | |
|         .then(function (r) { return r.text() });
 | |
| }
 | |
| function reset() {
 | |
|     ensurePass()
 | |
|         .then(function (hash) {
 | |
|             fetch('/api/reset?_hash='+encodeURIComponent(hash));
 | |
|             alertRestart();
 | |
|         })
 | |
|         .catch(function (e) { });
 | |
| }
 | |
| 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 == "salt"){
 | |
|                     lastSalt=jsonData[k];
 | |
|                 }
 | |
|                 if (typeof (jsonData[k]) === 'object') {
 | |
|                     for (let sk in jsonData[k]) {
 | |
|                         let key = k + "." + sk;
 | |
|                         if (typeof (jsonData[k][sk]) === 'object') {
 | |
|                             //msg details
 | |
|                             updateMsgDetails(key, jsonData[k][sk]);
 | |
|                         }
 | |
|                         else {
 | |
|                             let el = document.getElementById(key);
 | |
|                             if (el) el.textContent = jsonData[k][sk];
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|                 else {
 | |
|                     let el = document.getElementById(k);
 | |
|                     if (el) el.textContent = jsonData[k];
 | |
|                     forEl('.status-'+k,function(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) {
 | |
|                 if (k == "useAdminPass"){
 | |
|                     needAdminPass=jsonData[k] != 'false';
 | |
|                 }
 | |
|                 let el = document.querySelector("[name='" + k + "']");
 | |
|                 if (el) {
 | |
|                     let v = jsonData[k];
 | |
|                     let def=getConfigDefition(k);
 | |
|                     if (def.check == 'checkMinMax'){
 | |
|                         //simple migration if the current value is outside the range
 | |
|                         //we even "hide" this from the user
 | |
|                         v=parseFloat(v);
 | |
|                         if (! isNaN(v)){
 | |
|                             if (def.min !== undefined){
 | |
|                                 if (v < parseFloat(def.min)) v=parseFloat(def.min);
 | |
|                             }
 | |
|                             if (def.max !== undefined){
 | |
|                                 if (v > parseFloat(def.max)) v=parseFloat(def.max);
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
|                     el.value = v;
 | |
|                     el.setAttribute('data-loaded', v);
 | |
|                     let changeEvent = new Event('change');
 | |
|                     el.dispatchEvent(changeEvent);
 | |
|                 }
 | |
|             }
 | |
|             let name=jsonData.systemName;
 | |
|             if (name){
 | |
|                 let el=document.getElementById('headline');
 | |
|                 if (el) el.textContent=name;
 | |
|                 document.title=name;
 | |
|             }
 | |
|         });
 | |
| }
 | |
| function checkMinMax(v,allValues,def){
 | |
|     let parsed=parseFloat(v);
 | |
|     if (isNaN(parsed)) return "must be a number";
 | |
|     if (def.min !== undefined){
 | |
|         if (parsed < parseFloat(def.min)) return "must be >= "+def.min;
 | |
|     }
 | |
|     if (def.max !== undefined){
 | |
|         if (parsed > parseFloat(def.max)) return "must be <= "+def.max;
 | |
|     }
 | |
| }
 | |
| 
 | |
| 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 checkAdminPass(v){
 | |
|     return checkApPass(v);
 | |
| }
 | |
| 
 | |
| function checkXDR(v,allValues){
 | |
|     if (! v) return;
 | |
|     let parts=v.split(',');
 | |
|     if (parseInt(parts[1])  == 0) return;
 | |
|     if (parseInt(parts[1]) != 0 && ! parts[6]) return "missing transducer name";
 | |
|     for (let k in allValues){
 | |
|         if (! k.match(/^XDR/)) continue;
 | |
|         let cmp=allValues[k];
 | |
|         if (cmp == v){
 | |
|             return "same mapping already defined in "+k;
 | |
|         }
 | |
|         let cmpParts=cmp.split(',');
 | |
|         if (parseInt(cmpParts[1]) != 0 && parts[6] == cmpParts[6]){
 | |
|             return "transducer "+parts[6]+" already defined in "+k;
 | |
|         }
 | |
|         //check similar mappings
 | |
|         if (parts[0]==cmpParts[0] && parts[2] == cmpParts[2] && parts[3] == cmpParts[3]){
 | |
|             if (parts[4] == cmpParts[4] && parts[5] == cmpParts[5]){
 | |
|                 return "mapping for the same entity already defined in "+k;
 | |
|             }
 | |
|             if ((parseInt(parts[4]) == 0 && parseInt(cmpParts[4]) == 1)||
 | |
|             (parseInt(parts[4]) == 1 && parseInt(cmpParts[4]) == 0)
 | |
|             ){
 | |
|                 //ignore and single for the same mapping
 | |
|                 return "mapping for the same entity already defined in "+k;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| function changeConfig() {
 | |
|     ensurePass()
 | |
|         .then(function (pass) {
 | |
|             let newAdminPass;
 | |
|             let url = "/api/setConfig?_hash="+encodeURIComponent(pass)+"&";
 | |
|             let values = document.querySelectorAll('.configForm select , .configForm input');
 | |
|             let allValues = {};
 | |
|             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 def = getConfigDefition(name);
 | |
|                 if (def.type === 'password' && v.value == '') {
 | |
|                     continue;
 | |
|                 }
 | |
|                 let check = v.getAttribute('data-check');
 | |
|                 if (check) {
 | |
|                     if (typeof (self[check]) === 'function') {
 | |
|                         let res = self[check](v.value, allValues, getConfigDefition(name));
 | |
|                         if (res) {
 | |
|                             let value = v.value;
 | |
|                             if (v.type === 'password') value = "******";
 | |
|                             alert("invalid config for " + v.getAttribute('name') + "(" + value + "):\n" + res);
 | |
|                             return;
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|                 if (name == 'adminPassword'){
 | |
|                     newAdminPass=v.value;
 | |
|                 }
 | |
|                 allValues[name] = v.value;
 | |
|                 url += name + "=" + encodeURIComponent(v.value) + "&";
 | |
|             }
 | |
|             getJson(url)
 | |
|                 .then(function (status) {
 | |
|                     if (status.status == 'OK') {
 | |
|                         if (newAdminPass !== undefined) {
 | |
|                             forEl('#adminPassInput', function (el) {
 | |
|                                 el.valu = newAdminPass;
 | |
|                             });
 | |
|                             saveAdminPass(newAdminPass,true);
 | |
|                         }
 | |
|                         alertRestart();
 | |
|                     }
 | |
|                     else {
 | |
|                         alert("unable to set config: " + status.status);
 | |
|                     }
 | |
|                 })
 | |
|         })
 | |
|         .catch(function (e) { alert("Invalid password"); })
 | |
| }
 | |
| function factoryReset() {
 | |
|     ensurePass()
 | |
|         .then(function (hash) {
 | |
|             if (!confirm("Really delete all configuration?\n" +
 | |
|                 "This will reset all your Wifi settings and disconnect you.")) {
 | |
|                 return;
 | |
|             }
 | |
|             getJson("/api/resetConfig?_hash="+encodeURIComponent(hash))
 | |
|                 .then(function (status) {
 | |
|                     alertRestart();
 | |
|                 })
 | |
|         })
 | |
|         .catch(function (e) { });
 | |
| }
 | |
| function createCounterDisplay(parent,label,key,isEven){
 | |
|     let clazz="row icon-row counter-row";
 | |
|     if (isEven) clazz+=" even";
 | |
|     let row=addEl('div',clazz,parent);
 | |
|     let icon=addEl('span','icon icon-more',row);
 | |
|     addEl('span','label',row,label);
 | |
|     let value=addEl('span','value',row,'---');
 | |
|     value.setAttribute('id',key+".sumOk");
 | |
|     let display=addEl('div',clazz+" msgDetails hidden",parent);
 | |
|     display.setAttribute('id',key+".ok");
 | |
|     row.addEventListener('click',function(ev){
 | |
|         let rs=display.classList.toggle('hidden');
 | |
|         if (rs){
 | |
|             icon.classList.add('icon-more');
 | |
|             icon.classList.remove('icon-less');
 | |
|         }
 | |
|         else{
 | |
|             icon.classList.remove('icon-more');
 | |
|             icon.classList.add('icon-less');
 | |
|         }
 | |
|     });
 | |
| }
 | |
| 
 | |
| function updateMsgDetails(key, details) {
 | |
|     forEl('.msgDetails', function (frame) {
 | |
|         if (frame.getAttribute('id') !== key) return;
 | |
|         for (let k in details) {
 | |
|             let el = frame.querySelector("[data-id=\"" + k + "\"] ");
 | |
|             if (!el) {
 | |
|                 el = addEl('div', 'row', frame);
 | |
|                 let cv = addEl('span', 'label', el, k);
 | |
|                 cv = addEl('span', 'value', el, details[k]);
 | |
|                 cv.setAttribute('data-id', k);
 | |
|             }
 | |
|             else {
 | |
|                 el.textContent = details[k];
 | |
|             }
 | |
|         }
 | |
|         forEl('.value',function(el){
 | |
|             let k=el.getAttribute('data-id');
 | |
|             if (k && ! details[k]){
 | |
|                 el.parentElement.remove();
 | |
|             }
 | |
|         },frame);
 | |
|     });
 | |
| }
 | |
| let counters={
 | |
|     count2Kin: 'NMEA2000 in',
 | |
|     count2Kout: 'NMEA2000 out',
 | |
|     countTCPin: 'TCP in',
 | |
|     countTCPout: 'TCP out',
 | |
|     countUSBin: 'USB in',
 | |
|     countUSBout: 'USB out',
 | |
|     countSerialIn: 'Serial in',
 | |
|     countSerialOut: 'Serial out'
 | |
| }
 | |
| 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,name) {
 | |
|     let loaded = el.getAttribute('data-loaded');
 | |
|     if (loaded !== undefined) {
 | |
|         if (loaded != el.value) {
 | |
|             row.classList.add('changed');
 | |
|         }
 | |
|         else {
 | |
|             row.classList.remove("changed");
 | |
|         }
 | |
|     }
 | |
|     let dependend=conditionRelations[name];
 | |
|     if (dependend){
 | |
|         for (let el in dependend){
 | |
|             checkCondition(dependend[el]);
 | |
|         }
 | |
|     }
 | |
| }
 | |
| let configDefinitions={};
 | |
| let xdrConfig={};
 | |
| //a map between the name of a config item and a list of dependend items
 | |
| let conditionRelations={};
 | |
| function getConfigDefition(name){
 | |
|     if (! name) return {};
 | |
|     let def;
 | |
|     for (let k in configDefinitions){
 | |
|         if (configDefinitions[k].name === name){
 | |
|             def=configDefinitions[k];
 | |
|             break;
 | |
|         }
 | |
|     }
 | |
|     if (! def) return {};
 | |
|     return def;
 | |
| }
 | |
| function getConditions(name){
 | |
|     let def=getConfigDefition(name);
 | |
|     if (! def) return;
 | |
|     let condition=def.condition;
 | |
|     if (! condition) return;
 | |
|     if (! (condition instanceof Array)) condition=[condition];
 | |
|     return condition;
 | |
| }
 | |
| function checkCondition(element){
 | |
|     let name=element.getAttribute('name');
 | |
|     let condition=getConditions(name);
 | |
|     if (! condition) return;
 | |
|     let visible=false;
 | |
|     condition.forEach(function(cel){
 | |
|         let lvis=true;
 | |
|         for (let k in cel){
 | |
|             let item=document.querySelector('[name='+k+']');
 | |
|             if (item){
 | |
|                 if (item.value != cel[k]) lvis=false;
 | |
|             }
 | |
|         }
 | |
|         if (lvis) visible=true;
 | |
|     });
 | |
|     let row=closestParent(element,'row');
 | |
|     if (!row) return;
 | |
|     if (visible) row.classList.remove('hidden');
 | |
|     else row.classList.add('hidden');
 | |
| }
 | |
| function createInput(configItem, frame,clazz) {
 | |
|     let el;
 | |
|     if (configItem.type === 'boolean' || configItem.type === 'list' || configItem.type == 'boatData') {
 | |
|         el=addEl('select',clazz,frame);
 | |
|         el.setAttribute('name', configItem.name)
 | |
|         let slist = [];
 | |
|         if (configItem.list) {
 | |
|             configItem.list.forEach(function (v) {
 | |
|                 if (v instanceof Object){
 | |
|                     slist.push({l:v.l,v:v.v});
 | |
|                 }
 | |
|                 else{
 | |
|                     slist.push({ l: v, v: v });
 | |
|                 }
 | |
|             })
 | |
|         }
 | |
|         else if (configItem.type != 'boatData') {
 | |
|             slist.push({ l: 'on', v: 'true' })
 | |
|             slist.push({ l: 'off', v: 'false' })
 | |
|         }
 | |
|         slist.forEach(function (sitem) {
 | |
|             let sitemEl = addEl('option','',el,sitem.l);
 | |
|             sitemEl.setAttribute('value', sitem.v);
 | |
|         })
 | |
|         if (configItem.type == 'boatData'){
 | |
|             el.classList.add('boatDataSelect');
 | |
|         }
 | |
|         return el;
 | |
|     }
 | |
|     if (configItem.type === 'filter') {
 | |
|         return createFilterInput(configItem,frame,clazz);    
 | |
|     }
 | |
|     if (configItem.type === 'xdr'){
 | |
|         return createXdrInput(configItem,frame,clazz);
 | |
|     }
 | |
|     el = addEl('input',clazz,frame);
 | |
|     el.setAttribute('name', configItem.name)
 | |
|     if (configItem.type === 'password') {
 | |
|         el.setAttribute('type', 'password');
 | |
|         let vis = addEl('span', 'icon-eye icon', 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;
 | |
| }
 | |
| 
 | |
| function updateSelectList(item,slist){
 | |
|     item.innerHTML='';
 | |
|     let idx=0;
 | |
|     slist.forEach(function (sitem) {
 | |
|         let sitemEl = addEl('option','',item,sitem.l);
 | |
|         sitemEl.setAttribute('value', sitem.v !== undefined?sitem.v:idx);
 | |
|         idx++;
 | |
|     })
 | |
| }
 | |
| function getXdrCategories(){
 | |
|     let rt=[];
 | |
|     for (let c in xdrConfig){
 | |
|         if (xdrConfig[c].enabled !== false){
 | |
|             rt.push({l:c,v:xdrConfig[c].id});
 | |
|         }
 | |
|     }
 | |
|     return rt;
 | |
| }
 | |
| function getXdrCategoryName(cid){
 | |
|     category=parseInt(cid);
 | |
|     for (let c in xdrConfig){
 | |
|         let base=xdrConfig[c];
 | |
|         if (parseInt(base.id) == category){
 | |
|             return c;
 | |
|         }
 | |
|     }
 | |
| }
 | |
| function getXdrCategory(cid){
 | |
|     category=parseInt(cid);
 | |
|     for (let c in xdrConfig){
 | |
|         let base=xdrConfig[c];
 | |
|         if (parseInt(base.id) == category){
 | |
|             return base;
 | |
|         }
 | |
|     }
 | |
|     return {};
 | |
| }
 | |
| function getXdrSelectors(category){
 | |
|     let base=getXdrCategory(category);
 | |
|     return base.selector || [];
 | |
| }
 | |
| function getXdrFields(category){
 | |
|     let base=getXdrCategory(category);
 | |
|     if (!base.fields) return [];
 | |
|     let rt=[];
 | |
|     base.fields.forEach(function(f){
 | |
|         if (f.t === undefined) return;
 | |
|         if (parseInt(f.t) == 99) return; //unknown type
 | |
|         rt.push(f);
 | |
|     });
 | |
|     return rt;
 | |
| }
 | |
| 
 | |
| function createXdrLine(parent,label){
 | |
|     let d=addEl('div','xdrline',parent);
 | |
|     addEl('span','xdrlabel',d,label);
 | |
|     return d;
 | |
| }
 | |
| function showHideXdr(el,show,useParent){
 | |
|     if (useParent) el=el.parentElement;
 | |
|     if (show) el.classList.remove('xdrunused');
 | |
|     else el.classList.add('xdrunused');
 | |
| }
 | |
| 
 | |
| function createXdrInput(configItem,frame){
 | |
|     let configCategory=configItem.category;
 | |
|     let el = addEl('div','xdrinput',frame);
 | |
|     let d=createXdrLine(el,'Direction');
 | |
|     let direction=createInput({
 | |
|         type:'list',
 | |
|         name: configItem.name+"_dir",
 | |
|         list: [
 | |
|             //GwXDRMappingDef::Direction
 | |
|             {l:'off',v:0},
 | |
|             {l:'bidir',v:1},
 | |
|             {l:'to2K',v:2},
 | |
|             {l:'from2K',v:3}
 | |
|         ]
 | |
|     },d,'xdrdir');
 | |
|     d=createXdrLine(el,'Category');
 | |
|     let category=createInput({
 | |
|         type: 'list',
 | |
|         name: configItem.name+"_cat",
 | |
|         list:getXdrCategories()
 | |
|     },d,'xdrcat');
 | |
|     d=createXdrLine(el,'Source');
 | |
|     let selector=createInput({
 | |
|         type: 'list',
 | |
|         name: configItem.name+"_sel",
 | |
|         list:[]
 | |
|     },d,'xdrsel');
 | |
|     d=createXdrLine(el,'Field');
 | |
|     let field=createInput({
 | |
|         type:'list',
 | |
|         name: configItem.name+'_field',
 | |
|         list: []
 | |
|     },d,'xdrfield');
 | |
|     d=createXdrLine(el,'Instance');
 | |
|     let imode=createInput({
 | |
|         type:'list',
 | |
|         name: configItem.name+"_imode",
 | |
|         list:[
 | |
|             //GwXDRMappingDef::InstanceMode
 | |
|             {l:'single',v:0},
 | |
|             {l:'ignore',v:1},
 | |
|             {l:'auto',v:2}
 | |
|         ]
 | |
|     },d,'xdrimode');
 | |
|     let instance=createInput({
 | |
|         type:'number',
 | |
|         name: configItem.name+"_instance",
 | |
|     },d,'xdrinstance');
 | |
|     d=createXdrLine(el,'Transducer');
 | |
|     let xdrName=createInput({
 | |
|         type:'text',
 | |
|         name: configItem.name+"_xdr"
 | |
|     },d,'xdrname');
 | |
|     d=createXdrLine(el,'Example');
 | |
|     let example=addEl('div','xdrexample',d,'');
 | |
|     let data = addEl('input','xdrvalue',el);
 | |
|     data.setAttribute('type', 'hidden');
 | |
|     data.setAttribute('name', configItem.name);
 | |
|     let changeFunction = function () {
 | |
|         let parts=data.value.split(',');
 | |
|         direction.value=parts[1] || 0;
 | |
|         category.value=parts[0] || 0;
 | |
|         let selectors=getXdrSelectors(category.value);
 | |
|         updateSelectList(selector,selectors);
 | |
|         showHideXdr(selector,selectors.length>0);
 | |
|         let fields=getXdrFields(category.value);
 | |
|         updateSelectList(field,fields);
 | |
|         showHideXdr(field,fields.length>0);
 | |
|         selector.value=parts[2]||0;
 | |
|         field.value=parts[3]||0;
 | |
|         imode.value=parts[4]||0;
 | |
|         instance.value=parts[5]||0;
 | |
|         showHideXdr(instance,imode.value == 0);
 | |
|         xdrName.value=parts[6]||'';
 | |
|         let used=isXdrUsed(data);
 | |
|         let modified=data.value != data.getAttribute('data-loaded');
 | |
|         forEl('[data-category='+configCategory+']',function(el){
 | |
|             if (used) {
 | |
|                 el.classList.add('xdrcused');
 | |
|                 el.classList.remove('xdrcunused');
 | |
|                 forEl('.categoryAdd',function(add){
 | |
|                     add.textContent=xdrName.value;
 | |
|                 },el);
 | |
|             }
 | |
|             else {
 | |
|                 el.classList.remove('xdrcused');
 | |
|                 el.classList.add('xdrcunused');
 | |
|                 forEl('.categoryAdd',function(add){
 | |
|                     add.textContent='';
 | |
|                 },el);
 | |
|             }
 | |
|             if (modified){
 | |
|                 el.classList.add('changed');
 | |
|             }
 | |
|             else{
 | |
|                 el.classList.remove('changed');
 | |
|             }
 | |
|         });
 | |
|         if (used){
 | |
|             getText('/api/xdrExample?mapping='+encodeURIComponent(data.value)+'&value=2.1')
 | |
|                 .then(function(txt){
 | |
|                     example.textContent=txt;
 | |
|                 })
 | |
|         }
 | |
|         else{
 | |
|             example.textContent='';
 | |
|         }
 | |
|     }
 | |
|     let updateFunction = function (evt) {
 | |
|         if (evt.target == category){
 | |
|             selector.value=0;
 | |
|             field.value=0;
 | |
|             instance.value=0;
 | |
|         }
 | |
|         let txt=category.value+","+direction.value+","+
 | |
|             selector.value+","+field.value+","+imode.value;
 | |
|         let instanceValue=parseInt(instance.value||0);
 | |
|         if (isNaN(instanceValue)) instanceValue=0;
 | |
|         if (instanceValue<0) instanceValue=0;
 | |
|         if (instanceValue>255) instanceValue=255;
 | |
|         txt+=","+instanceValue;
 | |
|         let xdr=xdrName.value.replace(/[^a-zA-Z0-9]/g,'');
 | |
|         xdr=xdr.substr(0,12);
 | |
|         txt+=","+xdr;    
 | |
|         data.value=txt;
 | |
|         let ev=new Event('change');
 | |
|         data.dispatchEvent(ev);
 | |
|     }
 | |
|     category.addEventListener('change',updateFunction);
 | |
|     direction.addEventListener('change',updateFunction);
 | |
|     selector.addEventListener('change',updateFunction);
 | |
|     field.addEventListener('change',updateFunction);
 | |
|     imode.addEventListener('change',updateFunction);
 | |
|     instance.addEventListener('change',updateFunction);
 | |
|     xdrName.addEventListener('change',updateFunction);
 | |
|     data.addEventListener('change',changeFunction);
 | |
|     return data;
 | |
| }
 | |
| 
 | |
| function isXdrUsed(element){
 | |
|     let parts=element.value.split(',');
 | |
|     if (! parts[1]) return false;
 | |
|     if (! parseInt(parts[1])) return false;
 | |
|     if (! parts[6]) return false;
 | |
|     return true;
 | |
| }
 | |
| 
 | |
| function createFilterInput(configItem, frame) {
 | |
|     let el = addEl('div','filter',frame);
 | |
|     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 = addEl('input',undefined,el);
 | |
|     data.setAttribute('type', 'hidden');
 | |
|     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);
 | |
|     return data;
 | |
| }
 | |
| let moreicons=['icon-more','icon-less'];
 | |
| 
 | |
| function collapseCategories(parent,expand){
 | |
|     let doExpand=expand;
 | |
|     forEl('.category',function(cat){
 | |
|         if (typeof(expand) === 'function') doExpand=expand(cat);
 | |
|         forEl('.content',function(content){
 | |
|             if (doExpand){
 | |
|                 content.classList.remove('hidden');
 | |
|             }
 | |
|             else{
 | |
|                 content.classList.add('hidden');
 | |
|             }
 | |
|         },cat);
 | |
|         forEl('.title .icon',function(icon){
 | |
|             toggleClass(icon,doExpand?1:0,moreicons);    
 | |
|         },cat);
 | |
|     },parent);
 | |
| }
 | |
| 
 | |
| function findFreeXdr(data){
 | |
|     let page=document.getElementById('xdrPage');
 | |
|     let el=undefined;
 | |
|     collapseCategories(page,function(cat){
 | |
|         if (el) return false;
 | |
|         let vEl=cat.querySelector('.xdrvalue');
 | |
|         if (!vEl) return false;
 | |
|         if (isXdrUsed(vEl)) return false;
 | |
|         el=vEl;
 | |
|         if (data){
 | |
|             el.value=data;
 | |
|             let ev=new Event('change');
 | |
|             el.dispatchEvent(ev);
 | |
|             window.setTimeout(function(){
 | |
|                 cat.scrollIntoView();
 | |
|             },50);
 | |
|         }
 | |
|         return true;
 | |
|     });
 | |
| }
 | |
| 
 | |
| function convertUnassigned(value){
 | |
|     let rt={};
 | |
|     value=parseInt(value);
 | |
|     if (isNaN(value)) return;
 | |
|     //see GwXDRMappings::addUnknown
 | |
|     let instance=value & 0x1ff;
 | |
|     value = value >> 9;
 | |
|     let field=value & 0x7f;
 | |
|     value = value >> 7;
 | |
|     let selector=value & 0x7f;
 | |
|     value = value >> 7;
 | |
|     let cid=value & 0x7f;
 | |
|     let category=getXdrCategory(cid);
 | |
|     let cname=getXdrCategoryName(cid);
 | |
|     if (! cname) return rt;
 | |
|     let fieldName="";
 | |
|     let idx=0;
 | |
|     (category.fields || []).forEach(function(f){
 | |
|         if (f.v === undefined){
 | |
|             if (idx == field) fieldName=f.l;
 | |
|         }
 | |
|         else{
 | |
|             if (parseInt(f.v) == field) fieldName=f.l;
 | |
|         }
 | |
|         idx++;
 | |
|     });
 | |
|     let selectorName=selector+"";
 | |
|     (category.selector ||[]).forEach(function(s){
 | |
|         if (parseInt(s.v) == selector) selectorName=s.l;
 | |
|     });
 | |
|     rt.l=cname+","+selectorName+","+fieldName+","+instance;
 | |
|     rt.v=cid+",1,"+selector+","+field+",0,"+instance+",";
 | |
|     return rt;
 | |
| }
 | |
| 
 | |
| function unassignedAdd(ev) {
 | |
|     let dv = ev.target.getAttribute('data-value');
 | |
|     if (dv) {
 | |
|         findFreeXdr(dv);
 | |
|         hideOverlay();
 | |
|     }
 | |
| }
 | |
| function loadUnassigned(){
 | |
|     getText("/api/xdrUnmapped")
 | |
|         .then(function(txt){
 | |
|             let ot="";
 | |
|             txt.split('\n').forEach(function(line){
 | |
|                 let cv=convertUnassigned(line);
 | |
|                 if (!cv || !cv.l) return;
 | |
|                 ot+='<div class="xdrunassigned"><span>'+
 | |
|                     cv.l+'</span>'+
 | |
|                     '<button class="addunassigned" data-value="'+
 | |
|                     cv.v+
 | |
|                     '">+</button></div>';
 | |
|             })
 | |
|             showOverlay(ot,true);
 | |
|             forEl('.addunassigned',function(bt){
 | |
|                 bt.onclick=unassignedAdd;
 | |
|             });
 | |
|         })
 | |
| }
 | |
| function showXdrHelp(){
 | |
|     let helpContent=document.getElementById('xdrHelp');
 | |
|     if (helpContent){
 | |
|         showOverlay(helpContent.innerHTML,true);
 | |
|     }
 | |
| }
 | |
| function formatDate(d){
 | |
|     if (! d) d=new Date();
 | |
|     let rt=""+d.getFullYear();
 | |
|     let v=d.getMonth();
 | |
|     if (v < 10) rt+="0"+v;
 | |
|     else rt+=v;
 | |
|     v=d.getDate();
 | |
|     if (v < 10) rt+="0"+v;
 | |
|     else rt+=v;
 | |
|     return rt;
 | |
| }
 | |
| function exportXdr(){
 | |
|     let data={};
 | |
|     forEl('.xdrvalue',function(el) {
 | |
|         let name=el.getAttribute('name');
 | |
|         let value=el.value;
 | |
|         let err=checkXDR(value,data);
 | |
|         if (err){
 | |
|             alert("error in "+name+": "+value+"\n"+err);
 | |
|             return;
 | |
|         }
 | |
|         data[name]=value;
 | |
|     })
 | |
|     let url="data:application/octet-stream,"+encodeURIComponent(JSON.stringify(data,undefined,2));
 | |
|     let target=document.getElementById('downloadXdr');
 | |
|     if (! target) return;
 | |
|     target.setAttribute('href',url);
 | |
|     target.setAttribute('download',"xdr"+formatDate()+".json");
 | |
|     target.click();
 | |
| }
 | |
| function importXdr(){
 | |
|     forEl('.uploadXdr',function(ul){
 | |
|         ul.remove();
 | |
|     });
 | |
|     let ip=addEl('input','uploadXdr',document.body);
 | |
|     ip.setAttribute('type','file');
 | |
|     ip.addEventListener('change',function(ev){
 | |
|         if (ip.files.length > 0){
 | |
|             let f=ip.files[0];
 | |
|             let reader=new FileReader();
 | |
|             reader.onloadend=function(status){
 | |
|                 try{
 | |
|                     let idata=JSON.parse(reader.result);
 | |
|                     let hasOverwrites=false;
 | |
|                     for (let k in idata){
 | |
|                         if (! k.match(/^XDR[0-9][0-9]*/)){
 | |
|                             alert("file contains invalid key "+k);
 | |
|                             return;
 | |
|                         }
 | |
|                         let del=document.querySelector('input[name='+k+']');
 | |
|                         if (del){
 | |
|                             hasOverwrites=true;
 | |
|                         }        
 | |
|                     }
 | |
|                     if (hasOverwrites){
 | |
|                         if (!confirm("overwrite existing data?")) return;
 | |
|                     }
 | |
|                     for (let k in idata){
 | |
|                         let del=document.querySelector('input[name='+k+']');
 | |
|                         if (del){
 | |
|                             del.value=idata[k];
 | |
|                             let ev=new Event('change');
 | |
|                             del.dispatchEvent(ev);
 | |
|                         }
 | |
|                     }
 | |
|                 }catch (error){
 | |
|                     alert("unable to parse upload: "+error);
 | |
|                     return;
 | |
|                 }
 | |
|             };
 | |
|             reader.readAsBinaryString(f);
 | |
|         }
 | |
|         ip.remove();
 | |
|     });
 | |
|     ip.click(); 
 | |
| }
 | |
| function toggleClass(el,id,classList){
 | |
|     let nc=classList[id];
 | |
|     let rt=false;
 | |
|     if (nc && !el.classList.contains(nc)) rt=true;
 | |
|     for (let i in classList){
 | |
|         if (i == id) continue;
 | |
|         el.classList.remove(classList[i])
 | |
|     }
 | |
|     if (nc) el.classList.add(nc);
 | |
|     return rt;
 | |
| }
 | |
| 
 | |
| function createConfigDefinitions(parent, capabilities, defs,includeXdr) {
 | |
|     let category;
 | |
|     let categoryEl;
 | |
|     let categoryFrame;
 | |
|     let frame = parent.querySelector('.configFormRows');
 | |
|     if (!frame) throw Error("no config form");
 | |
|     frame.innerHTML = '';
 | |
|     configDefinitions = defs;
 | |
|     let currentCategoryPopulated=true;
 | |
|     defs.forEach(function (item) {
 | |
|         if (!item.type) return;
 | |
|         if (item.category.match(/^xdr/)){
 | |
|             if (! includeXdr) return;
 | |
|         }
 | |
|         else{
 | |
|             if(includeXdr) return;
 | |
|         } 
 | |
|         if (item.category != category || !categoryEl) {
 | |
|             if (categoryFrame && ! currentCategoryPopulated){
 | |
|                 categoryFrame.remove();
 | |
|             }
 | |
|             currentCategoryPopulated=false;
 | |
|             categoryFrame = addEl('div', 'category', frame);
 | |
|             categoryFrame.setAttribute('data-category',item.category)
 | |
|             let categoryTitle = addEl('div', 'title', categoryFrame);
 | |
|             let categoryButton = addEl('span', 'icon icon-more', categoryTitle);
 | |
|             addEl('span', 'label', categoryTitle, item.category);
 | |
|             addEl('span','categoryAdd',categoryTitle);
 | |
|             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) {
 | |
|                     toggleClass(categoryButton,0,moreicons);
 | |
|                 }
 | |
|                 else {
 | |
|                     toggleClass(categoryButton,1,moreicons);
 | |
|                 }
 | |
|             })
 | |
|             category = item.category;
 | |
|         }
 | |
|         let showItem=true;
 | |
|         if (item.capabilities !== undefined) {
 | |
|             for (let capability in item.capabilities) {
 | |
|                 let values = item.capabilities[capability];
 | |
|                 let found = false;
 | |
|                 if (! (values instanceof Array)) values=[values];
 | |
|                 values.forEach(function (v) {
 | |
|                     if (capabilities[capability] == v) found = true;
 | |
|                 });
 | |
|                 if (!found) showItem=false;
 | |
|             }
 | |
|         }
 | |
|         if (showItem) {
 | |
|             currentCategoryPopulated=true;
 | |
|             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, item.name);
 | |
|             })
 | |
|             let condition = getConditions(item.name);
 | |
|             if (condition) {
 | |
|                 condition.forEach(function (cel) {
 | |
|                     for (let c in cel) {
 | |
|                         if (!conditionRelations[c]) {
 | |
|                             conditionRelations[c] = [];
 | |
|                         }
 | |
|                         conditionRelations[c].push(valueEl);
 | |
|                     }
 | |
|                 })
 | |
|             }
 | |
|             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) {
 | |
|                 if (item.description) {
 | |
|                     showOverlay(item.description);
 | |
|                 }
 | |
|                 else {
 | |
|                     if (item.category.match(/^xdr/)) {
 | |
|                         showXdrHelp();
 | |
|                     }
 | |
|                 }
 | |
|             });
 | |
|         }
 | |
|     });
 | |
|     if (categoryFrame && ! currentCategoryPopulated){
 | |
|         categoryFrame.remove();
 | |
|     }
 | |
| }
 | |
| function loadConfigDefinitions() {
 | |
|     getJson("api/capabilities")
 | |
|         .then(function (capabilities) {
 | |
|             getJson("config.json")
 | |
|                 .then(function (defs) {
 | |
|                     getJson("xdrconfig.json")
 | |
|                         .then(function(xdr){
 | |
|                             xdrConfig=xdr;
 | |
|                             configDefinitions=defs;
 | |
|                             let normalConfig=document.getElementById('configPage');
 | |
|                             let xdrParent=document.getElementById('xdrPage');
 | |
|                             if (normalConfig) createConfigDefinitions(normalConfig,capabilities,defs,false);
 | |
|                             if (xdrParent) createConfigDefinitions(xdrParent,capabilities,defs,true);
 | |
|                             resetForm();
 | |
|                             getText('api/boatDataString').then(function (data) {
 | |
|                                 updateDashboard(data.split('\n'));
 | |
|                             });
 | |
|                         })
 | |
|                 })
 | |
|         })
 | |
|         .catch(function (err) { alert("unable to load config: " + err) })
 | |
| }
 | |
| function verifyPass(pass){
 | |
|     return new Promise(function(resolve,reject){
 | |
|         let hash=lastSalt+pass;
 | |
|         let md5hash=MD5(hash);
 | |
|         getJson('api/checkPass?hash='+encodeURIComponent(md5hash))
 | |
|             .then(function(jsonData){
 | |
|                 if (jsonData.status == 'OK') resolve(md5hash);
 | |
|                 else reject(jsonData.status);
 | |
|                 return;
 | |
|             })
 | |
|             .catch(function(error){reject(error);})
 | |
|     });
 | |
| }
 | |
| 
 | |
| 
 | |
| function adminPassCancel(){
 | |
|     forEl('#adminPassOverlay',function(el){el.classList.add('hidden')});
 | |
|     forEl('#adminPassInput',function(el){el.value=''});
 | |
| }
 | |
| function saveAdminPass(pass,forceIfSet){
 | |
|     forEl('#adminPassKeep',function(el){
 | |
|         try{
 | |
|             let old=localStorage.getItem('adminPass');
 | |
|             if (el.value == 'true' || (forceIfSet && old !== undefined)){
 | |
|                 localStorage.setItem('adminPass',pass);
 | |
|             }
 | |
|             else{
 | |
|                 localStorage.removeItem('adminPass');
 | |
|             }
 | |
|         }catch (e){}
 | |
|     });
 | |
| }
 | |
| function forgetPass(){
 | |
|     localStorage.removeItem('adminPass');
 | |
|     forEl('#adminPassInput',function(el){
 | |
|         el.value='';
 | |
|     });
 | |
| }
 | |
| function ensurePass(){
 | |
|     return new Promise(function(resolve,reject){
 | |
|         if (! needAdminPass) {
 | |
|             resolve('');
 | |
|             return;
 | |
|         }
 | |
|         let pe=document.getElementById('adminPassInput');
 | |
|         let hint=document.getElementById('adminPassError');
 | |
|         if (!pe) {
 | |
|             reject('no input');
 | |
|             return;
 | |
|         }
 | |
|         if (pe.value == ''){
 | |
|             let ok=document.getElementById('adminPassOk');
 | |
|             if (!ok) {
 | |
|                 reject('no button');
 | |
|                 return;
 | |
|             }
 | |
|             ok.onclick=function(){
 | |
|                 verifyPass(pe.value)
 | |
|                     .then(function(pass){
 | |
|                         forEl('#adminPassOverlay',function(el){el.classList.add('hidden')});
 | |
|                         saveAdminPass(pe.value);
 | |
|                         resolve(pass);
 | |
|                     })
 | |
|                     .catch(function(err){
 | |
|                         if (hint){
 | |
|                             hint.textContent="invalid password";
 | |
|                         }
 | |
|                     });
 | |
|             };
 | |
|             if (hint) hint.textContent='';
 | |
|             forEl('#adminPassOverlay',function(el){el.classList.remove('hidden')});
 | |
|         }
 | |
|         else{
 | |
|             verifyPass(pe.value)
 | |
|                 .then(function(np){resolve(np);})
 | |
|                 .catch(function(err){
 | |
|                     pe.value='';
 | |
|                     ensurePass()
 | |
|                         .then(function(p){resolve(p);})
 | |
|                         .catch(function(e){reject(e);});
 | |
|                 });
 | |
|             return;    
 | |
|         }
 | |
|     });
 | |
| }
 | |
| 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');
 | |
| }
 | |
| /**
 | |
|  *
 | |
|  * @param {number} coordinate
 | |
|  * @param axis
 | |
|  * @returns {string}
 | |
|  */
 | |
|  function formatLonLatsDecimal(coordinate,axis){
 | |
|     coordinate = (coordinate+540)%360 - 180; // normalize for sphere being round
 | |
| 
 | |
|     let abscoordinate = Math.abs(coordinate);
 | |
|     let coordinatedegrees = Math.floor(abscoordinate);
 | |
| 
 | |
|     let coordinateminutes = (abscoordinate - coordinatedegrees)/(1/60);
 | |
|     let numdecimal=2;
 | |
|     //correctly handle the toFixed(x) - will do math rounding
 | |
|     if (coordinateminutes.toFixed(numdecimal) == 60){
 | |
|         coordinatedegrees+=1;
 | |
|         coordinateminutes=0;
 | |
|     }
 | |
|     if( coordinatedegrees < 10 ) {
 | |
|         coordinatedegrees = "0" + coordinatedegrees;
 | |
|     }
 | |
|     if (coordinatedegrees < 100 && axis == 'lon'){
 | |
|         coordinatedegrees = "0" + coordinatedegrees;
 | |
|     }
 | |
|     let str = coordinatedegrees + "\u00B0";
 | |
| 
 | |
|     if( coordinateminutes < 10 ) {
 | |
|         str +="0";
 | |
|     }
 | |
|     str += coordinateminutes.toFixed(numdecimal) + "'";
 | |
|     if (axis == "lon") {
 | |
|         str += coordinate < 0 ? "W" :"E";
 | |
|     } else {
 | |
|         str += coordinate < 0 ? "S" :"N";
 | |
|     }
 | |
|     return str;
 | |
| };
 | |
| function formatFixed(v,dig,fract){
 | |
|     v=parseFloat(v);
 | |
|     if (dig === undefined) return v.toFixed(fract);
 | |
|     let s=v<0?"-":"";
 | |
|     v=Math.abs(v);
 | |
|     let rv=v.toFixed(fract);
 | |
|     let parts=rv.split('.');
 | |
|     parts[0]="0000000000"+parts[0];
 | |
|     if (dig >= 10) dig=10;
 | |
|     if (fract > 0){
 | |
|         return s+parts[0].substr(parts[0].length-dig)+"."+parts[1];
 | |
|     }
 | |
|     return s+parts[0].substr(parts[0].length-dig);
 | |
| }
 | |
| let valueFormatters = {
 | |
|     formatCourse: {
 | |
|         f: function (v) {
 | |
|             let x = parseFloat(v);
 | |
|             let rt = x * 180.0 / Math.PI;
 | |
|             if (rt > 360) rt -= 360;
 | |
|             if (rt < 0) rt += 360;
 | |
|             return rt.toFixed(0);
 | |
|         },
 | |
|         u: '°'
 | |
|     },
 | |
|     formatKnots: {
 | |
|         f: function (v) {
 | |
|             let x = parseFloat(v);
 | |
|             x = x * 3600.0 / 1852.0;
 | |
|             return x.toFixed(2);
 | |
|         },
 | |
|         u: 'kn'
 | |
|     },
 | |
|     formatWind: {
 | |
|         f: function (v) {
 | |
|             let x = parseFloat(v);
 | |
|             x = x * 180.0 / Math.PI;
 | |
|             if (x > 180) x = 180 - x;
 | |
|             return x.toFixed(0);
 | |
|         },
 | |
|         u: '°'
 | |
|     },
 | |
|     mtr2nm: {
 | |
|         f: function (v) {
 | |
|             let x = parseFloat(v);
 | |
|             x = x / 1852.0;
 | |
|             return x.toFixed(2);
 | |
|         },
 | |
|         u: 'nm'
 | |
|     },
 | |
|     kelvinToC: {
 | |
|         f: function (v) {
 | |
|             let x = parseFloat(v);
 | |
|             x = x - 273.15;
 | |
|             return x.toFixed(0);
 | |
|         },
 | |
|         u: '°'
 | |
|     },
 | |
|     formatFixed0: {
 | |
|         f: function (v) {
 | |
|             let x = parseFloat(v);
 | |
|             return x.toFixed(0);
 | |
|         },
 | |
|         u: ''
 | |
|     },
 | |
|     formatDepth: {
 | |
|         f: function (v) {
 | |
|             let x = parseFloat(v);
 | |
|             return x.toFixed(1);
 | |
|         },
 | |
|         u: 'm'
 | |
|     },
 | |
|     formatLatitude: {
 | |
|         f: function (v) {
 | |
|             let x = parseFloat(v);
 | |
|             if (isNaN(x)) return '-----';
 | |
|             return formatLonLatsDecimal(x,'lat');
 | |
|         },
 | |
|         u: '°'
 | |
|     },
 | |
|     formatLongitude: {
 | |
|         f: function (v) {
 | |
|             let x = parseFloat(v);
 | |
|             if (isNaN(x)) return '-----';
 | |
|             return formatLonLatsDecimal(x,'lon');
 | |
|         },
 | |
|         u: ''
 | |
|     },
 | |
|     formatRot:{
 | |
|         f: function(v){
 | |
|             let x=parseFloat(v);
 | |
|             if (isNaN(x)) return '---';
 | |
|             x = x * 180.0 / Math.PI;
 | |
|             return x.toFixed(2);
 | |
|         },
 | |
|         u:'°/s'
 | |
|     },
 | |
|     formatXte:{
 | |
|         f: function(v){
 | |
|             let x=parseFloat(v);
 | |
|             if (isNaN(x)) return '---';
 | |
|             return x.toFixed(0);
 | |
|         },
 | |
|         u:'m'
 | |
|     },
 | |
|     formatDate:{
 | |
|         f: function(v){
 | |
|             v=parseFloat(v);
 | |
|             if (isNaN(v)) return "----/--/--";
 | |
|             //strange day offset from NMEA0183 lib
 | |
|             let d=new Date("2010/01/01");
 | |
|             let days=14610-d.getTime()/1000/86400;
 | |
|             let tbase=(v-days)*1000*86400;
 | |
|             let od=new Date(tbase);
 | |
|             return formatFixed(od.getFullYear(),4,0)+
 | |
|                 "/"+formatFixed(od.getMonth()+1,2,0)+
 | |
|                 "/"+formatFixed(od.getDate(),2,0);
 | |
|         },
 | |
|         u:''
 | |
|     },
 | |
|     formatTime:{
 | |
|         f:function(v){
 | |
|             v=parseFloat(v);
 | |
|             if (isNaN(v)) return "--:--:--";
 | |
|             let hr=Math.floor(v/3600.0);
 | |
|             let min=Math.floor((v-hr*3600.0)/60);
 | |
|             let s=Math.floor((v-hr*3600.0-min*60.0));
 | |
|             return formatFixed(hr,2,0)+':'+
 | |
|                 formatFixed(min,2,0)+':'+
 | |
|                 formatFixed(s,2,0);
 | |
|         },
 | |
|         u:''
 | |
|     }
 | |
| 
 | |
| 
 | |
| }
 | |
| function resizeFont(el,reset,maxIt){
 | |
|     if (maxIt === undefined) maxIt=10;
 | |
|     if (! el) return;
 | |
|     if (reset) el.style.fontSize='';
 | |
|     while (el.scrollWidth > el.clientWidth && maxIt){
 | |
|         let next=parseFloat(window.getComputedStyle(el).fontSize)*0.9;
 | |
|         el.style.fontSize=next+"px";
 | |
|     }
 | |
| }
 | |
| function createDashboardItem(name, def, parent) {
 | |
|     if (! def.name) return;
 | |
|     let frame = addEl('div', 'dash', parent);
 | |
|     let title = addEl('span', 'dashTitle', frame, name);
 | |
|     let value = addEl('span', 'dashValue', frame);
 | |
|     value.setAttribute('id', 'data_' + name);
 | |
|     let fmt=valueFormatters[def.format];
 | |
|     if (def.format) value.classList.add(def.format);
 | |
|     let footer = addEl('div','footer',frame);
 | |
|     let src= addEl('span','source',footer);
 | |
|     src.setAttribute('id','source_'+name);
 | |
|     let u=fmt?fmt.u:' ';
 | |
|     if (! fmt && def.format && def.format.match(/formatXdr/)){
 | |
|         u=def.format.replace(/formatXdr/,'');
 | |
|     }
 | |
|     addEl('span','unit',footer,u);
 | |
|     return value;
 | |
| }
 | |
| function parseBoatDataLine(line){
 | |
|     let rt={};
 | |
|     let parts=line.split(',');
 | |
|     rt.name=parts[0];
 | |
|     rt.valid=parts[2] === '1';
 | |
|     rt.update=parseInt(parts[3]);
 | |
|     rt.source=parseInt(parts[4]);
 | |
|     rt.format=parts[1];
 | |
|     rt.value=parts[5];
 | |
|     return rt;
 | |
| }
 | |
| function createDashboard() {
 | |
|     let frame = document.getElementById('dashboardPage');
 | |
|     if (!frame) return;
 | |
|     frame.innerHTML = '';
 | |
| }
 | |
| function sourceName(v){
 | |
|     if (v == 0) return "N2K";
 | |
|     if (v == 1) return "USB";
 | |
|     if (v == 2) return "SER";
 | |
|     if (v >= 3) return "TCP";
 | |
|     return "---";
 | |
| }
 | |
| let lastSelectList=[];
 | |
| function updateDashboard(data) {
 | |
|     let frame = document.getElementById('dashboardPage');
 | |
|     let showInvalid=true;
 | |
|     forEl('select[name=showInvalidData]',function(el){
 | |
|         if (el.value == 'false') showInvalid=false;
 | |
|     })
 | |
|     let names={};
 | |
|     for (let n in data) {
 | |
|         let current=parseBoatDataLine(data[n]);
 | |
|         if (! current.name) continue;
 | |
|         names[current.name]=true;
 | |
|         let de = document.getElementById('data_' + current.name);
 | |
|         let isValid=current.valid;
 | |
|         if (! de && frame && (isValid || showInvalid)){
 | |
|             de=createDashboardItem(current.name,current,frame);   
 | |
|         }
 | |
|         if (de && (!isValid && !showInvalid)){
 | |
|             de.parentElement.remove();
 | |
|             continue;
 | |
|         }
 | |
|         if (de) {
 | |
|             let newContent='----';
 | |
|             if (current.valid) {
 | |
|                 let formatter;
 | |
|                 if (current.format && current.format != "NULL") {
 | |
|                     let key = current.format.replace(/^\&/, '');
 | |
|                     formatter = valueFormatters[key];
 | |
|                 }
 | |
|                 if (formatter) {
 | |
|                     newContent = formatter.f(current.value);
 | |
|                 }
 | |
|                 else {
 | |
|                     let v = parseFloat(current.value);
 | |
|                     if (!isNaN(v)) {
 | |
|                         v = v.toFixed(3)
 | |
|                         newContent = v;
 | |
|                     }
 | |
|                     else {
 | |
|                         newContent = current.value;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             else newContent = "---";
 | |
|             if (newContent !== de.textContent){
 | |
|                 de.textContent=newContent;
 | |
|                 resizeFont(de,true);
 | |
|             }
 | |
|         }
 | |
|         let src=document.getElementById('source_'+current.name);
 | |
|         if (src){
 | |
|             src.textContent=sourceName(current.source);
 | |
|         }
 | |
|     }
 | |
|     console.log("update");
 | |
|     forEl('.dashValue',function(el){
 | |
|         let id=el.getAttribute('id');
 | |
|         if (id){
 | |
|             if (! names[id.replace(/^data_/,'')]){
 | |
|                 el.parentElement.remove();
 | |
|             }
 | |
|         }
 | |
|     });
 | |
|     let selectList=[];
 | |
|     for (let n in names){
 | |
|         selectList.push({l:n,v:n});
 | |
|     }
 | |
|     let selectChanged=false;
 | |
|     if (lastSelectList.length == selectList.length){
 | |
|         for (let i=0;i<lastSelectList.length;i++){
 | |
|             if (selectList[i] != lastSelectList[i]){
 | |
|                 selectChanged=true;
 | |
|                 break;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     else{
 | |
|         selectChanged=true;
 | |
|     }
 | |
|     if (selectChanged){
 | |
|         forEl('.boatDataSelect',function(el){
 | |
|             updateSelectList(el,selectList);
 | |
|         });
 | |
|     }
 | |
| }
 | |
| function uploadBin(){
 | |
|     let el=document.getElementById("uploadFile");
 | |
|     let progressEl=document.getElementById("uploadDone");
 | |
|     if (! el) return;
 | |
|     if ( el.files.length < 1) return;
 | |
|     let file=el.files[0];
 | |
|     checkImageFile(file)
 | |
|         .then(function (result) {
 | |
|             let currentType;
 | |
|             let currentVersion;
 | |
|             forEl('.status-version', function (el) { currentVersion = el.textContent });
 | |
|             forEl('.status-fwtype', function (el) { currentType = el.textContent });
 | |
|             let confirmText = 'Ready to update firmware?\n';
 | |
|             if (currentType != result.fwtype) {
 | |
|                 confirmText += "WARNING: image has different type: " + result.fwtype + "\n";
 | |
|                 confirmText += "** Really update anyway? - device can become unusable **";
 | |
|             }
 | |
|             else {
 | |
|                 if (currentVersion == result.version) {
 | |
|                     confirmText += "WARNING: image has the same version as current " + result.version;
 | |
|                 }
 | |
|                 else {
 | |
|                     confirmText += "version in image: " + result.version;
 | |
|                 }
 | |
|             }
 | |
|             if (!confirm(confirmText)) return;
 | |
|             ensurePass()
 | |
|                 .then(function (hash) {
 | |
|                     let len = file.size;
 | |
|                     let req = new XMLHttpRequest();
 | |
|                     req.onloadend = function () {
 | |
|                         let result = "unknown error";
 | |
|                         try {
 | |
|                             let jresult = JSON.parse(req.responseText);
 | |
|                             if (jresult.status == 'OK') {
 | |
|                                 result = '';
 | |
|                             }
 | |
|                             else {
 | |
|                                 if (jresult.status) {
 | |
|                                     result = jresult.status;
 | |
|                                 }
 | |
|                             }
 | |
|                         } catch (e) {
 | |
|                             result = "Error " + req.status;
 | |
|                         }
 | |
|                         if (progressEl) {
 | |
|                             progressEl.style.width = 0;
 | |
|                         }
 | |
|                         if (!result) {
 | |
|                             alertRestart();
 | |
|                         }
 | |
|                         else {
 | |
|                             alert("update error: " + result);
 | |
|                         }
 | |
|                     }
 | |
|                     req.onerror = function (e) {
 | |
|                         alert("unable to upload: " + e);
 | |
|                     }
 | |
|                     if (progressEl) {
 | |
|                         progressEl.style.width = 0;
 | |
|                         req.upload.onprogress = function (ev) {
 | |
|                             if (ev.lengthComputable) {
 | |
|                                 let percent = 100 * ev.loaded / ev.total;
 | |
|                                 progressEl.style.width = percent + "%";
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
|                     let formData = new FormData();
 | |
|                     formData.append("file1", el.files[0]);
 | |
|                     req.open("POST", '/api/update?_hash=' + encodeURIComponent(hash));
 | |
|                     req.send(formData);
 | |
|                 })
 | |
|                 .catch(function (e) { });
 | |
|         })
 | |
|         .catch(function (e) {
 | |
|             alert("This file is an invalid image file:\n" + e);
 | |
|         })
 | |
| }
 | |
| let HDROFFSET=288;
 | |
| let VERSIONOFFSET=16;
 | |
| let NAMEOFFSET=48;
 | |
| let MINSIZE=HDROFFSET+NAMEOFFSET+32;
 | |
| let imageCheckBytes={
 | |
|     0: 0xe9, //image magic
 | |
|     288: 0x32, //app header magic
 | |
|     289: 0x54,
 | |
|     290: 0xcd,
 | |
|     291: 0xab
 | |
| };
 | |
| function decodeFromBuffer(buffer,start,length){
 | |
|     while (length > 0 && buffer[start+length-1] == 0){
 | |
|         length--;
 | |
|     }
 | |
|     if (length <= 0) return "";
 | |
|     let decoder=new TextDecoder();
 | |
|     let rt=decoder.decode(buffer.slice(
 | |
|             start,
 | |
|             start+length));
 | |
|     return rt;        
 | |
| }
 | |
| function checkImageFile(file){
 | |
|     return new Promise(function(resolve,reject){
 | |
|         if (! file) reject("no file");
 | |
|         if (file.size < MINSIZE) reject("file is too small");
 | |
|         let slice=file.slice(0,MINSIZE);
 | |
|     let reader=new FileReader();
 | |
|     reader.addEventListener('load',function(e){
 | |
|         let content=new Uint8Array(e.target.result);
 | |
|         for (let idx in imageCheckBytes){
 | |
|             if (content[idx] != imageCheckBytes[idx] ){
 | |
|                 reject("missing magic byte at position "+idx+", expected "+
 | |
|                     imageCheckBytes[idx]+", got "+content[idx]);
 | |
|             }
 | |
|         }
 | |
|         let version=decodeFromBuffer(content,HDROFFSET+VERSIONOFFSET,32);
 | |
|         let fwtype=decodeFromBuffer(content,HDROFFSET+NAMEOFFSET,32);
 | |
|         let rt={
 | |
|             fwtype:fwtype,
 | |
|             version: version,
 | |
|         };
 | |
|         resolve(rt);    
 | |
|     });
 | |
|     reader.readAsArrayBuffer(slice);
 | |
|     });
 | |
| }
 | |
| window.setInterval(update, 1000);
 | |
| window.setInterval(function () {
 | |
|     let dp = document.getElementById('dashboardPage');
 | |
|     if (dp.classList.contains('hidden')) return;
 | |
|     getText('api/boatDataString').then(function (data) {
 | |
|         updateDashboard(data.split('\n'));
 | |
|     });
 | |
| }, 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
 | |
|     }
 | |
|     forEl('.showMsgDetails', function (cd) {
 | |
|         cd.addEventListener('change', function (ev) {
 | |
|             let key = ev.target.getAttribute('data-key');
 | |
|             if (!key) return;
 | |
|             let el = document.getElementById(key);
 | |
|             if (!el) return;
 | |
|             if (ev.target.checked) el.classList.remove('hidden');
 | |
|             else (el.classList).add('hidden');
 | |
|         });
 | |
|     });
 | |
|     let tabs = document.querySelectorAll('.tab');
 | |
|     for (let i = 0; i < tabs.length; i++) {
 | |
|         tabs[i].addEventListener('click', function (ev) {
 | |
|             handleTab(ev.target);
 | |
|         });
 | |
|     }
 | |
|     createDashboard();
 | |
|     loadConfigDefinitions();
 | |
|     try{
 | |
|         let storedPass=localStorage.getItem('adminPass');
 | |
|         if (storedPass){
 | |
|             forEl('#adminPassInput',function(el){
 | |
|                 el.value=storedPass;
 | |
|             });
 | |
|         }
 | |
|     }catch(e){}
 | |
|     let statusPage=document.getElementById('statusPageContent');
 | |
|     if (statusPage){
 | |
|         let even=true;
 | |
|         for (let c in counters){
 | |
|             createCounterDisplay(statusPage,counters[c],c,even);
 | |
|             even=!even;
 | |
|         }
 | |
|     }
 | |
|     forEl('#uploadFile',function(el){
 | |
|         el.addEventListener('change',function(ev){
 | |
|             if (ev.target.files.length < 1) return;
 | |
|             let file=ev.target.files[0];
 | |
|             checkImageFile(file)
 | |
|                 .then(function(res){
 | |
|                     forEl('#imageProperties',function(iel){
 | |
|                         iel.textContent=res.fwtype+", "+res.version;
 | |
|                         iel.classList.remove("error");
 | |
|                     })    
 | |
|                 })
 | |
|                 .catch(function(e){
 | |
|                     forEl('#imageProperties',function(iel){
 | |
|                         iel.textContent=e;
 | |
|                         iel.classList.add("error");
 | |
|                     })
 | |
|                 })
 | |
|         })
 | |
|     })
 | |
| }); |