restructure boat data display handling, allow for more flexible user formatters

This commit is contained in:
andreas 2024-10-13 16:10:03 +02:00
parent e982389c75
commit 9c979657bf
4 changed files with 290 additions and 131 deletions

View File

@ -1,12 +1,39 @@
(function(){ (function(){
const api=window.esp32nmea2k;
if (! api) return;
let isActive=false; let isActive=false;
window.esp32nmea2k.registerListener((id,data)=>{ const tabName="example";
if (id === 0){ const configName="exampleBDSel";
let boatItemName;
api.registerListener((id,data)=>{
if (id === api.EVENTS.init){
//data is capabilities //data is capabilities
if (data.testboard) isActive=true; if (data.testboard) isActive=true;
if (isActive){
let page=api.addTabPage(tabName,"Example");
api.addEl('div','',page,"this is a test tab");
}
} }
if (isActive){ if (isActive){
console.log("exampletask listener",id,data); console.log("exampletask listener",id,data);
if (id === api.EVENTS.tab){
if (data === tabName){
console.log("example tab activated");
}
}
if (id == api.EVENTS.config){
let nextboatItemName=data[configName];
console.log("value of "+configName,nextboatItemName);
if (nextboatItemName){
api.addUserFormatter(nextboatItemName,"xxx",function(v){
return "X"+v+"X";
})
}
if (boatItemName !== undefined && boatItemName != nextboatItemName){
api.removeUserFormatter(boatItemName);
}
boatItemName=nextboatItemName;
}
} }
}) })
})(); })();

View File

@ -22,7 +22,7 @@ body {
overflow: hidden; overflow: hidden;
} }
.tabPage{ #tabPages{
overflow: auto; overflow: auto;
} }
@ -120,6 +120,9 @@ body {
.hidden{ .hidden{
display: none !important; display: none !important;
} }
.dash.invalid{
display: none;
}
#xdrPage .row>.label{ #xdrPage .row>.label{
display: none; display: none;
} }

View File

@ -23,104 +23,106 @@
<div class="tab" data-page="updatePage">Update</div> <div class="tab" data-page="updatePage">Update</div>
<div class="tab" data-url="https://github.com/wellenvogel/esp32-nmea2000" data-window="help" id="helpButton">Help</div> <div class="tab" data-url="https://github.com/wellenvogel/esp32-nmea2000" data-window="help" id="helpButton">Help</div>
</div> </div>
<div id="statusPage" class="tabPage"> <div id="tabPages">
<div id="statusPageContent"> <div id="statusPage" class="tabPage">
<div class="row"> <div id="statusPageContent">
<span class="label">VERSION</span> <div class="row">
<span class="value" id="version">---</span> <span class="label">VERSION</span>
<button class="infoButton" id="converterInfo">?</button> <span class="value" id="version">---</span>
</div> <button class="infoButton" id="converterInfo">?</button>
</div>
<div class="row even"> <div class="row even">
<span class="label">Access Point IP</span> <span class="label">Access Point IP</span>
<span class="value" id="apIp">---</span> <span class="value" id="apIp">---</span>
</div>
<div class="row ">
<span class="label">wifi client connected</span>
<span class="value" id="wifiConnected">---</span>&nbsp;[<span class="value" id="wifiSSID">---</span>]
</div>
<div class="row even">
<span class="label">wifi client IP</span>
<span class="value" id="clientIP">---</span>
</div>
<div class="row">
<span class="label"># clients</span>
<span class="value" id="numClients">---</span>
</div>
<div class="row even">
<span class="label">TCP client connected</span>
<span class="value" id="clientCon">---</span>
</div>
<div class="row">
<span class="label">TCP client error</span>
<span class="value" id="clientErr">---</span>
</div>
<div class="row even">
<span class="label">Free heap</span>
<span class="value" id="heap">---</span>
</div>
<div class="row">
<span class="label">NMEA2000 State</span>
[<span class="value" id="n2knode">---</span>]&nbsp;
<span class="value" id="n2kstate">UNKNOWN</span>
</div>
</div> </div>
<div class="row "> <button id="reset">Reset</button>
<span class="label">wifi client connected</span>
<span class="value" id="wifiConnected">---</span>&nbsp;[<span class="value" id="wifiSSID">---</span>]
</div>
<div class="row even">
<span class="label">wifi client IP</span>
<span class="value" id="clientIP">---</span>
</div>
<div class="row">
<span class="label"># clients</span>
<span class="value" id="numClients">---</span>
</div>
<div class="row even">
<span class="label">TCP client connected</span>
<span class="value" id="clientCon">---</span>
</div>
<div class="row">
<span class="label">TCP client error</span>
<span class="value" id="clientErr">---</span>
</div>
<div class="row even">
<span class="label">Free heap</span>
<span class="value" id="heap">---</span>
</div>
<div class="row">
<span class="label">NMEA2000 State</span>
[<span class="value" id="n2knode">---</span>]&nbsp;
<span class="value" id="n2kstate">UNKNOWN</span>
</div>
</div> </div>
<button id="reset">Reset</button> <div class="configForm tabPage hidden" id="configPage">
</div> <div class="buttons">
<div class="configForm tabPage hidden" id="configPage" > <button id="resetForm">ReloadConfig</button>
<div class="buttons"> <button id="forgetPass">ForgetPass</button>
<button id="resetForm">ReloadConfig</button> <button id="changeConfig">Save&Restart</button>
<button id="forgetPass">ForgetPass</button> <button id="exportConfig">Export</button>
<button id="changeConfig">Save&Restart</button> <button id="importConfig">Import</button>
<button id="exportConfig">Export</button> <button id="factoryReset">FactoryReset</button>
<button id="importConfig">Import</button> </div>
<button id="factoryReset">FactoryReset</button> <div class="configFormRows">
</div>
</div> </div>
<div class="configFormRows"> <div class="configForm tabPage hidden" id="xdrPage">
<div class="buttons">
<button id="resetForm">ReloadConfig</button>
<button id="changeConfig">Save&Restart</button>
<button id="loadUnassigned">Show Unmapped</button>
<button id="exportXdr">Export</button>
<button id="importXdr">Import</button>
</div>
<div class="configFormRows">
</div>
</div>
<div class="tabPage hidden" id="dashboardPage">
</div> </div>
</div> <div class="tabPage hidden" id="updatePage">
<div class="configForm tabPage hidden" id="xdrPage" > <div class="row">
<div class="buttons"> <span class="label">firmware type</span>
<button id="resetForm">ReloadConfig</button> <span class="value status-fwtype">---</span>
<button id="changeConfig">Save&Restart</button> </div>
<button id="loadUnassigned">Show Unmapped</button> <div class="row">
<button id="exportXdr">Export</button> <span class="label">chip type</span>
<button id="importXdr">Import</button> <span class="value status-chipid">---</span>
</div> </div>
<div class="configFormRows"> <div class="row">
<span class="label">currentVersion</span>
</div> <span class="value status-version">---</span>
</div> </div>
<div class="tabPage hidden" id="dashboardPage"> <div class="row">
<span class="label">New Firmware</span>
</div> <input type="file" name="file1" id="uploadFile">
<div class="tabPage hidden" id="updatePage"> </div>
<div class="row"> <div class="row">
<span class="label">firmware type</span> <span class="label"></span>
<span class="value status-fwtype">---</span> <span id="imageProperties" class="value"></span>
</div> </div>
<div class="row"> <div id="uploadProgress">
<span class="label">chip type</span> <div id="uploadDone"></div>
<span class="value status-chipid">---</span> </div>
</div> <div class="buttons">
<div class="row"> <button id="uploadBin">Upload</button>
<span class="label">currentVersion</span> </div>
<span class="value status-version">---</span>
</div>
<div class="row">
<span class="label">New Firmware</span>
<input type="file" name="file1" id="uploadFile">
</div>
<div class="row">
<span class="label"></span>
<span id="imageProperties" class="value"></span>
</div>
<div id="uploadProgress">
<div id="uploadDone"></div>
</div>
<div class="buttons">
<button id="uploadBin">Upload</button>
</div> </div>
</div> </div>

View File

@ -8,6 +8,7 @@
let listeners = []; let listeners = [];
let buttonHandlers={}; let buttonHandlers={};
let checkers={}; let checkers={};
let userFormatters={};
function addEl(type, clazz, parent, text) { function addEl(type, clazz, parent, text) {
let el = document.createElement(type); let el = document.createElement(type);
if (clazz) { if (clazz) {
@ -19,7 +20,12 @@
}); });
} }
if (text) el.textContent = text; if (text) el.textContent = text;
if (parent) parent.appendChild(el); if (parent) {
if (typeof(parent) != 'object'){
parent=document.querySelector(parent);
}
if (parent) parent.appendChild(el);
}
return el; return el;
} }
function forEl(query, callback, base) { function forEl(query, callback, base) {
@ -122,6 +128,7 @@
function resetForm(ev) { function resetForm(ev) {
getJson("/api/config") getJson("/api/config")
.then(function (jsonData) { .then(function (jsonData) {
callListeners(api.EVENTS.config,jsonData);
for (let k in jsonData) { for (let k in jsonData) {
if (k == "useAdminPass") { if (k == "useAdminPass") {
needAdminPass = jsonData[k] != 'false'; needAdminPass = jsonData[k] != 'false';
@ -1418,16 +1425,15 @@
} }
let activeTab = document.getElementById(activeName); let activeTab = document.getElementById(activeName);
if (!activeTab) return; if (!activeTab) return;
let all = document.querySelectorAll('.tabPage'); forEl('.tabPage',function(pel){
for (let i = 0; i < all.length; i++) { pel.classList.add('hidden');
all[i].classList.add('hidden'); });
} forEl('.tab',function(tel){
let tabs = document.querySelectorAll('.tab'); tel.classList.remove('active');
for (let i = 0; i < all.length; i++) { });
tabs[i].classList.remove('active');
}
el.classList.add('active'); el.classList.add('active');
activeTab.classList.remove('hidden'); activeTab.classList.remove('hidden');
callListeners(api.EVENTS.tab,activeName);
} }
/** /**
* *
@ -1602,6 +1608,10 @@
} }
}
Object.freeze(valueFormatters);
for (let k in valueFormatters){
Object.freeze(valueFormatters[k]);
} }
function resizeFont(el, reset, maxIt) { function resizeFont(el, reset, maxIt) {
if (maxIt === undefined) maxIt = 10; if (maxIt === undefined) maxIt = 10;
@ -1612,23 +1622,62 @@
el.style.fontSize = next + "px"; el.style.fontSize = next + "px";
} }
} }
function createDashboardItem(name, def, parent) { function getUnit(def,useUser){
if (!def.name) return; let fmt = useUser?(userFormatters[def.name] || valueFormatters[def.format]):valueFormatters[def.format] ;
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 : ' '; let u = fmt ? fmt.u : ' ';
if (!fmt && def.format && def.format.match(/formatXdr/)) { if (!fmt && def.format && def.format.match(/formatXdr/)) {
u = def.format.replace(/formatXdr:[^:]*:/, ''); u = def.format.replace(/formatXdr:[^:]*:/, '');
} }
addEl('span', 'unit', footer, u); return u;
return value; }
/**
* create a dashboard item if it does not exist
* @param {*} def
* @param {*} show
* @param {*} parent
* @returns the value div of the dashboard item
*/
function createOrHideDashboardItem(def,show, parent) {
if (!def.name) return;
let frame=document.getElementById('frame_'+def.name);
let build=false;
if (frame){
if (frame.classList.contains('invalid') && show){
build=true;
frame.classList.remove('invalid');
frame.innerHTML='';
}
}
else{
if (! parent) return;
frame = addEl('div', 'dash', parent);
frame.setAttribute('id','frame_'+def.name);
build=true;
}
if (! show){
if (!frame.classList.contains('invalid')){
frame.classList.add('invalid');
frame.innerHTML='';
}
return;
}
if (build) {
let title = addEl('span', 'dashTitle', frame, def.name);
let value = addEl('span', 'dashValue', frame);
value.setAttribute('id', 'data_' + def.name);
if (def.format) value.classList.add(def.format);
let footer = addEl('div', 'footer', frame);
let src = addEl('span', 'source', footer);
src.setAttribute('id', 'source_' + def.name);
let u = getUnit(def, true)
addEl('span', 'unit', footer, u);
callListeners(api.EVENTS.dataItemCreated, frame);
}
let de = document.getElementById('data_' + def.name);
return de;
}
function hideDashboardItem(name){
createOrHideDashboardItem({name:name},false);
} }
function parseBoatDataLine(line) { function parseBoatDataLine(line) {
let rt = {}; let rt = {};
@ -1659,6 +1708,7 @@
} }
let lastSelectList = []; let lastSelectList = [];
function updateDashboard(data) { function updateDashboard(data) {
callListeners(api.EVENTS.boatData,data);
let frame = document.getElementById('dashboardPage'); let frame = document.getElementById('dashboardPage');
let showInvalid = true; let showInvalid = true;
forEl('select[name=showInvalidData]', function (el) { forEl('select[name=showInvalidData]', function (el) {
@ -1669,24 +1719,17 @@
let current = parseBoatDataLine(data[n]); let current = parseBoatDataLine(data[n]);
if (!current.name) continue; if (!current.name) continue;
names[current.name] = true; names[current.name] = true;
let de = document.getElementById('data_' + current.name); let show = current.valid||showInvalid;
let isValid = current.valid; let de=createOrHideDashboardItem(current,show,frame);
if (!de && frame && (isValid || showInvalid)) {
de = createDashboardItem(current.name, current, frame);
}
if (de && (!isValid && !showInvalid)) {
de.parentElement.remove();
continue;
}
if (de) { if (de) {
let newContent = '----'; let newContent = '----';
if (current.valid) { if (current.valid) {
let formatter; let formatter;
if (current.format && current.format != "NULL") { if (current.format && current.format != "NULL") {
let key = current.format.replace(/^\&/, ''); let key = current.format.replace(/^\&/, '');
formatter = valueFormatters[key]; formatter = userFormatters[current.name]|| valueFormatters[key];
} }
if (formatter) { if (formatter && formatter.f) {
newContent = formatter.f(current.value); newContent = formatter.f(current.value);
} }
else { else {
@ -1711,11 +1754,14 @@
src.textContent = sourceName(current.source); src.textContent = sourceName(current.source);
} }
} }
console.log("update"); //console.log("update");
forEl('.dashValue', function (el) { //remove all items that are not send any more
//this can only happen if the device restarted
//otherwise data items will not go away - they will become invalid
forEl('.dash', function (el) {
let id = el.getAttribute('id'); let id = el.getAttribute('id');
if (id) { if (id) {
if (!names[id.replace(/^data_/, '')]) { if (!names[id.replace(/^frame_/, '')]) {
el.parentElement.remove(); el.parentElement.remove();
} }
} }
@ -1889,15 +1935,96 @@
reader.readAsArrayBuffer(slice); reader.readAsArrayBuffer(slice);
}); });
} }
function addTabPage(name,label){
if (label === undefined) label=name;
let tab=addEl('div','tab','#tabs',label);
tab.setAttribute('data-page',name);
tab.addEventListener('click',function(ev){
handleTab(ev.target);
})
let page=addEl('div','tabPage hidden','#tabPages');
page.setAttribute('id',name);
return page;
}
function addUserFormatter(name,unit,formatter){
if (unit !== undefined && formatter !== undefined){
userFormatters[name]={
u:unit,
f:formatter
}
}
else{
delete userFormatters[name];
}
hideDashboardItem(name); //will recreate it on next data receive
}
const api= { const api= {
registerListener: function (callback) { registerListener: function (callback) {
listeners.push(callback); listeners.push(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, 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, 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, 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: { EVENTS: {
init: 0, //called when capabilities are loaded, data is capabilities 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, //data is the config object
boatData: 3, //data is the list of boat Data items
dataItemCreated: 4, //data is the frame item of the boat data display
} }
}; };
function callListeners(event,data){ function callListeners(event,data){