Add pluggable detector system with scoring pipeline and device tracking#3
Add pluggable detector system with scoring pipeline and device tracking#3dougborg wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the Flock Safety detection system from hard-coded logic to a pluggable, weighted detector architecture with improved device tracking and thread safety.
Changes:
- Replaces boolean detection flags with a weighted scoring system that combines results from multiple detector functions
- Introduces DeviceTracker for temporal correlation, duplicate suppression, and device state management
- Adds BLE thread safety using FreeRTOS spinlocks and critical sections, mirroring the existing WiFi pattern
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| DetectorTypes.h | New file defining detector result types, flags, function pointers, device state machine, and tracking constants |
| Detectors.h | New file implementing modular detector functions for WiFi SSIDs, MAC OUIs, BLE names, and Raven UUIDs with weighted scoring |
| ThreatAnalyzer.h | Refactored from imperative to declarative style with detector registry, DeviceTracker class, and weighted scoring pipeline |
| TelemetryReporter.h | Simplified JSON telemetry output with flatter structure and detector weight reporting |
| EventBus.h | Modified ThreatEvent struct to use fixed-size char arrays instead of pointers and added fields for match flags, weights, and alert control |
| DeviceSignatures.h | Removed hard-coded name and UUID arrays (now in Detectors.h), retained MAC OUI prefixes |
| RadioScanner.h | Changed WiFi channel switch time from 500ms to 1000ms and made currentWifiChannel volatile for thread safety |
| flocksquawk_m5stick.ino | Removed inline ThreatAnalyzer/TelemetryReporter implementations, added BLE critical sections, integrated DeviceTracker tick() for heartbeat |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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; |
There was a problem hiding this comment.
The channel switch time has been doubled from 500ms to 1000ms. This change affects the rate at which WiFi channels are scanned, potentially reducing detection responsiveness. While this may be intentional (perhaps to improve stability or reduce power consumption), consider documenting the rationale for this change in the PR description or as a comment, especially if this timing is critical for detection performance.
There was a problem hiding this comment.
Intentional change. Longer dwell time per WiFi channel increases the chance of catching management frames from devices that don't transmit continuously. The tradeoff is a slower full-band sweep (~13s vs ~6.5s across 13 channels), which is acceptable for continuous passive monitoring. That said, the original 500ms value may have been fine — happy to revert if you have a preference.
There was a problem hiding this comment.
my concern with the extended channel time is driving, as this was the primary intention of this device. It is very likely that you will pass through the RF range of a target before the ~13s, missing the channel entirely. I will do some testing to determine which one provides more alerts. I think the ultimate solution would be to use 2 esp32's connected via UART, with channel splitting. Maybe we can add the ability to connect a 2nd M5Stick through the exposed GPIO, as the M5Stick seems to be the community favorite for this project.
Or even simpler, we can add a toggle to change the scan speed per the user's needs
There was a problem hiding this comment.
Yeah, I was even thinking you might be able to make it "speed sensitive" and adjust dynamically based on GPS coordinates coming back from the host, or even just allow the host to more directly control the scanner config for things like this to dynamically update scanning parameters based on other heuristics. I am happy to leave this where it was however if you have come to this value through real-world experience. Connecting multiple devices is an interesting idea - you could use the GPIO to stack them together. It might work to have them all connect on bluetooth, but then you get into duty cycling even more - it might balance out though. An ideal setup would be to have 4 or more of these all wired together and coordinating so you could potentially do direction-finding, and other fancy stuff.
| // 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; |
There was a problem hiding this comment.
The subsumption logic correctly removes the SSID_KEYWORD detector when both SSID_FORMAT and SSID_KEYWORD match. However, this creates an asymmetry: the weights array at bit position 1 (SSID_KEYWORD) is set to 0, but the detectorWeights array in ThreatEvent will still be copied with this 0 value. While this is technically correct (it shows the keyword detector didn't contribute to the final score), consider documenting this behavior or adding a comment explaining that zeroed weights indicate suppressed detectors.
| // 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; | |
| // Subsumption: SSID format supersedes SSID keyword. | |
| // If both detectors match, we keep only the SSID_FORMAT weight. | |
| if ((matchFlags & DET_SSID_FORMAT) && (matchFlags & DET_SSID_KEYWORD)) { | |
| totalWeight -= weights[detectorBitPosition(DET_SSID_KEYWORD)]; | |
| matchFlags &= ~DET_SSID_KEYWORD; | |
| // Note: we intentionally leave a zeroed entry in the weights array | |
| // for DET_SSID_KEYWORD. This is copied into ThreatEvent::detectorWeights | |
| // so that a 0 weight indicates the detector was matched but suppressed | |
| // by subsumption logic, and did not contribute to the final score. |
| // MAC address OUI prefixes for target devices | ||
| // MAC address OUI prefixes for target devices (Lite-On Technology) | ||
| const char* const MACPrefixes[] = { | ||
| "58:8e:81", "cc:cc:cc", "ec:1b:bd", "90:35:ea", "04:0d:84", |
There was a problem hiding this comment.
The comment indicates these OUIs are for "Lite-On Technology" (line 8), but the list includes "cc:cc:cc" which is not a valid OUI assigned to any organization. This might be a placeholder or test value. Consider verifying this OUI and documenting whether it's intentional for testing or if it should be removed in production.
There was a problem hiding this comment.
cc:cc:cc is actually a valid IEEE OUI assigned to Silicon Laboratories (MA-L, registered May 2019). In fact, all 20 OUI prefixes in this list are Silicon Labs assignments — the code comment labeling them as "Lite-On Technology" is incorrect. The confusion likely stems from Lite-On manufacturing WiFi modules that contain Silicon Labs chipsets. Silicon Labs makes BLE/wireless SoCs commonly used in IoT and security camera applications, which aligns with the target device profile.
Will fix the "Lite-On Technology" comment to "Silicon Laboratories" in a follow-up commit on this branch.
There was a problem hiding this comment.
We need to be careful about this, as flock-you is not public domain. Implementing code directly from flock-you is technically infringement
| if (slots[i].state != DeviceState::EMPTY && | ||
| slots[i].state != DeviceState::DEPARTED && | ||
| memcmp(slots[i].mac, mac, 6) == 0) { |
There was a problem hiding this comment.
DEPARTED devices are not matched in recordDetection (lines 60-62), meaning if a device times out (becomes DEPARTED) and is then seen again, it will be treated as a completely new detection and trigger a new alert. This might be intentional behavior (device left and returned), but could cause repeated alerts if devices intermittently go in and out of range due to signal fluctuations. Consider whether DEPARTED devices should remain in a "grace period" where they can be revived without triggering a new alert, or document this as intended behavior.
There was a problem hiding this comment.
Intentional. If a device leaves range (60s timeout → DEPARTED) and reappears, that's a genuine new presence event worth alerting on. The 60-second timeout is long enough to avoid false re-alerts from momentary signal fluctuations. A departed device's slot can also be reclaimed by findFreeSlot for a different device, which is the correct behavior.
| void handleThreatDetection(const ThreatEvent& threat) { | ||
| StaticJsonDocument<512> 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" | ||
| }; | ||
|
|
||
| for (uint8_t bit = 0; bit < 7; 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(); | ||
| } |
There was a problem hiding this comment.
The JSON telemetry structure has changed significantly from the old implementation. The old version had nested objects like target.identity.mac and target.indicators.ssid_match, while the new version has a flatter structure with target.mac and target.detectors.ssid_format. This is a breaking change for any systems or scripts consuming the JSON output. If there are downstream consumers, they will need to be updated. Consider documenting this breaking change in the PR description or migration guide.
There was a problem hiding this comment.
Acknowledged. No downstream consumers exist yet — this is a pre-release project. The new flatter structure (target.mac instead of target.identity.mac) is designed for the DeFlock app integration in later PRs. The telemetry format will be documented in a later PR that adds docs/telemetry-format.md.
|
Hey @f1yaw4y! Your project caught my eye and I had an itch to scratch wanting a way to tie the flock detector into apps like deflock. Let me know what you think about the approach I am taking. I made a lot of decisions along the way to get something working, but I do want to collaborate with you and figure out what makes sense, what you like, etc. I have a lot of experience around build, test, and release automation and have some more ideas to make it easier for people to install this on their devices as well. I laid some groundwork for those things along the way in these PRs. As far as this PR goes I ended up more or less abandoning the scoring idea in later commits, but there are some other more foundational things in here that we should sort out. I am happy to rework any of these commits / features as you would like. |
|
Addressed the actionable review feedback in 92e9aba:
For the remaining comments:
|
|
Hello!
I apologize for missing your email, I didn't notice it until all your pull requests last night
I would be very interested in making it easier for people to deploy, as well as deflock integration. I personally halted the project as far as designing new features myself, but I am more than willing to review and potentially merge
Also, I have noticed that the new models of Flock cameras do not broadcast SSID/BLE signals, so another solution is needed for the next-gen cameras
I am a bit new when it comes to pull requests and anyone interacting with my repos at all, so I will take a few days to review and catch up with what you've added. I really appreciate your interest and all your hard work!
Thank you
-Lucas
…On Monday, February 2nd, 2026 at 11:27 AM, Doug Borg ***@***.***> wrote:
dougborg left a comment [(f1yaw4y/FlockSquawk#3)](#3 (comment))
Hey ***@***.***(https://github.com/f1yaw4y)! Your project caught my eye and I had an itch to scratch wanting a way to tie the flock detector into apps like deflock. Let me know what you think about the approach I am taking. I made a lot of decisions along the way to get something working, but I do want to collaborate with you and figure out what makes sense, what you like, etc.
I have a lot of experience around build, test, and release automation and have some more ideas to make it easier for people to install this on their devices as well. I laid some groundwork for those things along the way in these PRs.
As far as this PR goes I ended up more or less abandoning the scoring idea in later commits, but there are some other more foundational things in here that we should sort out. I am happy to rework any of these commits / features as you would like.
—
Reply to this email directly, [view it on GitHub](#3 (comment)), or [unsubscribe](https://github.com/notifications/unsubscribe-auth/AVYUILP5R2C2JEWBZNHEMZ34J53IJAVCNFSM6AAAAACTUYWOUOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTQMZWGI3TCNJQHE).
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
- Reset evicted slot state to EMPTY in DeviceTracker::findFreeSlot() before returning, ensuring a clean slate for the caller - Replace hardcoded detector loop bound (7) with sizeof-derived DETECTOR_NAME_COUNT in TelemetryReporter - Increase StaticJsonDocument from 512 to 768 bytes to accommodate max-length payloads with all detectors firing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
92e9aba to
fcd4fe6
Compare
Series overview
This is the first in a series of 8 PRs that overhaul FlockSquawk's detection engine, build infrastructure, and M5StickC UI. The end goal is a two-way link between FlockSquawk hardware and the DeFlock mobile app: FlockSquawk passively scans WiFi and BLE for surveillance devices, then streams detection events to DeFlock over BLE GATT so users can see real-time RF alerts alongside the app's existing surveillance camera map.
The companion deflock-app PR series adds the app-side USB and BLE scanner integration that connects to FlockSquawk.
PR stack:
Summary
Test plan
🤖 Generated with Claude Code