-
- firmware type
- ---
-
-
- chip type
- ---
-
-
- currentVersion
- ---
-
-
- New Firmware
-
-
-
-
-
-
-
-
diff --git a/web/index.js b/web/index.js
index 032b087..538a51a 100644
--- a/web/index.js
+++ b/web/index.js
@@ -1,1665 +1,1763 @@
-let self = this;
-let lastUpdate = (new Date()).getTime();
-let reloadConfig = false;
-let needAdminPass=true;
-let lastSalt="";
-let channelList={};
-let minUser=200;
-function addEl(type, clazz, parent, text) {
- let el = document.createElement(type);
- if (clazz) {
- if (!(clazz instanceof Array)) {
- clazz = clazz.split(/ */);
- }
- clazz.forEach(function (ce) {
- el.classList.add(ce);
- });
- }
- if (text) el.textContent = text;
- if (parent) parent.appendChild(el);
- return el;
-}
-function forEl(query, callback,base) {
- if (! base) base=document;
- let all = base.querySelectorAll(query);
- for (let i = 0; i < all.length; i++) {
- callback(all[i]);
- }
-}
-function closestParent(element,clazz){
- while (true){
- let parent=element.parentElement;
- if (! parent) return;
- if (parent.classList.contains(clazz)) return parent;
- element=parent;
- }
-}
-function alertRestart() {
- reloadConfig = true;
- alert("Board reset triggered, reconnect WLAN if necessary");
-}
-function getJson(url) {
- return fetch(url)
- .then(function (r) { return r.json() });
-}
-function getText(url){
- return fetch(url)
- .then(function (r) { return r.text() });
-}
-function reset() {
- ensurePass()
- .then(function (hash) {
- fetch('/api/reset?_hash='+encodeURIComponent(hash));
- alertRestart();
- })
- .catch(function (e) { });
-}
-function update() {
- let now = (new Date()).getTime();
- let ce = document.getElementById('connected');
- if (ce) {
- if ((lastUpdate + 3000) > now) {
- ce.classList.add('ok');
- }
- else {
- ce.classList.remove('ok');
- }
- }
- getJson('/api/status')
- .then(function (jsonData) {
- let statusPage=document.getElementById('statusPageContent');
- let even=true; //first counter
- for (let k in jsonData) {
- if (k == "salt"){
- lastSalt=jsonData[k];
- continue;
- }
- if (k == "minUser"){
- minUser=parseInt(jsonData[k]);
- continue;
- }
- if (! statusPage) continue;
- 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];
- });
- }
+(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(/ */);
}
- lastUpdate = (new Date()).getTime();
- if (reloadConfig) {
- reloadConfig = false;
- resetForm();
- }
- })
-}
-function resetForm(ev) {
- getJson("/api/config")
- .then(function (jsonData) {
- for (let k in jsonData) {
- if (k == "useAdminPass"){
- needAdminPass=jsonData[k] != 'false';
- }
- let el = document.querySelector("[name='" + k + "']");
- if (el) {
- let v = jsonData[k];
- let def=getConfigDefition(k);
- if (def.check == 'checkMinMax'){
- //simple migration if the current value is outside the range
- //we even "hide" this from the user
- v=parseFloat(v);
- if (! isNaN(v)){
- if (def.min !== undefined){
- if (v < parseFloat(def.min)) v=parseFloat(def.min);
- }
- if (def.max !== undefined){
- if (v > parseFloat(def.max)) v=parseFloat(def.max);
- }
- }
- }
- if (el.tagName === 'SELECT') {
- setSelect(el,v);
- }
- else{
- el.value = v;
- }
- el.setAttribute('data-loaded', v);
- let changeEvent = new Event('change');
- el.dispatchEvent(changeEvent);
- }
- }
- let name=jsonData.systemName;
- if (name){
- let el=document.getElementById('headline');
- if (el) el.textContent=name;
- document.title=name;
- }
- });
-}
-function checkMinMax(v,allValues,def){
- let parsed=parseFloat(v);
- if (isNaN(parsed)) return "must be a number";
- if (def.min !== undefined){
- if (parsed < parseFloat(def.min)) return "must be >= "+def.min;
- }
- if (def.max !== undefined){
- if (parsed > parseFloat(def.max)) return "must be <= "+def.max;
- }
-}
-
-function checkSystemName(v) {
- //2...32 characters for ssid
- let allowed = v.replace(/[^a-zA-Z0-9]*/g, '');
- if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9";
- if (v.length < 2 || v.length > 32) return "invalid length (2...32)";
-}
-function checkApPass(v) {
- //min 8 characters
- if (v.length < 8) {
- return "password must be at least 8 characters";
- }
-}
-function checkAdminPass(v){
- return checkApPass(v);
-}
-
-function checkApIp(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;
- }
-}
-function checkNetMask(v,allValues){
- return checkApIp(v,allValues);
-}
-
-function checkIpAddress(v,allValues,def){
- if (allValues.tclEnabled != "true") return;
- if (! v) return "cannot be empty";
- if (! v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)
- && ! v.match(/.*\.local/))
- return "must be either in the form 192.168.1.1 or xxx.local";
-}
-
-function checkXDR(v,allValues){
- if (! v) return;
- let parts=v.split(',');
- if (parseInt(parts[1]) == 0) return;
- if (parseInt(parts[1]) != 0 && ! parts[6]) return "missing transducer name";
- for (let k in allValues){
- if (! k.match(/^XDR/)) continue;
- let cmp=allValues[k];
- if (cmp == v){
- return "same mapping already defined in "+k;
+ clazz.forEach(function (ce) {
+ el.classList.add(ce);
+ });
}
- 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 (text) el.textContent = text;
+ if (parent) {
+ if (typeof(parent) != 'object'){
+ parent=document.querySelector(parent);
}
- if ((parseInt(parts[4]) == 0 && parseInt(cmpParts[4]) == 1)||
- (parseInt(parts[4]) == 1 && parseInt(cmpParts[4]) == 0)
- ){
- //ignore and single for the same mapping
- return "mapping for the same entity already defined in "+k;
- }
- }
- }
-}
-function getAllConfigs(omitPass) {
- let values = document.querySelectorAll('.configForm select , .configForm input');
- let allValues = {};
- for (let i = 0; i < values.length; i++) {
- let v = values[i];
- let name = v.getAttribute('name');
- if (!name) continue;
- if (name.indexOf("_") >= 0) continue;
- 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) {
- if (typeof (self[check]) === 'function') {
- let res = self[check](v.value, allValues, getConfigDefition(name));
- if (res) {
- let value = v.value;
- if (v.type === 'password') value = "******";
- alert("invalid config for " + v.getAttribute('name') + "(" + value + "):\n" + res);
- return;
- }
- }
- }
- allValues[name] = v.value;
- }
- return allValues;
-}
-function changeConfig() {
- ensurePass()
- .then(function (pass) {
- let newAdminPass;
- let url = "/api/setConfig"
- let body="_hash="+encodeURIComponent(pass)+"&";
- let allValues=getAllConfigs();
- if (!allValues) return;
- for (let name in allValues){
- if (name == 'adminPassword'){
- newAdminPass=allValues[name];
- }
- body += encodeURIComponent(name) + "=" + encodeURIComponent(allValues[name]) + "&";
- }
- fetch(url,{
- method:'POST',
- headers:{
- 'Content-Type': 'application/octet-stream' //we must lie here
- },
- body: body
- })
- .then((rs)=>rs.json())
- .then(function (status) {
- if (status.status == 'OK') {
- if (newAdminPass !== undefined) {
- forEl('#adminPassInput', function (el) {
- el.valu = newAdminPass;
- });
- saveAdminPass(newAdminPass,true);
- }
- alertRestart();
- }
- else {
- alert("unable to set config: " + status.status);
- }
- })
- })
- .catch(function (e) { alert(e); })
-}
-function factoryReset() {
- ensurePass()
- .then(function (hash) {
- if (!confirm("Really delete all configuration?\n" +
- "This will reset all your Wifi settings and disconnect you.")) {
- return;
- }
- getJson("/api/resetConfig?_hash="+encodeURIComponent(hash))
- .then(function (status) {
- alertRestart();
- })
- })
- .catch(function (e) { });
-}
-function createCounterDisplay(parent,label,key,isEven){
- 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');
- }
- });
-}
-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) {
- let loaded = el.getAttribute('data-loaded');
- if (loaded !== undefined) {
- if (loaded != el.value) {
- row.classList.add('changed');
- }
- else {
- row.classList.remove("changed");
- }
- }
- let dependend=conditionRelations[name];
- if (dependend){
- for (let el in dependend){
- checkCondition(dependend[el]);
- }
- }
-}
-let configDefinitions={};
-let xdrConfig={};
-//a map between the name of a config item and a list of dependend items
-let conditionRelations={};
-function getConfigDefition(name){
- if (! name) return {};
- let def;
- for (let k in configDefinitions){
- if (configDefinitions[k].name === name){
- def=configDefinitions[k];
- break;
- }
- }
- if (! def) return {};
- return def;
-}
-function getConditions(name){
- let def=getConfigDefition(name);
- if (! def) return;
- let condition=def.condition;
- if (! condition) return;
- if (! (condition instanceof Array)) condition=[condition];
- return condition;
-}
-function checkCondition(element){
- let name=element.getAttribute('name');
- let condition=getConditions(name);
- if (! condition) return;
- let visible=false;
- if (! condition instanceof Array) condition=[condition];
- condition.forEach(function(cel){
- let lvis=true;
- for (let k in cel){
- let item=document.querySelector('[name='+k+']');
- if (item){
- let compare=cel[k];
- if (compare instanceof Array){
- if (compare.indexOf(item.value) < 0) lvis=false;
- }
- else{
- if (item.value != cel[k]) lvis=false;
- }
- }
- }
- if (lvis) visible=true;
- });
- let row=closestParent(element,'row');
- if (!row) return;
- if (visible) row.classList.remove('hidden');
- else row.classList.add('hidden');
-}
-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');
+ if (parent) parent.appendChild(el);
}
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});
+ 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]);
}
}
- 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 closestParent(element, clazz) {
+ while (true) {
+ let parent = element.parentElement;
+ if (!parent) return;
+ if (parent.classList.contains(clazz)) return parent;
+ element = parent;
}
}
-}
-function getXdrCategory(cid){
- category=parseInt(cid);
- for (let c in xdrConfig){
- let base=xdrConfig[c];
- if (parseInt(base.id) == category){
- return base;
- }
+ function alertRestart() {
+ reloadConfig = true;
+ alert("Board reset triggered, reconnect WLAN if necessary");
}
- 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='';
- }
+ function getJson(url) {
+ return fetch(url)
+ .then(function (r) { return r.json() });
}
- 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);
+ function getText(url) {
+ return fetch(url)
+ .then(function (r) { return r.text() });
}
- 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();
- }
-}
-function loadUnassigned(){
- getText("/api/xdrUnmapped")
- .then(function(txt){
- let ot="";
- txt.split('\n').forEach(function(line){
- let cv=convertUnassigned(line);
- if (!cv || !cv.l) return;
- ot+='
'+
- cv.l+' '+
- '+
';
+ buttonHandlers.reset=function() {
+ ensurePass()
+ .then(function (hash) {
+ fetch('/api/reset?_hash=' + encodeURIComponent(hash));
+ alertRestart();
})
- showOverlay(ot,true);
- forEl('.addunassigned',function(bt){
- bt.onclick=unassignedAdd;
+ .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;
+ }
});
- })
-}
-function showXdrHelp(){
- let helpContent=document.getElementById('xdrHelp');
- if (helpContent){
- showOverlay(helpContent.innerHTML,true);
}
-}
-function formatDateForFilename(usePrefix,d){
- let rt="";
- if (usePrefix){
- 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();
-}
-function exportConfig(){
- let data=getAllConfigs(true);
- if (! data) return;
- downloadData(data,formatDateForFilename(true)+".json");
-}
-function exportXdr(){
- let data={};
- forEl('.xdrvalue',function(el) {
- let name=el.getAttribute('name');
- let value=el.value;
- let err=checkXDR(value,data);
- if (err){
- alert("error in "+name+": "+value+"\n"+err);
- return;
+ 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;
}
- 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);
+ if (def.max !== undefined) {
+ if (parsed > parseFloat(def.max)) return "must be <= " + def.max;
}
- ip.remove();
- });
- ip.click();
-}
-function importXdr(){
- importJson(new RegExp(/^XDR[0-9][0-9]*/));
-}
-function importConfig(){
- 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) return;
- if (item.category.match(/^xdr/)){
- if (! includeXdr) return;
+ 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";
}
- else{
- if(includeXdr) return;
+ }
+ 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;
}
- let catEntry;
- if (categories[item.category] === undefined){
- catEntry={
- populated:false,
- frame: undefined,
- element: undefined
+ }
+ 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;
}
- 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);
+ 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;
}
- else {
- toggleClass(categoryButton,1,moreicons);
+ 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;
}
- })
+ }
}
- 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;
+ }
+ 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{
- if (capabilities[capability] == v) found = true;
+ console.log("check not found:",check);
}
- });
- if (!found) showItem=false;
+ }
+ allValues[name] = v.value;
}
- 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);
+ 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');
}
- if (item.check) valueEl.setAttribute('data-check', item.check);
- 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);
- })
+ else {
+ icon.classList.remove('icon-more');
+ icon.classList.add('icon-less');
}
- bt = addEl('button', 'infoButton', btContainer, '?');
- bt.addEventListener('click', function (ev) {
- if (item.description) {
- showOverlay(item.description);
+ });
+ 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 {
- if (item.category.match(/^xdr/)) {
- showXdrHelp();
+ 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');
}
});
}
- });
- for (let cat in categories){
- let catEntry=categories[cat];
- if (! catEntry.populated){
- catEntry.frame.remove();
+ 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 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);
- }
- 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'));
- });
- })
- })
+
+ 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++;
})
- .catch(function (err) { alert("unable to load config: " + err) })
-}
-function verifyPass(pass){
- return new Promise(function(resolve,reject){
- let hash=lastSalt+pass;
- let md5hash=MD5(hash);
- getJson('api/checkPass?hash='+encodeURIComponent(md5hash))
- .then(function(jsonData){
- if (jsonData.status == 'OK') resolve(md5hash);
- else reject(jsonData.status);
- return;
- })
- .catch(function(error){reject(error);})
- });
-}
-
-
-function adminPassCancel(){
- forEl('#adminPassOverlay',function(el){el.classList.add('hidden')});
- forEl('#adminPassInput',function(el){el.value=''});
-}
-function saveAdminPass(pass,forceIfSet){
- forEl('#adminPassKeep',function(el){
- try{
- let old=localStorage.getItem('adminPass');
- if (el.value == 'true' || (forceIfSet && old !== undefined)){
- localStorage.setItem('adminPass',pass);
- }
- else{
- localStorage.removeItem('adminPass');
- }
- }catch (e){}
- });
-}
-function forgetPass(){
- localStorage.removeItem('adminPass');
- forEl('#adminPassInput',function(el){
- el.value='';
- });
-}
-function ensurePass(){
- return new Promise(function(resolve,reject){
- if (! needAdminPass) {
- resolve('');
- return;
+ if (value !== undefined) {
+ setSelect(item, value);
}
- 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;
+ }
+ function getXdrCategories() {
+ let rt = [];
+ for (let c in xdrConfig) {
+ if (xdrConfig[c].enabled !== false) {
+ rt.push({ l: c, v: xdrConfig[c].id });
}
- ok.onclick=function(){
- verifyPass(pe.value)
- .then(function(pass){
- forEl('#adminPassOverlay',function(el){el.classList.add('hidden')});
- saveAdminPass(pe.value);
- resolve(pass);
+ }
+ 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;
})
- .catch(function(err){
- if (hint){
- hint.textContent="invalid password";
+ }
+ 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); });
});
- };
- if (hint) hint.textContent='';
- forEl('#adminPassOverlay',function(el){el.classList.remove('hidden')});
+ 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{
- verifyPass(pe.value)
- .then(function(np){resolve(np);})
- .catch(function(err){
- pe.value='';
- ensurePass()
- .then(function(p){resolve(p);})
- .catch(function(e){reject(e);});
- });
+ 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;
}
- });
-}
-function converterInfo() {
- 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;
- let all = document.querySelectorAll('.tabPage');
- for (let i = 0; i < all.length; i++) {
- all[i].classList.add('hidden');
- }
- let tabs = document.querySelectorAll('.tab');
- for (let i = 0; i < all.length; i++) {
- tabs[i].classList.remove('active');
- }
- el.classList.add('active');
- activeTab.classList.remove('hidden');
-}
-/**
- *
- * @param {number} coordinate
- * @param axis
- * @returns {string}
- */
- function formatLonLatsDecimal(coordinate,axis){
- coordinate = (coordinate+540)%360 - 180; // normalize for sphere being round
-
- let abscoordinate = Math.abs(coordinate);
- let coordinatedegrees = Math.floor(abscoordinate);
-
- let coordinateminutes = (abscoordinate - coordinatedegrees)/(1/60);
- let numdecimal=2;
- //correctly handle the toFixed(x) - will do math rounding
- if (coordinateminutes.toFixed(numdecimal) == 60){
- coordinatedegrees+=1;
- coordinateminutes=0;
- }
- if( coordinatedegrees < 10 ) {
- coordinatedegrees = "0" + coordinatedegrees;
- }
- if (coordinatedegrees < 100 && axis == 'lon'){
- coordinatedegrees = "0" + coordinatedegrees;
- }
- let str = coordinatedegrees + "\u00B0";
-
- if( coordinateminutes < 10 ) {
- str +="0";
- }
- str += coordinateminutes.toFixed(numdecimal) + "'";
- if (axis == "lon") {
- str += coordinate < 0 ? "W" :"E";
- } else {
- str += coordinate < 0 ? "S" :"N";
- }
- return str;
-};
-function formatFixed(v,dig,fract){
- v=parseFloat(v);
- if (dig === undefined) return v.toFixed(fract);
- let s=v<0?"-":"";
- v=Math.abs(v);
- let rv=v.toFixed(fract);
- let parts=rv.split('.');
- parts[0]="0000000000"+parts[0];
- if (dig >= 10) dig=10;
- if (fract > 0){
- return s+parts[0].substr(parts[0].length-dig)+"."+parts[1];
- }
- return s+parts[0].substr(parts[0].length-dig);
-}
-let valueFormatters = {
- formatCourse: {
- f: function (v) {
- let x = parseFloat(v);
- let rt = x * 180.0 / Math.PI;
- if (rt > 360) rt -= 360;
- if (rt < 0) rt += 360;
- return rt.toFixed(0);
- },
- u: '°'
- },
- formatKnots: {
- f: function (v) {
- let x = parseFloat(v);
- x = x * 3600.0 / 1852.0;
- return x.toFixed(2);
- },
- u: 'kn'
- },
- formatWind: {
- f: function (v) {
- let x = parseFloat(v);
- x = x * 180.0 / Math.PI;
- if (x > 180) x = -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:''
- }
-
-
-}
-function resizeFont(el,reset,maxIt){
- if (maxIt === undefined) maxIt=10;
- if (! el) return;
- if (reset) el.style.fontSize='';
- while (el.scrollWidth > el.clientWidth && maxIt){
- let next=parseFloat(window.getComputedStyle(el).fontSize)*0.9;
- el.style.fontSize=next+"px";
- }
-}
-function createDashboardItem(name, def, parent) {
- if (! def.name) return;
- let frame = addEl('div', 'dash', parent);
- let title = addEl('span', 'dashTitle', frame, name);
- let value = addEl('span', 'dashValue', frame);
- value.setAttribute('id', 'data_' + name);
- let fmt=valueFormatters[def.format];
- if (def.format) value.classList.add(def.format);
- let footer = addEl('div','footer',frame);
- let src= addEl('span','source',footer);
- src.setAttribute('id','source_'+name);
- let u=fmt?fmt.u:' ';
- if (! fmt && def.format && def.format.match(/formatXdr/)){
- u=def.format.replace(/formatXdr:[^:]*:/,'');
- }
- addEl('span','unit',footer,u);
- return value;
-}
-function parseBoatDataLine(line){
- let rt={};
- let parts=line.split(',');
- rt.name=parts[0];
- rt.valid=parts[2] === '1';
- rt.update=parseInt(parts[3]);
- rt.source=parseInt(parts[4]);
- rt.format=parts[1];
- rt.value=parts[5];
- return rt;
-}
-function createDashboard() {
- let frame = document.getElementById('dashboardPage');
- if (!frame) return;
- frame.innerHTML = '';
-}
-function sourceName(v){
- if (v == 0) return "N2K";
- for (let n in channelList){
- if (v == channelList[n].id) return n;
- if (v >= channelList[n].id && v <= channelList[n].max){
- return n;
+ 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;
}
- if (v < minUser) return "---";
- return "USER["+v+"]";
-}
-let lastSelectList=[];
-function updateDashboard(data) {
- let frame = document.getElementById('dashboardPage');
- let showInvalid=true;
- forEl('select[name=showInvalidData]',function(el){
- if (el.value == 'false') showInvalid=false;
- })
- let names={};
- for (let n in data) {
- let current=parseBoatDataLine(data[n]);
- if (! current.name) continue;
- names[current.name]=true;
- let de = document.getElementById('data_' + current.name);
- let isValid=current.valid;
- if (! de && frame && (isValid || showInvalid)){
- de=createDashboardItem(current.name,current,frame);
+ 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 (de && (!isValid && !showInvalid)){
- de.parentElement.remove();
- continue;
- }
- if (de) {
- let newContent='----';
+ 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 = valueFormatters[key];
+ formatter = userFormatters[current.name] || valueFormatters[key];
}
- if (formatter) {
- newContent = formatter.f(current.value);
+ if (formatter && formatter.f) {
+ newContent = formatter.f(current.value,true);
+ if (newContent === undefined) newContent = "";
}
else {
let v = parseFloat(current.value);
@@ -1672,262 +1770,397 @@ function updateDashboard(data) {
}
}
}
- else newContent = "---";
- if (newContent !== de.textContent){
- de.textContent=newContent;
- resizeFont(de,true);
- }
- }
- let src=document.getElementById('source_'+current.name);
- if (src){
- src.textContent=sourceName(current.source);
- }
- }
- console.log("update");
- forEl('.dashValue',function(el){
- let id=el.getAttribute('id');
- if (id){
- if (! names[id.replace(/^data_/,'')]){
- el.parentElement.remove();
- }
- }
- });
- let selectList=[];
- for (let n in names){
- selectList.push({l:n,v:n});
- }
- let selectChanged=false;
- if (lastSelectList.length == selectList.length){
- for (let i=0;i
0 && buffer[start+length-1] == 0){
- length--;
+ 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;
+ })
}
- if (length <= 0) return "";
- let decoder=new TextDecoder();
- let rt=decoder.decode(buffer.slice(
+ 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]);
+ 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
}
}
- 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);
- });
-}
-window.setInterval(update, 1000);
-window.setInterval(function () {
- let dp = document.getElementById('dashboardPage');
- if (dp.classList.contains('hidden')) return;
- getText('api/boatDataString').then(function (data) {
- updateDashboard(data.split('\n'));
- });
-}, 1000);
-window.addEventListener('load', function () {
- let buttons = document.querySelectorAll('button');
- for (let i = 0; i < buttons.length; i++) {
- let be = buttons[i];
- be.onclick = window[be.id]; //assume a function with the button id
+ else{
+ delete userFormatters[name];
+ }
+ hideDashboardItem(name); //will recreate it on next data receive
}
- 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);
- });
+ 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);
+ }
+ }
+ }
+ })
}
- createDashboard();
- loadConfigDefinitions();
- try{
- let storedPass=localStorage.getItem('adminPass');
- if (storedPass){
- forEl('#adminPassInput',function(el){
- el.value=storedPass;
+ 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);
});
}
- }catch(e){}
- let statusPage=document.getElementById('statusPageContent');
- /*if (statusPage){
- let even=true;
- for (let c in counters){
- createCounterDisplay(statusPage,counters[c],c,even);
- even=!even;
- }
- }*/
- forEl('#uploadFile',function(el){
- el.addEventListener('change',function(ev){
- if (ev.target.files.length < 1) return;
- let file=ev.target.files[0];
- checkImageFile(file)
- .then(function(res){
- forEl('#imageProperties',function(iel){
- 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");
+ 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");
+ })
+ })
+ })
})
- })
-});
\ No newline at end of file
+ });
+}());
+
diff --git a/webinstall/installUtil.js b/webinstall/installUtil.js
index 14910d8..c737d42 100644
--- a/webinstall/installUtil.js
+++ b/webinstall/installUtil.js
@@ -1,4 +1,4 @@
-import {ESPLoader,Transport} from "https://cdn.jsdelivr.net/npm/esptool-js@0.2.1/bundle.js";
+import {ESPLoader,Transport} from "https://cdn.jsdelivr.net/npm/esptool-js@0.4.5/bundle.js";
/**
* write all messages to the console
*/
@@ -102,10 +102,14 @@ class ESPInstaller{
await this.consoleReader.cancel();
this.consoleReader=undefined;
}
- await this.consoleDevice.close();
}catch(e){
console.log(`error cancel serial read ${e}`);
}
+ try{
+ await this.consoleDevice.close();
+ }catch(e){
+ console.log('error closing console device', this.consoleDevice,e);
+ }
this.consoleDevice=undefined;
}
if (this.transport){
@@ -126,13 +130,17 @@ class ESPInstaller{
}
try {
this.transport = new Transport(device);
- this.esploader = new ESPLoader(this.transport, 115200, this.espLoaderTerminal);
- let foundChip = await this.esploader.main_fn();
+ this.esploader = new ESPLoader({
+ transport:this.transport,
+ baudrate: 115200,
+ terminal: this.espLoaderTerminal});
+ //this.esploader.debugLogging=true;
+ let foundChip = await this.esploader.main();
if (!foundChip) {
throw new Error("unable to read chip id");
}
this.espLoaderTerminal.writeLine(`chip: ${foundChip}`);
- await this.esploader.flash_id();
+ //await this.esploader.flashId();
this.chipFamily = this.esploader.chip.CHIP_NAME;
this.imageChipId = this.esploader.chip.IMAGE_CHIP_ID;
this.espLoaderTerminal.writeLine(`chipFamily: ${this.chipFamily}`);
@@ -198,12 +206,17 @@ class ESPInstaller{
async writeFlash(fileList){
this.checkConnected();
this.espLoaderTerminal.writeLine(`Flashing....`);
- await this.esploader.write_flash(
- fileList,
- "keep",
- "keep",
- "keep",
- false
+ await this.esploader.writeFlash({
+ fileArray: fileList,
+ flashSize: "keep",
+ flashMode: "keep",
+ flashFreq: "keep",
+ eraseAll: false,
+ compress: true,
+ /*reportProgress: (fileIndex, written, total)=>{
+ this.espLoaderTerminal.writeLine(`file ${fileIndex}: ${written}/${total}`);
+ }*/
+ }
)
await this.resetTransport();
this.espLoaderTerminal.writeLine(`Done.`);