2167 lines
81 KiB
JavaScript
2167 lines
81 KiB
JavaScript
(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 += '<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 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 = "<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) {
|
|
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.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, 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");
|
|
})
|
|
})
|
|
})
|
|
})
|
|
});
|
|
}());
|
|
|