Compare commits

...

3 commits

Author SHA1 Message Date
9c05879451
Wire axon/vr_headset alerts + document new categories
- Morse alerts: axon -> A (threat tone), vr_headset -> E (single dit; benign,
  not treated as a surveillance threat)
- README: 10 categories (add axon, vr_headset), correct the now-stale
  false-positive caveat (Silicon Labs + Raspberry Pi OUIs were removed from the
  Flock list; real Flock-on-RPi relies on name/mfr-id, not bare OUI)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:38:49 -06:00
2685a6d71b
Hold screen wake-lock during GPS capture (firmware-todo #1)
Phone screen-sleep suspends the dashboard page and stops geolocation — the main
cause of detections logged without coords while walking. Add Screen Wake Lock API:
acquire on GPS start, re-acquire on visibilitychange when the page returns to
foreground. Keeps watchPosition alive with the screen on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:59:04 -06:00
967d0db853
Fix false-positive OUIs, add Axon WiFi detection, split VR/glasses
- Remove 04:0d:84 (Silicon Labs) and 08:3a:88 (Raspberry Pi) from Flock BLE
  prefixes — root cause of BlueDriver / Wyze Lock / Philips Hue false "flock" hits
- WiFi prefixes now carry a per-OUI category (struct WifiPrefixEntry);
  checkWiFiMACPrefix() returns the category instead of assuming Ring.
  Add Axon 00:25:df -> axon (catches Lightpost 2.4GHz / Outpost in setup)
- Split Quest/Oculus VR headsets (vr_headset) from Ray-Ban surveillance glasses
- Aligns firmware categorization with the shared signatures/ library

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:56:04 -06:00
2 changed files with 74 additions and 33 deletions

View file

@ -43,32 +43,37 @@ Dantir uses **seven detection methods** across BLE and WiFi:
| Method | Radio | How it matches |
|--------|-------|----------------|
| `mac_prefix` | BLE | MAC OUI against ~20 Flock Safety prefixes |
| `mac_prefix` | BLE | MAC OUI against the Flock Safety prefix set (lowest confidence — see note) |
| `device_name` | BLE | Advertised name patterns (Flock, Penguin, Pigvision, Ring, Ray-Ban…) |
| `ble_mfr_id` | BLE | Bluetooth manufacturer company IDs |
| `raven_uuid` | BLE | Raven gunshot-detector service UUIDs (+ firmware-version estimate) |
| `wifi_probe` | WiFi | Probe-request source MAC OUI (promiscuous mode) |
| `wifi_beacon` | WiFi | Beacon-frame source MAC OUI (promiscuous mode) |
Detections are grouped into **eight categories**, each with its own Morse-code
Detections are grouped into **ten categories**, each with its own Morse-code
audio signature on the buzzer:
| Category | Morse | What it covers |
|----------|:-----:|----------------|
| `flock` | ··-· (F) | Flock Safety / ALPR cameras, Penguin, Pigvision |
| `glasses` | --· (G) | Recording smart glasses (Ray-Ban Meta, Oakley, Snap) |
| `tracker` | - (T) | Bluetooth trackers (Tile, etc.) |
| `lawenf` | ·-·· (L) | Law-enforcement gear (TASER/Axon, Motorola Solutions) |
| `ring` | ·-· (R) | Ring / Blink / Amazon cameras |
| `camera` | ··· (S) | Other surveillance cameras (Hikvision, Arlo, Wyze) |
| `raven` | ···- (V) | Raven gunshot-detector nodes |
| `wifi` | ·-- (W) | Generic WiFi-side detections |
| `flock` | ··-· (F) | Flock Safety / ALPR cameras, Penguin, Pigvision |
| `axon` | ·- (A) | Axon ALPR cameras (the vendor replacing Flock in some cities) |
| `glasses` | --· (G) | Camera-equipped smart glasses (Ray-Ban Meta, Oakley, Snap) |
| `vr_headset` | · (E) | VR headsets (Quest/Oculus) — benign; logged with a quiet blip, not a threat alert |
| `tracker` | - (T) | Bluetooth trackers (Tile, etc.) |
| `lawenf` | ·-·· (L) | Law-enforcement gear (TASER/Axon body-cams, Motorola Solutions) |
| `ring` | ·-· (R) | Ring / Blink / Amazon cameras |
| `camera` | ··· (S) | Other surveillance cameras (Hikvision, Arlo, Wyze) |
| `raven` | ···- (V) | Raven gunshot-detector nodes |
| `wifi` | ·-- (W) | Generic WiFi-side detections |
> **Detection is signature-based, not proof.** OUI/manufacturer matches catch
> consumer hardware that shares the same chipset vendors (e.g. some Wyze, Hue,
> or OBD-II dongles can trip a low-confidence match). Treat `device_name` and
> `ble_mfr_id` hits as higher confidence than a bare `mac_prefix` match, and
> confirm visually before drawing conclusions.
> **Detection is signature-based, not proof.** Many OUIs are shared across a
> chipset vendor's entire catalog, so a bare `mac_prefix` match is the
> lowest-confidence signal. Treat `device_name`, `ble_mfr_id`, and `raven_uuid`
> hits as higher confidence, and confirm visually before drawing conclusions.
> Known shared-OUI false-positive sources — e.g. a Silicon Labs prefix that also
> covers Wyze locks and OBD-II dongles, and the Raspberry Pi prefix — have been
> pulled from the Flock list; real Flock-on-RPi now relies on name (Penguin /
> Pigvision) and manufacturer ID, not a bare OUI.
---

View file

@ -96,11 +96,16 @@
// Known Flock Safety MAC address prefixes (OUIs) — matched during BLE scan
static const char* ble_mac_prefixes[] = {
// Flock Safety — FS Ext Battery devices
"58:8e:81", "cc:cc:cc", "ec:1b:bd", "90:35:ea", "04:0d:84",
"58:8e:81", "cc:cc:cc", "ec:1b:bd", "90:35:ea",
"f0:82:c0", "1c:34:f1", "38:5b:44", "94:34:69", "b4:e3:f9",
// Flock Safety — WiFi-enabled 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"
"74:4c:a1", "9c:2f:9d", "94:08:53", "e4:aa:ea"
// REMOVED 2026-06-23 (false-positive sources — see signatures/ audit):
// "04:0d:84" — Silicon Labs OUI (Wyze Lock / BlueDriver / Philips Hue), NOT Flock
// "08:3a:88" — Raspberry Pi Foundation OUI (millions of hobby RPis), too ambiguous
// Both produced false "flock" tags. Real Flock-on-RPi now relies on name
// (Penguin/Pigvision) + mfr ID 0x09C8, not these shared OUIs.
};
// ============================================================================
@ -166,14 +171,19 @@ static const MfrEntry ble_manufacturer_entries[] = {
// These OUIs are checked against source MACs in WiFi management frames
// (probe requests, beacon frames) captured via promiscuous callback.
// Ring LLC OUI prefixes (IEEE registered, verified Feb 2026)
static const char* wifi_mac_prefixes[] = {
// WiFi-side surveillance OUI prefixes (matched in promiscuous mode). Each carries its
// own category so matches aren't all assumed Ring (mirrors the shared signatures/ library).
struct WifiPrefixEntry { const char* prefix; const char* category; };
static const WifiPrefixEntry wifi_mac_prefixes[] = {
// Ring LLC
"00:b4:63", "18:7f:88", "24:2b:d6", "34:3e:a4",
"54:e0:19", "5c:47:5e", "64:9a:63", "90:48:6c",
"9c:76:13", "ac:9f:c3", "c4:db:ad", "cc:3b:fb",
{"00:b4:63","ring"}, {"18:7f:88","ring"}, {"24:2b:d6","ring"}, {"34:3e:a4","ring"},
{"54:e0:19","ring"}, {"5c:47:5e","ring"}, {"64:9a:63","ring"}, {"90:48:6c","ring"},
{"9c:76:13","ring"}, {"ac:9f:c3","ring"}, {"c4:db:ad","ring"}, {"cc:3b:fb","ring"},
// Blink by Amazon (Ring's sister brand)
"70:ad:43"
{"70:ad:43","ring"},
// Axon ALPR — catches Lightpost's 2.4GHz radio / Outpost in setup mode.
// (Deployed Outpost is LTE-only/RF-silent; Lightpost's 5GHz is invisible to ESP32.)
{"00:25:df","axon"},
};
// ============================================================================
@ -440,6 +450,12 @@ static void fyMorseCategory(const char* category) {
} else if (strcmp(category, "raven") == 0) {
// V: ···-
fyMorseDit(); fyMorseDit(); fyMorseDit(); fyMorseDah();
} else if (strcmp(category, "axon") == 0) {
// A: ·- (Axon ALPR — Flock's municipal replacement)
fyMorseDit(); fyMorseDah();
} else if (strcmp(category, "vr_headset") == 0) {
// E: · (single dit — VR headset, benign; quiet acknowledgement, not a threat)
fyMorseDit();
} else {
// Unknown: single long beep
fyMorseDah(); fyMorseDah();
@ -562,13 +578,14 @@ static bool checkBLEMACPrefix(const uint8_t* mac) {
return false;
}
static bool checkWiFiMACPrefix(const uint8_t* mac) {
// Returns the category for a matching WiFi OUI, or nullptr if no match.
static const char* checkWiFiMACPrefix(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(wifi_mac_prefixes)/sizeof(wifi_mac_prefixes[0]); i++) {
if (strncasecmp(mac_str, wifi_mac_prefixes[i], 8) == 0) return true;
if (strncasecmp(mac_str, wifi_mac_prefixes[i].prefix, 8) == 0) return wifi_mac_prefixes[i].category;
}
return false;
return nullptr;
}
// ============================================================================
@ -608,7 +625,8 @@ static void fyWifiPromiscuousCB(void *buf, wifi_promiscuous_pkt_type_t type) {
// Address 2 (source/transmitter MAC) at offset 10
const uint8_t *src_mac = &frame[10];
if (!checkWiFiMACPrefix(src_mac)) return;
const char *wcat = checkWiFiMACPrefix(src_mac);
if (!wcat) return;
// Match! Build MAC string and register detection
char mac_str[18];
@ -619,12 +637,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, "ring"); // WiFi OUIs are all Ring/Blink
int idx = fyAddDetection(mac_str, "", rssi, method, wcat);
if (idx >= 0) {
if (fyDet[idx].count == 1) {
// First sighting — new WiFi surveillance device
fyWifiDetCount++;
printf("[DANTIR] WiFi DETECTED [ring]: %s RSSI:%d [%s]\n", mac_str, rssi, method);
printf("[DANTIR] WiFi DETECTED [%s]: %s RSSI:%d [%s]\n", wcat, mac_str, rssi, method);
// JSON serial output
char gpsBuf[80] = "";
@ -633,9 +651,9 @@ 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\",\"category\":\"ring\",\"protocol\":\"wifi\","
printf("{\"detection_method\":\"%s\",\"category\":\"%s\",\"protocol\":\"wifi\","
"\"mac_address\":\"%s\",\"rssi\":%d%s}\n",
method, mac_str, rssi, gpsBuf);
method, wcat, mac_str, rssi, gpsBuf);
}
// Defer buzzer/LED alert to main loop (can't call delay() here)
@ -913,6 +931,18 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks {
}
}
// 5. Refine glasses: Meta's mfr ID covers BOTH Ray-Ban Meta (camera/surveillance
// glasses) and Quest/Oculus (VR headsets) — split by device name (different
// threat models). Mirrors firmware-todo #6 / shared signatures library.
if (cat && strcmp(cat, "glasses") == 0 && !name.empty()) {
std::string lname = name;
for (auto& c : lname) c = (char)tolower((unsigned char)c);
if (lname.find("quest") != std::string::npos ||
lname.find("oculus") != std::string::npos) {
cat = "vr_headset";
}
}
if (detected) {
int idx = fyAddDetection(addrStr.c_str(), name.c_str(), rssi,
method, cat ? cat : "unknown",
@ -1496,6 +1526,12 @@ h+='<div class="pg"><h3>Raven UUIDs ('+p.raven.length+')</h3><div class="it">'+p
document.getElementById('pC').innerHTML=h;window._pL=1;}).catch(()=>{});}
// === GPS ===
let _gW=null,_gOk=false,_gTried=false;
// Screen Wake Lock — keep the screen on during GPS capture. Phone screen-sleep suspends the
// page and stops geolocation (the #1 cause of GPS-less detections while walking). Re-acquire
// when the page returns to the foreground. Needs a secure context (same flag GPS already needs).
let _wl=null;
async function acquireWake(){try{if('wakeLock' in navigator){_wl=await navigator.wakeLock.request('screen');_wl.addEventListener('release',function(){_wl=null;});}}catch(e){}}
document.addEventListener('visibilitychange',function(){if(document.visibilityState==='visible'&&_gW!==null&&_wl===null){acquireWake();}});
function sendGPS(p){_gOk=true;let g=document.getElementById('sG');g.textContent='OK';g.style.color='#22c55e';
fetch('/api/gps?lat='+p.coords.latitude+'&lon='+p.coords.longitude+'&acc='+(p.coords.accuracy||0)).catch(()=>{});}
function gpsErr(e){_gOk=false;let g=document.getElementById('sG');
@ -1506,7 +1542,7 @@ g.textContent=msg;}
function startGPS(){if(!navigator.geolocation){return false;}
if(_gW!==null){navigator.geolocation.clearWatch(_gW);_gW=null;}
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;}
_gW=navigator.geolocation.watchPosition(sendGPS,gpsErr,{enableHighAccuracy:true,maximumAge:5000,timeout:15000});acquireWake();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.');}
@ -1602,7 +1638,7 @@ static void fySetupServer() {
resp->print("],\"wifi_macs\":[");
for (size_t i = 0; i < sizeof(wifi_mac_prefixes)/sizeof(wifi_mac_prefixes[0]); i++) {
if (i > 0) resp->print(",");
resp->printf("\"%s\"", wifi_mac_prefixes[i]);
resp->printf("{\"p\":\"%s\",\"c\":\"%s\"}", wifi_mac_prefixes[i].prefix, wifi_mac_prefixes[i].category);
}
resp->print("],\"names\":[");
for (size_t i = 0; i < sizeof(device_name_entries)/sizeof(device_name_entries[0]); i++) {