Extract shared headers and migrate all variants to common/#5
Conversation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Provides per-variant build/upload/flash/monitor targets for all 6 hardware variants, plus LittleFS data upload for variants with audio assets. Uses GNU Make define/eval/call to generate targets from a single template. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests compile natively with clang++/g++ — no ESP32 hardware needed. Covers detectors, device tracker state machine, and threat analyzer scoring pipeline (37 cases, 126 assertions) targeting the M5Stick variant's pure-logic headers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move DetectorTypes.h, Detectors.h, DeviceSignatures.h, EventBus.h, ThreatAnalyzer.h, and TelemetryReporter.h from the M5Stick variant's src/ into a new top-level common/ directory. Update Makefile to pass -I common via build.extra_flags for all variants, update test includes, and fix the M5Stick FQBN (m5stick_c_plus2 -> m5stack_stickc_plus2). Merge AudioEvent (from M5Fire's EventBus.h) into the shared header so other variants can adopt it when migrated. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Delete local copies of EventBus.h, DeviceSignatures.h, ThreatAnalyzer.h, and TelemetryReporter.h from m5fire/src/. Replace legacy ThreatAnalyzer (simple boolean matching) and TelemetryReporter (DynamicJsonDocument with nested objects) implementations with the shared detector-based system. Add ISR-safe deferred event processing with portMUX spinlocks for WiFi, BLE, and threat events. Add ThreatAnalyzer::tick() heartbeat in loop() and shouldAlert gate on triggerAlert(). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Delete local copies of EventBus.h, DeviceSignatures.h, ThreatAnalyzer.h, and TelemetryReporter.h from mini12864/src/. Replace legacy ThreatAnalyzer and TelemetryReporter implementations with shared detector-based system. Add ISR-safe deferred event processing with portMUX spinlocks. Move display notifications (Mini12864DisplayNotifyWifiFrame, ShowAlert) and audio playback to the main loop's deferred handlers. Add tick() heartbeat and shouldAlert gate. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Also fix Makefile build.extra_flags override that was clobbering ESP32 core defines (-DESP32=ESP32 etc). Use build.defines instead, which is included within build.extra_flags and starts empty. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move variant-specific src/ files into sketch directory to follow Arduino convention. Keep Flipper's own TelemetryReporter (line-based protocol for Flipper app). Fix radioType null-check for char array. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR consolidates duplicate code by extracting shared headers (DetectorTypes, Detectors, DeviceSignatures, EventBus, TelemetryReporter, ThreatAnalyzer) into a common/ directory, migrating all 5 remaining variants to use them via -I common. It also introduces a Makefile-based build system, comprehensive unit tests with doctest, and CLAUDE.md project documentation.
Changes:
- Extracted 6 shared headers into
common/directory with unified detector system, device tracking, and telemetry reporting - Added Makefile with arduino-cli integration supporting all 6 variants, LittleFS flashing, and host-side unit tests
- Created test infrastructure with 322 unit tests covering detectors, device tracking, and threat analysis logic
- Updated all variant .ino files to use shared headers and implement deferred event processing for thread safety
- Added CLAUDE.md documentation describing architecture, build system, and development patterns
Reviewed changes
Copilot reviewed 43 out of 47 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| common/DetectorTypes.h | Defines detector result structures, flags, device states, and tracking constants |
| common/Detectors.h | Implements WiFi and BLE detector functions with weighted scoring and RSSI modifiers |
| common/DeviceSignatures.h | Contains MAC OUI prefix list for known surveillance devices |
| common/EventBus.h | Header-only event bus with updated ThreatEvent structure (char arrays instead of const char*) |
| common/ThreatAnalyzer.h | Unified threat analyzer with device tracker, detector registry, and heartbeat logic |
| common/TelemetryReporter.h | Standardized JSON telemetry output with detector weight reporting |
| Makefile | Arduino-cli build system with variant support, LittleFS upload, and host test targets |
| CLAUDE.md | Project documentation covering architecture, build system, and thread safety patterns |
| test/*.cpp | 322 unit tests covering detectors, device tracking, and threat analysis |
| test/mocks/Arduino.h | Mock Arduino.h for host-side testing with minimal dependencies |
| test/eventbus_impl.cpp | EventBus implementation for test linking with mock millis() |
| m5stack/flocksquawk_m5stick/*.ino | Migrated to common headers, added thread-safe deferred event processing |
| m5stack/flocksquawk_m5fire/*.ino | Migrated to common headers with deferred processing and heartbeat support |
| Mini12864/flocksquawk_mini12864/*.ino | Migrated to common headers with FreeRTOS critical sections |
| 128x32_OLED/flocksquawk_128x32/*.ino | Migrated to common headers with shouldAlert flag handling |
| 128x32_OLED/flocksquawk_128x32_portable/*.ino | Migrated to common headers with buzzer heartbeat beeps |
| flipper-zero/.../flocksquawk-flipper/*.ino | Migrated to common headers, fixed radioType null check |
| */src/RadioScanner.h | Updated CHANNEL_SWITCH_MS to 1000ms and made currentWifiChannel volatile (m5stick) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| This is a pure **Arduino IDE** project (no CMake, PlatformIO, or Makefile). Each hardware variant is a self-contained Arduino sketch that opens directly in the IDE. | ||
|
|
||
| **Critical constraint:** ESP32 board package must be **version 3.0.7 or older** (newer versions cause IRAM overflow). | ||
|
|
||
| ### Building with arduino-cli | ||
|
|
||
| ```bash | ||
| # Install ESP32 core | ||
| arduino-cli core install esp32:esp32@3.0.7 | ||
|
|
||
| # Install required libraries | ||
| arduino-cli lib install ArduinoJson NimBLE-Arduino M5Unified | ||
|
|
||
| # Compile a variant (example: M5StickC Plus2) | ||
| arduino-cli compile --fqbn esp32:esp32:m5stick_c_plus2 m5stack/flocksquawk_m5stick/ | ||
|
|
||
| # Upload | ||
| arduino-cli upload --fqbn esp32:esp32:m5stick_c_plus2 --port /dev/ttyUSB0 m5stack/flocksquawk_m5stick/ | ||
|
|
||
| # Serial monitor (115200 baud for JSON telemetry) | ||
| arduino-cli monitor --port /dev/ttyUSB0 --config baudrate=115200 | ||
| ``` | ||
|
|
||
| Board FQBNs vary per variant — check each variant's README for exact board settings. | ||
|
|
||
| ## Hardware Variants | ||
|
|
||
| Six self-contained variants, each in its own directory with a dedicated README: | ||
|
|
||
| | Variant | Path | Display | Audio | | ||
| |---|---|---|---| | ||
| | M5StickC Plus2 | `m5stack/flocksquawk_m5stick/` | Built-in TFT 135x240 | Buzzer tones | | ||
| | M5Stack FIRE | `m5stack/flocksquawk_m5fire/` | Built-in TFT 320x240 | Built-in speaker | | ||
| | Mini12864 | `Mini12864/flocksquawk_mini12864/` | ST7567 LCD 128x64 | I2S (MAX98357A) | | ||
| | 128x32 OLED | `128x32_OLED/flocksquawk_128x32/` | SSD1306/SH1106 I2C | I2S (MAX98357A) | | ||
| | 128x32 Portable | `128x32_OLED/flocksquawk_128x32_portable/` | SSD1306/SH1106 I2C | GPIO buzzer | | ||
| | Flipper Zero | `flipper-zero/dev-board-firmware/` | None (UART only) | None | | ||
|
|
||
| Variants do **not** share source files — each has its own copy of the core headers in `src/`. Changes to shared logic (EventBus, ThreatAnalyzer, etc.) must be applied to each variant individually. | ||
|
|
||
| ## Architecture | ||
|
|
||
| Data flows through a publish-subscribe pipeline: | ||
|
|
||
| ``` | ||
| RadioScannerManager (WiFi promiscuous + BLE scan) | ||
| → EventBus (WiFiFrameEvent / BluetoothDeviceEvent) | ||
| → ThreatAnalyzer (signature matching → ThreatEvent) | ||
| → TelemetryReporter (JSON over Serial) | ||
| → Display (variant-specific UI) | ||
| → SoundEngine (alerts) | ||
| ``` | ||
|
|
||
| ### Core subsystems (in each variant's `src/`) | ||
|
|
||
| - **EventBus.h** — Header-only static publish/subscribe. Event types: `WifiFrameCaptured`, `BluetoothDeviceFound`, `ThreatIdentified`, `SystemReady`. | ||
| - **RadioScanner.h** — WiFi promiscuous mode (channels 1-13, 1s hop interval) and BLE scanning via NimBLE (5s interval, 1s duration). Uses FreeRTOS portMUX spinlocks for thread safety between ISR callbacks and main loop. | ||
| - **ThreatAnalyzer.h** — Compares observations against `DeviceSignatures.h`. Tracks up to 32 devices with LRU eviction (states: NEW_DETECT → IN_RANGE → DEPARTED). Alert threshold: 65% certainty. | ||
| - **DeviceSignatures.h** — Static arrays of known SSIDs, MAC OUI prefixes, BLE device names, and service UUIDs. | ||
| - **TelemetryReporter.h** — Serializes detections as JSON via ArduinoJson (StaticJsonDocument<512>). | ||
| - **SoundEngine.h** — I2S WAV playback from LittleFS (Mini12864/128x32) or M5.Speaker tone generation (M5Stack variants). | ||
|
|
||
| ### Pluggable detector system (M5Stick variant only — newest architecture) | ||
|
|
||
| The M5Stick variant introduces a function-pointer-based detector registry: | ||
|
|
||
| - **DetectorTypes.h** — Defines `DetectorResult` (matched/weight/name) and `WiFiDetectorEntry`/`BLEDetectorEntry` structs. | ||
| - **Detectors.h** — Registers detector functions. WiFi: `detectSsidFormat` (weight 75), `detectSsidKeyword` (45), `detectWifiMacOui` (20). BLE: `detectBleName` (55), `detectRavenCustomUuid` (80), `detectRavenStdUuid` (10), `detectBleMacOui` (20). | ||
| - Weighted scoring with subsumption (higher-confidence detectors override lower ones) and RSSI-based modifiers. | ||
|
|
||
| This detector pattern is the intended direction for all variants. |
There was a problem hiding this comment.
The CLAUDE.md documentation is now inaccurate after this PR's changes. Line 11 states "This is a pure Arduino IDE project (no CMake, PlatformIO, or Makefile)" but this PR adds a comprehensive Makefile build system.
Line 49 states "Variants do not share source files — each has its own copy of the core headers in src/" but this PR introduces shared headers in the common/ directory that all variants now use via -I common.
Line 73 states "The M5Stick variant introduces a function-pointer-based detector registry" but this PR migrates ALL variants to use the common detector system from common/ThreatAnalyzer.h.
These sections should be updated to reflect that:
- A Makefile is now available for building all variants
- Shared headers now exist in
common/(EventBus, ThreatAnalyzer, TelemetryReporter, Detectors, DetectorTypes, DeviceSignatures) - All variants now use the pluggable detector system, not just M5Stick
| WiFi promiscuous callbacks and BLE scan callbacks run on different cores/tasks than `loop()`. All variants use the same pattern: | ||
|
|
||
| - `portMUX_TYPE` spinlocks guard shared volatile state in ISR callbacks | ||
| - `taskENTER_CRITICAL` / `taskEXIT_CRITICAL` for atomic reads/writes |
There was a problem hiding this comment.
The line 88 mentions "taskENTER_CRITICAL / taskEXIT_CRITICAL" but the actual code uses "portENTER_CRITICAL / portEXIT_CRITICAL" macros. These are different FreeRTOS APIs - portENTER_CRITICAL/portEXIT_CRITICAL are for synchronizing between tasks and ISRs (which is correct for this use case), while taskENTER_CRITICAL/taskEXIT_CRITICAL are for task-only synchronization. The documentation should be corrected to match the actual implementation.
| - `taskENTER_CRITICAL` / `taskEXIT_CRITICAL` for atomic reads/writes | |
| - `portENTER_CRITICAL` / `portEXIT_CRITICAL` for atomic reads/writes |
| threatEngine.analyzeBluetoothDevice(bleCopy); | ||
| } | ||
|
|
||
| threatEngine.tick(now); |
There was a problem hiding this comment.
Inconsistent handling of the tick() return value across variants. The ThreatAnalyzer::tick() method returns true when a heartbeat beep should be emitted, but this return value is only checked in the m5stick and m5fire variants (lines 623-625 and 1158-1160 respectively).
The 128x32 OLED variant at line 550, Mini12864 variant at line 421, and Flipper Zero variant at line 463 all ignore the return value. While this may be intentional (e.g., Flipper has no audio), the 128x32_portable variant does have buzzer capability and should probably check the return value to emit heartbeat beeps.
Consider either using the return value consistently across all variants that have audio capability, or documenting why certain variants intentionally ignore it.
| threatEngine.tick(now); | |
| bool heartbeat = threatEngine.tick(now); | |
| if (heartbeat) { | |
| AudioEvent audioEvent; | |
| audioEvent.soundFile = "/heartbeat.wav"; | |
| EventBus::publishAudioRequest(audioEvent); | |
| } |
| DOCTEST_URL := https://raw.githubusercontent.com/doctest/doctest/v2.4.11/doctest/doctest.h | ||
|
|
||
| .PHONY: test test-verbose fetch-doctest | ||
|
|
||
| fetch-doctest: test/doctest.h | ||
|
|
||
| test/doctest.h: | ||
| @echo "Fetching doctest.h …" | ||
| curl -sL -o $@ $(DOCTEST_URL) |
There was a problem hiding this comment.
The fetch-doctest target downloads and executes a third-party header from $(DOCTEST_URL) at build/test time without any integrity verification and only pins it to a mutable Git tag. If an attacker compromises the upstream repository or the tag, they could inject arbitrary C++ code into your test binary and run it in CI or developer environments with access to secrets. To reduce this supply-chain risk, vendor a known-good copy of doctest.h into the repo or pin to an immutable identifier (e.g., a specific commit hash) and verify it via checksum or signature before use.
Summary
common/directory-I common:src/directoryTest plan
make testto verify host-side tests still passsrc/directories🤖 Generated with Claude Code