Skip to content
Open
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
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,53 @@ Flock-You is part of the OUI-SPY firmware family:

---

## Testing

Detection logic is extracted into `src/fy_detect.h` and tested on the host machine using PlatformIO's native test framework (Unity). No hardware required.

### Running Tests

```bash
pio test -e native # run all tests
pio test -e native -f test_ble_matching # MAC, name, manufacturer ID
pio test -e native -f test_uuid_matching # Raven UUID/FW estimation
```

### Test Structure

```
test/
test_ble_matching/
test_ble_matching.cpp # MAC prefix, device name, manufacturer ID matching
test_uuid_matching/
test_uuid_matching.cpp # Raven UUID matching, firmware version estimation
```

### Hardware Testing (Test Firmware)

A separate test build includes an endpoint that injects simulated detections through the full pipeline — storage, dashboard, buzzer, serial output, BLE notification, and SPIFFS persistence.

```bash
pio run -e xiao_esp32s3_test -t upload # flash test firmware
```

Then trigger simulated detections:

```bash
curl http://192.168.4.1/api/test/inject?type=flock # Flock camera
curl http://192.168.4.1/api/test/inject?type=raven # Raven gunshot detector
curl http://192.168.4.1/api/test/inject?type=soundthinking # SoundThinking sensor
curl http://192.168.4.1/api/test/inject?type=mfr # low-confidence mfr match (no buzzer)
```

The test endpoint is compiled out of production firmware (`pio run -e xiao_esp32s3`) and does not exist in the binary.

### Architecture

`src/fy_detect.h` contains all detection patterns, the `FYDetection` struct, and pure matching functions shared between the firmware and test builds. `main.cpp` includes this header and adds hardware-dependent code (BLE, WiFi, SPIFFS, buzzer). This separation allows the detection logic to be compiled and tested natively without ESP32 toolchains or hardware.

---

## Author

**colonelpanichacks**
Expand Down
18 changes: 18 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[platformio]
default_envs = xiao_esp32s3

[env:xiao_esp32s3]
platform = espressif32@^6.3.0
board = seeed_xiao_esp32s3
Expand Down Expand Up @@ -28,3 +31,18 @@ board_build.filesystem = spiffs
board_build.f_cpu = 240000000L
board_build.f_flash = 80000000L
board_build.flash_mode = qio

; Test firmware — includes /api/test/inject endpoint for hardware verification.
; Build with: pio run -e xiao_esp32s3_test
[env:xiao_esp32s3_test]
extends = env:xiao_esp32s3
build_flags =
${env:xiao_esp32s3.build_flags}
-DFY_ENABLE_TEST_API

; Native test environment (runs on host, no hardware needed)
[env:native]
platform = native
test_framework = unity
build_flags = -std=c++11 -D_GNU_SOURCE
build_src_filter = -<*>
173 changes: 173 additions & 0 deletions src/fy_detect.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// fy_detect.h — Detection logic and pattern data for Flock-You
// Shared between firmware (main.cpp) and native unit tests.
// All functions are static to avoid linker conflicts.

#ifndef FY_DETECT_H
#define FY_DETECT_H

#include <string.h>
#include <stdint.h>

#ifndef ARDUINO
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <strings.h> // strncasecmp, strcasecmp on POSIX
#include <stdio.h>
#endif

// ============================================================================
// DETECTION PATTERNS
// ============================================================================

// 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
"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",
// Flock Safety (direct IEEE registration)
"b4:1e:52"
};

// Flock Safety contract manufacturers — lower confidence alone.
// These OUIs belong to Liteon Technology and USI, which produce Flock
// hardware but also ship unrelated consumer/enterprise devices.
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.
static const char* soundthinking_mac_prefixes[] = {
"d4:11:d6"
};

// BLE device name patterns (matched case-insensitive substring)
static const char* device_name_patterns[] = {
"FS Ext Battery",
"Penguin",
"Flock",
"Pigvision"
};

// BLE Manufacturer Company IDs
// Source: wgreenberg/flock-you — XUNTONG ID associated with Flock Safety devices
static const uint16_t ble_manufacturer_ids[] = {
0x09C8 // XUNTONG
};

// ============================================================================
// RAVEN SURVEILLANCE DEVICE UUID PATTERNS
// ============================================================================

#define RAVEN_DEVICE_INFO_SERVICE "0000180a-0000-1000-8000-00805f9b34fb"
#define RAVEN_GPS_SERVICE "00003100-0000-1000-8000-00805f9b34fb"
#define RAVEN_POWER_SERVICE "00003200-0000-1000-8000-00805f9b34fb"
#define RAVEN_NETWORK_SERVICE "00003300-0000-1000-8000-00805f9b34fb"
#define RAVEN_UPLOAD_SERVICE "00003400-0000-1000-8000-00805f9b34fb"
#define RAVEN_ERROR_SERVICE "00003500-0000-1000-8000-00805f9b34fb"
#define RAVEN_OLD_HEALTH_SERVICE "00001809-0000-1000-8000-00805f9b34fb"
#define RAVEN_OLD_LOCATION_SERVICE "00001819-0000-1000-8000-00805f9b34fb"

static const char* raven_service_uuids[] = {
RAVEN_DEVICE_INFO_SERVICE,
RAVEN_GPS_SERVICE,
RAVEN_POWER_SERVICE,
RAVEN_NETWORK_SERVICE,
RAVEN_UPLOAD_SERVICE,
RAVEN_ERROR_SERVICE,
RAVEN_OLD_HEALTH_SERVICE,
RAVEN_OLD_LOCATION_SERVICE
};

// ============================================================================
// DETECTION STORAGE
// ============================================================================

#define MAX_DETECTIONS 200

struct FYDetection {
char mac[18];
char name[48];
int rssi;
char method[32];
unsigned long firstSeen;
unsigned long lastSeen;
int count;
bool isRaven;
char ravenFW[16];
double gpsLat;
double gpsLon;
float gpsAcc;
bool hasGPS;
};

// ============================================================================
// DETECTION HELPERS
// ============================================================================

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;
}

static bool checkDeviceName(const char* name) {
if (!name || !name[0]) return false;
for (size_t i = 0; i < sizeof(device_name_patterns)/sizeof(device_name_patterns[0]); i++) {
if (strcasestr(name, device_name_patterns[i])) return true;
}
return false;
}

static bool checkManufacturerID(uint16_t id) {
for (size_t i = 0; i < sizeof(ble_manufacturer_ids)/sizeof(ble_manufacturer_ids[0]); i++) {
if (ble_manufacturer_ids[i] == id) return true;
}
return false;
}

// ============================================================================
// RAVEN DETECTION (hardware-independent)
// ============================================================================

// Check a list of UUID strings against known Raven service UUIDs.
static bool checkRavenUUIDFromStrings(const char** uuids, int count, char* out_uuid) {
for (int i = 0; i < count; i++) {
for (size_t j = 0; j < sizeof(raven_service_uuids)/sizeof(raven_service_uuids[0]); j++) {
if (strcasecmp(uuids[i], raven_service_uuids[j]) == 0) {
if (out_uuid) strncpy(out_uuid, uuids[i], 40);
return true;
}
}
}
return false;
}

// Estimate Raven firmware version from which service categories are present.
static const char* estimateRavenFWFromFlags(bool has_new_gps, bool has_old_loc, bool has_power) {
if (has_old_loc && !has_new_gps) return "1.1.x";
if (has_new_gps && !has_power) return "1.2.x";
if (has_new_gps && has_power) return "1.3.x";
return "?";
}

#endif // FY_DETECT_H
Loading