add support for up to 10 ESPNow remotes (#4654)

* add support for up to 10 ESPNow remotes

* removed debug line

* changed todo comment

* fixed some issues, shortened html/java code

- reverting name to `linked_remote`
- ESPNow remote list is now hidden if unchecked
- shortened java script function names and variables to save flash
- removed now obsolete settings in xml.cpp
- correct checking of valid hex string for remote list in java script

* fixed indentation, using emplace_back instead of push_back, using JsonVariant, replaced buttons with +/-

* shortened java code

* updated java code, fixed bug

- element is now properly removed
- `+` button is hidden if list is full
- user needs to remove a remote, then reload the page to add it (workaround for edge case that needs more code to handle otherwise)

* add limit

* clearer usage description
This commit is contained in:
Damian Schneider 2025-05-19 20:34:27 +02:00 committed by GitHub
parent d9ad4ec743
commit 66ad27ad3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 108 additions and 28 deletions

View File

@ -38,8 +38,24 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
JsonObject nw = doc["nw"];
#ifndef WLED_DISABLE_ESPNOW
CJSON(enableESPNow, nw[F("espnow")]);
getStringFromJson(linked_remote, nw[F("linked_remote")], 13);
linked_remote[12] = '\0';
linked_remotes.clear();
JsonVariant lrem = nw[F("linked_remote")];
if (!lrem.isNull()) {
if (lrem.is<JsonArray>()) {
for (size_t i = 0; i < lrem.size(); i++) {
std::array<char, 13> entry{};
getStringFromJson(entry.data(), lrem[i], 13);
entry[12] = '\0';
linked_remotes.emplace_back(entry);
}
}
else { // legacy support for single MAC address in config
std::array<char, 13> entry{};
getStringFromJson(entry.data(), lrem, 13);
entry[12] = '\0';
linked_remotes.emplace_back(entry);
}
}
#endif
size_t n = 0;
@ -725,7 +741,10 @@ void serializeConfig(JsonObject root) {
JsonObject nw = root.createNestedObject("nw");
#ifndef WLED_DISABLE_ESPNOW
nw[F("espnow")] = enableESPNow;
nw[F("linked_remote")] = linked_remote;
JsonArray lrem = nw.createNestedArray(F("linked_remote"));
for (size_t i = 0; i < linked_remotes.size(); i++) {
lrem.add(linked_remotes[i].data());
}
#endif
JsonArray nw_ins = nw.createNestedArray("ins");

View File

@ -136,12 +136,52 @@ Static subnet mask:<br>
getLoc();
loadJS(getURL('/settings/s.js?p=1'), false); // If we set async false, file is loaded and executed, then next statement is processed
if (loc) d.Sf.action = getURL('/settings/wifi');
setTimeout(tE, 500); // wait for DOM to load before calling tE()
}
var rC = 0; // remote count
// toggle visibility of ESP-NOW remote list based on checkbox state
function tE() {
// keep the hidden input with MAC addresses, only toggle visibility of the list UI
gId('rlc').style.display = d.Sf.RE.checked ? 'block' : 'none';
}
// reset remotes: initialize empty list (called from xml.cpp)
function rstR() {
gId('rml').innerHTML = ''; // clear remote list
}
// add remote MAC to the list
function aR(id, mac) {
if (!/^[0-9A-F]{12}$/i.test(mac)) return; // check for valid hex string
let inputs = d.querySelectorAll("#rml input");
for (let i of (inputs || [])) {
if (i.value === mac) return;
}
let l = gId('rml'), r = cE('div'), i = cE('input');
i.type = 'text';
i.name = id;
i.value = mac;
i.maxLength = 12;
i.minLength = 12;
//i.onchange = uR;
r.appendChild(i);
let b = cE('button');
b.type = 'button';
b.className = 'sml';
b.innerText = '-';
b.onclick = (e) => {
r.remove();
};
r.appendChild(b);
l.appendChild(r);
rC++;
gId('+').style.display = gId("rml").childElementCount < 10 ? 'inline' : 'none'; // can't append to list anymore, hide button
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<form id="form_s" name="Sf" method="post">
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#wifi-settings')">?</button></div>
<button type="button" onclick="B()">Back</button><button type="submit">Save & Connect</button><hr>
@ -202,11 +242,16 @@ Static subnet mask:<br>
<i class="warn">This firmware build does not include ESP-NOW support.<br></i>
</div>
<div id="ESPNOW">
Enable ESP-NOW: <input type="checkbox" name="RE"><br>
Enable ESP-NOW: <input type="checkbox" name="RE" onchange="tE()"><br>
<i>Listen for events over ESP-NOW<br>
Keep disabled if not using a remote or wireless sync, increases power consumption.<br></i>
Paired Remote MAC: <input type="text" name="RMAC" minlength="12" maxlength="12"><br>
Last device seen: <span class="rlid" onclick="d.Sf.RMAC.value=this.textContent;" style="cursor:pointer;">None</span> <br>
Keep disabled if not using a remote or ESP-NOW sync, increases power consumption.<br></i>
<div id="rlc">
Last device seen: <span class="rlid" id="ld">None</span>
<button type="button" class="sml" id="+" onclick="aR('RM'+rC,gId('ld').textContent)">+</button><br>
Linked MACs (10 max):<br>
<div id="rml">
</div>
</div>
</div>
<div id="ethd">

View File

@ -181,16 +181,10 @@ static bool remoteJson(int button)
return parsed;
}
// Callback function that will be executed when data is received
// Callback function that will be executed when data is received from a linked remote
void handleWiZdata(uint8_t *incomingData, size_t len) {
message_structure_t *incoming = reinterpret_cast<message_structure_t *>(incomingData);
if (strcmp(last_signal_src, linked_remote) != 0) {
DEBUG_PRINT(F("ESP Now Message Received from Unlinked Sender: "));
DEBUG_PRINTLN(last_signal_src);
return;
}
if (len != sizeof(message_structure_t)) {
DEBUG_PRINTF_P(PSTR("Unknown incoming ESP Now message received of length %u\n"), len);
return;

View File

@ -91,8 +91,21 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
bool oldESPNow = enableESPNow;
enableESPNow = request->hasArg(F("RE"));
if (oldESPNow != enableESPNow) forceReconnect = true;
strlcpy(linked_remote, request->arg(F("RMAC")).c_str(), 13);
strlwr(linked_remote); //Normalize MAC format to lowercase
linked_remotes.clear(); // clear old remotes
for (size_t n = 0; n < 10; n++) {
char rm[4];
snprintf(rm, sizeof(rm), "RM%d", n); // "RM0" to "RM9"
if (request->hasArg(rm)) {
const String& arg = request->arg(rm);
if (arg.isEmpty()) continue;
std::array<char, 13> mac{};
strlcpy(mac.data(), request->arg(rm).c_str(), 13);
strlwr(mac.data());
if (mac[0] != '\0') {
linked_remotes.emplace_back(mac);
}
}
}
#endif
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)

View File

@ -959,14 +959,22 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs
// usermods hook can override processing
if (UsermodManager::onEspNowMessage(address, data, len)) return;
// handle WiZ Mote data
if (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80) {
handleWiZdata(data, len);
bool knownRemote = false;
for (const auto& mac : linked_remotes) {
if (strlen(mac.data()) == 12 && strcmp(last_signal_src, mac.data()) == 0) {
knownRemote = true;
break;
}
}
if (!knownRemote) {
DEBUG_PRINT(F("ESP Now Message Received from Unlinked Sender: "));
DEBUG_PRINTLN(last_signal_src);
return;
}
if (strlen(linked_remote) == 12 && strcmp(last_signal_src, linked_remote) != 0) {
DEBUG_PRINTLN(F("ESP-NOW unpaired remote sender."));
// handle WiZ Mote data
if (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80) {
handleWiZdata(data, len);
return;
}

View File

@ -538,7 +538,8 @@ WLED_GLOBAL bool serialCanTX _INIT(false);
WLED_GLOBAL bool enableESPNow _INIT(false); // global on/off for ESP-NOW
WLED_GLOBAL byte statusESPNow _INIT(ESP_NOW_STATE_UNINIT); // state of ESP-NOW stack (0 uninitialised, 1 initialised, 2 error)
WLED_GLOBAL bool useESPNowSync _INIT(false); // use ESP-NOW wireless technology for sync
WLED_GLOBAL char linked_remote[13] _INIT(""); // MAC of ESP-NOW remote (Wiz Mote)
//WLED_GLOBAL char linked_remote[13] _INIT(""); // MAC of ESP-NOW remote (Wiz Mote)
WLED_GLOBAL std::vector<std::array<char, 13>> linked_remotes; // MAC of ESP-NOW remotes (Wiz Mote)
WLED_GLOBAL char last_signal_src[13] _INIT(""); // last seen ESP-NOW sender
#endif

View File

@ -216,7 +216,11 @@ void getSettingsJS(byte subPage, Print& settingsScript)
#ifndef WLED_DISABLE_ESPNOW
printSetFormCheckbox(settingsScript,PSTR("RE"),enableESPNow);
printSetFormValue(settingsScript,PSTR("RMAC"),linked_remote);
settingsScript.printf_P(PSTR("rstR();")); // reset remote list
for (size_t i = 0; i < linked_remotes.size(); i++) {
settingsScript.printf_P(PSTR("aR(\"RM%u\",\"%s\");"), i, linked_remotes[i].data()); // add remote to list
}
settingsScript.print(F("tE();")); // fill fields
#else
//hide remote settings if not compiled
settingsScript.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting
@ -258,10 +262,6 @@ void getSettingsJS(byte subPage, Print& settingsScript)
#ifndef WLED_DISABLE_ESPNOW
if (strlen(last_signal_src) > 0) { //Have seen an ESP-NOW Remote
printSetClassElementHTML(settingsScript,PSTR("rlid"),0,last_signal_src);
} else if (!enableESPNow) {
printSetClassElementHTML(settingsScript,PSTR("rlid"),0,(char*)F("(Enable ESP-NOW to listen)"));
} else {
printSetClassElementHTML(settingsScript,PSTR("rlid"),0,(char*)F("None"));
}
#endif
}