diff --git a/m5stack/flocksquawk_m5stick/flocksquawk_m5stick.ino b/m5stack/flocksquawk_m5stick/flocksquawk_m5stick.ino index 412428b..65443ec 100644 --- a/m5stack/flocksquawk_m5stick/flocksquawk_m5stick.ino +++ b/m5stack/flocksquawk_m5stick/flocksquawk_m5stick.ino @@ -78,6 +78,9 @@ namespace { portMUX_TYPE wifiMux = portMUX_INITIALIZER_UNLOCKED; volatile bool wifiFramePending = false; WiFiFrameEvent pendingWiFiFrame; + portMUX_TYPE bleMux = portMUX_INITIALIZER_UNLOCKED; + volatile bool bleDevicePending = false; + BluetoothDeviceEvent pendingBleDevice; bool statusMessageActive = false; uint32_t statusMessageUntilMs = 0; @@ -510,193 +513,12 @@ void RadioScannerManager::wifiPacketHandler(void* buffer, wifi_promiscuous_pkt_t EventBus::publishWifiFrame(event); } -uint8_t RadioScannerManager::currentWifiChannel = 1; +volatile uint8_t RadioScannerManager::currentWifiChannel = 1; unsigned long RadioScannerManager::lastChannelSwitch = 0; unsigned long RadioScannerManager::lastBLEScan = 0; NimBLEScan* RadioScannerManager::bleScanner = nullptr; bool RadioScannerManager::isScanningBLE = false; -// ThreatAnalyzer implementation -void ThreatAnalyzer::initialize() { - // Analyzer ready -} - -void ThreatAnalyzer::analyzeWiFiFrame(const WiFiFrameEvent& frame) { - bool nameMatch = strlen(frame.ssid) > 0 && matchesNetworkName(frame.ssid); - bool macMatch = matchesMACPrefix(frame.mac); - - if (nameMatch || macMatch) { - uint8_t certainty = calculateCertainty(nameMatch, macMatch, false); - emitThreatDetection(frame, "wifi", certainty); - } -} - -void ThreatAnalyzer::analyzeBluetoothDevice(const BluetoothDeviceEvent& device) { - bool nameMatch = strlen(device.name) > 0 && matchesBLEName(device.name); - bool macMatch = matchesMACPrefix(device.mac); - bool uuidMatch = device.hasServiceUUID && matchesRavenService(device.serviceUUID); - - if (nameMatch || macMatch || uuidMatch) { - uint8_t certainty = calculateCertainty(nameMatch, macMatch, uuidMatch); - const char* category = determineCategory(uuidMatch); - emitThreatDetection(device, "bluetooth", certainty, category); - } -} - -bool ThreatAnalyzer::matchesNetworkName(const char* ssid) { - if (!ssid) return false; - - for (size_t i = 0; i < DeviceProfiles::NetworkNameCount; i++) { - if (strcasestr(ssid, DeviceProfiles::NetworkNames[i])) { - return true; - } - } - return false; -} - -bool ThreatAnalyzer::matchesMACPrefix(const uint8_t* mac) { - char macStr[9]; - snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x", mac[0], mac[1], mac[2]); - - for (size_t i = 0; i < DeviceProfiles::MACPrefixCount; i++) { - if (strncasecmp(macStr, DeviceProfiles::MACPrefixes[i], 8) == 0) { - return true; - } - } - return false; -} - -bool ThreatAnalyzer::matchesBLEName(const char* name) { - if (!name) return false; - - for (size_t i = 0; i < DeviceProfiles::BLEIdentifierCount; i++) { - if (strcasestr(name, DeviceProfiles::BLEIdentifiers[i])) { - return true; - } - } - return false; -} - -bool ThreatAnalyzer::matchesRavenService(const char* uuid) { - if (!uuid) return false; - - for (size_t i = 0; i < DeviceProfiles::RavenServiceCount; i++) { - if (strcasecmp(uuid, DeviceProfiles::RavenServices[i]) == 0) { - return true; - } - } - return false; -} - -uint8_t ThreatAnalyzer::calculateCertainty(bool nameMatch, bool macMatch, bool uuidMatch) { - if (nameMatch && macMatch && uuidMatch) return 100; - if (nameMatch && macMatch) return 95; - if (uuidMatch) return 90; - if (nameMatch || macMatch) return 85; - return 70; -} - -const char* ThreatAnalyzer::determineCategory(bool isRaven) { - return isRaven ? "acoustic_detector" : "surveillance_device"; -} - -void ThreatAnalyzer::emitThreatDetection(const WiFiFrameEvent& frame, const char* radio, uint8_t certainty) { - ThreatEvent threat; - memset(&threat, 0, sizeof(threat)); - memcpy(threat.mac, frame.mac, 6); - strncpy(threat.identifier, frame.ssid, sizeof(threat.identifier) - 1); - threat.rssi = frame.rssi; - threat.channel = frame.channel; - threat.radioType = radio; - threat.certainty = certainty; - threat.category = "surveillance_device"; - - EventBus::publishThreat(threat); -} - -void ThreatAnalyzer::emitThreatDetection(const BluetoothDeviceEvent& device, const char* radio, uint8_t certainty, const char* category) { - ThreatEvent threat; - memset(&threat, 0, sizeof(threat)); - memcpy(threat.mac, device.mac, 6); - strncpy(threat.identifier, device.name, sizeof(threat.identifier) - 1); - threat.rssi = device.rssi; - threat.channel = 0; - threat.radioType = radio; - threat.certainty = certainty; - threat.category = category; - - EventBus::publishThreat(threat); -} - -// TelemetryReporter implementation -void TelemetryReporter::initialize() { - bootTime = millis(); -} - -void TelemetryReporter::handleThreatDetection(const ThreatEvent& threat) { - DynamicJsonDocument doc(2048); - - doc["event"] = "target_detected"; - doc["ms_since_boot"] = millis() - bootTime; - - appendSourceInfo(threat, doc); - appendTargetIdentity(threat, doc); - appendIndicators(threat, doc); - appendMetadata(threat, doc); - - outputJSON(doc); -} - -void TelemetryReporter::appendSourceInfo(const ThreatEvent& threat, JsonDocument& doc) { - JsonObject source = doc.createNestedObject("source"); - source["radio"] = threat.radioType; - source["channel"] = threat.channel; - source["rssi"] = threat.rssi; -} - -void TelemetryReporter::appendTargetIdentity(const ThreatEvent& threat, JsonDocument& doc) { - JsonObject target = doc.createNestedObject("target"); - JsonObject identity = target.createNestedObject("identity"); - - char macStr[18]; - snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x", - threat.mac[0], threat.mac[1], threat.mac[2], - threat.mac[3], threat.mac[4], threat.mac[5]); - identity["mac"] = macStr; - - char oui[9]; - snprintf(oui, sizeof(oui), "%02x:%02x:%02x", threat.mac[0], threat.mac[1], threat.mac[2]); - identity["oui"] = oui; - - identity["label"] = threat.identifier; -} - -void TelemetryReporter::appendIndicators(const ThreatEvent& threat, JsonDocument& doc) { - JsonObject indicators = doc["target"].createNestedObject("indicators"); - - bool hasName = strlen(threat.identifier) > 0; - indicators["ssid_match"] = (hasName && strcmp(threat.radioType, "wifi") == 0); - indicators["mac_match"] = true; - indicators["name_match"] = (hasName && strcmp(threat.radioType, "bluetooth") == 0); - indicators["service_uuid_match"] = (strcmp(threat.category, "acoustic_detector") == 0); -} - -void TelemetryReporter::appendMetadata(const ThreatEvent& threat, JsonDocument& doc) { - JsonObject metadata = doc.createNestedObject("metadata"); - - if (strcmp(threat.radioType, "wifi") == 0) { - metadata["frame_type"] = "beacon"; - } else { - metadata["frame_type"] = "advertisement"; - } - - metadata["detection_method"] = "combined_signature"; -} - -void TelemetryReporter::outputJSON(const JsonDocument& doc) { - serializeJson(doc, Serial); - Serial.println(); -} // Main system initialization void setup() { @@ -726,7 +548,10 @@ void setup() { }); EventBus::subscribeBluetoothDevice([](const BluetoothDeviceEvent& event) { - threatEngine.analyzeBluetoothDevice(event); + portENTER_CRITICAL(&bleMux); + pendingBleDevice = event; + bleDevicePending = true; + portEXIT_CRITICAL(&bleMux); }); EventBus::subscribeThreat([](const ThreatEvent& event) { @@ -777,6 +602,19 @@ void loop() { threatEngine.analyzeWiFiFrame(frameCopy); } + if (bleDevicePending) { + BluetoothDeviceEvent bleCopy; + portENTER_CRITICAL(&bleMux); + bleCopy = pendingBleDevice; + bleDevicePending = false; + portEXIT_CRITICAL(&bleMux); + threatEngine.analyzeBluetoothDevice(bleCopy); + } + + if (threatEngine.tick(now)) { + M5.Speaker.tone(1800, 40); + } + if (threatPending) { ThreatEvent threatCopy; portENTER_CRITICAL(&threatMux); @@ -784,7 +622,7 @@ void loop() { threatPending = false; portEXIT_CRITICAL(&threatMux); reporter.handleThreatDetection(threatCopy); - triggerAlert(now); + if (threatCopy.shouldAlert) triggerAlert(now); } if (M5.BtnB.pressedFor(2000) && !powerToggleHandled) { diff --git a/m5stack/flocksquawk_m5stick/src/DetectorTypes.h b/m5stack/flocksquawk_m5stick/src/DetectorTypes.h new file mode 100644 index 0000000..8d2d1fd --- /dev/null +++ b/m5stack/flocksquawk_m5stick/src/DetectorTypes.h @@ -0,0 +1,69 @@ +#ifndef DETECTOR_TYPES_H +#define DETECTOR_TYPES_H + +#include +#include + +// Result returned by every detector function. Stack-allocated, no heap. +struct DetectorResult { + bool matched; + uint8_t weight; + const char* detectorName; +}; + +// Bitmask flags for tracking which detectors fired. +// Each detector gets one bit; stored in ThreatEvent::matchFlags. +enum DetectorFlag : uint16_t { + DET_NONE = 0, + DET_SSID_FORMAT = (1 << 0), + DET_SSID_KEYWORD = (1 << 1), + DET_MAC_OUI = (1 << 2), + DET_BLE_NAME = (1 << 3), + DET_RAVEN_CUSTOM_UUID = (1 << 4), + DET_RAVEN_STD_UUID = (1 << 5), + DET_RSSI_MODIFIER = (1 << 6), +}; + +// Forward declarations (defined in EventBus.h) +struct WiFiFrameEvent; +struct BluetoothDeviceEvent; + +// Detector function pointer types +typedef DetectorResult (*WiFiDetectorFn)(const WiFiFrameEvent& frame); +typedef DetectorResult (*BLEDetectorFn)(const BluetoothDeviceEvent& device); + +// Registry entries pair a function with its flag bit +struct WiFiDetectorEntry { + WiFiDetectorFn fn; + DetectorFlag flag; +}; + +struct BLEDetectorEntry { + BLEDetectorFn fn; + DetectorFlag flag; +}; + +// Device presence tracking +enum class DeviceState : uint8_t { + EMPTY, + NEW_DETECT, + IN_RANGE, + DEPARTED, +}; + +struct TrackedDevice { + uint8_t mac[6]; + uint32_t firstSeenMs; + uint32_t lastSeenMs; + uint8_t maxCertainty; + DeviceState state; + // 6 + 4 + 4 + 1 + 1 = 16 bytes per slot +}; + +// Constants +static const uint8_t MAX_TRACKED_DEVICES = 32; +static const uint32_t DEVICE_TIMEOUT_MS = 60000; +static const uint32_t HEARTBEAT_INTERVAL_MS = 10000; +static const uint8_t ALERT_THRESHOLD = 65; + +#endif diff --git a/m5stack/flocksquawk_m5stick/src/Detectors.h b/m5stack/flocksquawk_m5stick/src/Detectors.h new file mode 100644 index 0000000..c78d00f --- /dev/null +++ b/m5stack/flocksquawk_m5stick/src/Detectors.h @@ -0,0 +1,183 @@ +#ifndef DETECTORS_H +#define DETECTORS_H + +#include "DetectorTypes.h" +#include "EventBus.h" +#include "DeviceSignatures.h" +#include +#include + +// ============================================================ +// Helpers +// ============================================================ + +inline bool isHexChar(char c) { + return (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); +} + +inline bool isHexSuffix(const char* s, uint8_t len) { + for (uint8_t i = 0; i < len; i++) { + if (!isHexChar(s[i])) return false; + } + return s[len] == '\0'; +} + +inline bool isDecimalSuffix(const char* s, uint8_t len) { + for (uint8_t i = 0; i < len; i++) { + if (s[i] < '0' || s[i] > '9') return false; + } + return s[len] == '\0'; +} + +inline bool ouiMatchesKnownPrefix(const uint8_t* mac) { + char macStr[9]; + snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x", mac[0], mac[1], mac[2]); + for (size_t i = 0; i < DeviceProfiles::MACPrefixCount; i++) { + if (strncasecmp(macStr, DeviceProfiles::MACPrefixes[i], 8) == 0) + return true; + } + return false; +} + +// ============================================================ +// WiFi Detectors +// ============================================================ + +// SSID Format Match (weight 75) +// Validates highly specific patterns: +// "Flock-" + exactly 6 hex chars +// "Penguin-" + exactly 10 decimal digits +// Exact "FS Ext Battery" +inline DetectorResult detectSsidFormat(const WiFiFrameEvent& frame) { + DetectorResult r = { false, 75, "ssid_format" }; + const char* ssid = frame.ssid; + if (ssid[0] == '\0') return r; + size_t len = strlen(ssid); + + if (len == 12 && strncmp(ssid, "Flock-", 6) == 0 && + isHexSuffix(ssid + 6, 6)) { + r.matched = true; + return r; + } + + if (len == 18 && strncmp(ssid, "Penguin-", 8) == 0 && + isDecimalSuffix(ssid + 8, 10)) { + r.matched = true; + return r; + } + + if (strcmp(ssid, "FS Ext Battery") == 0) { + r.matched = true; + return r; + } + + return r; +} + +// SSID Keyword Match (weight 45) +// Case-insensitive substring search for known keywords. +inline DetectorResult detectSsidKeyword(const WiFiFrameEvent& frame) { + DetectorResult r = { false, 45, "ssid_keyword" }; + const char* ssid = frame.ssid; + if (ssid[0] == '\0') return r; + + static const char* const keywords[] = { + "flock", "penguin", "pigvision" + }; + static const uint8_t count = sizeof(keywords) / sizeof(keywords[0]); + + for (uint8_t i = 0; i < count; i++) { + if (strcasestr(ssid, keywords[i])) { + r.matched = true; + return r; + } + } + return r; +} + +// WiFi MAC OUI Match (weight 20) +inline DetectorResult detectWifiMacOui(const WiFiFrameEvent& frame) { + DetectorResult r = { false, 20, "mac_oui" }; + if (ouiMatchesKnownPrefix(frame.mac)) r.matched = true; + return r; +} + +// ============================================================ +// BLE Detectors +// ============================================================ + +// BLE Device Name Match (weight 55) +inline DetectorResult detectBleName(const BluetoothDeviceEvent& device) { + DetectorResult r = { false, 55, "ble_name" }; + if (device.name[0] == '\0') return r; + + static const char* const names[] = { + "Flock", "Penguin", "FS Ext Battery", "Pigvision" + }; + static const uint8_t count = sizeof(names) / sizeof(names[0]); + + for (uint8_t i = 0; i < count; i++) { + if (strcasestr(device.name, names[i])) { + r.matched = true; + return r; + } + } + return r; +} + +// Raven Custom UUID Match (weight 80) +// Matches UUIDs with 16-bit short IDs 0x3100 through 0x3500. +// Format: "0000XXXX-0000-1000-8000-00805f9b34fb" +inline DetectorResult detectRavenCustomUuid(const BluetoothDeviceEvent& device) { + DetectorResult r = { false, 80, "raven_custom_uuid" }; + if (!device.hasServiceUUID || device.serviceUUID[0] == '\0') return r; + + const char* uuid = device.serviceUUID; + if (strlen(uuid) < 8) return r; + + // Check prefix "00003X00" where X is 1-5 + if (uuid[0] == '0' && uuid[1] == '0' && uuid[2] == '0' && uuid[3] == '0' && + uuid[4] == '3' && uuid[5] >= '1' && uuid[5] <= '5' && + uuid[6] == '0' && uuid[7] == '0') { + r.matched = true; + } + return r; +} + +// Raven Standard UUID Match (weight 10) +// Matches standard BLE SIG UUIDs that Raven also uses. +// Low weight because these are very common across consumer devices. +// 0x180A = Device Information, 0x1809 = Health Thermometer, 0x1819 = Location/Navigation +inline DetectorResult detectRavenStdUuid(const BluetoothDeviceEvent& device) { + DetectorResult r = { false, 10, "raven_std_uuid" }; + if (!device.hasServiceUUID || device.serviceUUID[0] == '\0') return r; + + if (strncasecmp(device.serviceUUID, "0000180a", 8) == 0 || + strncasecmp(device.serviceUUID, "00001809", 8) == 0 || + strncasecmp(device.serviceUUID, "00001819", 8) == 0) { + r.matched = true; + } + return r; +} + +// BLE MAC OUI Match (weight 20) +inline DetectorResult detectBleMacOui(const BluetoothDeviceEvent& device) { + DetectorResult r = { false, 20, "mac_oui" }; + if (ouiMatchesKnownPrefix(device.mac)) r.matched = true; + return r; +} + +// ============================================================ +// RSSI Modifier +// ============================================================ + +inline int8_t rssiModifier(int8_t rssi) { + if (rssi > -50) return 10; + if (rssi > -70) return 0; + if (rssi > -85) return -5; + return -10; +} + +#endif diff --git a/m5stack/flocksquawk_m5stick/src/DeviceSignatures.h b/m5stack/flocksquawk_m5stick/src/DeviceSignatures.h index 78a105f..c5eada4 100644 --- a/m5stack/flocksquawk_m5stick/src/DeviceSignatures.h +++ b/m5stack/flocksquawk_m5stick/src/DeviceSignatures.h @@ -4,19 +4,8 @@ #include namespace DeviceProfiles { - - // Network name patterns for target identification - const char* const NetworkNames[] = { - "flock", - "Flock", - "FLOCK", - "FS Ext Battery", - "Penguin", - "Pigvision", - }; - const size_t NetworkNameCount = sizeof(NetworkNames) / sizeof(NetworkNames[0]); - // MAC address OUI prefixes for target devices + // MAC address OUI prefixes for target devices (Silicon Laboratories) const char* const MACPrefixes[] = { "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", @@ -24,28 +13,6 @@ namespace DeviceProfiles { "74:4c:a1", "08:3a:88", "9c:2f:9d", "94:08:53", "e4:aa:ea" }; const size_t MACPrefixCount = sizeof(MACPrefixes) / sizeof(MACPrefixes[0]); - - // Bluetooth device name patterns - const char* const BLEIdentifiers[] = { - "FS Ext Battery", - "Penguin", - "Flock", - "Pigvision" - }; - const size_t BLEIdentifierCount = sizeof(BLEIdentifiers) / sizeof(BLEIdentifiers[0]); - - // Raven acoustic detection device service UUIDs - const char* const RavenServices[] = { - "0000180a-0000-1000-8000-00805f9b34fb", // Device info (all versions) - "00003100-0000-1000-8000-00805f9b34fb", // GPS (1.2.0+) - "00003200-0000-1000-8000-00805f9b34fb", // Power/Battery (1.2.0+) - "00003300-0000-1000-8000-00805f9b34fb", // Network (1.2.0+) - "00003400-0000-1000-8000-00805f9b34fb", // Upload stats (1.2.0+) - "00003500-0000-1000-8000-00805f9b34fb", // Error tracking (1.2.0+) - "00001809-0000-1000-8000-00805f9b34fb", // Health/Temp (legacy 1.1.7) - "00001819-0000-1000-8000-00805f9b34fb" // Location (legacy 1.1.7) - }; - const size_t RavenServiceCount = sizeof(RavenServices) / sizeof(RavenServices[0]); } #endif diff --git a/m5stack/flocksquawk_m5stick/src/EventBus.h b/m5stack/flocksquawk_m5stick/src/EventBus.h index 6fa14fa..237a7d2 100644 --- a/m5stack/flocksquawk_m5stick/src/EventBus.h +++ b/m5stack/flocksquawk_m5stick/src/EventBus.h @@ -32,9 +32,13 @@ struct ThreatEvent { char identifier[64]; int8_t rssi; uint8_t channel; - const char* radioType; + char radioType[16]; uint8_t certainty; - const char* category; + char category[24]; + uint16_t matchFlags; + uint8_t detectorWeights[8]; + int8_t rssiModifier; + bool shouldAlert; }; class EventBus { diff --git a/m5stack/flocksquawk_m5stick/src/RadioScanner.h b/m5stack/flocksquawk_m5stick/src/RadioScanner.h index 0d72b08..fb369bd 100644 --- a/m5stack/flocksquawk_m5stick/src/RadioScanner.h +++ b/m5stack/flocksquawk_m5stick/src/RadioScanner.h @@ -13,16 +13,16 @@ class RadioScannerManager { public: static const uint8_t MAX_WIFI_CHANNEL = 13; - static const uint16_t CHANNEL_SWITCH_MS = 500; + static const uint16_t CHANNEL_SWITCH_MS = 1000; static const uint8_t BLE_SCAN_SECONDS = 1; static const uint32_t BLE_SCAN_INTERVAL_MS = 5000; void initialize(); void update(); // Call from main loop static uint8_t getCurrentWifiChannel(); - + private: - static uint8_t currentWifiChannel; + static volatile uint8_t currentWifiChannel; static unsigned long lastChannelSwitch; static unsigned long lastBLEScan; static NimBLEScan* bleScanner; diff --git a/m5stack/flocksquawk_m5stick/src/TelemetryReporter.h b/m5stack/flocksquawk_m5stick/src/TelemetryReporter.h index ca38c42..64c1f33 100644 --- a/m5stack/flocksquawk_m5stick/src/TelemetryReporter.h +++ b/m5stack/flocksquawk_m5stick/src/TelemetryReporter.h @@ -4,21 +4,68 @@ #include #include #include "EventBus.h" +#include "DetectorTypes.h" class TelemetryReporter { public: - void initialize(); - void handleThreatDetection(const ThreatEvent& threat); - + void initialize() { + bootTime = millis(); + } + + void handleThreatDetection(const ThreatEvent& threat) { + StaticJsonDocument<768> doc; + + doc["event"] = "target_detected"; + doc["ms_since_boot"] = millis() - bootTime; + + // Source info + JsonObject source = doc.createNestedObject("source"); + source["radio"] = threat.radioType; + source["channel"] = threat.channel; + source["rssi"] = threat.rssi; + + // Target identity + JsonObject target = doc.createNestedObject("target"); + + char macStr[18]; + snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x", + threat.mac[0], threat.mac[1], threat.mac[2], + threat.mac[3], threat.mac[4], threat.mac[5]); + target["mac"] = macStr; + target["label"] = threat.identifier; + target["certainty"] = threat.certainty; + target["category"] = threat.category; + target["should_alert"] = threat.shouldAlert; + + // Detector details from matchFlags + JsonObject detectors = target.createNestedObject("detectors"); + + static const char* const detectorNames[] = { + "ssid_format", "ssid_keyword", "mac_oui", + "ble_name", "raven_custom_uuid", "raven_std_uuid", + "rssi_modifier" + }; + + static const uint8_t DETECTOR_NAME_COUNT = + sizeof(detectorNames) / sizeof(detectorNames[0]); + + for (uint8_t bit = 0; bit < DETECTOR_NAME_COUNT; bit++) { + if (threat.matchFlags & (1 << bit)) { + if (bit == 6) { + // rssi_modifier is signed + detectors[detectorNames[bit]] = threat.rssiModifier; + } else { + detectors[detectorNames[bit]] = threat.detectorWeights[bit]; + } + } + } + + serializeJson(doc, Serial); + Serial.println(); + } + private: unsigned long bootTime; - - void serializeThreatToJSON(const ThreatEvent& threat, JsonDocument& doc); - void appendSourceInfo(const ThreatEvent& threat, JsonDocument& doc); - void appendTargetIdentity(const ThreatEvent& threat, JsonDocument& doc); - void appendIndicators(const ThreatEvent& threat, JsonDocument& doc); - void appendMetadata(const ThreatEvent& threat, JsonDocument& doc); - void outputJSON(const JsonDocument& doc); }; -#endif \ No newline at end of file +#endif diff --git a/m5stack/flocksquawk_m5stick/src/ThreatAnalyzer.h b/m5stack/flocksquawk_m5stick/src/ThreatAnalyzer.h index 12c95ae..5666289 100644 --- a/m5stack/flocksquawk_m5stick/src/ThreatAnalyzer.h +++ b/m5stack/flocksquawk_m5stick/src/ThreatAnalyzer.h @@ -3,25 +3,264 @@ #include #include "EventBus.h" -#include "DeviceSignatures.h" +#include "DetectorTypes.h" +#include "Detectors.h" + +// ============================================================ +// Detector Registry +// To add a detector: append one entry to the appropriate array. +// ============================================================ + +static const WiFiDetectorEntry wifiDetectors[] = { + { detectSsidFormat, DET_SSID_FORMAT }, + { detectSsidKeyword, DET_SSID_KEYWORD }, + { detectWifiMacOui, DET_MAC_OUI }, +}; +static const uint8_t WIFI_DETECTOR_COUNT = + sizeof(wifiDetectors) / sizeof(wifiDetectors[0]); + +static const BLEDetectorEntry bleDetectors[] = { + { detectBleName, DET_BLE_NAME }, + { detectRavenCustomUuid, DET_RAVEN_CUSTOM_UUID }, + { detectRavenStdUuid, DET_RAVEN_STD_UUID }, + { detectBleMacOui, DET_MAC_OUI }, +}; +static const uint8_t BLE_DETECTOR_COUNT = + sizeof(bleDetectors) / sizeof(bleDetectors[0]); + +// ============================================================ +// Device Presence Tracker +// ============================================================ + +class DeviceTracker { +public: + void initialize() { + for (uint8_t i = 0; i < MAX_TRACKED_DEVICES; i++) { + slots[i].state = DeviceState::EMPTY; + } + } + + // Age out stale devices. Call every loop iteration. + void tick(uint32_t nowMs) { + for (uint8_t i = 0; i < MAX_TRACKED_DEVICES; i++) { + if (slots[i].state == DeviceState::IN_RANGE || + slots[i].state == DeviceState::NEW_DETECT) { + if (nowMs - slots[i].lastSeenMs > DEVICE_TIMEOUT_MS) { + slots[i].state = DeviceState::DEPARTED; + } + } + } + } + + // Record a detection. Returns the state the device was in BEFORE + // this update (EMPTY = first time seen). + DeviceState recordDetection(const uint8_t* mac, uint32_t nowMs, + uint8_t certainty) { + for (uint8_t i = 0; i < MAX_TRACKED_DEVICES; i++) { + if (slots[i].state != DeviceState::EMPTY && + slots[i].state != DeviceState::DEPARTED && + memcmp(slots[i].mac, mac, 6) == 0) { + DeviceState prev = slots[i].state; + slots[i].lastSeenMs = nowMs; + slots[i].state = DeviceState::IN_RANGE; + if (certainty > slots[i].maxCertainty) + slots[i].maxCertainty = certainty; + return prev; + } + } + + uint8_t slot = findFreeSlot(); + memcpy(slots[slot].mac, mac, 6); + slots[slot].firstSeenMs = nowMs; + slots[slot].lastSeenMs = nowMs; + slots[slot].maxCertainty = certainty; + slots[slot].state = DeviceState::NEW_DETECT; + return DeviceState::EMPTY; + } + + // Returns true if any tracked device is IN_RANGE and above threshold. + bool hasHighConfidenceInRange() const { + for (uint8_t i = 0; i < MAX_TRACKED_DEVICES; i++) { + if (slots[i].state == DeviceState::IN_RANGE && + slots[i].maxCertainty >= ALERT_THRESHOLD) { + return true; + } + } + return false; + } + +private: + TrackedDevice slots[MAX_TRACKED_DEVICES]; + + uint8_t findFreeSlot() { + for (uint8_t i = 0; i < MAX_TRACKED_DEVICES; i++) { + if (slots[i].state == DeviceState::EMPTY) return i; + } + uint8_t oldest = 0; + uint32_t oldestTime = UINT32_MAX; + for (uint8_t i = 0; i < MAX_TRACKED_DEVICES; i++) { + if (slots[i].state == DeviceState::DEPARTED && + slots[i].lastSeenMs < oldestTime) { + oldest = i; + oldestTime = slots[i].lastSeenMs; + } + } + if (oldestTime < UINT32_MAX) return oldest; + // All slots active -- evict LRU + oldest = 0; + oldestTime = UINT32_MAX; + for (uint8_t i = 0; i < MAX_TRACKED_DEVICES; i++) { + if (slots[i].lastSeenMs < oldestTime) { + oldest = i; + oldestTime = slots[i].lastSeenMs; + } + } + slots[oldest].state = DeviceState::EMPTY; + return oldest; + } +}; + +// ============================================================ +// Bit position helper +// ============================================================ + +inline uint8_t detectorBitPosition(uint16_t flag) { + uint8_t pos = 0; + while (flag > 1) { flag >>= 1; pos++; } + return pos; +} + +// ============================================================ +// ThreatAnalyzer +// Pure logic -- no display/buzzer access. Safe from any context. +// ============================================================ class ThreatAnalyzer { public: - void initialize(); - void analyzeWiFiFrame(const WiFiFrameEvent& frame); - void analyzeBluetoothDevice(const BluetoothDeviceEvent& device); - + void initialize() { + tracker.initialize(); + lastHeartbeatMs = 0; + } + + void analyzeWiFiFrame(const WiFiFrameEvent& frame) { + uint16_t matchFlags = 0; + uint8_t weights[8]; + memset(weights, 0, sizeof(weights)); + int16_t totalWeight = 0; + + for (uint8_t i = 0; i < WIFI_DETECTOR_COUNT; i++) { + DetectorResult res = wifiDetectors[i].fn(frame); + if (res.matched) { + matchFlags |= wifiDetectors[i].flag; + uint8_t bit = detectorBitPosition(wifiDetectors[i].flag); + if (bit < sizeof(weights)) weights[bit] = res.weight; + totalWeight += res.weight; + } + } + + // Subsumption: SSID format supersedes SSID keyword + if ((matchFlags & DET_SSID_FORMAT) && (matchFlags & DET_SSID_KEYWORD)) { + totalWeight -= weights[detectorBitPosition(DET_SSID_KEYWORD)]; + matchFlags &= ~DET_SSID_KEYWORD; + weights[detectorBitPosition(DET_SSID_KEYWORD)] = 0; + } + + if (matchFlags == DET_NONE) return; + + int8_t rssiMod = rssiModifier(frame.rssi); + totalWeight += rssiMod; + uint8_t certainty = (uint8_t)constrain(totalWeight, 0, 100); + + uint32_t nowMs = millis(); + DeviceState prevState = tracker.recordDetection( + frame.mac, nowMs, certainty); + + ThreatEvent threat; + memset(&threat, 0, sizeof(threat)); + memcpy(threat.mac, frame.mac, 6); + strncpy(threat.identifier, frame.ssid, + sizeof(threat.identifier) - 1); + threat.rssi = frame.rssi; + threat.channel = frame.channel; + strncpy(threat.radioType, "wifi", sizeof(threat.radioType) - 1); + threat.certainty = certainty; + strncpy(threat.category, "surveillance_device", sizeof(threat.category) - 1); + threat.matchFlags = matchFlags | DET_RSSI_MODIFIER; + memcpy(threat.detectorWeights, weights, sizeof(weights)); + threat.rssiModifier = rssiMod; + threat.shouldAlert = (certainty >= ALERT_THRESHOLD && + prevState == DeviceState::EMPTY); + + EventBus::publishThreat(threat); + } + + void analyzeBluetoothDevice(const BluetoothDeviceEvent& device) { + uint16_t matchFlags = 0; + uint8_t weights[8]; + memset(weights, 0, sizeof(weights)); + int16_t totalWeight = 0; + + for (uint8_t i = 0; i < BLE_DETECTOR_COUNT; i++) { + DetectorResult res = bleDetectors[i].fn(device); + if (res.matched) { + matchFlags |= bleDetectors[i].flag; + uint8_t bit = detectorBitPosition(bleDetectors[i].flag); + if (bit < sizeof(weights)) weights[bit] = res.weight; + totalWeight += res.weight; + } + } + + if (matchFlags == DET_NONE) return; + + int8_t rssiMod = rssiModifier(device.rssi); + totalWeight += rssiMod; + uint8_t certainty = (uint8_t)constrain(totalWeight, 0, 100); + + uint32_t nowMs = millis(); + DeviceState prevState = tracker.recordDetection( + device.mac, nowMs, certainty); + + const char* cat = + (matchFlags & (DET_RAVEN_CUSTOM_UUID | DET_RAVEN_STD_UUID)) + ? "acoustic_detector" + : "surveillance_device"; + + ThreatEvent threat; + memset(&threat, 0, sizeof(threat)); + memcpy(threat.mac, device.mac, 6); + strncpy(threat.identifier, device.name, + sizeof(threat.identifier) - 1); + threat.rssi = device.rssi; + threat.channel = 0; + strncpy(threat.radioType, "bluetooth", sizeof(threat.radioType) - 1); + threat.certainty = certainty; + strncpy(threat.category, cat, sizeof(threat.category) - 1); + threat.matchFlags = matchFlags | DET_RSSI_MODIFIER; + memcpy(threat.detectorWeights, weights, sizeof(weights)); + threat.rssiModifier = rssiMod; + threat.shouldAlert = (certainty >= ALERT_THRESHOLD && + prevState == DeviceState::EMPTY); + + EventBus::publishThreat(threat); + } + + // Call from loop(). Ages out stale devices and returns true + // if a heartbeat beep should be emitted (caller handles hardware). + bool tick(uint32_t nowMs) { + tracker.tick(nowMs); + + if (nowMs - lastHeartbeatMs >= HEARTBEAT_INTERVAL_MS) { + lastHeartbeatMs = nowMs; + if (tracker.hasHighConfidenceInRange()) { + return true; + } + } + return false; + } + private: - bool matchesNetworkName(const char* ssid); - bool matchesMACPrefix(const uint8_t* mac); - bool matchesBLEName(const char* name); - bool matchesRavenService(const char* uuid); - uint8_t calculateCertainty(bool nameMatch, bool macMatch, bool uuidMatch); - const char* determineCategory(bool isRaven); - void emitThreatDetection(const WiFiFrameEvent& frame, const char* radio, uint8_t certainty); - void emitThreatDetection(const BluetoothDeviceEvent& device, const char* radio, uint8_t certainty, const char* category); - void formatMACAddress(const uint8_t* mac, char* output); - void extractOUI(const uint8_t* mac, char* output); + DeviceTracker tracker; + uint32_t lastHeartbeatMs; }; -#endif \ No newline at end of file +#endif