From d255408bb869f8c9b69bf5c05837e3c9c4a01630 Mon Sep 17 00:00:00 2001 From: Doug Borg Date: Sat, 7 Feb 2026 20:38:09 -0700 Subject: [PATCH 01/19] Add BLE GATT server, serial host detection, and companion mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable DeFlock mobile app connectivity via BLE GATT notifications, and desktop host detection via USB serial heartbeat. When a companion is connected, WiFi AP is disabled to free radio bandwidth and BLE scan duty cycle is increased for better detection performance. - BLE GATT server advertising service UUID a1b2c3d4-e5f6-7890-abcd-ef0123456789 with TX characteristic (NOTIFY) for streaming detection JSON - Chunked BLE notification sender respecting negotiated MTU - "event":"detection" field added to JSON output for DeFlock parser - Serial host detection via heartbeat timeout (5s) - Companion mode: WiFi AP off + scan duration 2s→3s when connected - Scan interval/duration converted from #define to mutable variables Co-Authored-By: Claude Opus 4.6 --- src/main.cpp | 167 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 147 insertions(+), 20 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 42d68db..4d2bee8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -43,9 +43,9 @@ #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 +// BLE scanning (mutable — companion mode increases scan duty cycle) +static int fyBleScanDuration = 2; // seconds per scan +static unsigned long fyBleScanInterval = 3000; // ms between scans // Detection storage #define MAX_DETECTIONS 200 @@ -144,6 +144,19 @@ static unsigned long fyLastHB = 0; static NimBLEScan* fyBLEScan = NULL; static AsyncWebServer fyServer(80); +// BLE GATT server (DeFlock app connectivity) +#define FY_SERVICE_UUID "a1b2c3d4-e5f6-7890-abcd-ef0123456789" +#define FY_TX_CHAR_UUID "a1b2c3d4-e5f6-7890-abcd-ef01234567aa" +static NimBLEServer* fyBLEServer = NULL; +static NimBLECharacteristic* fyTxChar = NULL; +static volatile bool fyBLEClientConnected = false; +static volatile uint16_t fyNegotiatedMTU = 23; + +// Serial host detection (USB heartbeat from DeFlock desktop) +static volatile bool fySerialHostConnected = false; +static unsigned long fyLastSerialHeartbeat = 0; +#define FY_SERIAL_TIMEOUT_MS 5000 + // Phone GPS state (updated via browser Geolocation API -> /api/gps) static double fyGPSLat = 0; static double fyGPSLon = 0; @@ -368,6 +381,74 @@ static int fyAddDetection(const char* mac, const char* name, int rssi, return -1; } +// ============================================================================ +// BLE GATT SERVER (DeFlock companion connectivity) +// ============================================================================ + +// Forward declaration +static void fyOnCompanionChange(); + +class FYServerCallbacks : public NimBLEServerCallbacks { + void onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) override { + fyBLEClientConnected = true; + printf("[FLOCK-YOU] BLE client connected\n"); + fyOnCompanionChange(); + } + void onDisconnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) override { + fyBLEClientConnected = false; + fyNegotiatedMTU = 23; + printf("[FLOCK-YOU] BLE client disconnected\n"); + NimBLEDevice::startAdvertising(); + fyOnCompanionChange(); + } + void onMTUChange(uint16_t mtu, ble_gap_conn_desc* desc) override { + fyNegotiatedMTU = mtu; + printf("[FLOCK-YOU] MTU negotiated: %u\n", mtu); + } +}; + +static void fySendBLE(const char* data, size_t len) { + if (!fyBLEClientConnected || !fyTxChar) return; + uint16_t chunkSize = fyNegotiatedMTU - 3; + if (chunkSize < 1) chunkSize = 1; + if (len <= chunkSize) { + fyTxChar->setValue((const uint8_t*)data, len); + fyTxChar->notify(); + } else { + size_t offset = 0; + while (offset < len) { + size_t remaining = len - offset; + size_t send = remaining < chunkSize ? remaining : chunkSize; + fyTxChar->setValue((const uint8_t*)(data + offset), send); + fyTxChar->notify(); + offset += send; + } + } +} + +// ============================================================================ +// COMPANION MODE (WiFi AP vs BLE/serial) +// ============================================================================ + +static void fyOnCompanionChange() { + if (fyBLEClientConnected || fySerialHostConnected) { + // Companion mode — disable WiFi AP, boost BLE scanning + WiFi.softAPdisconnect(true); + WiFi.mode(WIFI_OFF); + fyBleScanDuration = 3; + printf("[FLOCK-YOU] Companion mode: WiFi AP OFF, scan duration %ds\n", + fyBleScanDuration); + } else { + // Standalone mode — re-enable WiFi AP and web dashboard + WiFi.mode(WIFI_AP); + delay(100); + WiFi.softAP(FY_AP_SSID, FY_AP_PASS); + fyBleScanDuration = 2; + printf("[FLOCK-YOU] Standalone mode: WiFi AP ON (%s), scan duration %ds\n", + FY_AP_SSID, fyBleScanDuration); + } +} + // ============================================================================ // BLE SCANNING // ============================================================================ @@ -440,24 +521,34 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks { 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 + // JSON output — build into buffer for serial + BLE char gpsBuf[80] = ""; if (fyGPSIsFresh()) { snprintf(gpsBuf, sizeof(gpsBuf), ",\"gps\":{\"latitude\":%.8f,\"longitude\":%.8f,\"accuracy\":%.1f}", fyGPSLat, fyGPSLon, fyGPSAcc); } + char jsonBuf[512]; + int jsonLen; 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); + jsonLen = snprintf(jsonBuf, sizeof(jsonBuf), + "{\"event\":\"detection\",\"detection_method\":\"%s\"," + "\"protocol\":\"bluetooth_le\",\"mac_address\":\"%s\"," + "\"device_name\":\"%s\",\"rssi\":%d," + "\"is_raven\":true,\"raven_fw\":\"%s\"%s}", + 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); + jsonLen = snprintf(jsonBuf, sizeof(jsonBuf), + "{\"event\":\"detection\",\"detection_method\":\"%s\"," + "\"protocol\":\"bluetooth_le\",\"mac_address\":\"%s\"," + "\"device_name\":\"%s\",\"rssi\":%d%s}", + method, addrStr.c_str(), name.c_str(), rssi, gpsBuf); + } + printf("%s\n", jsonBuf); + // Append newline for BLE framing and send + if (jsonLen > 0 && jsonLen < (int)sizeof(jsonBuf) - 1) { + jsonBuf[jsonLen] = '\n'; + fySendBLE(jsonBuf, jsonLen + 1); } if (!fyTriggered) { @@ -929,8 +1020,11 @@ void setup() { printf(" Buzzer: %s\n", fyBuzzerOn ? "ON" : "OFF"); printf("========================================\n"); - // Init BLE scanner FIRST -- start scanning immediately - NimBLEDevice::init(""); + // Init BLE with device name and large MTU for GATT notifications + NimBLEDevice::init("flockyou"); + NimBLEDevice::setMTU(512); + + // BLE scanner setup fyBLEScan = NimBLEDevice::getScan(); fyBLEScan->setAdvertisedDeviceCallbacks(new FYBLECallbacks()); fyBLEScan->setActiveScan(true); @@ -938,10 +1032,27 @@ void setup() { fyBLEScan->setWindow(99); // Kick off the first scan right away - fyBLEScan->start(BLE_SCAN_DURATION, false); + fyBLEScan->start(fyBleScanDuration, false); fyLastBleScan = millis(); printf("[FLOCK-YOU] BLE scanning ACTIVE\n"); + // BLE GATT server — DeFlock app connectivity + fyBLEServer = NimBLEDevice::createServer(); + fyBLEServer->setCallbacks(new FYServerCallbacks()); + NimBLEService* pService = fyBLEServer->createService(FY_SERVICE_UUID); + fyTxChar = pService->createCharacteristic( + FY_TX_CHAR_UUID, + NIMBLE_PROPERTY::NOTIFY + ); + pService->start(); + + NimBLEAdvertising* pAdv = NimBLEDevice::getAdvertising(); + pAdv->addServiceUUID(FY_SERVICE_UUID); + pAdv->setName("flockyou"); + pAdv->setScanResponse(true); + pAdv->start(); + printf("[FLOCK-YOU] BLE GATT server advertising (service %s)\n", FY_SERVICE_UUID); + // Crow calls play WHILE BLE is already scanning fyBootBeep(); @@ -957,17 +1068,33 @@ void setup() { 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"); + printf("[FLOCK-YOU] Ready - BLE GATT + AP mode\n\n"); } void loop() { + // Serial host detection (heartbeat from DeFlock desktop app) + if (Serial.available()) { + while (Serial.available()) Serial.read(); // drain buffer + fyLastSerialHeartbeat = millis(); + if (!fySerialHostConnected) { + fySerialHostConnected = true; + printf("[FLOCK-YOU] Serial host connected\n"); + fyOnCompanionChange(); + } + } else if (fySerialHostConnected && + millis() - fyLastSerialHeartbeat >= FY_SERIAL_TIMEOUT_MS) { + fySerialHostConnected = false; + printf("[FLOCK-YOU] Serial host disconnected (timeout)\n"); + fyOnCompanionChange(); + } + // BLE scanning cycle - if (millis() - fyLastBleScan >= BLE_SCAN_INTERVAL && !fyBLEScan->isScanning()) { - fyBLEScan->start(BLE_SCAN_DURATION, false); + if (millis() - fyLastBleScan >= fyBleScanInterval && !fyBLEScan->isScanning()) { + fyBLEScan->start(fyBleScanDuration, false); fyLastBleScan = millis(); } - if (!fyBLEScan->isScanning() && millis() - fyLastBleScan > BLE_SCAN_DURATION * 1000) { + if (!fyBLEScan->isScanning() && millis() - fyLastBleScan > (unsigned long)fyBleScanDuration * 1000) { fyBLEScan->clearResults(); } From a8882b44f23a9fd6866cd3825ea21a7fe400aea9 Mon Sep 17 00:00:00 2001 From: Doug Borg Date: Sat, 7 Feb 2026 20:38:51 -0700 Subject: [PATCH 02/19] Add OUI prefixes for Flock WiFi cameras, Flock Safety direct, and ShotSpotter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand MAC prefix detection with entries sourced from: - Flock WiFi (Liteon/USI): f4:6a:dd, f8:a2:d6, e0:0a:f6, 00:f4:8d, d0:39:57, e8:d0:fc — contract manufacturer OUIs (Liteon Technology and USI/Universal Scientific Industrial) identified via the OUI-SPY firmware ecosystem table and cross-referenced against the IEEE OUI registry. These manufacturers produce Flock Safety's WiFi-enabled camera hardware. - Flock Safety direct: b4:1e:52 — registered directly to "Flock Safety" in the IEEE OUI database (MA-L assignment). This is their own prefix rather than a contract manufacturer's. - SoundThinking/ShotSpotter: d4:11:d6 — registered to "SoundThinking Inc" (formerly ShotSpotter) in the IEEE OUI database. Their acoustic gunshot detection sensors use BLE for local diagnostics and provisioning. Co-Authored-By: Claude Opus 4.6 --- src/main.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 42d68db..0f3a976 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -63,9 +63,24 @@ 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 + // 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" + "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) From 00afc2826a006fa49f9e8aa49af0a0b2660982d2 Mon Sep 17 00:00:00 2001 From: Doug Borg Date: Sat, 7 Feb 2026 21:03:19 -0700 Subject: [PATCH 03/19] Defer companion mode switch from BLE callbacks to loop() BLE server callbacks run on the NimBLE host task, not the Arduino loop task. Calling WiFi state changes and delay() from that context can stall BLE processing or trip watchdogs, and mutating scan duration creates a cross-task data race. Fix: callbacks now just set a volatile pending flag. The actual WiFi/scan changes are applied in loop() where they're safe. Co-Authored-By: Claude Opus 4.6 --- src/main.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 4d2bee8..da7e14d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -157,6 +157,10 @@ static volatile bool fySerialHostConnected = false; static unsigned long fyLastSerialHeartbeat = 0; #define FY_SERIAL_TIMEOUT_MS 5000 +// Deferred companion mode switch — BLE callbacks set this flag, +// loop() applies the WiFi/scan changes in the Arduino task context. +static volatile bool fyCompanionChangePending = false; + // Phone GPS state (updated via browser Geolocation API -> /api/gps) static double fyGPSLat = 0; static double fyGPSLon = 0; @@ -385,21 +389,16 @@ static int fyAddDetection(const char* mac, const char* name, int rssi, // BLE GATT SERVER (DeFlock companion connectivity) // ============================================================================ -// Forward declaration -static void fyOnCompanionChange(); - class FYServerCallbacks : public NimBLEServerCallbacks { void onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) override { fyBLEClientConnected = true; - printf("[FLOCK-YOU] BLE client connected\n"); - fyOnCompanionChange(); + fyCompanionChangePending = true; } void onDisconnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) override { fyBLEClientConnected = false; fyNegotiatedMTU = 23; - printf("[FLOCK-YOU] BLE client disconnected\n"); NimBLEDevice::startAdvertising(); - fyOnCompanionChange(); + fyCompanionChangePending = true; } void onMTUChange(uint16_t mtu, ble_gap_conn_desc* desc) override { fyNegotiatedMTU = mtu; @@ -1078,13 +1077,17 @@ void loop() { fyLastSerialHeartbeat = millis(); if (!fySerialHostConnected) { fySerialHostConnected = true; - printf("[FLOCK-YOU] Serial host connected\n"); - fyOnCompanionChange(); + fyCompanionChangePending = true; } } else if (fySerialHostConnected && millis() - fyLastSerialHeartbeat >= FY_SERIAL_TIMEOUT_MS) { fySerialHostConnected = false; - printf("[FLOCK-YOU] Serial host disconnected (timeout)\n"); + fyCompanionChangePending = true; + } + + // Apply deferred companion mode switch (from BLE callbacks or serial detection) + if (fyCompanionChangePending) { + fyCompanionChangePending = false; fyOnCompanionChange(); } From 06d734992f466788527c26d524395969e02aede9 Mon Sep 17 00:00:00 2001 From: Doug Borg Date: Sat, 7 Feb 2026 21:46:51 -0700 Subject: [PATCH 04/19] Always emit is_raven and raven_fw in detection JSON Collapse the two-branch snprintf into a single call so every detection message includes is_raven (true/false) and raven_fw, making the format self-describing regardless of device type. Co-Authored-By: Claude Opus 4.6 --- src/main.cpp | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index da7e14d..08e1f33 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -528,21 +528,13 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks { fyGPSLat, fyGPSLon, fyGPSAcc); } char jsonBuf[512]; - int jsonLen; - if (isRaven) { - jsonLen = snprintf(jsonBuf, sizeof(jsonBuf), - "{\"event\":\"detection\",\"detection_method\":\"%s\"," - "\"protocol\":\"bluetooth_le\",\"mac_address\":\"%s\"," - "\"device_name\":\"%s\",\"rssi\":%d," - "\"is_raven\":true,\"raven_fw\":\"%s\"%s}", - method, addrStr.c_str(), name.c_str(), rssi, ravenFW, gpsBuf); - } else { - jsonLen = snprintf(jsonBuf, sizeof(jsonBuf), - "{\"event\":\"detection\",\"detection_method\":\"%s\"," - "\"protocol\":\"bluetooth_le\",\"mac_address\":\"%s\"," - "\"device_name\":\"%s\",\"rssi\":%d%s}", - method, addrStr.c_str(), name.c_str(), rssi, gpsBuf); - } + int jsonLen = snprintf(jsonBuf, sizeof(jsonBuf), + "{\"event\":\"detection\",\"detection_method\":\"%s\"," + "\"protocol\":\"bluetooth_le\",\"mac_address\":\"%s\"," + "\"device_name\":\"%s\",\"rssi\":%d," + "\"is_raven\":%s,\"raven_fw\":\"%s\"%s}", + method, addrStr.c_str(), name.c_str(), rssi, + isRaven ? "true" : "false", isRaven ? ravenFW : "", gpsBuf); printf("%s\n", jsonBuf); // Append newline for BLE framing and send if (jsonLen > 0 && jsonLen < (int)sizeof(jsonBuf) - 1) { From c9afc3438de40b79c436289c044ba312f0edecdc Mon Sep 17 00:00:00 2001 From: Doug Borg Date: Sat, 7 Feb 2026 21:58:54 -0700 Subject: [PATCH 05/19] Split MAC prefixes by vendor/confidence to reduce false positives Address Copilot review: contract manufacturer OUIs (Liteon/USI) are now in a separate flock_mfr_mac_prefixes[] array emitting "mac_prefix_mfr" as the detection method. SoundThinking/ShotSpotter gets its own array and "mac_prefix_soundthinking" method. Low-confidence detections (mfr OUIs) suppress buzzer/heartbeat but still emit JSON events so consumers can apply their own thresholds. Co-Authored-By: Claude Opus 4.6 --- src/main.cpp | 115 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 78 insertions(+), 37 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 0f3a976..6ce4b03 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -58,28 +58,32 @@ // DETECTION PATTERNS // ============================================================================ -// Known Flock Safety MAC address prefixes (OUIs) -static const char* mac_prefixes[] = { +// MAC address prefixes (OUIs) + +// Flock Safety — high-confidence OUIs (direct registration or exclusive use) +static const char* flock_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. + // Flock WiFi devices "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. + "b4:1e:52" +}; + +// Flock Safety contract manufacturers — lower confidence alone. +// These OUIs belong to Liteon Technology and USI (Universal Scientific +// Industrial), which produce Flock hardware but also ship unrelated +// consumer/enterprise devices. MAC match alone may be a false positive. +static const char* flock_mfr_mac_prefixes[] = { + "f4:6a:dd", "f8:a2:d6", "e0:0a:f6", "00:f4:8d", "d0:39:57", + "e8:d0:fc" +}; + +// SoundThinking (formerly ShotSpotter) — acoustic gunshot detection sensors. +// d4:11:d6 is registered to SoundThinking in the IEEE OUI database. +static const char* soundthinking_mac_prefixes[] = { "d4:11:d6" }; @@ -251,11 +255,23 @@ static void fyHeartbeat() { // 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; +static bool checkFlockMAC(const char* mac_str) { + for (size_t i = 0; i < sizeof(flock_mac_prefixes)/sizeof(flock_mac_prefixes[0]); i++) { + if (strncasecmp(mac_str, flock_mac_prefixes[i], 8) == 0) return true; + } + return false; +} + +static bool checkFlockMfrMAC(const char* mac_str) { + for (size_t i = 0; i < sizeof(flock_mfr_mac_prefixes)/sizeof(flock_mfr_mac_prefixes[0]); i++) { + if (strncasecmp(mac_str, flock_mfr_mac_prefixes[i], 8) == 0) return true; + } + return false; +} + +static bool checkSoundThinkingMAC(const char* mac_str) { + for (size_t i = 0; i < sizeof(soundthinking_mac_prefixes)/sizeof(soundthinking_mac_prefixes[0]); i++) { + if (strncasecmp(mac_str, soundthinking_mac_prefixes[i], 8) == 0) return true; } return false; } @@ -392,34 +408,45 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks { 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]}; + // Extract MAC prefix string for OUI checks + char macPrefix[9]; + snprintf(macPrefix, sizeof(macPrefix), "%.8s", addrStr.c_str()); int rssi = dev->getRSSI(); std::string name = dev->haveName() ? dev->getName() : ""; bool detected = false; + bool highConfidence = true; const char* method = ""; bool isRaven = false; const char* ravenFW = ""; - // 1. Check MAC prefix against known Flock Safety OUIs - if (checkMACPrefix(mac)) { + // 1. Check Flock Safety direct OUIs (high confidence) + if (checkFlockMAC(macPrefix)) { detected = true; method = "mac_prefix"; } - // 2. Check BLE device name patterns + // 2. Check SoundThinking/ShotSpotter OUIs (high confidence) + if (!detected && checkSoundThinkingMAC(macPrefix)) { + detected = true; + method = "mac_prefix_soundthinking"; + } + + // 3. Check Flock contract manufacturer OUIs (low confidence) + if (!detected && checkFlockMfrMAC(macPrefix)) { + detected = true; + method = "mac_prefix_mfr"; + highConfidence = false; + } + + // 4. 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) + // 5. 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); @@ -435,7 +462,7 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks { } } - // 4. Check Raven gunshot detector service UUIDs + // 6. Check Raven gunshot detector service UUIDs if (!detected) { char detUUID[41] = {0}; if (checkRavenUUID(dev, detUUID)) { @@ -475,11 +502,13 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks { method, addrStr.c_str(), name.c_str(), rssi, gpsBuf); } - if (!fyTriggered) { + if (!fyTriggered && highConfidence) { fyTriggered = true; fyDetectBeep(); } - fyDeviceInRange = true; + if (highConfidence) { + fyDeviceInRange = true; + } fyLastDetTime = millis(); fyLastHB = millis(); } @@ -685,7 +714,9 @@ function card(d){return '
'+d.mac+(d.name?'r.json()).then(d=>{H=d;let el=document.getElementById('hL');if(!H.length){el.innerHTML='
No prior session data
';return;} H.sort((a,b)=>b.last-a.last);el.innerHTML='
'+H.length+' detections from prior session
'+H.map(card).join('');window._hL=1;}).catch(()=>{document.getElementById('hL').innerHTML='
No prior session data
';});} function loadPat(){fetch('/api/patterns').then(r=>r.json()).then(p=>{let h=''; -h+='

MAC Prefixes ('+p.macs.length+')

'+p.macs.map(m=>''+m+'').join('')+'
'; +h+='

Flock MAC Prefixes ('+p.macs.length+')

'+p.macs.map(m=>''+m+'').join('')+'
'; +h+='

Contract Mfr MACs ('+p.macs_mfr.length+')

'+p.macs_mfr.map(m=>''+m+'').join('')+'
'; +h+='

SoundThinking MACs ('+p.macs_soundthinking.length+')

'+p.macs_soundthinking.map(m=>''+m+'').join('')+'
'; h+='

BLE Device Names ('+p.names.length+')

'+p.names.map(n=>''+n+'').join('')+'
'; h+='

BLE Manufacturer IDs ('+p.mfr.length+')

'+p.mfr.map(m=>'0x'+m.toString(16).toUpperCase().padStart(4,'0')+'').join('')+'
'; h+='

Raven UUIDs ('+p.raven.length+')

'+p.raven.map(u=>''+u+'').join('')+'
'; @@ -771,9 +802,19 @@ static void fySetupServer() { 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++) { + for (size_t i = 0; i < sizeof(flock_mac_prefixes)/sizeof(flock_mac_prefixes[0]); i++) { + if (i > 0) resp->print(","); + resp->printf("\"%s\"", flock_mac_prefixes[i]); + } + resp->print("],\"macs_mfr\":["); + for (size_t i = 0; i < sizeof(flock_mfr_mac_prefixes)/sizeof(flock_mfr_mac_prefixes[0]); i++) { + if (i > 0) resp->print(","); + resp->printf("\"%s\"", flock_mfr_mac_prefixes[i]); + } + resp->print("],\"macs_soundthinking\":["); + for (size_t i = 0; i < sizeof(soundthinking_mac_prefixes)/sizeof(soundthinking_mac_prefixes[0]); i++) { if (i > 0) resp->print(","); - resp->printf("\"%s\"", mac_prefixes[i]); + resp->printf("\"%s\"", soundthinking_mac_prefixes[i]); } resp->print("],\"names\":["); for (size_t i = 0; i < sizeof(device_name_patterns)/sizeof(device_name_patterns[0]); i++) { From 064b43c1c84a8e31b142c9441a18af6f5e7182fa Mon Sep 17 00:00:00 2001 From: Doug Borg Date: Sun, 8 Feb 2026 14:16:53 -0700 Subject: [PATCH 06/19] Fix heartbeat timer leak for low-confidence detections, bump method buffer Low-confidence mac_prefix_mfr hits no longer update fyLastDetTime/fyLastHB, preventing them from keeping the heartbeat alive after a high-confidence device leaves range. Bump FYDetection::method from 24 to 32 bytes so "mac_prefix_soundthinking" (23 chars) has headroom. Co-Authored-By: Claude Opus 4.6 --- src/main.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 6ce4b03..5a235dc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -133,7 +133,7 @@ struct FYDetection { char mac[18]; char name[48]; int rssi; - char method[24]; + char method[32]; unsigned long firstSeen; unsigned long lastSeen; int count; @@ -508,9 +508,9 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks { } if (highConfidence) { fyDeviceInRange = true; + fyLastDetTime = millis(); + fyLastHB = millis(); } - fyLastDetTime = millis(); - fyLastHB = millis(); } } }; From 7aaaaacc12f188afd0a78c9a908ba788345b4729 Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 15:09:52 -0600 Subject: [PATCH 07/19] Adding Rayhunter/external AP integration --- src/main.cpp | 113 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 14 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index ef6832d..2c09903 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -54,6 +54,21 @@ static unsigned long fyBleScanInterval = 3000; // ms between scans #define FY_AP_SSID "flockyou" #define FY_AP_PASS "flockyou123" +// Rayhunter Configuration +// Either hardcode Rayhunter config here (bad), or set via build flags and add to platformio.ini +// Default behavior is to disable Rayhunter and run in standalone AP mode +#ifndef RAYHUNTER_ENABLED +#define RAYHUNTER_ENABLED false // Set to true to enable Rayhunter integration, false to run in standalone mode with WiFi AP +#endif + +#ifndef RAYHUNTER_SSID +#define RAYHUNTER_SSID "Rayhunter-AP" // Rayhunter SSID (default placeholder) +#endif + +#ifndef RAYHUNTER_PASS +#define RAYHUNTER_PASS "rayhunter123" // Rayhunter WiFi Password (default placeholder) +#endif + // ============================================================================ // DETECTION PATTERNS // ============================================================================ @@ -469,14 +484,50 @@ static void fyOnCompanionChange() { printf("[FLOCK-YOU] Companion mode: WiFi AP OFF, scan duration %ds\n", fyBleScanDuration); } else { - // Standalone mode — re-enable WiFi AP and web dashboard - WiFi.mode(WIFI_AP); - delay(100); - WiFi.softAP(FY_AP_SSID, FY_AP_PASS); - fyBleScanDuration = 2; - printf("[FLOCK-YOU] Standalone mode: WiFi AP ON (%s), scan duration %ds\n", - FY_AP_SSID, fyBleScanDuration); - } + // Standalone mode + + // If Rayhunter is enabled, try to connect to their AP, but fall back to standalone AP if connection fails + if ( RAYHUNTER_ENABLED ) { + WiFi.begin(RAYHUNTER_SSID, RAYHUNTER_PASS); + fyBleScanDuration = 2; + printf("[FLOCK-YOU] Attempting to connect to Rayhunter WiFi network"); + + int attempts = 0; + while(WiFi.status() != WL_CONNECTED) { + delay(500); + printf("."); + attempts++; + if (attempts >= 20) { // ~10 seconds timeout + printf("\n[FLOCK-YOU] Failed to connect to RayHunter WiFi - starting in AP mode\n"); + break; + } + } + + // If we connected to RayHunter's WiFi, use that; otherwise, fallback to our own AP + if (WiFi.status() == WL_CONNECTED) { + printf("\n[FLOCK-YOU] Connected to RayHunter WiFi\n"); + printf("[FLOCK-YOU] IP: %s\n", WiFi.localIP().toString()); + printf("[FLOCK-YOU] Companion mode with RayHunter: scan duration %ds\n", fyBleScanDuration); + } else { + printf("[FLOCK-YOU] Connection to Rayhunter AP Failed. Falling back to standalone AP mode.\n"); + WiFi.mode(WIFI_AP); + delay(100); + WiFi.softAP(FY_AP_SSID, FY_AP_PASS); + fyBleScanDuration = 2; + printf("[FLOCK-YOU] Standalone mode: WiFi AP ON (%s), scan duration %ds\n", + FY_AP_SSID, fyBleScanDuration); + } + + + // If Rayhunter is not enabled, re-enable WiFi AP + } else { + WiFi.mode(WIFI_AP); + delay(100); + WiFi.softAP(FY_AP_SSID, FY_AP_PASS); + fyBleScanDuration = 2; + printf("[FLOCK-YOU] Standalone mode: WiFi AP ON (%s), scan duration %ds\n", + FY_AP_SSID, fyBleScanDuration); + } } // ============================================================================ @@ -1146,12 +1197,46 @@ void setup() { // 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()); + // Set an empty localIP for now; will be updated after WiFi setup + IPAddress localIP(0,0,0,0); + +// If Rayhunter is enabled, try to connect to their AP, but fall back to standalone AP if connection fails + if ( RAYHUNTER_ENABLED ) { + WiFi.setHostname("flockyou"); + printf("[FLOCK-YOU] Attempting to connect to RayHunter WiFi (SSID: %s)\n", RAYHUNTER_SSID); + WiFi.begin(RAYHUNTER_SSID, RAYHUNTER_PASS); + + int attempts = 0; + while(WiFi.status() != WL_CONNECTED) { + delay(500); + printf("."); + attempts++; + if (attempts >= 20) { // ~10 seconds timeout + printf("\n[FLOCK-YOU] Failed to connect to RayHunter WiFi - starting in AP mode\n"); + break; + } + } + + // If we connected to RayHunter's WiFi, use that; otherwise, fallback to our own AP + if (WiFi.status() == WL_CONNECTED) { + printf("\n[FLOCK-YOU] Connected to RayHunter WiFi\n"); + localIP = WiFi.localIP(); + } else { + 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); + localIP = WiFi.softAPIP(); + } + + // If Rayhunter is not enabled, start WiFi AP (no need to connect to anything -- AP only) + } else { + 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); + localIP = WiFi.softAPIP(); + } // Start web dashboard fySetupServer(); From 434b8e7fc2523e9f6ba253dffe32bbfa2f4e2cba Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 15:10:55 -0600 Subject: [PATCH 08/19] updating log output to reflect new localIP value --- src/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 2c09903..df7808a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1241,8 +1241,9 @@ void setup() { // Start web dashboard fySetupServer(); + printf("[FLOCK-YOU] IP: %s\n", localIP.toString().c_str()); 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] Dashboard: http://%s\n", localIP.toString().c_str()); printf("[FLOCK-YOU] Ready - BLE GATT + AP mode\n\n"); } From 7cf683834f90b423137ca098d5aeb44aba002a91 Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 15:12:16 -0600 Subject: [PATCH 09/19] updating web content to use new localIP value --- src/main.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index df7808a..38dbb3d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -899,7 +899,7 @@ let g=document.getElementById('sG');g.textContent='...';g.style.color='#facc15'; _gW=navigator.geolocation.watchPosition(sendGPS,gpsErr,{enableHighAccuracy:true,maximumAge:5000,timeout:15000});return true;} function reqGPS(){if(!navigator.geolocation){alert('GPS not available in this browser.');return;} if(_gOk){return;} -if(!window.isSecureContext){alert('GPS requires a secure context (HTTPS). This HTTP page may not get GPS permission.\\n\\nAndroid Chrome: try chrome://flags and enable "Insecure origins treated as secure", add http://192.168.4.1\\n\\niPhone: GPS will not work over HTTP.');} +if(!window.isSecureContext){alert('GPS requires a secure context (HTTPS). This HTTP page may not get GPS permission.\n\nAndroid Chrome: try chrome://flags and enable "Insecure origins treated as secure", add http://{{LOCALIP}}\n\niPhone: GPS will not work over HTTP.');} startGPS();_gTried=true;} refresh();setInterval(refresh,2500); @@ -909,10 +909,13 @@ refresh();setInterval(refresh,2500); // WEB SERVER SETUP // ============================================================================ -static void fySetupServer() { +static void fySetupServer(IPAddress localIP) { // Dashboard - fyServer.on("/", HTTP_GET, [](AsyncWebServerRequest *r) { - r->send(200, "text/html", FY_HTML); + fyServer.on("/", HTTP_GET, [localIP](AsyncWebServerRequest *r) { + // Serve the dashboard HTML, but replace {{LOCALIP}} with the actual IP address for better user instructions + String html = FY_HTML; + html.replace("{{LOCALIP}}", localIP.toString().c_str()); + r->send(200, "text/html", html); }); // API: Detection list @@ -1239,7 +1242,7 @@ void setup() { } // Start web dashboard - fySetupServer(); + fySetupServer(localIP); printf("[FLOCK-YOU] IP: %s\n", localIP.toString().c_str()); printf("[FLOCK-YOU] Detection methods: MAC prefix, device name, manufacturer ID, Raven UUID\n"); From 00a0ba30369e210a746ad911076e6ebba5cc492d Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 15:15:54 -0600 Subject: [PATCH 10/19] updating comments --- src/main.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 38dbb3d..4d2700e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,7 +8,8 @@ // 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 +// Optionally connects to a specified Rayhunter AP, serving web dashboard at DHCP IP +// Standalone 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 // ============================================================================ @@ -528,6 +529,7 @@ static void fyOnCompanionChange() { printf("[FLOCK-YOU] Standalone mode: WiFi AP ON (%s), scan duration %ds\n", FY_AP_SSID, fyBleScanDuration); } + } } // ============================================================================ @@ -1203,7 +1205,7 @@ void setup() { // Set an empty localIP for now; will be updated after WiFi setup IPAddress localIP(0,0,0,0); -// If Rayhunter is enabled, try to connect to their AP, but fall back to standalone AP if connection fails + // If Rayhunter is enabled, try to connect to their AP, but fall back to standalone AP if connection fails if ( RAYHUNTER_ENABLED ) { WiFi.setHostname("flockyou"); printf("[FLOCK-YOU] Attempting to connect to RayHunter WiFi (SSID: %s)\n", RAYHUNTER_SSID); @@ -1240,8 +1242,7 @@ void setup() { printf("[FLOCK-YOU] AP: %s / %s\n", FY_AP_SSID, FY_AP_PASS); localIP = WiFi.softAPIP(); } - - // Start web dashboard + // Start web dashboard, passing in the localIP fySetupServer(localIP); printf("[FLOCK-YOU] IP: %s\n", localIP.toString().c_str()); From 63beaf2a31ac71ff4c4cc3bd7aab4d204e0a5b1d Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 15:17:40 -0600 Subject: [PATCH 11/19] Adding build params for rayhunter --- platformio.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/platformio.ini b/platformio.ini index 4bf3b22..9118b7c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,6 +12,9 @@ build_flags = -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_BT_NIMBLE_ENABLED=1 + -DRAYHUNTER_ENABLED=false ; Set to true to enable Rayhunter Integration + -DRAYHUNTER_SSID="Rayhunter-SSID" ; Rayhunter SSID (Default Placeholder) + -DRAYHUNTER_PASS="Rayhunter-PASS" ; Rayhunter Password (Default Placeholder) ; Libraries lib_deps = From dcaaa7ac3d086caa0c551e5d5b731be53f886342 Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 16:04:55 -0600 Subject: [PATCH 12/19] Fixing string quoting --- platformio.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 9118b7c..1a74ccf 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,9 +12,9 @@ build_flags = -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DCONFIG_BT_NIMBLE_ENABLED=1 - -DRAYHUNTER_ENABLED=false ; Set to true to enable Rayhunter Integration - -DRAYHUNTER_SSID="Rayhunter-SSID" ; Rayhunter SSID (Default Placeholder) - -DRAYHUNTER_PASS="Rayhunter-PASS" ; Rayhunter Password (Default Placeholder) + -DRAYHUNTER_ENABLED=false ; Set to true to enable Rayhunter Integration + -DRAYHUNTER_SSID='"Rayhunter-SSID"' ; Rayhunter SSID (Default Placeholder) + -DRAYHUNTER_PASS='"Rayhunter-PASS"' ; Rayhunter Password (Default Placeholder) ; Libraries lib_deps = From 32f19ab4e32ead29a236183bd6bead9bde8e6635 Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 16:21:30 -0600 Subject: [PATCH 13/19] Changing replace string to potentially include hostname in the future --- src/main.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 4d2700e..ac6d7d6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -901,7 +901,7 @@ let g=document.getElementById('sG');g.textContent='...';g.style.color='#facc15'; _gW=navigator.geolocation.watchPosition(sendGPS,gpsErr,{enableHighAccuracy:true,maximumAge:5000,timeout:15000});return true;} function reqGPS(){if(!navigator.geolocation){alert('GPS not available in this browser.');return;} if(_gOk){return;} -if(!window.isSecureContext){alert('GPS requires a secure context (HTTPS). This HTTP page may not get GPS permission.\n\nAndroid Chrome: try chrome://flags and enable "Insecure origins treated as secure", add http://{{LOCALIP}}\n\niPhone: GPS will not work over HTTP.');} +if(!window.isSecureContext){alert('GPS requires a secure context (HTTPS). This HTTP page may not get GPS permission.\n\nAndroid Chrome: try chrome://flags and enable "Insecure origins treated as secure", add http://{{ADDRESS}}\n\niPhone: GPS will not work over HTTP.');} startGPS();_gTried=true;} refresh();setInterval(refresh,2500); @@ -914,9 +914,9 @@ refresh();setInterval(refresh,2500); static void fySetupServer(IPAddress localIP) { // Dashboard fyServer.on("/", HTTP_GET, [localIP](AsyncWebServerRequest *r) { - // Serve the dashboard HTML, but replace {{LOCALIP}} with the actual IP address for better user instructions + // Serve the dashboard HTML, but replace {{ADDRESS}} with the actual IP address for better user instructions String html = FY_HTML; - html.replace("{{LOCALIP}}", localIP.toString().c_str()); + html.replace("{{ADDRESS}}", localIP.toString().c_str()); r->send(200, "text/html", html); }); From 56749a4f65652b147d846698a5bbd9a1eed4a3d9 Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 16:28:32 -0600 Subject: [PATCH 14/19] Removing string replace in favor of javascript. --- src/main.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index ac6d7d6..58b72aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -901,7 +901,7 @@ let g=document.getElementById('sG');g.textContent='...';g.style.color='#facc15'; _gW=navigator.geolocation.watchPosition(sendGPS,gpsErr,{enableHighAccuracy:true,maximumAge:5000,timeout:15000});return true;} function reqGPS(){if(!navigator.geolocation){alert('GPS not available in this browser.');return;} if(_gOk){return;} -if(!window.isSecureContext){alert('GPS requires a secure context (HTTPS). This HTTP page may not get GPS permission.\n\nAndroid Chrome: try chrome://flags and enable "Insecure origins treated as secure", add http://{{ADDRESS}}\n\niPhone: GPS will not work over HTTP.');} +if(!window.isSecureContext){alert('GPS requires a secure context (HTTPS). This HTTP page may not get GPS permission.\n\nAndroid Chrome: try chrome://flags and enable "Insecure origins treated as secure", add http://'+window.location.hostname+'\n\niPhone: GPS will not work over HTTP.');} startGPS();_gTried=true;} refresh();setInterval(refresh,2500); @@ -911,13 +911,10 @@ refresh();setInterval(refresh,2500); // WEB SERVER SETUP // ============================================================================ -static void fySetupServer(IPAddress localIP) { +static void fySetupServer() { // Dashboard fyServer.on("/", HTTP_GET, [localIP](AsyncWebServerRequest *r) { - // Serve the dashboard HTML, but replace {{ADDRESS}} with the actual IP address for better user instructions - String html = FY_HTML; - html.replace("{{ADDRESS}}", localIP.toString().c_str()); - r->send(200, "text/html", html); + r->send(200, "text/html", FY_HTML); }); // API: Detection list @@ -1242,8 +1239,9 @@ void setup() { printf("[FLOCK-YOU] AP: %s / %s\n", FY_AP_SSID, FY_AP_PASS); localIP = WiFi.softAPIP(); } - // Start web dashboard, passing in the localIP - fySetupServer(localIP); + + // Start web dashboard + fySetupServer(); printf("[FLOCK-YOU] IP: %s\n", localIP.toString().c_str()); printf("[FLOCK-YOU] Detection methods: MAC prefix, device name, manufacturer ID, Raven UUID\n"); From 0352dd2ef13a5c38f6cbec15920e09321c9a2f8c Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 16:29:11 -0600 Subject: [PATCH 15/19] Adding rayhunter details to readme --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7526737..1b798e6 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,9 @@ All detection is BLE-based: ## Features -- **WiFi AP**: `flockyou` / password `flockyou123` -- **Web dashboard** at `192.168.4.1` — live detection feed, pattern database, export tools +- **Standalone WiFi AP** (ssid `flockyou` / password `flockyou123`) or connects to Rayhunter AP for multi-tool usage +- **Web dashboard** live detection feed, pattern database, export tools. `192.168.4.1` for standalone AP mode, or at + either `http://flockyou` or the DHCP address with Rayhunter - **GPS wardriving** — phone GPS via browser Geolocation API tags every detection with coordinates - **Session persistence** — detections auto-save to flash (SPIFFS) every 60 seconds - **Prior session tab** — previous session survives reboot and is viewable in the PREV tab @@ -83,6 +84,17 @@ pio run # build pio run -t upload # flash pio device monitor # serial output ``` +### Rayhunter/External AP Integration (Optional) + +Update `platformio.ini` with the following values under the `build_flags` section: + +```ini +-DRAYHUNTER_ENABLED=true ; Set to true to enable Rayhunter Integration +-DRAYHUNTER_SSID='"Rayhunter-SSID"' ; Your Rayhunter SSID +-DRAYHUNTER_PASS='"Rayhunter-PASS"' ; Your Rayhunter WiFi Password +``` + +Build and run as above. **Dependencies** (managed by PlatformIO): From c2dde9045f25c938924ace37c88a3e5c731b7c76 Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 16:38:39 -0600 Subject: [PATCH 16/19] Removing string replace in favor of javascript. --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 58b72aa..b045df5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -913,7 +913,7 @@ refresh();setInterval(refresh,2500); static void fySetupServer() { // Dashboard - fyServer.on("/", HTTP_GET, [localIP](AsyncWebServerRequest *r) { + fyServer.on("/", HTTP_GET, (AsyncWebServerRequest *r) { r->send(200, "text/html", FY_HTML); }); From d1f72be478251f3133d2eab10cac44c619556a41 Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 16:39:29 -0600 Subject: [PATCH 17/19] re-adding brackets --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index b045df5..10985c9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -913,7 +913,7 @@ refresh();setInterval(refresh,2500); static void fySetupServer() { // Dashboard - fyServer.on("/", HTTP_GET, (AsyncWebServerRequest *r) { + fyServer.on("/", HTTP_GET, [](AsyncWebServerRequest *r) { r->send(200, "text/html", FY_HTML); }); From 41cf4a32ab736b599a30688540807d3bcac3a13a Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 16:46:40 -0600 Subject: [PATCH 18/19] Using consistent placehold values for ssid and pass --- src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 10985c9..c4850af 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -63,11 +63,11 @@ static unsigned long fyBleScanInterval = 3000; // ms between scans #endif #ifndef RAYHUNTER_SSID -#define RAYHUNTER_SSID "Rayhunter-AP" // Rayhunter SSID (default placeholder) +#define RAYHUNTER_SSID "Rayhunter-SSID" // Rayhunter SSID (default placeholder) #endif #ifndef RAYHUNTER_PASS -#define RAYHUNTER_PASS "rayhunter123" // Rayhunter WiFi Password (default placeholder) +#define RAYHUNTER_PASS "Rayhunter-PASS" // Rayhunter WiFi Password (default placeholder) #endif // ============================================================================ From 71f3fe7607b661b8acae75f53c06bfdf16f9e58f Mon Sep 17 00:00:00 2001 From: pezhore Date: Sat, 7 Mar 2026 16:47:31 -0600 Subject: [PATCH 19/19] Updating comments to reflect hostname based url --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index c4850af..d99dfb3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,7 +8,7 @@ // 4. Raven gunshot detector service UUID matching // 5. Raven firmware version estimation from service UUID patterns // -// Optionally connects to a specified Rayhunter AP, serving web dashboard at DHCP IP +// Optionally connects to a specified Rayhunter AP, serving web dashboard at DHCP IP or http://flockyou // Standalone 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