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)},
{"key6", ConfigType::BYTE, uint8_t(6)},
{"key6long", ConfigType::BYTE, uint8_t(16)},
{"n2kDestA", ConfigType::STRING, String("")},
{"n2kDestB", ConfigType::STRING, String("")},
{"n2kDestC", ConfigType::STRING, String("")},
{"n2kDestA", ConfigType::STRING, String("none")},
{"n2kDestB", ConfigType::STRING, String("none")},
{"n2kDestC", ConfigType::STRING, String("none")},
{"envInterval", ConfigType::SHORT, int16_t(5)},
// no user access

View File

@@ -99,7 +99,7 @@ void webserver_init() {
});
server.on("/api/config", HTTP_GET, [](AsyncWebServerRequest *request) {
StaticJsonDocument<512> doc;
StaticJsonDocument<1024> doc;
doc["systemName"] = config.getString("systemName");
doc["systemMode"] = String(config.getChar("systemMode"));
doc["logLevel"] = loglevel;
@@ -130,6 +130,9 @@ void webserver_init() {
doc["key4long"] = longcode[BUTTON_4];
doc["key5long"] = longcode[BUTTON_5];
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");
String out;
serializeJson(doc, out);
@@ -230,7 +233,7 @@ void webserver_init() {
AsyncResponseStream *response = request->beginResponseStream("application/json");
response->print("[");
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);
if (d == nullptr) {
continue;
@@ -244,8 +247,36 @@ void webserver_init() {
first = false;
}
// TODO last seen?
response->printf("{\"source\":%d,\"name\":\"%s\",\"manuf\":\"%d\",\"model\":\"%s\"}",
i, hex_name, d->GetManufacturerCode(), d->GetModelID());
uint16_t mfcode = d->GetManufacturerCode();
// 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("]");
request->send(response);

View File

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

View File

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

View File

@@ -10,6 +10,7 @@
let buttonHandlers = {};
let checkers = {};
let userFormatters = {};
let dynLists = {}; // dynamic selection lists
let apiPrefix = "";
function addEl(type, clazz, parent, text) {
let el = document.createElement(type);
@@ -131,6 +132,12 @@
resetForm();
}
})
// check if any dynamic list needs update
for (let l in dynLists) {
if (loadDynList(l)) {
updateDynLists(l, configDefinitions)
}
}
}
function resetForm(ev) {
getJson(apiPrefix + "/api/config")
@@ -614,6 +621,12 @@
})
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') {
return createFilterInput(configItem, frame, clazz);
}
@@ -834,6 +847,50 @@
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) {
let categories = {};
let frame = parent.querySelector('.configFormRows');
@@ -843,6 +900,22 @@
let currentCategoryPopulated = true;
defs.forEach(function (item) {
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;
if (categories[item.category] === undefined) {
catEntry = {
@@ -974,10 +1047,32 @@
.catch(function (err) { alert("unable to load config: " + err) })
}
function loadDeviceList() {
getJson("api/devices")
getJson("api/devicelist")
.then(function(devices) {
let deviceForm = document.getElementById('devicePage');
console.log("Device form called");
let deviceList = document.getElementById('deviceList');
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) })
}
@@ -1097,7 +1192,7 @@
});
}
buttonHandlers.reloadDevices=function() {
console.log("Button reload devices");
loadDeviceList()
}
function handleTab(el) {
let activeName = el.getAttribute('data-page');
@@ -1664,6 +1759,7 @@
})
})
})
loadDeviceList();
});
}());