(function () { let lastUpdate = (new Date()).getTime(); let reloadConfig = false; let needAdminPass = true; let lastSalt = ""; let channelList = {}; let minUser = 200; let listeners = []; let buttonHandlers={}; let checkers={}; let userFormatters={}; 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) { if (typeof(parent) != 'object'){ parent=document.querySelector(parent); } 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() }); } buttonHandlers.reset=function() { 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) { if (jsonData.salt !== undefined) { lastSalt=jsonData.salt; delete jsonData.salt; } if (jsonData.minUser !== undefined){ minUser=jsonData.minUser; delete jsonData.minUser; } callListeners(api.EVENTS.status,jsonData); let statusPage = document.getElementById('statusPageContent'); let even = true; //first counter if (statusPage){ for (let k in jsonData) { if (typeof (jsonData[k]) === 'object') { if (k.indexOf('count') == 0) { createCounterDisplay(statusPage, k.replace("count", "").replace(/in$/, " in").replace(/out$/, " out"), k, even); even = !even; 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]; } } } if (k.indexOf("ch") == 0) { //channel def let name = k.substring(2); channelList[name] = jsonData[k]; } } 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) { callListeners(api.EVENTS.config,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; } }); } buttonHandlers.resetForm=resetForm; checkers.checkMinMax=function(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; } } checkers.checkPort=function(v,allValues,def){ let parsed=parseInt(v); if (isNaN(parsed)) return "must be a number"; if (parsed <1 || parsed >= 65536) return "port must be in the range 1..65536"; } checkers.checkSystemName=function(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"; } } checkers.checkAdminPass=function(v) { return checkApPass(v); } checkers.checkApIp=function(v, allValues) { if (!v) return "cannot be empty"; let err1 = "must be in the form 192.168.x.x"; if (!v.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) return err1; let parts = v.split("."); if (parts.length != 4) return err1; for (let idx = 0; idx < 4; idx++) { let iv = parseInt(parts[idx]); if (iv < 0 || iv > 255) return err1; } } checkers.checkNetMask=function(v, allValues) { return checkers.checkApIp(v, allValues); } checkers.checkIpAddress=function(v, allValues, def) { 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"; } checkers.checkMCAddress=function(v, allValues, def) { if (!v) return "cannot be empty"; if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)) return "must be in the form 224.0.0.1"; let parts=v.split("."); let o1=parseInt(parts[0]); if (o1 < 224 || o1 > 239) return "mulicast address must be in the range 224.0.0.0 to 239.255.255.255" } checkers.checkXDR=function(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; } } } } let loggedChecks={}; 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; if (v.getAttribute('disabled')) continue; let def = getConfigDefition(name); if (def.type === 'password' && (v.value == '' || omitPass)) { continue; } let check = v.getAttribute('data-check'); if (check && conditionOk(name)) { let cfgDef=getConfigDefition(name); let checkFunction=checkers[check]; if (typeof (checkFunction) === 'function') { if (! loggedChecks[check]){ loggedChecks[check]=true; //console.log("check:"+check); } let res = checkFunction(v.value, allValues, cfgDef); if (res) { let value = v.value; if (v.type === 'password') value = "******"; let label = v.getAttribute('data-label'); if (!label) label = v.getAttribute('name'); v.classList.add("error"); alert("invalid config for "+cfgDef.category+":" + label + "(" + value + "):\n" + res); return; } } else{ console.log("check not found:",check); } } allValues[name] = v.value; } return allValues; } buttonHandlers.changeConfig=function() { 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); }) } buttonHandlers.factoryReset=function() { 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) { if (parent.querySelector("#" + key)) { return; } let clazz = "row icon-row counter-row"; if (isEven) clazz += " even"; let row = addEl('div', clazz, parent); row.setAttribute("id", key); 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'); } }); callListeners(api.EVENTS.counterDisplayCreated,row); } function validKey(key) { if (!key) return; return key.replace(/[^a-z_:A-Z0-9-]/g, ''); } function updateMsgDetails(key, details) { forEl('.msgDetails', function (frame) { if (frame.getAttribute('id') !== key) return; for (let k in details) { k = validKey(k); 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); }); } function showOverlay(text, isHtml, buttons) { let el = document.getElementById('overlayContent'); if (text instanceof Object) { el.textContent = ''; el.appendChild(text); } else { if (isHtml) { el.innerHTML = text; el.classList.remove("text"); } else { el.textContent = text; el.classList.add("text"); } } buttons = (buttons ? buttons : []).concat([{ label: "Close", click: hideOverlay }]); let container = document.getElementById('overlayContainer'); let btframe = container.querySelector('.overlayButtons'); btframe.textContent = ''; buttons.forEach((btconfig) => { let bt = addEl('button', '', btframe, btconfig.label); bt.addEventListener("click", btconfig.click); }); container.classList.remove('hidden'); } function hideOverlay() { let container = document.getElementById('overlayContainer'); container.classList.add('hidden'); let el = document.getElementById('overlayContent'); el.textContent = ''; } function checkChange(el, row, name) { el.classList.remove("error"); 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 conditionOk(name){ let condition = getConditions(name); if (!condition) return true; 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; }); return visible; } function checkCondition(element) { let name = element.getAttribute('name'); let visible=conditionOk(name); let row = closestParent(element, 'row'); if (!row) return; if (visible) row.classList.remove('hidden'); else row.classList.add('hidden'); } let caliv = 0; function createCalSetInput(configItem, frame, clazz) { let el = addEl('input', clazz, frame); let cb = addEl('button', '', frame, 'C'); //el.disabled=true; cb.addEventListener('click', (ev) => { let cs = document.getElementById("calset").cloneNode(true); cs.classList.remove("hidden"); cs.querySelector(".heading").textContent = configItem.label || configItem.name; let vel = cs.querySelector(".val"); if (caliv != 0) window.clearInterval(caliv); caliv = window.setInterval(() => { if (document.body.contains(cs)) { fetch("/api/calibrate?name=" + encodeURIComponent(configItem.name)) .then((r) => r.text()) .then((txt) => { if (txt != vel.textContent) { vel.textContent = txt; } }) .catch((e) => { alert(e); hideOverlay(); window.clearInterval(caliv); }) } else { window.clearInterval(caliv); } }, 200); showOverlay(cs, false, [{ label: 'Set', click: () => { el.value = vel.textContent; let cev = new Event('change'); el.dispatchEvent(cev); } }]); }) el.setAttribute('name', configItem.name) return el; } function createCalValInput(configItem, frame, clazz) { let el = addEl('input', clazz, frame); let cb = addEl('button', '', frame, 'C'); //el.disabled=true; cb.addEventListener('click', (ev) => { const sv = function (val, cfg) { if (configItem.eval) { let v = parseFloat(val); let c = parseFloat(cfg); return (eval(configItem.eval)); } return v; }; let cs = document.getElementById("calval").cloneNode(true); cs.classList.remove("hidden"); cs.querySelector(".heading").textContent = configItem.label || configItem.name; let vel = cs.querySelector(".val"); let vinp = cs.querySelector("input"); vinp.value = el.value; if (caliv != 0) window.clearInterval(caliv); caliv = window.setInterval(() => { if (document.body.contains(cs)) { fetch("/api/calibrate?name=" + encodeURIComponent(configItem.name)) .then((r) => r.text()) .then((txt) => { txt = sv(txt, vinp.value); if (txt != vel.textContent) { vel.textContent = txt; } }) .catch((e) => { alert(e); hideOverlay(); window.clearInterval(caliv); }) } else { window.clearInterval(caliv); } }, 200); showOverlay(cs, false, [{ label: 'Set', click: () => { el.value = vinp.value; let cev = new Event('change'); el.dispatchEvent(cev); } }]); }) el.setAttribute('name', configItem.name) return el; } function createInput(configItem, frame, clazz) { let el; if (configItem.type === 'boolean' || configItem.type === 'list' || configItem.type == 'boatData') { el = addEl('select', clazz, frame); if (configItem.readOnly) el.setAttribute('disabled', true); 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); } if (configItem.type === "calset") { return createCalSetInput(configItem, frame, clazz); } if (configItem.type === "calval") { return createCalValInput(configItem, frame, clazz); } el = addEl('input', clazz, frame); if (configItem.readOnly) el.setAttribute('disabled', true); 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 } ], readOnly: configItem.readOnly }, d, 'xdrdir'); d = createXdrLine(el, 'Category'); let category = createInput({ type: 'list', name: configItem.name + "_cat", list: getXdrCategories(), readOnly: configItem.readOnly }, d, 'xdrcat'); d = createXdrLine(el, 'Source'); let selector = createInput({ type: 'list', name: configItem.name + "_sel", list: [], readOnly: configItem.readOnly }, d, 'xdrsel'); d = createXdrLine(el, 'Field'); let field = createInput({ type: 'list', name: configItem.name + '_field', list: [], readOnly: configItem.readOnly }, 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 } ], readOnly: configItem.readOnly }, d, 'xdrimode'); let instance = createInput({ type: 'number', name: configItem.name + "_instance", readOnly: configItem.readOnly }, d, 'xdrinstance'); d = createXdrLine(el, 'Transducer'); let xdrName = createInput({ type: 'text', name: configItem.name + "_xdr", readOnly: configItem.readOnly }, 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); if (configItem.readOnly) data.setAttribute('disabled', true); 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'], readOnly: configItem.readOnly }, el); let mode = createInput({ type: 'list', name: configItem.name + "_mode", list: ['whitelist', 'blacklist'], readOnly: configItem.readOnly }, el); let sentences = createInput({ type: 'text', name: configItem.name + "_sentences", readOnly: configItem.readOnly }, 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); if (configItem.readOnly) data.setAttribute('disabled', true); 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(); } } buttonHandlers.loadUnassigned=function() { getText("/api/xdrUnmapped") .then(function (txt) { let ot = ""; txt.split('\n').forEach(function (line) { let cv = convertUnassigned(line); if (!cv || !cv.l) return; ot += '
' + cv.l + '' + '
'; }) showOverlay(ot, true); forEl('.addunassigned', function (bt) { bt.onclick = unassignedAdd; }); }) } function showXdrHelp() { let helpContent = document.getElementById('xdrHelp'); if (helpContent) { showOverlay(helpContent.innerHTML, true); } } function formatDateForFilename(usePrefix, d) { let rt = ""; if (usePrefix) { let hdl= document.getElementById('headline'); if (hdl){ rt=hdl.textContent+"_"; } let fwt = document.querySelector('.status-fwtype'); if (fwt) rt += fwt.textContent; rt += "_"; } if (!d) d = new Date(); [d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()] .forEach(function (v) { if (v < 10) rt += "0" + v; else rt += "" + v; }) return rt; } function downloadData(data, name) { 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', name); target.click(); } buttonHandlers.exportConfig=function() { let data = getAllConfigs(true); if (!data) return; downloadData(data, formatDateForFilename(true) + ".json"); } buttonHandlers.exportXdr=function() { let data = {}; forEl('.xdrvalue', function (el) { let name = el.getAttribute('name'); let value = el.value; let err = checkers.checkXDR(value, data); if (err) { alert("error in " + name + ": " + value + "\n" + err); return; } data[name] = value; }) downloadData(data, "xdr" + formatDateForFilename(true) + ".json"); } function importJson(opt_keyPattern) { let clazz = 'importJson'; forEl('.' + clazz, function (ul) { ul.remove(); }); let ip = addEl('input', clazz, 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 (opt_keyPattern && !k.match(opt_keyPattern)) { alert("file contains invalid key " + k); return; } let del = document.querySelector('[name=' + k + ']'); if (del) { hasOverwrites = true; } } if (hasOverwrites) { if (!confirm("overwrite existing data?")) return; } for (let k in idata) { let del = document.querySelector('[name=' + k + ']'); if (del) { if (del.tagName === 'SELECT') { setSelect(del, idata[k]); } else { 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(); } buttonHandlers.importXdr=function() { importJson(new RegExp(/^XDR[0-9][0-9]*/)); } buttonHandlers.importConfig=function() { importJson(); } 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 categories = {}; 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 || item.category === undefined) return; if (item.category.match(/^xdr/)) { if (!includeXdr) return; } else { if (includeXdr) return; } let catEntry; if (categories[item.category] === undefined) { catEntry = { populated: false, frame: undefined, element: undefined } categories[item.category] = catEntry catEntry.frame = addEl('div', 'category', frame); catEntry.frame.setAttribute('data-category', item.category) let categoryTitle = addEl('div', 'title', catEntry.frame); let categoryButton = addEl('span', 'icon icon-more', categoryTitle); addEl('span', 'label', categoryTitle, item.category); addEl('span', 'categoryAdd', categoryTitle); catEntry.element = addEl('div', 'content', catEntry.frame); catEntry.element.classList.add('hidden'); categoryTitle.addEventListener('click', function (ev) { let rs = catEntry.element.classList.toggle('hidden'); if (rs) { toggleClass(categoryButton, 0, moreicons); } else { toggleClass(categoryButton, 1, moreicons); } }) } else { catEntry = categories[item.category]; } let showItem = true; let itemCapabilities = item.capabilities || {}; itemCapabilities['HIDE' + item.name] = null; for (let capability in itemCapabilities) { let values = itemCapabilities[capability]; let found = false; if (!(values instanceof Array)) values = [values]; values.forEach(function (v) { if (v === null) { if (capabilities[capability] === undefined) found = true; } else { if (capabilities[capability] == v) found = true; } }); if (!found) showItem = false; } let readOnly = false; let mode = capabilities['CFGMODE' + item.name]; if (mode == 1) { //hide showItem = false; } if (mode == 2) { readOnly = true; } if (showItem) { item.readOnly = readOnly; catEntry.populated = true; let row = addEl('div', 'row', catEntry.element); 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); if (!readOnly) 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); valueEl.setAttribute('data-label', label); let btContainer = addEl('div', 'buttonContainer', row); if (!readOnly) { 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(); } } }); } }); for (let cat in categories) { let catEntry = categories[cat]; if (!catEntry.populated) { catEntry.frame.remove(); } } } function loadConfigDefinitions() { getJson("api/capabilities") .then(function (capabilities) { if (capabilities.HELP_URL) { let el = document.getElementById('helpButton'); if (el) el.setAttribute('data-url', capabilities.HELP_URL); } try{ Object.freeze(capabilities); callListeners(api.EVENTS.init,capabilities); }catch (e){ console.log(e); } 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); }) }); } buttonHandlers.adminPassCancel=function() { 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) { } }); } buttonHandlers.forgetPass=function() { 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; } }); } buttonHandlers.converterInfo=function() { getJson("api/converterInfo").then(function (json) { let text = "

Converted entities

"; text += "

NMEA0183 to NMEA2000:
"; text += " " + (json.nmea0183 || "").replace(/,/g, ", "); text += "

"; text += "

NMEA2000 to NMEA0183:
"; text += " " + (json.nmea2000 || "").replace(/,/g, ", "); text += "

"; showOverlay(text, true); }); } function handleTab(el) { let activeName = el.getAttribute('data-page'); if (!activeName) { let extUrl = el.getAttribute('data-url'); if (!extUrl) return; window.open(extUrl, el.getAttribute('data-window') || '_'); } let activeTab = document.getElementById(activeName); if (!activeTab) return; forEl('.tabPage',function(pel){ pel.classList.add('hidden'); }); forEl('.tab',function(tel){ tel.classList.remove('active'); }); el.classList.add('active'); activeTab.classList.remove('hidden'); callListeners(api.EVENTS.tab,activeName); } /** * * @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 = -1 * (360 - 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: '' } } Object.freeze(valueFormatters); for (let k in valueFormatters){ Object.freeze(valueFormatters[k]); } 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 getUnit(def,useUser){ let fmt = useUser?(userFormatters[def.name] || valueFormatters[def.format]):valueFormatters[def.format] ; let u = fmt ? fmt.u : ' '; if (!fmt && def.format && def.format.match(/formatXdr/)) { u = def.format.replace(/formatXdr:[^:]*:/, ''); } return u; } /** * create a dashboard item if it does not exist * @param {*} def * @param {*} show * @param {*} parent * @returns the value div of the dashboard item */ function createOrHideDashboardItem(def,show, parent) { if (!def.name) return; let frame=document.getElementById('frame_'+def.name); let build=false; if (frame){ if (frame.classList.contains('invalid') && show){ build=true; frame.classList.remove('invalid'); frame.innerHTML=''; } } else{ if (! parent) return; frame = addEl('div', 'dash', parent); frame.setAttribute('id','frame_'+def.name); build=true; } if (! show){ if (!frame.classList.contains('invalid')){ frame.classList.add('invalid'); frame.innerHTML=''; } return; } if (build) { let title = addEl('span', 'dashTitle', frame, def.name); let value = addEl('span', 'dashValue', frame); value.setAttribute('id', 'data_' + def.name); if (def.format) value.classList.add(def.format); let footer = addEl('div', 'footer', frame); let src = addEl('span', 'source', footer); src.setAttribute('id', 'source_' + def.name); let u = getUnit(def, true) addEl('span', 'unit', footer, u); callListeners(api.EVENTS.dataItemCreated,{name:def.name,element:frame}); } let de = document.getElementById('data_' + def.name); return de; } function hideDashboardItem(name){ createOrHideDashboardItem({name:name},false); } 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"; for (let n in channelList) { if (v == channelList[n].id) return n; if (v >= channelList[n].id && v <= channelList[n].max) { return n; } } if (v < minUser) return "---"; return "USER[" + v + "]"; } let lastSelectList = []; function updateDashboard(data) { callListeners(api.EVENTS.boatData,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 show = current.valid||showInvalid; let de = createOrHideDashboardItem(current, show, frame); let newContent = '---'; if (current.valid) { let formatter; if (current.format && current.format != "NULL") { let key = current.format.replace(/^\&/, ''); formatter = userFormatters[current.name] || valueFormatters[key]; } if (formatter && formatter.f) { newContent = formatter.f(current.value,true); if (newContent === undefined) newContent = ""; } else { let v = parseFloat(current.value); if (!isNaN(v)) { v = v.toFixed(3) newContent = v; } else { newContent = current.value; } } } else { let uf=userFormatters[current.name]; if (uf && uf.f){ //call the user formatter //so that it can detect the invalid state newContent=uf.f(undefined,false); } if (newContent === undefined)newContent = "---"; } if (de) { 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"); //remove all items that are not send any more //this can only happen if the device restarted //otherwise data items will not go away - they will become invalid forEl('.dash', function (el) { let id = el.getAttribute('id'); if (id) { if (!names[id.replace(/^frame_/, '')]) { el.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, true); }); } } buttonHandlers.uploadBin=function(ev) { let el = document.getElementById("uploadFile"); let progressEl = document.getElementById("uploadDone"); if (!el) return; if (el.files.length < 1) return; ev.target.disabled = true; let file = el.files[0]; checkImageFile(file) .then(function (result) { let currentType; let currentVersion; let chipid; forEl('.status-version', function (el) { currentVersion = el.textContent }); forEl('.status-fwtype', function (el) { currentType = el.textContent }); forEl('.status-chipid', function (el) { chipid = el.textContent }); let confirmText = 'Ready to update firmware?\n'; if (result.chipId != chipid) { confirmText += "WARNING: the chipid in the image (" + result.chipId; confirmText += ") does not match the current chip id (" + chipid + ").\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)) { ev.target.disabled = false; return; } ensurePass() .then(function (hash) { let len = file.size; let req = new XMLHttpRequest(); req.onloadend = function () { ev.target.disabled = false; 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) { ev.target.disabled = false; 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) { ev.target.disabled = false; }); }) .catch(function (e) { alert("This file is an invalid image file:\n" + e); ev.target.disabled = false; }) } let HDROFFSET = 288; let VERSIONOFFSET = 16; let NAMEOFFSET = 48; let MINSIZE = HDROFFSET + NAMEOFFSET + 32; let CHIPIDOFFSET = 12; //2 bytes chip id here 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 getChipId(buffer) { if (buffer.length < CHIPIDOFFSET + 2) return -1; return buffer[CHIPIDOFFSET] + 256 * buffer[CHIPIDOFFSET + 1]; } 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 chipId = getChipId(content); let rt = { fwtype: fwtype, version: version, chipId: chipId }; resolve(rt); }); reader.readAsArrayBuffer(slice); }); } function addTabPage(name,label,url){ if (label === undefined) label=name; let tab=addEl('div','tab','#tabs',label); tab.addEventListener('click',function(ev){ handleTab(ev.target); }) if (url !== undefined){ tab.setAttribute('data-url',url); return; } tab.setAttribute('data-page',name); let page=addEl('div','tabPage hidden','#tabPages'); page.setAttribute('id',name); return page; } function addUserFormatter(name,unit,formatter){ if (unit !== undefined && formatter !== undefined){ userFormatters[name]={ u:unit, f:formatter } } else{ delete userFormatters[name]; } hideDashboardItem(name); //will recreate it on next data receive } const api= { registerListener: function (callback,opt_event) { if (opt_event === undefined){ listeners.push(callback); } else{ listeners.push({ event:opt_event, callback:callback }) } }, /** * helper for creating dom elements * parameters: * type: the element type (e.g. div) * class: a list of classes separated by space * parent (opt): a parent element (either a dom element vor a query selector) * text (opt): the text to be set as textContent * returns: the newly created element */ addEl: addEl, /** * iterator helper for a query selector * parameters: * query: the query selector * callback: the callback function (will be called with the element as param) * base (opt): a dome element to be used as the root (defaults to document) */ forEl: forEl, /** * find the closest parent that has a particular class * parameters: * element: the element to start with * class: the class to be searched for * returns: the element or undefined/null */ closestParent: closestParent, /** * add a new tab * parameters: * name - the name of the page * label (opt): the label for the new page * returns: the newly created element */ addTabPage: addTabPage, /** * add a user defined formatter for a boat data item * parameters: * name : the boat data item name * unit: the unit to be displayed * formatter: the formatter function (must return a string) */ addUserFormatter: addUserFormatter, removeUserFormatter: function(name){ addUserFormatter(name); }, /** * a dict of formatters * each one has 2 members: * u: the unit * f: the formatter function */ formatters: valueFormatters, /** * parse a line of boat data * the line has name,format,valid,update,source,value */ parseBoatDataLine: parseBoatDataLine, EVENTS: { init: 0, //called when capabilities are loaded, data is capabilities tab: 1, //tab page activated, data is the id of the tab page config: 2, //called when the config data is loaded,data is the config object boatData: 3, //called when boatData is received, data is the list of boat Data items dataItemCreated: 4, //data is an object with // name: the item name, element: the frame item of the boat data display status: 5, //status received, data is the status object counterDisplayCreated: 6 //data is the row for the display } }; function callListeners(event,data){ listeners.forEach((listener)=>{ if (typeof(listener) === 'function'){ listener(event,data); } else if (typeof(listener) === 'object'){ if (listener.event === event){ if (typeof(listener.callback) === 'function'){ listener.callback(event,data); } } } }) } window.esp32nmea2k = api; 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]; let buttonFunction=buttonHandlers[be.id]; if (typeof(buttonFunction) === 'function'){ be.onclick = buttonFunction; //assume a function with the button id //console.log("button: "+be.id); } else{ console.log("no handler for button "+be.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) { } 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) { let txt = "[" + res.chipId + "] "; txt += res.fwtype + ", " + res.version; iel.textContent = txt; iel.classList.remove("error"); }) }) .catch(function (e) { forEl('#imageProperties', function (iel) { iel.textContent = e; iel.classList.add("error"); }) }) }) }) }); }());