From 5271b1fca03ea9eb56d6d687bcad03540493a2a5 Mon Sep 17 00:00:00 2001 From: rpriven Date: Sat, 28 Feb 2026 12:26:36 -0700 Subject: [PATCH] Phase 3: Categorized detections, Morse code alerts, alert system fixes - Restructure detection arrays to categorized structs (MfrEntry, NameEntry) - Add 8 threat categories: flock, glasses, tracker, lawenf, ring, camera, raven, wifi - Add 10 new BLE manufacturer IDs: Meta (x2), Luxottica, Snapchat, Axon, Motorola Solutions, Tile, Hikvision, Arlo, Wyze - Add smart glasses name patterns (rayban, ray-ban, ray ban) - Category field flows through all JSON output (API, SPIFFS, serial) - Morse code buzzer alerts per category (F=flock, G=glasses, T=tracker, L=lawenf, R=ring, S=camera, V=raven) - Per-category alert triggering replaces global suppression flag - Decouple heartbeat timer from re-detections for clean 10s cadence - Fix heartbeat firing immediately after initial detection alert - Dashboard: full category legend, color-coded charts for all categories - Color swap: Flock=red, Ring=blue, Raven=amber Co-Authored-By: Claude Opus 4.6 --- src/main.cpp | 403 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 320 insertions(+), 83 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index a764851..94c4719 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -103,19 +103,61 @@ static const char* ble_mac_prefixes[] = { "74:4c:a1", "08:3a:88", "9c:2f:9d", "94:08:53", "e4:aa:ea" }; +// ============================================================================ +// DETECTION CATEGORIES +// ============================================================================ +// Each detection is tagged with a category for differentiated alerts. +// Categories map to Morse code letters for buzzer/haptic patterns: +// F (··-·) = Flock/ALPR G (--·) = Glasses/recording +// T (-) = Tracker L (·-··) = Law enforcement +// R (·-·) = Ring/consumer S (···) = Surveillance cameras +// V (···-) = Raven W (·--) = WiFi detection + // BLE device name patterns (matched case-insensitive substring) -static const char* device_name_patterns[] = { - "FS Ext Battery", - "Penguin", - "Flock", - "Pigvision", - "Ring" // Ring cameras/doorbells during BLE setup +struct NameEntry { + const char* pattern; + const char* category; }; -// BLE Manufacturer Company IDs -static const uint16_t ble_manufacturer_ids[] = { - 0x09C8, // XUNTONG — Flock Safety (via wgreenberg/flock-you) - 0x0171 // Amazon — Ring doorbells, Echo, security devices +static const NameEntry device_name_entries[] = { + // Flock Safety + {"FS Ext Battery", "flock"}, + {"Penguin", "flock"}, + {"Flock", "flock"}, + {"Pigvision", "flock"}, + // Ring / consumer cameras + {"Ring", "ring"}, + // Smart glasses / recording devices + {"rayban", "glasses"}, + {"ray-ban", "glasses"}, + {"ray ban", "glasses"}, +}; + +// BLE Manufacturer Company IDs (from Bluetooth SIG assigned numbers) +struct MfrEntry { + uint16_t id; + const char* category; +}; + +static const MfrEntry ble_manufacturer_entries[] = { + // Flock Safety / ALPR + {0x09C8, "flock"}, // XUNTONG — Flock Safety cameras + // Consumer surveillance (Amazon ecosystem) + {0x0171, "ring"}, // Amazon — Ring doorbells, Echo, Blink + // Smart glasses / recording devices + {0x01AB, "glasses"}, // Meta Platforms — Ray-Ban Meta + {0x058E, "glasses"}, // Meta Platforms Technologies — Ray-Ban Meta + {0x0D53, "glasses"}, // Luxottica/EssilorLuxottica — Oakley smart glasses + {0x03C2, "glasses"}, // Snapchat — Spectacles + // Law enforcement equipment + {0x034D, "lawenf"}, // TASER International / Axon — body cameras + {0x04EC, "lawenf"}, // Motorola Solutions — police radios/cameras + // Tracking devices + {0x067C, "tracker"}, // Tile — BLE trackers + // Surveillance cameras + {0x0E25, "camera"}, // Hangzhou Hikvision — surveillance cameras + {0x0C19, "camera"}, // Arlo Technologies — security cameras + {0x0870, "camera"}, // Wyze Labs — security cameras }; // ============================================================================ @@ -173,6 +215,7 @@ struct FYDetection { char name[48]; int rssi; char method[24]; + char category[12]; // flock, glasses, tracker, lawenf, ring, camera, raven, wifi unsigned long firstSeen; unsigned long lastSeen; int count; @@ -206,10 +249,35 @@ static unsigned long fyPixelAlertStart = 0; static unsigned long fyLastBleScan = 0; static volatile bool fyWifiAlertPending = false; // Deferred from promiscuous CB static int fyWifiDetCount = 0; -static bool fyTriggered = false; static bool fyDeviceInRange = false; static unsigned long fyLastDetTime = 0; static unsigned long fyLastHB = 0; + +// Per-category alert tracking — each category beeps once, then heartbeat takes over +#define FY_MAX_ALERTED_CATS 8 +static char fyAlertedCats[FY_MAX_ALERTED_CATS][12] = {}; +static int fyAlertedCatCount = 0; + +static bool fyCategoryAlerted(const char* cat) { + if (!cat) return false; + for (int i = 0; i < fyAlertedCatCount; i++) { + if (strcmp(fyAlertedCats[i], cat) == 0) return true; + } + return false; +} + +static void fyMarkCategoryAlerted(const char* cat) { + if (!cat || fyCategoryAlerted(cat)) return; + if (fyAlertedCatCount < FY_MAX_ALERTED_CATS) { + strlcpy(fyAlertedCats[fyAlertedCatCount], cat, sizeof(fyAlertedCats[0])); + fyAlertedCatCount++; + } +} + +static void fyClearAlertedCategories() { + fyAlertedCatCount = 0; + memset(fyAlertedCats, 0, sizeof(fyAlertedCats)); +} static NimBLEScan* fyBLEScan = NULL; static AsyncWebServer fyServer(80); @@ -317,17 +385,83 @@ static void fyBootBeep() { printf("[DANTIR] *caw caw caw*\n"); } -static void fyDetectBeep() { - printf("[DANTIR] Detection alert!\n"); +// ============================================================================ +// MORSE CODE ALERT SYSTEM +// ============================================================================ +// Each detection category plays its Morse code letter on the buzzer. +// This serves dual purpose: identify device type + learn Morse code. +// +// Morse patterns (. = dit, - = dah): +// F ··-· = Flock/ALPR G --· = Glasses/recording +// T - = Tracker L ·-·· = Law enforcement +// R ·-· = Ring/consumer S ··· = Surveillance cameras +// V ···- = Raven W ·-- = WiFi detection + +#define MORSE_FREQ 800 // Hz — clean, distinct tone +#define MORSE_DIT_MS 80 // dit duration +#define MORSE_DAH_MS 240 // dah = 3x dit +#define MORSE_GAP_MS 80 // inter-element gap = 1x dit +#define MORSE_LEAD_MS 30 // tiny pre-delay for audio clarity + +// Play a Morse code element (dit or dah) +static void fyMorseDit() { + tone(BUZZER_PIN, MORSE_FREQ, MORSE_DIT_MS); + delay(MORSE_DIT_MS + MORSE_GAP_MS); +} + +static void fyMorseDah() { + tone(BUZZER_PIN, MORSE_FREQ, MORSE_DAH_MS); + delay(MORSE_DAH_MS + MORSE_GAP_MS); +} + +// Play Morse pattern for a detection category +static void fyMorseCategory(const char* category) { + if (!fyBuzzerOn || !category) return; + delay(MORSE_LEAD_MS); + + if (strcmp(category, "flock") == 0) { + // F: ··-· + fyMorseDit(); fyMorseDit(); fyMorseDah(); fyMorseDit(); + } else if (strcmp(category, "glasses") == 0) { + // G: --· + fyMorseDah(); fyMorseDah(); fyMorseDit(); + } else if (strcmp(category, "tracker") == 0) { + // T: - + fyMorseDah(); + } else if (strcmp(category, "lawenf") == 0) { + // L: ·-·· + fyMorseDit(); fyMorseDah(); fyMorseDit(); fyMorseDit(); + } else if (strcmp(category, "ring") == 0) { + // R: ·-· + fyMorseDit(); fyMorseDah(); fyMorseDit(); + } else if (strcmp(category, "camera") == 0) { + // S: ··· + fyMorseDit(); fyMorseDit(); fyMorseDit(); + } else if (strcmp(category, "raven") == 0) { + // V: ···- + fyMorseDit(); fyMorseDit(); fyMorseDit(); fyMorseDah(); + } else { + // Unknown: single long beep + fyMorseDah(); fyMorseDah(); + } + noTone(BUZZER_PIN); +} + +static void fyDetectBeep(const char* category = nullptr) { + printf("[DANTIR] Detection alert! [%s]\n", category ? category : "?"); fyPixelAlertMode = true; fyPixelAlertStart = millis(); if (!fyBuzzerOn) return; - // Alarm crow: two sharp ascending chirps then a caw - fyCaw(400, 900, 100, 30); // rising alarm chirp - delay(60); - fyCaw(450, 950, 100, 30); // second chirp, higher - delay(60); - fyCaw(900, 350, 200, 50); // descending caw + if (category) { + fyMorseCategory(category); + } else { + // Fallback: original crow caw for uncategorized + fyCaw(400, 900, 100, 30); + delay(60); + fyCaw(450, 950, 100, 30); + delay(60); + fyCaw(900, 350, 200, 50); + } } static void fyHeartbeat() { @@ -443,8 +577,8 @@ static bool checkWiFiMACPrefix(const uint8_t* mac) { // Forward declarations for functions called by WiFi promiscuous callback static bool fyGPSIsFresh(); static int fyAddDetection(const char* mac, const char* name, int rssi, - const char* method, bool isRaven = false, - const char* ravenFW = ""); + const char* method, const char* category = "unknown", + bool isRaven = false, const char* ravenFW = ""); // Called by the WiFi driver for every frame on the AP's channel. // Probe requests from devices scanning (on ANY channel) are caught because @@ -485,12 +619,12 @@ static void fyWifiPromiscuousCB(void *buf, wifi_promiscuous_pkt_type_t type) { const char *method = (subtype == WIFI_MGMT_PROBE_REQ) ? "wifi_probe" : "wifi_beacon"; int rssi = pkt->rx_ctrl.rssi; - int idx = fyAddDetection(mac_str, "", rssi, method); + int idx = fyAddDetection(mac_str, "", rssi, method, "ring"); // WiFi OUIs are all Ring/Blink if (idx >= 0) { if (fyDet[idx].count == 1) { // First sighting — new WiFi surveillance device fyWifiDetCount++; - printf("[DANTIR] WiFi DETECTED: %s RSSI:%d [%s]\n", mac_str, rssi, method); + printf("[DANTIR] WiFi DETECTED [ring]: %s RSSI:%d [%s]\n", mac_str, rssi, method); // JSON serial output char gpsBuf[80] = ""; @@ -499,7 +633,7 @@ static void fyWifiPromiscuousCB(void *buf, wifi_promiscuous_pkt_type_t type) { ",\"gps\":{\"latitude\":%.8f,\"longitude\":%.8f,\"accuracy\":%.1f}", fyGPSLat, fyGPSLon, fyGPSAcc); } - printf("{\"detection_method\":\"%s\",\"protocol\":\"wifi\"," + printf("{\"detection_method\":\"%s\",\"category\":\"ring\",\"protocol\":\"wifi\"," "\"mac_address\":\"%s\",\"rssi\":%d%s}\n", method, mac_str, rssi, gpsBuf); } @@ -515,19 +649,21 @@ static void fyWifiPromiscuousCB(void *buf, wifi_promiscuous_pkt_type_t type) { // DETECTION HELPERS — NAME & MANUFACTURER // ============================================================================ -static bool checkDeviceName(const char* name) { - if (!name || !name[0]) return false; - for (size_t i = 0; i < sizeof(device_name_patterns)/sizeof(device_name_patterns[0]); i++) { - if (strcasestr(name, device_name_patterns[i])) return true; +// Returns category string if name matches, or nullptr +static const char* checkDeviceName(const char* name) { + if (!name || !name[0]) return nullptr; + for (size_t i = 0; i < sizeof(device_name_entries)/sizeof(device_name_entries[0]); i++) { + if (strcasestr(name, device_name_entries[i].pattern)) return device_name_entries[i].category; } - return false; + return nullptr; } -static bool checkManufacturerID(uint16_t id) { - for (size_t i = 0; i < sizeof(ble_manufacturer_ids)/sizeof(ble_manufacturer_ids[0]); i++) { - if (ble_manufacturer_ids[i] == id) return true; +// Returns category string if manufacturer ID matches, or nullptr +static const char* checkManufacturerID(uint16_t id) { + for (size_t i = 0; i < sizeof(ble_manufacturer_entries)/sizeof(ble_manufacturer_entries[0]); i++) { + if (ble_manufacturer_entries[i].id == id) return ble_manufacturer_entries[i].category; } - return false; + return nullptr; } // ============================================================================ @@ -640,8 +776,8 @@ static void fyProcessHardwareGPS() { // ============================================================================ static int fyAddDetection(const char* mac, const char* name, int rssi, - const char* method, bool isRaven, - const char* ravenFW) { + const char* method, const char* category, + bool isRaven, const char* ravenFW) { if (!fyMutex || xSemaphoreTake(fyMutex, pdMS_TO_TICKS(100)) != pdTRUE) return -1; // Update existing by MAC @@ -682,6 +818,7 @@ static int fyAddDetection(const char* mac, const char* name, int rssi, d.rssi = rssi; d.bestRSSI = rssi; // First sighting = initial best strncpy(d.method, method, sizeof(d.method) - 1); + strncpy(d.category, category ? category : "unknown", sizeof(d.category) - 1); d.firstSeen = millis(); d.lastSeen = millis(); d.count = 1; @@ -729,26 +866,33 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks { bool isRaven = false; const char* ravenFW = ""; + const char* cat = nullptr; + // 1. Check MAC prefix against known surveillance device OUIs (BLE) if (checkBLEMACPrefix(mac)) { detected = true; method = "mac_prefix"; + cat = "flock"; // MAC prefixes are all Flock Safety OUIs } - // 2. Check BLE device name patterns - if (!detected && !name.empty() && checkDeviceName(name.c_str())) { - detected = true; - method = "device_name"; + // 2. Check BLE device name patterns (returns category or nullptr) + if (!detected && !name.empty()) { + cat = checkDeviceName(name.c_str()); + if (cat) { + detected = true; + method = "device_name"; + } } - // 3. Check BLE manufacturer company IDs (from wgreenberg/flock-you) + // 3. Check BLE manufacturer company IDs (returns category or nullptr) if (!detected) { for (int i = 0; i < (int)dev->getManufacturerDataCount(); i++) { std::string data = dev->getManufacturerData(i); if (data.size() >= 2) { uint16_t code = ((uint16_t)(uint8_t)data[1] << 8) | (uint16_t)(uint8_t)data[0]; - if (checkManufacturerID(code)) { + cat = checkManufacturerID(code); + if (cat) { detected = true; method = "ble_mfr_id"; break; @@ -763,6 +907,7 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks { if (checkRavenUUID(dev, detUUID)) { detected = true; method = "raven_uuid"; + cat = "raven"; isRaven = true; ravenFW = estimateRavenFW(dev); } @@ -770,11 +915,13 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks { if (detected) { int idx = fyAddDetection(addrStr.c_str(), name.c_str(), rssi, - method, isRaven, ravenFW); + method, cat ? cat : "unknown", + isRaven, ravenFW); // Human-readable log - printf("[DANTIR] DETECTED: %s %s RSSI:%d [%s] count:%d\n", - addrStr.c_str(), name.c_str(), rssi, method, + const char* catStr = cat ? cat : "unknown"; + printf("[DANTIR] DETECTED [%s]: %s %s RSSI:%d [%s] count:%d\n", + catStr, addrStr.c_str(), name.c_str(), rssi, method, idx >= 0 ? fyDet[idx].count : 0); // JSON serial output (Flask-compatible format for live ingestion) @@ -786,24 +933,24 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks { fyGPSLat, fyGPSLon, fyGPSAcc); } if (isRaven) { - printf("{\"detection_method\":\"%s\",\"protocol\":\"bluetooth_le\"," + printf("{\"detection_method\":\"%s\",\"category\":\"%s\",\"protocol\":\"bluetooth_le\"," "\"mac_address\":\"%s\",\"device_name\":\"%s\"," "\"rssi\":%d,\"is_raven\":true,\"raven_fw\":\"%s\"%s}\n", - method, addrStr.c_str(), name.c_str(), rssi, ravenFW, gpsBuf); + method, catStr, addrStr.c_str(), name.c_str(), rssi, ravenFW, gpsBuf); } else { - printf("{\"detection_method\":\"%s\",\"protocol\":\"bluetooth_le\"," + printf("{\"detection_method\":\"%s\",\"category\":\"%s\",\"protocol\":\"bluetooth_le\"," "\"mac_address\":\"%s\",\"device_name\":\"%s\"," "\"rssi\":%d%s}\n", - method, addrStr.c_str(), name.c_str(), rssi, gpsBuf); + method, catStr, addrStr.c_str(), name.c_str(), rssi, gpsBuf); } - if (!fyTriggered) { - fyTriggered = true; - fyDetectBeep(); + if (!fyCategoryAlerted(catStr)) { + fyMarkCategoryAlerted(catStr); + fyDetectBeep(catStr); + fyLastHB = millis(); // Start heartbeat countdown AFTER the alert beep } fyDeviceInRange = true; fyLastDetTime = millis(); - fyLastHB = millis(); } } }; @@ -819,9 +966,11 @@ static void writeDetectionsJSON(AsyncResponseStream *resp) { if (i > 0) resp->print(","); resp->printf( "{\"mac\":\"%s\",\"name\":\"%s\",\"rssi\":%d,\"method\":\"%s\"," + "\"cat\":\"%s\"," "\"first\":%lu,\"last\":%lu,\"count\":%d," "\"raven\":%s,\"fw\":\"%s\"", fyDet[i].mac, fyDet[i].name, fyDet[i].rssi, fyDet[i].method, + fyDet[i].category, fyDet[i].firstSeen, fyDet[i].lastSeen, fyDet[i].count, fyDet[i].isRaven ? "true" : "false", fyDet[i].ravenFW); // Append GPS if present (first-seen position) @@ -858,9 +1007,11 @@ static void fySaveSession() { if (i > 0) f.print(","); FYDetection& d = fyDet[i]; f.printf("{\"mac\":\"%s\",\"name\":\"%s\",\"rssi\":%d,\"method\":\"%s\"," + "\"cat\":\"%s\"," "\"first\":%lu,\"last\":%lu,\"count\":%d," "\"raven\":%s,\"fw\":\"%s\"", d.mac, d.name, d.rssi, d.method, + d.category, d.firstSeen, d.lastSeen, d.count, d.isRaven ? "true" : "false", d.ravenFW); if (d.hasGPS) { @@ -916,6 +1067,80 @@ static void fyPromotePrevSession() { printf("[DANTIR] Prior session promoted: %d bytes\n", data.length()); } +// Restore detections from prev_session into live array (append mode) +// Called after promotion — loads prev_session data so dashboard survives reboots +static void fyRestoreSession() { + if (!fySpiffsReady || !SPIFFS.exists(FY_PREV_FILE)) { + printf("[DANTIR] No prev_session to restore\n"); + return; + } + + File f = SPIFFS.open(FY_PREV_FILE, "r"); + if (!f) { + printf("[DANTIR] Failed to open prev_session for restore\n"); + return; + } + String data = f.readString(); + f.close(); + + if (data.length() == 0) { + printf("[DANTIR] prev_session empty, nothing to restore\n"); + return; + } + + JsonDocument doc; + DeserializationError err = deserializeJson(doc, data); + if (err || !doc.is()) { + printf("[DANTIR] prev_session JSON parse failed: %s\n", err.c_str()); + return; + } + + int restored = 0; + for (JsonObject d : doc.as()) { + if (fyDetCount >= MAX_DETECTIONS) break; + + FYDetection& det = fyDet[fyDetCount]; + memset(&det, 0, sizeof(FYDetection)); + + strlcpy(det.mac, d["mac"] | "", sizeof(det.mac)); + strlcpy(det.name, d["name"] | "", sizeof(det.name)); + det.rssi = d["rssi"] | 0; + strlcpy(det.method, d["method"] | "", sizeof(det.method)); + strlcpy(det.category, d["cat"] | "unknown", sizeof(det.category)); + det.firstSeen = d["first"] | 0UL; + det.lastSeen = d["last"] | 0UL; + det.count = d["count"] | 1; + det.isRaven = d["raven"] | false; + strlcpy(det.ravenFW, d["fw"] | "", sizeof(det.ravenFW)); + + JsonObject gps = d["gps"]; + if (gps && gps["lat"]) { + det.gpsLat = gps["lat"] | 0.0; + det.gpsLon = gps["lon"] | 0.0; + det.gpsAcc = gps["acc"] | 0.0f; + det.hasGPS = true; + } + + if (d["best_rssi"].is()) { + det.bestRSSI = d["best_rssi"] | 0; + JsonObject bgps = d["best_gps"]; + if (bgps && bgps["lat"]) { + det.bestGPSLat = bgps["lat"] | 0.0; + det.bestGPSLon = bgps["lon"] | 0.0; + det.bestGPSAcc = bgps["acc"] | 0.0f; + det.hasBestGPS = true; + } + } + + fyDetCount++; + restored++; + } + + // Mark as already saved so we don't immediately re-write the same data + fyLastSaveCount = fyDetCount; + printf("[DANTIR] Restored %d detections from prev_session\n", restored); +} + // ============================================================================ // KML EXPORT // ============================================================================ @@ -979,7 +1204,7 @@ static const char FY_HTML[] PROGMEM = R"rawliteral( --t1:#e0e0e0;--t2:rgba(139,92,246,.5); --grid:rgba(236,72,153,.02); --rr:rgba(139,92,246,.2);--rs:rgba(236,72,153,.4); ---bl-flock:#8b5cf6;--bl-ring:#ef4444;--bl-raven:#dc2626;--bl-other:#6b7280; +--bl-flock:#ef4444;--bl-ring:#60a5fa;--bl-raven:#f59e0b;--bl-other:#6b7280; --btn-bg:#8b5cf6;--btn-act:#ec4899; } *{margin:0;padding:0;box-sizing:border-box} @@ -1008,6 +1233,9 @@ font-family:inherit;font-size:11px;font-weight:bold;letter-spacing:1px;cursor:po .det{background:var(--bg-card);border:1px solid var(--b2);border-radius:7px;padding:10px;margin-bottom:8px;border-left:3px solid var(--a2)} .det.t-flock{border-left-color:var(--bl-flock)}.det.t-ring{border-left-color:var(--bl-ring)} .det.t-raven{border-left-color:var(--bl-raven)}.det.t-wifi{border-left-color:#22c55e} +.det.t-glasses{border-left-color:#e879f9}.det.t-lawenf{border-left-color:#f43f5e} +.det.t-tracker{border-left-color:#fb923c}.det.t-camera{border-left-color:#94a3b8} +.det.t-unknown{border-left-color:#6b7280} .det .mac{color:var(--a1);font-weight:bold;font-size:14px} .det .nm{color:var(--a3);font-size:13px;margin-left:4px} .det .inf{display:flex;flex-wrap:wrap;gap:5px;margin-top:5px;font-size:12px} @@ -1022,8 +1250,8 @@ font-family:inherit;font-size:11px;font-weight:bold;letter-spacing:1px;cursor:po .rp-b.open{display:block} #rC{max-width:100%;border-radius:7px} .rp-lg{margin-top:8px;font-size:11px;display:flex;justify-content:center;gap:12px;color:var(--t1)} -.ch{background:var(--bg-card);border:1px solid var(--b2);border-radius:7px;margin-bottom:10px;overflow:hidden} -.ch canvas{width:100%;display:block} +.ch{background:var(--bg-card);border:1px solid var(--b2);border-radius:7px;margin-bottom:10px;overflow:hidden;max-height:160px} +.ch canvas{width:100%;display:block;max-height:150px} .pg{margin-bottom:12px} .pg h3{color:var(--a1);font-size:14px;margin-bottom:4px;border-bottom:1px solid var(--b3);padding-bottom:4px} .pg .it{display:flex;flex-wrap:wrap;gap:4px;font-size:12px} @@ -1061,7 +1289,7 @@ h4{color:var(--a1);font-size:14px;margin-bottom:8px}
PROXIMITY RADAR
0 devices
-
Flock Ring Raven Other
+
Flock Ring Raven WiFi Glasses LawEnf Tracker Other
Scanning for surveillance devices...
BLE + WiFi promiscuous active
@@ -1071,13 +1299,13 @@ h4{color:var(--a1);font-size:14px;margin-bottom:8px}

EXPORT DETECTIONS

Download current session data

- - - + + +

PRIOR SESSION

- - + +
@@ -1094,7 +1322,7 @@ purple:{ '--t1':'#e0e0e0','--t2':'rgba(139,92,246,.5)', '--grid':'rgba(236,72,153,.02)', '--rr':'rgba(139,92,246,.2)','--rs':'rgba(236,72,153,.4)', -'--bl-flock':'#8b5cf6','--bl-ring':'#ef4444','--bl-raven':'#dc2626','--bl-other':'#6b7280', +'--bl-flock':'#ef4444','--bl-ring':'#60a5fa','--bl-raven':'#f59e0b','--bl-other':'#6b7280', '--btn-bg':'#8b5cf6','--btn-act':'#ec4899'}, tactical:{ '--bg-body':'#0a0f0a','--bg-hdr':'#0f1a0f', @@ -1105,7 +1333,7 @@ tactical:{ '--t1':'#d4f4dd','--t2':'rgba(34,197,94,.4)', '--grid':'rgba(34,197,94,.03)', '--rr':'rgba(34,197,94,.25)','--rs':'rgba(250,204,21,.4)', -'--bl-flock':'#84cc16','--bl-ring':'#ef4444','--bl-raven':'#dc2626','--bl-other':'#a3a3a3', +'--bl-flock':'#ef4444','--bl-ring':'#3b82f6','--bl-raven':'#f59e0b','--bl-other':'#a3a3a3', '--btn-bg':'#16a34a','--btn-act':'#facc15'}, ithildin:{ '--bg-body':'#080811','--bg-hdr':'#0f0f1f', @@ -1116,7 +1344,7 @@ ithildin:{ '--t1':'#f1f5f9','--t2':'rgba(148,163,184,.4)', '--grid':'rgba(96,165,250,.02)', '--rr':'rgba(148,163,184,.2)','--rs':'rgba(96,165,250,.35)', -'--bl-flock':'#60a5fa','--bl-ring':'#ef4444','--bl-raven':'#dc2626','--bl-other':'#94a3b8', +'--bl-flock':'#ef4444','--bl-ring':'#60a5fa','--bl-raven':'#f59e0b','--bl-other':'#94a3b8', '--btn-bg':'#3b82f6','--btn-act':'#60a5fa'} }; function setTheme(n){const t=TH[n];if(!t)return;const r=document.documentElement; @@ -1134,14 +1362,12 @@ function tab(i,el){document.querySelectorAll('.tb button').forEach(b=>b.classLis document.querySelectorAll('.pn').forEach(p=>p.classList.remove('a')); el.classList.add('a');document.getElementById('p'+i).classList.add('a'); if(i===1&&!window._hL)loadHistory();if(i===2&&!window._pL)loadPat();} +// === TIMESTAMPED DOWNLOAD === +function dlTS(url,prefix,ext){const d=new Date();const ts=d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0')+'_'+String(d.getHours()).padStart(2,'0')+String(d.getMinutes()).padStart(2,'0')+String(d.getSeconds()).padStart(2,'0');fetch(url).then(r=>r.blob()).then(b=>{const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=prefix+'_'+ts+'.'+ext;a.click();URL.revokeObjectURL(a.href);});} // === REFRESH === function refresh(){fetch('/api/detections').then(r=>r.json()).then(d=>{D=d;render();stats();drawChart();}).catch(()=>{});} // === DETECT TYPE === -function dtype(d){if(d.raven)return'raven'; -const m=d.method||'',n=(d.name||'').toLowerCase(); -if(m==='wifi_beacon'||m==='wifi_probe')return'wifi'; -if(n.indexOf('ring')>=0||n.indexOf('blink')>=0||m==='ble_mfr_id')return'ring'; -return'flock';} +function dtype(d){return d.cat||'unknown';} // === RENDER LIST === function render(){const el=document.getElementById('dL'); if(!D.length){el.innerHTML='
Scanning for surveillance devices...
BLE + WiFi promiscuous active
';return;} @@ -1212,6 +1438,11 @@ if(t==='raven')col=cs.getPropertyValue('--bl-raven').trim(); else if(t==='ring')col=cs.getPropertyValue('--bl-ring').trim(); else if(t==='flock')col=cs.getPropertyValue('--bl-flock').trim(); else if(t==='wifi')col='#22c55e'; +else if(t==='glasses')col='#e879f9'; +else if(t==='lawenf')col='#f43f5e'; +else if(t==='tracker')col='#fb923c'; +else if(t==='camera')col='#94a3b8'; +else if(t==='wifi')col='#22c55e'; ctx.fillStyle=col;ctx.globalAlpha=1;ctx.beginPath();ctx.arc(bx,by,4,0,Math.PI*2);ctx.fill(); // glow for high count if(det.count>5){ctx.strokeStyle=col;ctx.lineWidth=1.5;ctx.globalAlpha=.35; @@ -1235,6 +1466,10 @@ if(t==='raven')col=cs.getPropertyValue('--bl-raven').trim(); else if(t==='ring')col=cs.getPropertyValue('--bl-ring').trim(); else if(t==='flock')col=cs.getPropertyValue('--bl-flock').trim(); else if(t==='wifi')col='#22c55e'; +else if(t==='glasses')col='#e879f9'; +else if(t==='lawenf')col='#f43f5e'; +else if(t==='tracker')col='#fb923c'; +else if(t==='camera')col='#94a3b8'; // label ctx.fillStyle=cs.getPropertyValue('--t1').trim();ctx.font='10px monospace';ctx.textAlign='right'; const lbl=d.name?d.name.substring(0,10):d.mac.substring(9); @@ -1370,14 +1605,14 @@ static void fySetupServer() { resp->printf("\"%s\"", wifi_mac_prefixes[i]); } resp->print("],\"names\":["); - for (size_t i = 0; i < sizeof(device_name_patterns)/sizeof(device_name_patterns[0]); i++) { + for (size_t i = 0; i < sizeof(device_name_entries)/sizeof(device_name_entries[0]); i++) { if (i > 0) resp->print(","); - resp->printf("\"%s\"", device_name_patterns[i]); + resp->printf("{\"p\":\"%s\",\"c\":\"%s\"}", device_name_entries[i].pattern, device_name_entries[i].category); } resp->print("],\"mfr\":["); - for (size_t i = 0; i < sizeof(ble_manufacturer_ids)/sizeof(ble_manufacturer_ids[0]); i++) { + for (size_t i = 0; i < sizeof(ble_manufacturer_entries)/sizeof(ble_manufacturer_entries[0]); i++) { if (i > 0) resp->print(","); - resp->printf("%u", ble_manufacturer_ids[i]); + resp->printf("{\"id\":%u,\"c\":\"%s\"}", ble_manufacturer_entries[i].id, ble_manufacturer_entries[i].category); } resp->print("],\"raven\":["); for (size_t i = 0; i < sizeof(raven_service_uuids)/sizeof(raven_service_uuids[0]); i++) { @@ -1520,7 +1755,7 @@ static void fySetupServer() { if (fyMutex && xSemaphoreTake(fyMutex, pdMS_TO_TICKS(200)) == pdTRUE) { fyDetCount = 0; memset(fyDet, 0, sizeof(fyDet)); - fyTriggered = false; + fyClearAlertedCategories(); fyDeviceInRange = false; xSemaphoreGive(fyMutex); } @@ -1573,8 +1808,10 @@ void setup() { if (SPIFFS.begin(true)) { fySpiffsReady = true; printf("[DANTIR] SPIFFS ready\n"); - // Promote last session to prev_session before we start a new one + // Promote last session to prev_session (backup), then restore into live array + // This means: prev_session always has a backup, AND dashboard keeps all detections fyPromotePrevSession(); + fyRestoreSession(); } else { printf("[DANTIR] SPIFFS init failed - no persistence\n"); } @@ -1624,8 +1861,8 @@ void setup() { printf("[DANTIR] BLE: %d OUI prefixes, %d name patterns, %d mfr IDs, %d Raven UUIDs\n", (int)(sizeof(ble_mac_prefixes)/sizeof(ble_mac_prefixes[0])), - (int)(sizeof(device_name_patterns)/sizeof(device_name_patterns[0])), - (int)(sizeof(ble_manufacturer_ids)/sizeof(ble_manufacturer_ids[0])), + (int)(sizeof(device_name_entries)/sizeof(device_name_entries[0])), + (int)(sizeof(ble_manufacturer_entries)/sizeof(ble_manufacturer_entries[0])), (int)(sizeof(raven_service_uuids)/sizeof(raven_service_uuids[0]))); printf("[DANTIR] WiFi: %d OUI prefixes (promiscuous mode ACTIVE)\n", (int)(sizeof(wifi_mac_prefixes)/sizeof(wifi_mac_prefixes[0]))); @@ -1651,14 +1888,14 @@ void loop() { // WiFi detection alert (deferred from promiscuous callback — can't buzz there) if (fyWifiAlertPending) { fyWifiAlertPending = false; - if (!fyTriggered) { - fyTriggered = true; - fyDetectBeep(); + if (!fyCategoryAlerted("ring")) { + fyMarkCategoryAlerted("ring"); + fyDetectBeep("ring"); // WiFi OUIs are all Ring/Blink + fyLastHB = millis(); } - fyLastHB = millis(); } - // Heartbeat tracking + // Heartbeat tracking — runs on clean 10s cadence, independent of re-detections if (fyDeviceInRange) { if (millis() - fyLastHB >= 10000) { fyHeartbeat(); @@ -1667,7 +1904,7 @@ void loop() { if (millis() - fyLastDetTime >= 30000) { printf("[DANTIR] Device out of range - stopping heartbeat\n"); fyDeviceInRange = false; - fyTriggered = false; + fyClearAlertedCategories(); } }