Files
esp32-cluster/firmware/node/node.ino
T
2026-04-03 14:38:38 +03:00

310 lines
10 KiB
Arduino
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <WiFi.h>
#include <WiFiUdp.h>
#include "esp_efuse.h"
#include "esp_mac.h"
#include "config.h"
#if PROBE_SNIFF
#include "esp_wifi.h"
#include "freertos/queue.h"
#endif
WiFiUDP udp;
String nodeId;
unsigned long lastScan = 0;
unsigned long lastHeartbeat = 0;
// ─── Probe sniffing globals ───────────────────────────────────────────────────
#if PROBE_SNIFF
struct ProbeEvent {
uint8_t src_mac[6];
char ssid[33];
int8_t rssi;
};
static QueueHandle_t probeQueue;
// Dedup cache — suppress repeated MAC+SSID pairs within PROBE_DEDUP_SECS
struct DedupEntry { uint8_t mac[6]; char ssid[33]; uint32_t last_ms; };
static DedupEntry dedupCache[32];
static int dedupNext = 0;
static bool isDuplicate(const uint8_t* mac, const char* ssid) {
uint32_t now = millis();
for (int i = 0; i < 32; i++) {
if (dedupCache[i].last_ms == 0) continue;
if (memcmp(dedupCache[i].mac, mac, 6) == 0 &&
strcmp(dedupCache[i].ssid, ssid) == 0) {
if ((now - dedupCache[i].last_ms) < (uint32_t)(PROBE_DEDUP_SECS * 1000))
return true;
dedupCache[i].last_ms = now;
return false;
}
}
// Not found — write to next slot (ring)
memcpy(dedupCache[dedupNext].mac, mac, 6);
strncpy(dedupCache[dedupNext].ssid, ssid, 32);
dedupCache[dedupNext].ssid[32] = '\0';
dedupCache[dedupNext].last_ms = now;
dedupNext = (dedupNext + 1) % 32;
return false;
}
// Promiscuous callback — runs in WiFi task context, not safe to call UDP here.
// Parse probe request frames and push to queue for the main loop to send.
static void promiscuous_rx_cb(void* buf, wifi_promiscuous_pkt_type_t type) {
if (type != WIFI_PKT_MGMT) return;
const wifi_promiscuous_pkt_t* pkt = (const wifi_promiscuous_pkt_t*)buf;
const uint8_t* d = pkt->payload;
uint16_t len = pkt->rx_ctrl.sig_len;
if (len < 28) return;
if (d[0] != 0x40) return; // byte 0 = 0x40 → management, probe request subtype
const uint8_t* src = d + 10; // source address at bytes 1015
// Parse SSID tag (tag 0) from frame body starting at byte 24
char ssid[33] = "";
if (d[24] == 0x00) {
uint8_t slen = d[25];
if (slen > 0 && slen <= 32 && (26 + slen) <= len) {
bool printable = true;
for (uint8_t i = 0; i < slen; i++) {
if (d[26 + i] < 32 || d[26 + i] > 126) { printable = false; break; }
}
if (printable) { memcpy(ssid, d + 26, slen); ssid[slen] = '\0'; }
}
}
if (isDuplicate(src, ssid)) return;
ProbeEvent ev;
memcpy(ev.src_mac, src, 6);
memcpy(ev.ssid, ssid, 33);
ev.rssi = (int8_t)pkt->rx_ctrl.rssi;
xQueueSend(probeQueue, &ev, 0); // 0 timeout: drop if queue full
}
#endif // PROBE_SNIFF
// ─── Setup ───────────────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
delay(500);
// Derive node ID from MAC (last 4 bytes, no colons)
uint8_t mac[6];
esp_efuse_mac_get_default(mac);
char macBuf[9];
snprintf(macBuf, sizeof(macBuf), "%02X%02X%02X%02X", mac[2], mac[3], mac[4], mac[5]);
nodeId = String(macBuf);
Serial.printf("\n[NODE] ID: %s\n", nodeId.c_str());
connectWiFi();
#if PROBE_SNIFF
memset(dedupCache, 0, sizeof(dedupCache));
probeQueue = xQueueCreate(PROBE_QUEUE_SIZE, sizeof(ProbeEvent));
esp_wifi_set_promiscuous_rx_cb(promiscuous_rx_cb);
esp_wifi_set_promiscuous(true);
Serial.println("[PROBE] Promiscuous mode enabled");
#endif
}
// ─── Loop ────────────────────────────────────────────────────────────────────
void loop() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[WIFI] Lost connection, reconnecting...");
connectWiFi();
return;
}
unsigned long now = millis();
if (now - lastHeartbeat >= HEARTBEAT_INTERVAL_MS) {
lastHeartbeat = now;
sendHeartbeat();
}
if (now - lastScan >= SCAN_INTERVAL_MS) {
lastScan = now;
scanAndSend();
}
delay(100);
}
// ─── WiFi ────────────────────────────────────────────────────────────────────
void connectWiFi() {
Serial.printf("[WIFI] Connecting to %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\n[WIFI] Connected — IP: %s\n", WiFi.localIP().toString().c_str());
} else {
Serial.println("\n[WIFI] Failed to connect, will retry in loop");
}
}
// ─── Scan ────────────────────────────────────────────────────────────────────
void scanAndSend() {
Serial.println("[SCAN] Starting...");
int n = WiFi.scanNetworks(false, true); // async=false, show_hidden=true
if (n == WIFI_SCAN_FAILED) {
Serial.println("[SCAN] Failed");
return;
}
Serial.printf("[SCAN] Found %d networks\n", n);
for (int i = 0; i < n; i++) {
sendBeaconEvent(i);
}
WiFi.scanDelete();
#if PROBE_SNIFF
// Scan may have disabled promiscuous mode internally — re-enable it.
esp_wifi_set_promiscuous(true);
flushProbeQueue();
#endif
}
// ─── Event helpers ───────────────────────────────────────────────────────────
const char* encStr(wifi_auth_mode_t enc) {
switch (enc) {
case WIFI_AUTH_OPEN: return "OPEN";
case WIFI_AUTH_WEP: return "WEP";
case WIFI_AUTH_WPA_PSK: return "WPA";
case WIFI_AUTH_WPA2_PSK: return "WPA2";
case WIFI_AUTH_WPA_WPA2_PSK: return "WPA/WPA2";
case WIFI_AUTH_WPA3_PSK: return "WPA3";
case WIFI_AUTH_WPA2_WPA3_PSK: return "WPA2/WPA3";
default: return "UNKNOWN";
}
}
const char* importance(int rssi) {
if (rssi >= -50) return "high";
if (rssi <= -80) return "low";
return "normal";
}
// ─── Send ────────────────────────────────────────────────────────────────────
void sendBeaconEvent(int idx) {
String ssid = WiFi.SSID(idx);
ssid.replace("\"", "\\\""); // escape quotes for JSON
String bssid = WiFi.BSSIDstr(idx);
int rssi = WiFi.RSSI(idx);
int ch = WiFi.channel(idx);
const char* enc = encStr(WiFi.encryptionType(idx));
const char* imp = importance(rssi);
unsigned long ts = millis();
char buf[512];
snprintf(buf, sizeof(buf),
"{\"node_id\":\"%s\",\"ts\":%lu,\"type\":\"beacon\","
"\"conf\":\"high\",\"imp\":\"%s\","
"\"ssid\":\"%s\",\"bssid\":\"%s\","
"\"rssi\":%d,\"ch\":%d,\"enc\":\"%s\"}",
nodeId.c_str(), ts, imp,
ssid.c_str(), bssid.c_str(),
rssi, ch, enc
);
udp.beginPacket(COORDINATOR_IP, COORDINATOR_PORT);
udp.print(buf);
udp.endPacket();
Serial.printf(" → %-32s %s ch%-3d %4ddBm %s\n",
ssid.c_str(), bssid.c_str(), ch, rssi, enc);
}
// ─── Heartbeat ───────────────────────────────────────────────────────────────
void sendHeartbeat() {
char buf[256];
snprintf(buf, sizeof(buf),
"{\"node_id\":\"%s\",\"ts\":%lu,\"type\":\"heartbeat\","
"\"uptime_ms\":%lu,\"free_heap\":%lu,\"wifi_rssi\":%d}",
nodeId.c_str(), millis(),
millis(),
(unsigned long)ESP.getFreeHeap(),
WiFi.RSSI()
);
udp.beginPacket(COORDINATOR_IP, COORDINATOR_PORT);
udp.print(buf);
udp.endPacket();
Serial.printf("[HB] uptime=%lus heap=%luK ap=%ddBm\n",
millis() / 1000,
(unsigned long)ESP.getFreeHeap() / 1024,
WiFi.RSSI()
);
}
// ─── Probe send / flush ───────────────────────────────────────────────────────
#if PROBE_SNIFF
void sendProbeEvent(const ProbeEvent& ev) {
char mac_str[18];
snprintf(mac_str, sizeof(mac_str), "%02X:%02X:%02X:%02X:%02X:%02X",
ev.src_mac[0], ev.src_mac[1], ev.src_mac[2],
ev.src_mac[3], ev.src_mac[4], ev.src_mac[5]);
// Escape any quotes in SSID
char ssidEsc[66];
int j = 0;
for (int i = 0; ev.ssid[i] && j < 64; i++) {
if (ev.ssid[i] == '"' || ev.ssid[i] == '\\') ssidEsc[j++] = '\\';
ssidEsc[j++] = ev.ssid[i];
}
ssidEsc[j] = '\0';
char buf[256];
snprintf(buf, sizeof(buf),
"{\"node_id\":\"%s\",\"ts\":%lu,\"type\":\"probe\","
"\"conf\":\"high\",\"imp\":\"%s\","
"\"src_mac\":\"%s\",\"ssid\":\"%s\",\"rssi\":%d}",
nodeId.c_str(), millis(), importance(ev.rssi),
mac_str, ssidEsc, (int)ev.rssi
);
udp.beginPacket(COORDINATOR_IP, COORDINATOR_PORT);
udp.print(buf);
udp.endPacket();
Serial.printf(" [PROBE] %s → \"%s\" %ddBm\n",
mac_str, ev.ssid[0] ? ev.ssid : "<wildcard>", (int)ev.rssi);
}
void flushProbeQueue() {
if (WiFi.status() != WL_CONNECTED) return;
ProbeEvent ev;
while (xQueueReceive(probeQueue, &ev, 0) == pdTRUE) {
sendProbeEvent(ev);
}
}
#endif // PROBE_SNIFF