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); } } } if (el.tagName === 'SELECT') { setSelect(el,v); } else{ 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 checkIpAddress(v,allValues,def){ if (allValues.tclEnabled != "true") return; if (! v) return "cannot be empty"; if (! v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/) && ! v.match(/.*\.local/)) return "must be either in the form 192.168.1.1 or xxx.local"; } 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 getAllConfigs(omitPass) { 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 == '' || omitPass)) { 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; } } } allValues[name] = v.value; } return allValues; } function changeConfig() { ensurePass() .then(function (pass) { let newAdminPass; let url = "/api/setConfig" let body="_hash="+encodeURIComponent(pass)+"&"; let allValues=getAllConfigs(); if (!allValues) return; for (let name in allValues){ if (name == 'adminPassword'){ newAdminPass=allValues[name]; } body += encodeURIComponent(name) + "=" + encodeURIComponent(allValues[name]) + "&"; } fetch(url,{ method:'POST', headers:{ 'Content-Type': 'application/octet-stream' //we must lie here }, body: body }) .then((rs)=>rs.json()) .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(e); }) } 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: 'TCPserver in', countTCPout: 'TCPserver out', countTCPClientin: 'TCPclient in', countTCPClientout: 'TCPclient out', countUSBin: 'USB in', countUSBout: 'USB out', countSERin: 'Serial in', countSERout: '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; if (! condition instanceof Array) condition=[condition]; condition.forEach(function(cel){ let lvis=true; for (let k in cel){ let item=document.querySelector('[name='+k+']'); if (item){ let compare=cel[k]; if (compare instanceof Array){ if (compare.indexOf(item.value) < 0) lvis=false; } else{ 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 setSelect(item,value){ if (!item) return; item.value=value; if (item.value !== value){ //missing option with his value let o=addEl('option',undefined,item,value); o.setAttribute('value',value); item.value=value; } } function updateSelectList(item,slist,opt_keepValue){ let idx=0; let value; if (opt_keepValue) value=item.value; item.innerHTML=''; slist.forEach(function (sitem) { let sitemEl = addEl('option','',item,sitem.l); sitemEl.setAttribute('value', sitem.v !== undefined?sitem.v:idx); idx++; }) if (value !== undefined){ setSelect(item,value); } } 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+='
NMEA0183 to NMEA2000:
";
text += " " + (json.nmea0183 || "").replace(/,/g, ", ");
text += "
NMEA2000 to NMEA0183:
";
text += " " + (json.nmea2000 || "").replace(/,/g, ", ");
text += "