Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.build/
.git/
images/
.claude/
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.build/
test/doctest.h
261 changes: 67 additions & 194 deletions 128x32_OLED/flocksquawk_128x32/flocksquawk_128x32.ino
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
#include <string.h>
#include <ctype.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "esp_wifi.h"
#include "esp_wifi_types.h"

#include "src/EventBus.h"
#include "src/DeviceSignatures.h"
#include "EventBus.h"
#include "DeviceSignatures.h"
#include "src/RadioScanner.h"
#include "src/ThreatAnalyzer.h"
#include "ThreatAnalyzer.h"
#include "src/SoundEngine.h"
#include "src/TelemetryReporter.h"
#include "TelemetryReporter.h"
#include "src/DisplayEngine.h"

// Global system components
Expand Down Expand Up @@ -75,6 +77,17 @@ void EventBus::subscribeAudioRequest(AudioHandler handler) {
audioHandler = handler;
}

// Thread-safe deferred event processing
static portMUX_TYPE wifiMux = portMUX_INITIALIZER_UNLOCKED;
static volatile bool wifiFramePending = false;
static WiFiFrameEvent pendingWiFiFrame;
static portMUX_TYPE bleMux = portMUX_INITIALIZER_UNLOCKED;
static volatile bool bleDevicePending = false;
static BluetoothDeviceEvent pendingBleDevice;
static portMUX_TYPE threatMux = portMUX_INITIALIZER_UNLOCKED;
static volatile bool threatPending = false;
static ThreatEvent pendingThreat;

// RadioScannerManager implementation
void RadioScannerManager::initialize() {
configureWiFiSniffer();
Expand Down Expand Up @@ -224,118 +237,6 @@ 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);
}

// SoundEngine implementation
void SoundEngine::initialize() {
volumeLevel = DEFAULT_VOLUME;
Expand Down Expand Up @@ -566,76 +467,6 @@ void DisplayEngine::clearDisplay() {
}
}

// 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() {
Serial.begin(115200);
Expand All @@ -648,18 +479,24 @@ void setup() {
audioSystem.initialize();

EventBus::subscribeWifiFrame([](const WiFiFrameEvent& event) {
threatEngine.analyzeWiFiFrame(event);
portENTER_CRITICAL(&wifiMux);
pendingWiFiFrame = event;
wifiFramePending = true;
portEXIT_CRITICAL(&wifiMux);
});

EventBus::subscribeBluetoothDevice([](const BluetoothDeviceEvent& event) {
threatEngine.analyzeBluetoothDevice(event);
portENTER_CRITICAL(&bleMux);
pendingBleDevice = event;
bleDevicePending = true;
portEXIT_CRITICAL(&bleMux);
});

EventBus::subscribeThreat([](const ThreatEvent& event) {
reporter.handleThreatDetection(event);
AudioEvent audioEvent;
audioEvent.soundFile = "/alert.wav";
EventBus::publishAudioRequest(audioEvent);
portENTER_CRITICAL(&threatMux);
pendingThreat = event;
threatPending = true;
portEXIT_CRITICAL(&threatMux);
});

EventBus::subscribeAudioRequest([](const AudioEvent& event) {
Expand Down Expand Up @@ -690,6 +527,42 @@ void setup() {

void loop() {
rfScanner.update();
uint32_t now = millis();

if (wifiFramePending) {
WiFiFrameEvent frameCopy;
portENTER_CRITICAL(&wifiMux);
frameCopy = pendingWiFiFrame;
wifiFramePending = false;
portEXIT_CRITICAL(&wifiMux);
threatEngine.analyzeWiFiFrame(frameCopy);
}

if (bleDevicePending) {
BluetoothDeviceEvent bleCopy;
portENTER_CRITICAL(&bleMux);
bleCopy = pendingBleDevice;
bleDevicePending = false;
portEXIT_CRITICAL(&bleMux);
threatEngine.analyzeBluetoothDevice(bleCopy);
}

threatEngine.tick(now);

if (threatPending) {
ThreatEvent threatCopy;
portENTER_CRITICAL(&threatMux);
threatCopy = pendingThreat;
threatPending = false;
portEXIT_CRITICAL(&threatMux);
reporter.handleThreatDetection(threatCopy);
if (threatCopy.shouldAlert) {
AudioEvent audioEvent;
audioEvent.soundFile = "/alert.wav";
EventBus::publishAudioRequest(audioEvent);
}
}

displaySystem.update();
delay(100);
}
51 changes: 0 additions & 51 deletions 128x32_OLED/flocksquawk_128x32/src/DeviceSignatures.h

This file was deleted.

Loading
Loading