// ============================================================================ // FLOCK-YOU: Surveillance Device Detector with Web Dashboard // ============================================================================ // Detection methods (BLE only - WiFi radio used for AP): // 1. BLE MAC prefix matching (known Flock Safety OUIs) // 2. BLE device name pattern matching (case-insensitive substring) // 3. BLE manufacturer company ID matching (0x09C8 XUNTONG) [from wgreenberg] // 4. Raven gunshot detector service UUID matching // 5. Raven firmware version estimation from service UUID patterns // // WiFi AP "flockyou" / "flockyou123" serves web dashboard at 192.168.4.1 // All detections stored in memory, exportable as JSON or CSV // Optional WiFi STA connection for future features // ============================================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "esp_wifi.h" // ============================================================================ // CONFIGURATION // ============================================================================ #define BUZZER_PIN 3 // Audio #define LOW_FREQ 200 #define HIGH_FREQ 800 #define DETECT_FREQ 1000 #define HEARTBEAT_FREQ 600 #define BOOT_BEEP_DURATION 300 #define DETECT_BEEP_DURATION 150 #define HEARTBEAT_DURATION 100 // BLE scanning #define BLE_SCAN_DURATION 2 // seconds per scan #define BLE_SCAN_INTERVAL 3000 // ms between scans // Detection storage #define MAX_DETECTIONS 200 // WiFi AP credentials #define FY_AP_SSID "flockyou" #define FY_AP_PASS "flockyou123" // ============================================================================ // DETECTION PATTERNS // ============================================================================ // Known Flock Safety MAC address prefixes (OUIs) static const char* mac_prefixes[] = { // FS Ext Battery devices "58:8e:81", "cc:cc:cc", "ec:1b:bd", "90:35:ea", "04:0d:84", "f0:82:c0", "1c:34:f1", "38:5b:44", "94:34:69", "b4:e3:f9", // Flock WiFi devices (Liteon Technology + USI) // These OUIs belong to Liteon Technology and USI (Universal Scientific // Industrial) — contract manufacturers that produce Flock Safety's // WiFi-enabled cameras. Sourced from OUI-SPY firmware ecosystem table // cross-referenced with IEEE OUI registry and field observations. "70:c9:4e", "3c:91:80", "d8:f3:bc", "80:30:49", "14:5a:fc", "74:4c:a1", "08:3a:88", "9c:2f:9d", "94:08:53", "e4:aa:ea", "f4:6a:dd", "f8:a2:d6", "e0:0a:f6", "00:f4:8d", "d0:39:57", "e8:d0:fc", // Flock Safety (direct IEEE registration) // b4:1e:52 is registered directly to "Flock Safety" in the IEEE OUI // database — this is their own prefix, not a contract manufacturer. "b4:1e:52", // ShotSpotter / SoundThinking // d4:11:d6 is registered to SoundThinking (formerly ShotSpotter) in // the IEEE OUI database. Their acoustic gunshot detection sensors use // BLE for local diagnostics and provisioning. "d4:11:d6" }; // BLE device name patterns (matched case-insensitive substring) static const char* device_name_patterns[] = { "FS Ext Battery", "Penguin", "Flock", "Pigvision" }; // BLE Manufacturer Company IDs // Source: wgreenberg/flock-you - XUNTONG ID associated with Flock Safety devices static const uint16_t ble_manufacturer_ids[] = { 0x09C8 // XUNTONG }; // ============================================================================ // RAVEN SURVEILLANCE DEVICE UUID PATTERNS // ============================================================================ #define RAVEN_DEVICE_INFO_SERVICE "0000180a-0000-1000-8000-00805f9b34fb" #define RAVEN_GPS_SERVICE "00003100-0000-1000-8000-00805f9b34fb" #define RAVEN_POWER_SERVICE "00003200-0000-1000-8000-00805f9b34fb" #define RAVEN_NETWORK_SERVICE "00003300-0000-1000-8000-00805f9b34fb" #define RAVEN_UPLOAD_SERVICE "00003400-0000-1000-8000-00805f9b34fb" #define RAVEN_ERROR_SERVICE "00003500-0000-1000-8000-00805f9b34fb" #define RAVEN_OLD_HEALTH_SERVICE "00001809-0000-1000-8000-00805f9b34fb" #define RAVEN_OLD_LOCATION_SERVICE "00001819-0000-1000-8000-00805f9b34fb" static const char* raven_service_uuids[] = { RAVEN_DEVICE_INFO_SERVICE, RAVEN_GPS_SERVICE, RAVEN_POWER_SERVICE, RAVEN_NETWORK_SERVICE, RAVEN_UPLOAD_SERVICE, RAVEN_ERROR_SERVICE, RAVEN_OLD_HEALTH_SERVICE, RAVEN_OLD_LOCATION_SERVICE }; // ============================================================================ // DETECTION STORAGE // ============================================================================ struct FYDetection { char mac[18]; char name[48]; int rssi; char method[24]; unsigned long firstSeen; unsigned long lastSeen; int count; bool isRaven; char ravenFW[16]; // GPS from phone (wardriving) double gpsLat; double gpsLon; float gpsAcc; bool hasGPS; }; static FYDetection fyDet[MAX_DETECTIONS]; static int fyDetCount = 0; static SemaphoreHandle_t fyMutex = NULL; // ============================================================================ // GLOBALS // ============================================================================ static bool fyBuzzerOn = true; static unsigned long fyLastBleScan = 0; static bool fyTriggered = false; static bool fyDeviceInRange = false; static unsigned long fyLastDetTime = 0; static unsigned long fyLastHB = 0; static NimBLEScan* fyBLEScan = NULL; static AsyncWebServer fyServer(80); // Phone GPS state (updated via browser Geolocation API -> /api/gps) static double fyGPSLat = 0; static double fyGPSLon = 0; static float fyGPSAcc = 0; static bool fyGPSValid = false; static unsigned long fyGPSLastUpdate = 0; #define GPS_STALE_MS 30000 // GPS considered stale after 30s without update // Session persistence (SPIFFS) #define FY_SESSION_FILE "/session.json" #define FY_PREV_FILE "/prev_session.json" #define FY_SAVE_INTERVAL 60000 // Auto-save every 60 seconds static unsigned long fyLastSave = 0; static int fyLastSaveCount = 0; // Track changes to avoid unnecessary writes static bool fySpiffsReady = false; // ============================================================================ // AUDIO SYSTEM // ============================================================================ static void fyBeep(int freq, int dur) { if (!fyBuzzerOn) return; tone(BUZZER_PIN, freq, dur); delay(dur + 50); } // Crow caw: harsh descending sweep with warble texture static void fyCaw(int startFreq, int endFreq, int durationMs, int warbleHz) { if (!fyBuzzerOn) return; int steps = durationMs / 8; // 8ms per step float fStep = (float)(endFreq - startFreq) / steps; for (int i = 0; i < steps; i++) { int f = startFreq + (int)(fStep * i); // Add warble: oscillate frequency +/- for raspy texture if (warbleHz > 0 && (i % 3 == 0)) { f += ((i % 6 < 3) ? warbleHz : -warbleHz); } if (f < 100) f = 100; tone(BUZZER_PIN, f, 10); delay(8); } noTone(BUZZER_PIN); } static void fyBootBeep() { printf("[FLOCK-YOU] Boot sound (buzzer %s)\n", fyBuzzerOn ? "ON" : "OFF"); if (!fyBuzzerOn) return; // === CROW CALL SEQUENCE === // Caw 1: sharp descending caw fyCaw(850, 380, 180, 40); delay(100); // Caw 2: slightly lower, shorter fyCaw(780, 350, 150, 50); delay(100); // Caw 3: longer trailing caw with more rasp fyCaw(820, 280, 220, 60); delay(80); // Quick staccato ending "kk-kk" tone(BUZZER_PIN, 600, 25); delay(40); tone(BUZZER_PIN, 550, 25); delay(40); noTone(BUZZER_PIN); printf("[FLOCK-YOU] *caw caw caw*\n"); } static void fyDetectBeep() { printf("[FLOCK-YOU] Detection alert!\n"); 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 } static void fyHeartbeat() { if (!fyBuzzerOn) return; // Soft double coo - like a distant crow fyCaw(500, 400, 80, 20); delay(120); fyCaw(480, 380, 80, 20); } // ============================================================================ // DETECTION HELPERS // ============================================================================ static bool checkMACPrefix(const uint8_t* mac) { char mac_str[9]; snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x", mac[0], mac[1], mac[2]); for (size_t i = 0; i < sizeof(mac_prefixes)/sizeof(mac_prefixes[0]); i++) { if (strncasecmp(mac_str, mac_prefixes[i], 8) == 0) return true; } return false; } 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; } return false; } 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; } return false; } // ============================================================================ // RAVEN UUID DETECTION // ============================================================================ static bool checkRavenUUID(NimBLEAdvertisedDevice* device, char* out_uuid = nullptr) { if (!device || !device->haveServiceUUID()) return false; int count = device->getServiceUUIDCount(); if (count == 0) return false; for (int i = 0; i < count; i++) { NimBLEUUID svc = device->getServiceUUID(i); std::string str = svc.toString(); for (size_t j = 0; j < sizeof(raven_service_uuids)/sizeof(raven_service_uuids[0]); j++) { if (strcasecmp(str.c_str(), raven_service_uuids[j]) == 0) { if (out_uuid) strncpy(out_uuid, str.c_str(), 40); return true; } } } return false; } static const char* estimateRavenFW(NimBLEAdvertisedDevice* device) { if (!device || !device->haveServiceUUID()) return "?"; bool has_new_gps = false, has_old_loc = false, has_power = false; int count = device->getServiceUUIDCount(); for (int i = 0; i < count; i++) { std::string u = device->getServiceUUID(i).toString(); if (strcasecmp(u.c_str(), RAVEN_GPS_SERVICE) == 0) has_new_gps = true; if (strcasecmp(u.c_str(), RAVEN_OLD_LOCATION_SERVICE) == 0) has_old_loc = true; if (strcasecmp(u.c_str(), RAVEN_POWER_SERVICE) == 0) has_power = true; } if (has_old_loc && !has_new_gps) return "1.1.x"; if (has_new_gps && !has_power) return "1.2.x"; if (has_new_gps && has_power) return "1.3.x"; return "?"; } // ============================================================================ // GPS HELPERS // ============================================================================ static bool fyGPSIsFresh() { return fyGPSValid && (millis() - fyGPSLastUpdate < GPS_STALE_MS); } static void fyAttachGPS(FYDetection& d) { if (fyGPSIsFresh()) { d.hasGPS = true; d.gpsLat = fyGPSLat; d.gpsLon = fyGPSLon; d.gpsAcc = fyGPSAcc; } } // ============================================================================ // DETECTION MANAGEMENT // ============================================================================ static int fyAddDetection(const char* mac, const char* name, int rssi, const char* method, bool isRaven = false, const char* ravenFW = "") { if (!fyMutex || xSemaphoreTake(fyMutex, pdMS_TO_TICKS(100)) != pdTRUE) return -1; // Update existing by MAC for (int i = 0; i < fyDetCount; i++) { if (strcasecmp(fyDet[i].mac, mac) == 0) { fyDet[i].count++; fyDet[i].lastSeen = millis(); fyDet[i].rssi = rssi; if (name && name[0]) { strncpy(fyDet[i].name, name, sizeof(fyDet[i].name) - 1); } // Update GPS on every re-sighting (captures movement) fyAttachGPS(fyDet[i]); xSemaphoreGive(fyMutex); return i; } } // Add new if (fyDetCount < MAX_DETECTIONS) { FYDetection& d = fyDet[fyDetCount]; memset(&d, 0, sizeof(d)); strncpy(d.mac, mac, sizeof(d.mac) - 1); // Sanitize name for JSON safety if (name) { for (int j = 0; j < (int)sizeof(d.name) - 1 && name[j]; j++) { d.name[j] = (name[j] == '"' || name[j] == '\\') ? '_' : name[j]; } } d.rssi = rssi; strncpy(d.method, method, sizeof(d.method) - 1); d.firstSeen = millis(); d.lastSeen = millis(); d.count = 1; d.isRaven = isRaven; strncpy(d.ravenFW, ravenFW ? ravenFW : "", sizeof(d.ravenFW) - 1); // Attach GPS from phone fyAttachGPS(d); int idx = fyDetCount++; xSemaphoreGive(fyMutex); return idx; } xSemaphoreGive(fyMutex); return -1; } // ============================================================================ // BLE SCANNING // ============================================================================ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks { void onResult(NimBLEAdvertisedDevice* dev) override { NimBLEAddress addr = dev->getAddress(); std::string addrStr = addr.toString(); // Safe MAC byte extraction unsigned int m[6]; sscanf(addrStr.c_str(), "%02x:%02x:%02x:%02x:%02x:%02x", &m[0], &m[1], &m[2], &m[3], &m[4], &m[5]); uint8_t mac[6] = {(uint8_t)m[0], (uint8_t)m[1], (uint8_t)m[2], (uint8_t)m[3], (uint8_t)m[4], (uint8_t)m[5]}; int rssi = dev->getRSSI(); std::string name = dev->haveName() ? dev->getName() : ""; bool detected = false; const char* method = ""; bool isRaven = false; const char* ravenFW = ""; // 1. Check MAC prefix against known Flock Safety OUIs if (checkMACPrefix(mac)) { detected = true; method = "mac_prefix"; } // 2. Check BLE device name patterns if (!detected && !name.empty() && checkDeviceName(name.c_str())) { detected = true; method = "device_name"; } // 3. Check BLE manufacturer company IDs (from wgreenberg/flock-you) 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)) { detected = true; method = "ble_mfr_id"; break; } } } } // 4. Check Raven gunshot detector service UUIDs if (!detected) { char detUUID[41] = {0}; if (checkRavenUUID(dev, detUUID)) { detected = true; method = "raven_uuid"; isRaven = true; ravenFW = estimateRavenFW(dev); } } if (detected) { int idx = fyAddDetection(addrStr.c_str(), name.c_str(), rssi, method, isRaven, ravenFW); // Human-readable log printf("[FLOCK-YOU] DETECTED: %s %s RSSI:%d [%s] count:%d\n", addrStr.c_str(), name.c_str(), rssi, method, idx >= 0 ? fyDet[idx].count : 0); // JSON serial output (Flask-compatible format for live ingestion) // Build GPS fragment if available char gpsBuf[80] = ""; if (fyGPSIsFresh()) { snprintf(gpsBuf, sizeof(gpsBuf), ",\"gps\":{\"latitude\":%.8f,\"longitude\":%.8f,\"accuracy\":%.1f}", fyGPSLat, fyGPSLon, fyGPSAcc); } if (isRaven) { printf("{\"detection_method\":\"%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); } else { printf("{\"detection_method\":\"%s\",\"protocol\":\"bluetooth_le\"," "\"mac_address\":\"%s\",\"device_name\":\"%s\"," "\"rssi\":%d%s}\n", method, addrStr.c_str(), name.c_str(), rssi, gpsBuf); } if (!fyTriggered) { fyTriggered = true; fyDetectBeep(); } fyDeviceInRange = true; fyLastDetTime = millis(); fyLastHB = millis(); } } }; // ============================================================================ // JSON HELPER // ============================================================================ static void writeDetectionsJSON(AsyncResponseStream *resp) { resp->print("["); if (fyMutex && xSemaphoreTake(fyMutex, pdMS_TO_TICKS(200)) == pdTRUE) { for (int i = 0; i < fyDetCount; i++) { if (i > 0) resp->print(","); resp->printf( "{\"mac\":\"%s\",\"name\":\"%s\",\"rssi\":%d,\"method\":\"%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].firstSeen, fyDet[i].lastSeen, fyDet[i].count, fyDet[i].isRaven ? "true" : "false", fyDet[i].ravenFW); // Append GPS if present if (fyDet[i].hasGPS) { resp->printf(",\"gps\":{\"lat\":%.8f,\"lon\":%.8f,\"acc\":%.1f}", fyDet[i].gpsLat, fyDet[i].gpsLon, fyDet[i].gpsAcc); } resp->print("}"); } xSemaphoreGive(fyMutex); } resp->print("]"); } // ============================================================================ // SESSION PERSISTENCE (SPIFFS) // ============================================================================ static void fySaveSession() { if (!fySpiffsReady || !fyMutex) return; if (xSemaphoreTake(fyMutex, pdMS_TO_TICKS(300)) != pdTRUE) return; File f = SPIFFS.open(FY_SESSION_FILE, "w"); if (!f) { xSemaphoreGive(fyMutex); return; } f.print("["); for (int i = 0; i < fyDetCount; i++) { if (i > 0) f.print(","); FYDetection& d = fyDet[i]; f.printf("{\"mac\":\"%s\",\"name\":\"%s\",\"rssi\":%d,\"method\":\"%s\"," "\"first\":%lu,\"last\":%lu,\"count\":%d," "\"raven\":%s,\"fw\":\"%s\"", d.mac, d.name, d.rssi, d.method, d.firstSeen, d.lastSeen, d.count, d.isRaven ? "true" : "false", d.ravenFW); if (d.hasGPS) { f.printf(",\"gps\":{\"lat\":%.8f,\"lon\":%.8f,\"acc\":%.1f}", d.gpsLat, d.gpsLon, d.gpsAcc); } f.print("}"); } f.print("]"); f.close(); fyLastSaveCount = fyDetCount; printf("[FLOCK-YOU] Session saved: %d detections\n", fyDetCount); xSemaphoreGive(fyMutex); } static void fyPromotePrevSession() { // Move current session file to prev_session on boot if (!fySpiffsReady) return; if (SPIFFS.exists(FY_SESSION_FILE)) { // Remove old prev if exists if (SPIFFS.exists(FY_PREV_FILE)) SPIFFS.remove(FY_PREV_FILE); SPIFFS.rename(FY_SESSION_FILE, FY_PREV_FILE); printf("[FLOCK-YOU] Prior session promoted from flash\n"); } } // ============================================================================ // KML EXPORT // ============================================================================ static void writeDetectionsKML(AsyncResponseStream *resp) { resp->print("\n" "\n\n" "Flock-You Detections\n" "Surveillance device detections with GPS\n"); // Detection pin style resp->print("\n" "\n"); if (fyMutex && xSemaphoreTake(fyMutex, pdMS_TO_TICKS(300)) == pdTRUE) { for (int i = 0; i < fyDetCount; i++) { FYDetection& d = fyDet[i]; if (!d.hasGPS) continue; // Skip detections without GPS resp->print("\n"); resp->printf("%s\n", d.mac); resp->printf("#%s\n", d.isRaven ? "raven" : "det"); resp->print("printf("Name: %s
", d.name); resp->printf("Method: %s
" "RSSI: %d dBm
" "Count: %d
", d.method, d.rssi, d.count); if (d.isRaven) resp->printf("Raven FW: %s
", d.ravenFW); resp->printf("Accuracy: %.1f m", d.gpsAcc); resp->print("]]>
\n"); resp->printf("%.8f,%.8f,0\n", d.gpsLon, d.gpsLat); resp->print("
\n"); } xSemaphoreGive(fyMutex); } resp->print("
\n
"); } // ============================================================================ // DASHBOARD HTML // ============================================================================ static const char FY_HTML[] PROGMEM = R"rawliteral( FLOCK-YOU

FLOCK-YOU

Surveillance Device Detector • Wardriving + GPS
0
DETECTED
0
RAVEN
ON
BLE
TAP
GPS
Scanning for surveillance devices...
BLE active on all channels
Loading prior session...
Loading patterns...

EXPORT DETECTIONS

Download current session to import into Flask dashboard


PRIOR SESSION


)rawliteral"; // ============================================================================ // WEB SERVER SETUP // ============================================================================ static void fySetupServer() { // Dashboard fyServer.on("/", HTTP_GET, [](AsyncWebServerRequest *r) { r->send(200, "text/html", FY_HTML); }); // API: Detection list fyServer.on("/api/detections", HTTP_GET, [](AsyncWebServerRequest *r) { AsyncResponseStream *resp = r->beginResponseStream("application/json"); writeDetectionsJSON(resp); r->send(resp); }); // API: Stats (includes GPS status) fyServer.on("/api/stats", HTTP_GET, [](AsyncWebServerRequest *r) { int raven = 0, withGPS = 0; if (fyMutex && xSemaphoreTake(fyMutex, pdMS_TO_TICKS(100)) == pdTRUE) { for (int i = 0; i < fyDetCount; i++) { if (fyDet[i].isRaven) raven++; if (fyDet[i].hasGPS) withGPS++; } xSemaphoreGive(fyMutex); } char buf[256]; snprintf(buf, sizeof(buf), "{\"total\":%d,\"raven\":%d,\"ble\":\"active\"," "\"gps_valid\":%s,\"gps_age\":%lu,\"gps_tagged\":%d}", fyDetCount, raven, fyGPSIsFresh() ? "true" : "false", fyGPSValid ? (millis() - fyGPSLastUpdate) : 0UL, withGPS); r->send(200, "application/json", buf); }); // API: Receive GPS from phone browser fyServer.on("/api/gps", HTTP_GET, [](AsyncWebServerRequest *r) { if (r->hasParam("lat") && r->hasParam("lon")) { fyGPSLat = r->getParam("lat")->value().toDouble(); fyGPSLon = r->getParam("lon")->value().toDouble(); fyGPSAcc = r->hasParam("acc") ? r->getParam("acc")->value().toFloat() : 0; fyGPSValid = true; fyGPSLastUpdate = millis(); r->send(200, "application/json", "{\"status\":\"ok\"}"); } else { r->send(400, "application/json", "{\"error\":\"lat,lon required\"}"); } }); // API: Pattern database fyServer.on("/api/patterns", HTTP_GET, [](AsyncWebServerRequest *r) { AsyncResponseStream *resp = r->beginResponseStream("application/json"); resp->print("{\"macs\":["); for (size_t i = 0; i < sizeof(mac_prefixes)/sizeof(mac_prefixes[0]); i++) { if (i > 0) resp->print(","); resp->printf("\"%s\"", mac_prefixes[i]); } resp->print("],\"names\":["); for (size_t i = 0; i < sizeof(device_name_patterns)/sizeof(device_name_patterns[0]); i++) { if (i > 0) resp->print(","); resp->printf("\"%s\"", device_name_patterns[i]); } resp->print("],\"mfr\":["); for (size_t i = 0; i < sizeof(ble_manufacturer_ids)/sizeof(ble_manufacturer_ids[0]); i++) { if (i > 0) resp->print(","); resp->printf("%u", ble_manufacturer_ids[i]); } resp->print("],\"raven\":["); for (size_t i = 0; i < sizeof(raven_service_uuids)/sizeof(raven_service_uuids[0]); i++) { if (i > 0) resp->print(","); resp->printf("\"%s\"", raven_service_uuids[i]); } resp->print("]}"); r->send(resp); }); // API: Export JSON (downloadable file) fyServer.on("/api/export/json", HTTP_GET, [](AsyncWebServerRequest *r) { AsyncResponseStream *resp = r->beginResponseStream("application/json"); resp->addHeader("Content-Disposition", "attachment; filename=\"flockyou_detections.json\""); writeDetectionsJSON(resp); r->send(resp); }); // API: Export CSV (downloadable file, includes GPS) fyServer.on("/api/export/csv", HTTP_GET, [](AsyncWebServerRequest *r) { AsyncResponseStream *resp = r->beginResponseStream("text/csv"); resp->addHeader("Content-Disposition", "attachment; filename=\"flockyou_detections.csv\""); resp->println("mac,name,rssi,method,first_seen_ms,last_seen_ms,count,is_raven,raven_fw,latitude,longitude,gps_accuracy"); if (fyMutex && xSemaphoreTake(fyMutex, pdMS_TO_TICKS(200)) == pdTRUE) { for (int i = 0; i < fyDetCount; i++) { FYDetection& d = fyDet[i]; if (d.hasGPS) { resp->printf("\"%s\",\"%s\",%d,\"%s\",%lu,%lu,%d,%s,\"%s\",%.8f,%.8f,%.1f\n", d.mac, d.name, d.rssi, d.method, d.firstSeen, d.lastSeen, d.count, d.isRaven ? "true" : "false", d.ravenFW, d.gpsLat, d.gpsLon, d.gpsAcc); } else { resp->printf("\"%s\",\"%s\",%d,\"%s\",%lu,%lu,%d,%s,\"%s\",,,\n", d.mac, d.name, d.rssi, d.method, d.firstSeen, d.lastSeen, d.count, d.isRaven ? "true" : "false", d.ravenFW); } } xSemaphoreGive(fyMutex); } r->send(resp); }); // API: Export KML (GPS-tagged detections for Google Earth) fyServer.on("/api/export/kml", HTTP_GET, [](AsyncWebServerRequest *r) { AsyncResponseStream *resp = r->beginResponseStream("application/vnd.google-earth.kml+xml"); resp->addHeader("Content-Disposition", "attachment; filename=\"flockyou_detections.kml\""); writeDetectionsKML(resp); r->send(resp); }); // API: Prior session history (JSON) fyServer.on("/api/history", HTTP_GET, [](AsyncWebServerRequest *r) { if (fySpiffsReady && SPIFFS.exists(FY_PREV_FILE)) { r->send(SPIFFS, FY_PREV_FILE, "application/json"); } else { r->send(200, "application/json", "[]"); } }); // API: Download prior session as JSON file fyServer.on("/api/history/json", HTTP_GET, [](AsyncWebServerRequest *r) { if (fySpiffsReady && SPIFFS.exists(FY_PREV_FILE)) { AsyncWebServerResponse *resp = r->beginResponse(SPIFFS, FY_PREV_FILE, "application/json"); resp->addHeader("Content-Disposition", "attachment; filename=\"flockyou_prev_session.json\""); r->send(resp); } else { r->send(404, "application/json", "{\"error\":\"no prior session\"}"); } }); // API: Download prior session as KML (reads JSON from SPIFFS, converts) fyServer.on("/api/history/kml", HTTP_GET, [](AsyncWebServerRequest *r) { if (!fySpiffsReady || !SPIFFS.exists(FY_PREV_FILE)) { r->send(404, "application/json", "{\"error\":\"no prior session\"}"); return; } AsyncResponseStream *resp = r->beginResponseStream("application/vnd.google-earth.kml+xml"); resp->addHeader("Content-Disposition", "attachment; filename=\"flockyou_prev_session.kml\""); // Read prev session and generate KML File f = SPIFFS.open(FY_PREV_FILE, "r"); if (!f) { r->send(500, "text/plain", "read error"); return; } String content = f.readString(); f.close(); resp->print("\n" "\n\n" "Flock-You Prior Session\n"); // Parse JSON array and emit placemarks JsonDocument doc; DeserializationError err = deserializeJson(doc, content); if (!err && doc.is()) { for (JsonObject d : doc.as()) { JsonObject gps = d["gps"]; if (!gps || !gps.containsKey("lat")) continue; resp->printf("%s\n", d["mac"] | "?"); resp->print("() && strlen(d["name"] | "") > 0) resp->printf("Name: %s
", d["name"] | ""); resp->printf("Method: %s
RSSI: %d
Count: %d", d["method"] | "?", d["rssi"] | 0, d["count"] | 1); resp->print("]]>
\n"); resp->printf("%.8f,%.8f,0\n", (double)(gps["lon"] | 0.0), (double)(gps["lat"] | 0.0)); resp->print("
\n"); } } resp->print("
\n
"); r->send(resp); }); // API: Clear all detections (saves current session first) fyServer.on("/api/clear", HTTP_GET, [](AsyncWebServerRequest *r) { fySaveSession(); // Persist before clearing if (fyMutex && xSemaphoreTake(fyMutex, pdMS_TO_TICKS(200)) == pdTRUE) { fyDetCount = 0; memset(fyDet, 0, sizeof(fyDet)); fyTriggered = false; fyDeviceInRange = false; xSemaphoreGive(fyMutex); } r->send(200, "application/json", "{\"status\":\"cleared\"}"); printf("[FLOCK-YOU] All detections cleared (session saved)\n"); }); fyServer.begin(); printf("[FLOCK-YOU] Web server started on port 80\n"); } // ============================================================================ // MAIN FUNCTIONS // ============================================================================ void setup() { Serial.begin(115200); delay(500); // Standalone mode: buzzer always on by default fyBuzzerOn = true; pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW); fyMutex = xSemaphoreCreateMutex(); // Init SPIFFS for session persistence if (SPIFFS.begin(true)) { fySpiffsReady = true; printf("[FLOCK-YOU] SPIFFS ready\n"); // Promote last session to prev_session before we start a new one fyPromotePrevSession(); } else { printf("[FLOCK-YOU] SPIFFS init failed - no persistence\n"); } printf("\n========================================\n"); printf(" FLOCK-YOU Surveillance Detector\n"); printf(" Buzzer: %s\n", fyBuzzerOn ? "ON" : "OFF"); printf("========================================\n"); // Init BLE scanner FIRST -- start scanning immediately NimBLEDevice::init(""); fyBLEScan = NimBLEDevice::getScan(); fyBLEScan->setAdvertisedDeviceCallbacks(new FYBLECallbacks()); fyBLEScan->setActiveScan(true); fyBLEScan->setInterval(100); fyBLEScan->setWindow(99); // Kick off the first scan right away fyBLEScan->start(BLE_SCAN_DURATION, false); fyLastBleScan = millis(); printf("[FLOCK-YOU] BLE scanning ACTIVE\n"); // Crow calls play WHILE BLE is already scanning fyBootBeep(); // Start WiFi AP (no need to connect to anything -- AP only) WiFi.mode(WIFI_AP); delay(100); WiFi.softAP(FY_AP_SSID, FY_AP_PASS); printf("[FLOCK-YOU] AP: %s / %s\n", FY_AP_SSID, FY_AP_PASS); printf("[FLOCK-YOU] IP: %s\n", WiFi.softAPIP().toString().c_str()); // Start web dashboard fySetupServer(); printf("[FLOCK-YOU] Detection methods: MAC prefix, device name, manufacturer ID, Raven UUID\n"); printf("[FLOCK-YOU] Dashboard: http://192.168.4.1\n"); printf("[FLOCK-YOU] Ready - no WiFi connection needed, BLE + AP only\n\n"); } void loop() { // BLE scanning cycle if (millis() - fyLastBleScan >= BLE_SCAN_INTERVAL && !fyBLEScan->isScanning()) { fyBLEScan->start(BLE_SCAN_DURATION, false); fyLastBleScan = millis(); } if (!fyBLEScan->isScanning() && millis() - fyLastBleScan > BLE_SCAN_DURATION * 1000) { fyBLEScan->clearResults(); } // Heartbeat tracking if (fyDeviceInRange) { if (millis() - fyLastHB >= 10000) { fyHeartbeat(); fyLastHB = millis(); } if (millis() - fyLastDetTime >= 30000) { printf("[FLOCK-YOU] Device out of range - stopping heartbeat\n"); fyDeviceInRange = false; fyTriggered = false; } } // Auto-save session to SPIFFS every 60s if detections changed if (fySpiffsReady && millis() - fyLastSave >= FY_SAVE_INTERVAL) { if (fyDetCount > 0 && fyDetCount != fyLastSaveCount) { fySaveSession(); } fyLastSave = millis(); } delay(100); }