Feature: ESP-NOW auto mesh + Nodes (ESP-NOW) UI
- Broadcast beacons with magic KLNK; no MAC pairing - Track up to 8 peer MACs; stale after 12s - Telemetry/health JSON: espnow_ok, espnow_peers - Web metric + OLED health line - README: ESP-NOW section and channel requirement Made-with: Cursor
This commit is contained in:
29
README.md
29
README.md
@@ -237,7 +237,7 @@ Connect to the WiFi access point, then open the control panel in a browser.
|
||||
Temperature (Celsius)
|
||||
Free heap (KB)
|
||||
|
||||
[METRICS GRID - 12 stats updated every 1 second]
|
||||
[METRICS GRID - stats updated every 1 second]
|
||||
|
||||
Effective TX power (dBm)
|
||||
Radio 1 status
|
||||
@@ -249,6 +249,7 @@ Connect to the WiFi access point, then open the control panel in a browser.
|
||||
Hop count Radio 2 (total since boot)
|
||||
Combined hops per second
|
||||
WiFi clients on AP
|
||||
Nodes (ESP-NOW): count of other boards running this firmware in range
|
||||
Uptime
|
||||
24-hour mission progress bar in the header
|
||||
|
||||
@@ -262,6 +263,32 @@ Connect to the WiFi access point, then open the control panel in a browser.
|
||||
|
||||
---
|
||||
|
||||
## ESP-NOW NODE MESH
|
||||
|
||||
Multiple boards running the same firmware discover each other automatically
|
||||
over ESP-NOW. No MAC address entry and no pairing step.
|
||||
|
||||
How it works:
|
||||
Each unit broadcasts a small beacon every 750 ms to the ESP-NOW
|
||||
broadcast address. The payload starts with a fixed magic signature
|
||||
so only this firmware is counted.
|
||||
When a unit hears a valid beacon, it records the sender MAC and
|
||||
refreshes a last-seen time. The web UI metric "Nodes (ESP-NOW)" is
|
||||
the number of other units heard within the last 12 seconds.
|
||||
The OLED health page shows the same count after "ESPNOW".
|
||||
|
||||
Requirements for links to work:
|
||||
All units must share the same Wi-Fi radio channel as the soft-AP.
|
||||
This build starts the AP on channel 1. Do not run different channel
|
||||
settings on different boards unless you change the code consistently.
|
||||
Range is typical 2.4 GHz ESP-NOW range (often tens of meters indoors,
|
||||
more line-of-sight).
|
||||
|
||||
Note: This release only counts peers and logs new MACs. It does not yet
|
||||
sync jamming state or share telemetry over ESP-NOW.
|
||||
|
||||
---
|
||||
|
||||
## RELIABILITY FEATURES (24-HOUR OPERATION)
|
||||
|
||||
The system is designed to run unattended at full power indefinitely.
|
||||
|
||||
@@ -89,4 +89,9 @@
|
||||
#define CAP_DURATION_S 4 // max capture window (seconds)
|
||||
#define CAP_BUF_BYTES ((CAP_SAMPLE_HZ * CAP_DURATION_S) / 8 + 8) // ~50 KB
|
||||
|
||||
// ESP-NOW mesh: auto-discover other boards on same firmware (same AP WiFi channel)
|
||||
#define ESPNOW_BEACON_MS 750 // broadcast presence interval
|
||||
#define ESPNOW_PEER_STALE_MS 12000 // drop peer if silent this long
|
||||
#define ESPNOW_MAX_PEERS 8 // max other nodes tracked (4+ boards)
|
||||
|
||||
#endif
|
||||
|
||||
149
src/main.cpp
149
src/main.cpp
@@ -18,6 +18,8 @@
|
||||
#include <math.h>
|
||||
#include <Wire.h>
|
||||
#include <U8g2lib.h>
|
||||
#include <esp_now.h>
|
||||
#include <esp_wifi.h>
|
||||
#include "config.h"
|
||||
|
||||
// Shared SPI; each Module uses its own CS.
|
||||
@@ -168,6 +170,119 @@ static String jsonEscape(const String& in) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// ─── ESP-NOW peer mesh (auto-discover same firmware, no MAC entry) ───────────
|
||||
// All units must use the same WiFi AP channel (softAP uses ch 1). Beacons go to
|
||||
// broadcast; packets with magic "KLNK" mark another board running this build.
|
||||
static constexpr uint8_t kEspNowMagic[4] = { 'K', 'L', 'N', 'K' };
|
||||
|
||||
struct EspNowPeerEntry {
|
||||
uint8_t mac[6];
|
||||
uint32_t lastSeenMs;
|
||||
};
|
||||
|
||||
static EspNowPeerEntry s_espNowPeers[ESPNOW_MAX_PEERS];
|
||||
static uint8_t s_espNowPeerCount = 0;
|
||||
static uint32_t s_espNowBootToken = 0;
|
||||
static bool s_espNowReady = false;
|
||||
static uint32_t s_espNowLastTxMs = 0;
|
||||
static uint8_t s_espNowSelfMac[6];
|
||||
|
||||
static uint8_t espNowActivePeerCount() {
|
||||
const uint32_t now = millis();
|
||||
uint8_t n = 0;
|
||||
for (uint8_t i = 0; i < s_espNowPeerCount; i++) {
|
||||
if (now - s_espNowPeers[i].lastSeenMs < ESPNOW_PEER_STALE_MS) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void espNowTouchPeer(const uint8_t mac[6]) {
|
||||
if (memcmp(mac, s_espNowSelfMac, 6) == 0) return;
|
||||
|
||||
for (uint8_t i = 0; i < s_espNowPeerCount; i++) {
|
||||
if (memcmp(s_espNowPeers[i].mac, mac, 6) == 0) {
|
||||
s_espNowPeers[i].lastSeenMs = millis();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (s_espNowPeerCount < ESPNOW_MAX_PEERS) {
|
||||
memcpy(s_espNowPeers[s_espNowPeerCount].mac, mac, 6);
|
||||
s_espNowPeers[s_espNowPeerCount].lastSeenMs = millis();
|
||||
s_espNowPeerCount++;
|
||||
char m[24];
|
||||
snprintf(m, sizeof(m), "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
logLine("[ESPNOW] node " + String(m));
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t bi = 0;
|
||||
uint32_t oldest = s_espNowPeers[0].lastSeenMs;
|
||||
for (uint8_t i = 1; i < ESPNOW_MAX_PEERS; i++) {
|
||||
if (s_espNowPeers[i].lastSeenMs < oldest) {
|
||||
oldest = s_espNowPeers[i].lastSeenMs;
|
||||
bi = i;
|
||||
}
|
||||
}
|
||||
memcpy(s_espNowPeers[bi].mac, mac, 6);
|
||||
s_espNowPeers[bi].lastSeenMs = millis();
|
||||
}
|
||||
|
||||
static void espNowOnRecv(const uint8_t* mac, const uint8_t* data, int len) {
|
||||
if (len < 12 || mac == nullptr || data == nullptr) return;
|
||||
if (memcmp(data, kEspNowMagic, 4) != 0) return;
|
||||
espNowTouchPeer(mac);
|
||||
}
|
||||
|
||||
static void espNowInit() {
|
||||
s_espNowBootToken = esp_random();
|
||||
if (s_espNowBootToken == 0) s_espNowBootToken = 0xC0FFEE01u;
|
||||
|
||||
if (esp_read_mac(s_espNowSelfMac, ESP_MAC_WIFI_SOFTAP) != ESP_OK) {
|
||||
memset(s_espNowSelfMac, 0, 6);
|
||||
}
|
||||
|
||||
if (esp_now_init() != ESP_OK) {
|
||||
logLine("[ESPNOW] esp_now_init failed");
|
||||
return;
|
||||
}
|
||||
esp_now_register_recv_cb(espNowOnRecv);
|
||||
|
||||
uint8_t bcast[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
esp_now_peer_info_t peer = {};
|
||||
memcpy(peer.peer_addr, bcast, 6);
|
||||
peer.channel = 0;
|
||||
peer.encrypt = false;
|
||||
peer.ifidx = WIFI_IF_AP;
|
||||
|
||||
esp_err_t e = esp_now_add_peer(&peer);
|
||||
if (e != ESP_OK) {
|
||||
logLine("[ESPNOW] add_peer(broadcast) failed: " + String((int)e));
|
||||
esp_now_deinit();
|
||||
return;
|
||||
}
|
||||
|
||||
s_espNowReady = true;
|
||||
logLine("[ESPNOW] mesh listening; beacons every " + String(ESPNOW_BEACON_MS) + " ms (same AP channel)");
|
||||
}
|
||||
|
||||
static void espNowTick() {
|
||||
if (!s_espNowReady) return;
|
||||
const uint32_t now = millis();
|
||||
if (now - s_espNowLastTxMs < ESPNOW_BEACON_MS) return;
|
||||
s_espNowLastTxMs = now;
|
||||
|
||||
uint8_t pkt[12];
|
||||
memcpy(pkt, kEspNowMagic, 4);
|
||||
memcpy(pkt + 4, &s_espNowBootToken, 4);
|
||||
uint32_t up = now - uptimeStart;
|
||||
memcpy(pkt + 8, &up, 4);
|
||||
|
||||
uint8_t bcast[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
(void)esp_now_send(bcast, pkt, sizeof(pkt));
|
||||
}
|
||||
|
||||
// Forward declarations
|
||||
static void noiseGenStart();
|
||||
static void startJamming();
|
||||
@@ -829,7 +944,8 @@ static void oledDrawHealth() {
|
||||
snprintf(buf, sizeof(buf), "PWR %ddBm / %umW", effDbm, min(effMw, (uint32_t)9999));
|
||||
u8g2.drawStr(0, 55, buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "WIFI %d client(s)", WiFi.softAPgetStationNum());
|
||||
snprintf(buf, sizeof(buf), "WIFI %d ESPNOW %u",
|
||||
WiFi.softAPgetStationNum(), (unsigned)espNowActivePeerCount());
|
||||
u8g2.drawStr(0, 63, buf);
|
||||
}
|
||||
|
||||
@@ -1090,6 +1206,7 @@ h1{animation:flicker .4s ease-out}
|
||||
<div class="s"><div class="sl">Hops R2</div><div class="sv" id="mH2">—</div></div>
|
||||
<div class="s"><div class="sl">Hops/sec</div><div class="sv" id="mHR">—</div></div>
|
||||
<div class="s"><div class="sl">AP Clients</div><div class="sv" id="mCl">—</div></div>
|
||||
<div class="s"><div class="sl">Nodes (ESP-NOW)</div><div class="sv" id="mEn">—</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1362,6 +1479,12 @@ function applyTelemetry(t){
|
||||
document.getElementById('mH2').textContent=h2.toLocaleString();
|
||||
document.getElementById('mHR').innerHTML=rate+'<span class="su">/s</span>';
|
||||
document.getElementById('mCl').textContent=t.ap_clients??'—';
|
||||
const en=document.getElementById('mEn');
|
||||
if(en){
|
||||
if(typeof t.espnow_ok==='boolean'){
|
||||
en.textContent=t.espnow_ok?String(t.espnow_peers!==undefined?t.espnow_peers:0):'off';
|
||||
}else en.textContent='—';
|
||||
}
|
||||
// Trails + sweep canvases
|
||||
const sp1=Math.max(t.sweep_span1||20,0.001),sp2=Math.max(t.sweep_span2||46,0.001);
|
||||
updTr(tr1,(t.sweep_freq1-(t.sweep_center1-sp1/2))/sp1);
|
||||
@@ -1562,7 +1685,9 @@ static void handleTelemetry() {
|
||||
"\"hop_count1\":%lu,"
|
||||
"\"hop_count2\":%lu,"
|
||||
"\"min_heap\":%lu,"
|
||||
"\"ap_clients\":%d"
|
||||
"\"ap_clients\":%d,"
|
||||
"\"espnow_ok\":%s,"
|
||||
"\"espnow_peers\":%u"
|
||||
"}",
|
||||
(unsigned long)(millis() - uptimeStart),
|
||||
(unsigned long)ESP.getFreeHeap(),
|
||||
@@ -1593,7 +1718,9 @@ static void handleTelemetry() {
|
||||
(unsigned long)hopCount1,
|
||||
(unsigned long)hopCount2,
|
||||
(unsigned long)(minFreeHeap == 0xFFFFFFFF ? ESP.getFreeHeap() : minFreeHeap),
|
||||
(int)WiFi.softAPgetStationNum()
|
||||
(int)WiFi.softAPgetStationNum(),
|
||||
s_espNowReady ? "true" : "false",
|
||||
(unsigned)espNowActivePeerCount()
|
||||
);
|
||||
|
||||
server.send(200, "application/json; charset=utf-8", jsonBuf);
|
||||
@@ -1728,12 +1855,15 @@ static void handleAmpSettings() {
|
||||
}
|
||||
|
||||
static void handleHealth() {
|
||||
static char buf[128];
|
||||
static char buf[160];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"ok\":true,\"uptime_ms\":%lu,\"heap\":%lu,\"ap_clients\":%d}",
|
||||
"{\"ok\":true,\"uptime_ms\":%lu,\"heap\":%lu,\"ap_clients\":%d,"
|
||||
"\"espnow_ok\":%s,\"espnow_peers\":%u}",
|
||||
(unsigned long)(millis() - uptimeStart),
|
||||
(unsigned long)ESP.getFreeHeap(),
|
||||
(int)WiFi.softAPgetStationNum());
|
||||
(int)WiFi.softAPgetStationNum(),
|
||||
s_espNowReady ? "true" : "false",
|
||||
(unsigned)espNowActivePeerCount());
|
||||
server.send(200, "application/json; charset=utf-8", buf);
|
||||
}
|
||||
|
||||
@@ -1955,6 +2085,12 @@ void setup() {
|
||||
Serial.println("[WIFI] AP IP: " + ip.toString());
|
||||
Serial.flush();
|
||||
|
||||
if (apOk) {
|
||||
espNowInit();
|
||||
} else {
|
||||
logLine("[ESPNOW] skipped (AP not up — need WiFi channel for ESP-NOW)");
|
||||
}
|
||||
|
||||
if (MDNS.begin("killer")) {
|
||||
MDNS.addService("http", "tcp", WEB_PORT);
|
||||
Serial.println("[MDNS] Started: http://killer.local");
|
||||
@@ -2051,6 +2187,7 @@ static void tickSweepFast(uint8_t csPin, uint8_t& step, uint8_t steps,
|
||||
}
|
||||
|
||||
void loop() {
|
||||
espNowTick();
|
||||
ArduinoOTA.handle();
|
||||
server.handleClient();
|
||||
oledTick();
|
||||
|
||||
Reference in New Issue
Block a user