Device list and selection integrated into configuration interface

This commit is contained in:
2026-02-01 14:37:06 +01:00
parent 3729fcb0f1
commit 59cbc64b08
5 changed files with 165 additions and 24 deletions

View File

@@ -60,9 +60,9 @@ static const ConfigDef configdefs[] = {
{"key5long", ConfigType::BYTE, uint8_t(15)}, {"key5long", ConfigType::BYTE, uint8_t(15)},
{"key6", ConfigType::BYTE, uint8_t(6)}, {"key6", ConfigType::BYTE, uint8_t(6)},
{"key6long", ConfigType::BYTE, uint8_t(16)}, {"key6long", ConfigType::BYTE, uint8_t(16)},
{"n2kDestA", ConfigType::STRING, String("")}, {"n2kDestA", ConfigType::STRING, String("none")},
{"n2kDestB", ConfigType::STRING, String("")}, {"n2kDestB", ConfigType::STRING, String("none")},
{"n2kDestC", ConfigType::STRING, String("")}, {"n2kDestC", ConfigType::STRING, String("none")},
{"envInterval", ConfigType::SHORT, int16_t(5)}, {"envInterval", ConfigType::SHORT, int16_t(5)},
// no user access // no user access

View File

@@ -99,7 +99,7 @@ void webserver_init() {
}); });
server.on("/api/config", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/api/config", HTTP_GET, [](AsyncWebServerRequest *request) {
StaticJsonDocument<512> doc; StaticJsonDocument<1024> doc;
doc["systemName"] = config.getString("systemName"); doc["systemName"] = config.getString("systemName");
doc["systemMode"] = String(config.getChar("systemMode")); doc["systemMode"] = String(config.getChar("systemMode"));
doc["logLevel"] = loglevel; doc["logLevel"] = loglevel;
@@ -130,6 +130,9 @@ void webserver_init() {
doc["key4long"] = longcode[BUTTON_4]; doc["key4long"] = longcode[BUTTON_4];
doc["key5long"] = longcode[BUTTON_5]; doc["key5long"] = longcode[BUTTON_5];
doc["key6long"] = longcode[BUTTON_6]; doc["key6long"] = longcode[BUTTON_6];
doc["n2kDestA"] = config.getString("n2kDestA");
doc["n2kDestB"] = config.getString("n2kDestB");
doc["n2kDestC"] = config.getString("n2kDestC");
doc["envInterval"] = config.getShort("envInterval"); doc["envInterval"] = config.getShort("envInterval");
String out; String out;
serializeJson(doc, out); serializeJson(doc, out);
@@ -230,7 +233,7 @@ void webserver_init() {
AsyncResponseStream *response = request->beginResponseStream("application/json"); AsyncResponseStream *response = request->beginResponseStream("application/json");
response->print("["); response->print("[");
bool first = true; bool first = true;
for (int i = 0; i <= 252; i++) { for (uint8_t i = 0; i <= 252; i++) {
const tNMEA2000::tDevice *d = pN2kDeviceList->FindDeviceBySource(i); const tNMEA2000::tDevice *d = pN2kDeviceList->FindDeviceBySource(i);
if (d == nullptr) { if (d == nullptr) {
continue; continue;
@@ -244,8 +247,36 @@ void webserver_init() {
first = false; first = false;
} }
// TODO last seen? // TODO last seen?
response->printf("{\"source\":%d,\"name\":\"%s\",\"manuf\":\"%d\",\"model\":\"%s\"}", uint16_t mfcode = d->GetManufacturerCode();
i, hex_name, d->GetManufacturerCode(), d->GetModelID()); // TODO RAW-String!
response->printf("{\"source\":%d,\"name\":\"%s\",\"manufcode\":\"%d\",\"model\":\"%s\",\"manufname\":\"%s\"}",
i, hex_name, mfcode, d->GetModelID(), NMEA2000.GetManufacturerName(mfcode));
}
response->print("]");
request->send(response);
});
server.on("/api/dyndevlist", HTTP_GET, [](AsyncWebServerRequest *request) {
// NMEA2000 dynmic config list: devices
AsyncResponseStream *response = request->beginResponseStream("application/json");
response->print("[");
bool first = true;
for (uint8_t i = 0; i <= 252; i++) {
const tNMEA2000::tDevice *d = pN2kDeviceList->FindDeviceBySource(i);
if (d == nullptr) {
continue;
}
uint64_t NAME = d->GetName();
char hex_name[17];
snprintf(hex_name, sizeof(hex_name), "%08X%08X", (uint32_t)(NAME >> 32), (uint32_t)(NAME & 0xFFFFFFFF));
if (!first) {
response->print(",");
} else {
first = false;
}
uint16_t mfcode = d->GetManufacturerCode();
response->printf(R"DELIM({"v":"%s","l":"%s - %s (%d)"})DELIM",
hex_name, d->GetModelID(), NMEA2000.GetManufacturerName(mfcode), mfcode);
} }
response->print("]"); response->print("]");
request->send(response); request->send(response);

View File

@@ -54,6 +54,20 @@
"description": "A password for configuration modifications is required.", "description": "A password for configuration modifications is required.",
"category": "System" "category": "System"
}, },
{
"name": "instDesc1",
"label": "Description 1",
"type": "string",
"description": "NMEA2000 installation description 1",
"category": "System"
},
{
"name": "instDesc2",
"label": "Description 2",
"type": "string",
"description": "NMEA2000 installation description 2",
"category": "System"
},
{ {
"name": "apEnable", "name": "apEnable",
"label": "Enable AP", "label": "Enable AP",
@@ -120,8 +134,8 @@
"type": "number", "type": "number",
"default": 64, "default": 64,
"min": 0, "min": 0,
"max": 255, "max": 4095,
"description": "The brightness of the destination leds (0..255).", "description": "The brightness of the destination leds (0..4095).",
"category": "Hardware" "category": "Hardware"
}, },
{ {
@@ -130,8 +144,8 @@
"type": "number", "type": "number",
"default": 64, "default": 64,
"min": 0, "min": 0,
"max": 255, "max": 4095,
"description": "The brightness of the rgb status led (0..255).", "description": "The brightness of the rgb status led (0..4095).",
"category": "Hardware" "category": "Hardware"
}, },
{ {
@@ -281,8 +295,8 @@
"name": "n2kDestA", "name": "n2kDestA",
"label": "Destination A", "label": "Destination A",
"type": "dynlist", "type": "dynlist",
"source": "devicelist", "source": "dyndevlist",
"interval": "5000", "interval": "30000",
"description": "Destination device A", "description": "Destination device A",
"category": "NMEA2000" "category": "NMEA2000"
}, },
@@ -290,8 +304,8 @@
"name": "n2kDestB", "name": "n2kDestB",
"label": "Destination B", "label": "Destination B",
"type": "dynlist", "type": "dynlist",
"source": "devicelist", "source": "dyndevlist",
"interval": "5000", "interval": "30000",
"description": "Destination device B", "description": "Destination device B",
"category": "NMEA2000" "category": "NMEA2000"
}, },
@@ -299,8 +313,8 @@
"name": "n2kDestC", "name": "n2kDestC",
"label": "Destination C", "label": "Destination C",
"type": "dynlist", "type": "dynlist",
"source": "devicelist", "source": "dyndevlist",
"interval": "5000", "interval": "30000",
"description": "Destination device C", "description": "Destination device C",
"category": "NMEA2000" "category": "NMEA2000"
}, },
@@ -312,7 +326,7 @@
"check": "checkMinMax", "check": "checkMinMax",
"min": 1, "min": 1,
"max": 300, "max": 300,
"description": "interval seconds to send environment data [1..300]", "description": "interval in seconds to send environment data [1..300]",
"category": "NMEA2000" "category": "NMEA2000"
} }
] ]

View File

@@ -104,11 +104,11 @@ if (!window.isSecureContext) {
</div> </div>
</div> </div>
<div class="deviceForm tabPage hidden" id="devicePage"> <div class="tabPage hidden" id="devicePage">
<div class="buttons"> <div class="buttons">
<button id="reloadDevices">Reload</button> <button id="reloadDevices">Reload</button>
</div> </div>
<div class="deviceFormRows"> <div class="deviceListRows" id="deviceList">
</div> </div>
</div> </div>

View File

@@ -10,6 +10,7 @@
let buttonHandlers = {}; let buttonHandlers = {};
let checkers = {}; let checkers = {};
let userFormatters = {}; let userFormatters = {};
let dynLists = {}; // dynamic selection lists
let apiPrefix = ""; let apiPrefix = "";
function addEl(type, clazz, parent, text) { function addEl(type, clazz, parent, text) {
let el = document.createElement(type); let el = document.createElement(type);
@@ -131,6 +132,12 @@
resetForm(); resetForm();
} }
}) })
// check if any dynamic list needs update
for (let l in dynLists) {
if (loadDynList(l)) {
updateDynLists(l, configDefinitions)
}
}
} }
function resetForm(ev) { function resetForm(ev) {
getJson(apiPrefix + "/api/config") getJson(apiPrefix + "/api/config")
@@ -614,6 +621,12 @@
}) })
return el; return el;
} }
if (configItem.type === 'dynlist') {
el = addEl('select', clazz, frame);
if (configItem.readOnly) el.setAttribute('disabled', true);
el.setAttribute('name', configItem.name);
return el;
}
if (configItem.type === 'filter') { if (configItem.type === 'filter') {
return createFilterInput(configItem, frame, clazz); return createFilterInput(configItem, frame, clazz);
} }
@@ -834,6 +847,50 @@
return rt; return rt;
} }
function loadDynList(apiname) {
// returns true if list was loaded
// TODO
// - implement generic all/none or static entries from config.json
// - check if current list is updated, so later dom refresh is not needed
let now = (new Date()).getTime();
if (now - dynLists[apiname].timestamp < dynLists[apiname].interval) {
// console.log("loading dynamic list: update interval not reached")
return false;
}
// console.log("loading dynamic list: " + apiname)
getJson("api/"+apiname)
.then(function(list) {
dynLists[apiname].timestamp = (new Date()).getTime();
dynLists[apiname].updated = true
dynLists[apiname].data = []
dynLists[apiname].data.push({"l": "all devices (broadcast)", "v": "all"})
dynLists[apiname].data.push({"l": "disabled (none)", "v": "none"})
for (let i in list) {
// let l = list[i].model + " - " + list[i].manufname + " (" + list[i].manufcode + ")"
// dynLists[apiname].data.push({"l": l, "v": list[i].name})
dynLists[apiname].data.push({"l": list[i].l, "v": list[i].v})
}
})
.catch(function (err) {
alert("unable to load dynamic list: " + err)
return false
})
return true
}
function updateDynLists(listname, defs) {
for (i in defs) {
if ((defs[i].type === "dynlist") && (defs[i].source === listname)) {
let el = document.querySelector("[name='" + defs[i].name + "']");
// TODO append here static list from configdefinition
// if defs[i] has key "list" then prepend it
// let options =
// options += dynLists[listname].data
updateSelectList(el, dynLists[listname].data, true)
}
}
}
function createConfigDefinitions(parent, capabilities, defs) { function createConfigDefinitions(parent, capabilities, defs) {
let categories = {}; let categories = {};
let frame = parent.querySelector('.configFormRows'); let frame = parent.querySelector('.configFormRows');
@@ -843,6 +900,22 @@
let currentCategoryPopulated = true; let currentCategoryPopulated = true;
defs.forEach(function (item) { defs.forEach(function (item) {
if (!item.type || item.category === undefined) return; if (!item.type || item.category === undefined) return;
// look for dynamic lists
if (item.type === 'dynlist') {
if (item.source in dynLists) {
// check if interval is smaller as already defined
if (item.interval < dynLists[item.source].interval) {
dynLists[item.source].interval = item.interval;
}
} else {
// define new dynamic list
dynLists[item.source] = {
interval: item.interval,
updated: true
};
loadDynList(item.source);
}
}
let catEntry; let catEntry;
if (categories[item.category] === undefined) { if (categories[item.category] === undefined) {
catEntry = { catEntry = {
@@ -974,10 +1047,32 @@
.catch(function (err) { alert("unable to load config: " + err) }) .catch(function (err) { alert("unable to load config: " + err) })
} }
function loadDeviceList() { function loadDeviceList() {
getJson("api/devices") getJson("api/devicelist")
.then(function(devices) { .then(function(devices) {
let deviceForm = document.getElementById('devicePage'); let deviceList = document.getElementById('deviceList');
console.log("Device form called"); let row;
let el;
let even = true;
let n = 0
deviceList.replaceChildren();
for (let d in devices) {
row = addEl('div', even ? 'row even' : 'row', deviceList);
el = addEl('span', 'label', row);
el.innerHTML = devices[d].name;
el = addEl('span', 'value', row);
el.innerHTML = devices[d].model;
el.innerHTML += " - " + devices[d].manufname;
el.innerHTML += " (" + devices[d].manufcode+ "): "
el.innerHTML += devices[d].source
even = ! even;
n += 1;
}
addEl('hr', '', deviceList);
row = addEl('div', even ? 'row even' : 'row', deviceList);
el = addEl('span', 'label', row);
el.innerHTML = "total devices";
el = addEl('span', 'value', row);
el.innerHTML = n;
}) })
.catch(function (err) { alert("unable to load devicelist: " + err) }) .catch(function (err) { alert("unable to load devicelist: " + err) })
} }
@@ -1097,7 +1192,7 @@
}); });
} }
buttonHandlers.reloadDevices=function() { buttonHandlers.reloadDevices=function() {
console.log("Button reload devices"); loadDeviceList()
} }
function handleTab(el) { function handleTab(el) {
let activeName = el.getAttribute('data-page'); let activeName = el.getAttribute('data-page');
@@ -1664,6 +1759,7 @@
}) })
}) })
}) })
loadDeviceList();
}); });
}()); }());