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
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
99 changes: 99 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

FlockSquawk is an ESP32 Arduino project for passive RF awareness. It sniffs WiFi management frames and BLE advertisements, matches them against known surveillance device signatures, and outputs alerts via displays, audio, and JSON telemetry over Serial.

## Build System

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.

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states "This is a pure Arduino IDE project (no CMake, PlatformIO, or Makefile)" but this PR adds a Makefile. This is contradictory. The statement should be updated to acknowledge that a Makefile wrapper around arduino-cli is now available, or the phrasing should be clarified to indicate the project doesn't require these tools but now supports optional Makefile-based workflows.

Suggested change
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.
This project is designed to be built with the **Arduino IDE** or `arduino-cli`; no external build system like CMake or PlatformIO is required. An optional Makefile wrapper around `arduino-cli` is available for CLI-based workflows. Each hardware variant is a self-contained Arduino sketch that opens directly in the IDE.

Copilot uses AI. Check for mistakes.

**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.

## Thread Safety Pattern

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
- Main loop copies event data under lock, then processes outside the critical section

## Key Constants

- WiFi channel hop: 1 second
- BLE scan interval: 5 seconds, 1 second duration
- Device tracking slots: 32 (LRU eviction)
- Device departure timeout: 60 seconds
- Heartbeat re-alert interval: 10 seconds
- Alert certainty threshold: 65%
- Serial baud rate: 115200
220 changes: 220 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# FlockSquawk β€” arduino-cli build system
# Usage: make help

# ──────────────────────────────────────────────
# User-configurable variables
# ──────────────────────────────────────────────
PORT ?=
BAUD ?= 115200
VARIANT ?= m5stick
CORE_VERSION ?= 3.0.7

BUILD_DIR := $(CURDIR)/.build

# Arduino toolchain paths (Linux default; macOS overrides below)
ARDUINO_DATA ?= $(HOME)/.arduino15
ifeq ($(shell uname),Darwin)
ARDUINO_DATA := $(HOME)/Library/Arduino15
endif

ESPTOOL = $(firstword $(wildcard $(ARDUINO_DATA)/packages/esp32/tools/esptool_py/*/esptool.py) \
$(wildcard $(ARDUINO_DATA)/packages/esp32/tools/esptool_py/*/esptool))
MKLITTLEFS = $(firstword $(wildcard $(ARDUINO_DATA)/packages/esp32/tools/mklittlefs/*/mklittlefs))

# ──────────────────────────────────────────────
# Variant definitions: NAME FQBN SKETCH_DIR HAS_DATA
# ──────────────────────────────────────────────
VARIANTS := m5stick m5fire mini12864 oled portable flipper

m5stick_FQBN := esp32:esp32:m5stick_c_plus2
m5stick_SKETCH := m5stack/flocksquawk_m5stick
m5stick_DATA :=

m5fire_FQBN := esp32:esp32:m5stack_fire
m5fire_SKETCH := m5stack/flocksquawk_m5fire
m5fire_DATA := 1

mini12864_FQBN := esp32:esp32:esp32
mini12864_SKETCH := Mini12864/flocksquawk_mini12864
mini12864_DATA := 1

oled_FQBN := esp32:esp32:esp32
oled_SKETCH := 128x32_OLED/flocksquawk_128x32
oled_DATA := 1

portable_FQBN := esp32:esp32:esp32
portable_SKETCH := 128x32_OLED/flocksquawk_128x32_portable
portable_DATA :=

flipper_FQBN := esp32:esp32:esp32s2
flipper_SKETCH := flipper-zero/dev-board-firmware/flocksquawk-flipper
flipper_DATA :=

# LittleFS defaults (4 MB flash, default partition table)
LITTLEFS_OFFSET ?= 0x290000
LITTLEFS_SIZE ?= 0x160000
LITTLEFS_PAGE ?= 256
LITTLEFS_BLOCK ?= 4096

# ──────────────────────────────────────────────
# Auto-detect serial port when PORT is empty
# ──────────────────────────────────────────────
define detect_port
$(or $(PORT),$(firstword $(wildcard /dev/ttyUSB* /dev/ttyACM* /dev/cu.usbserial* /dev/cu.usbmodem* /dev/cu.wchusbserial*)))
endef

# ──────────────────────────────────────────────
# Per-variant target template
# ──────────────────────────────────────────────
define VARIANT_TARGETS

.PHONY: build-$(1) upload-$(1) flash-$(1) monitor-$(1)

build-$(1):
arduino-cli compile \
--fqbn $($(1)_FQBN) \
--output-dir $(BUILD_DIR)/$(1) \
$($(1)_SKETCH)

upload-$(1):
$$(if $$(call detect_port),, $$(error PORT not set and no device auto-detected))
arduino-cli upload \
--fqbn $($(1)_FQBN) \
--port $$(call detect_port) \
--input-dir $(BUILD_DIR)/$(1) \
$($(1)_SKETCH)

flash-$(1): build-$(1) upload-$(1)

monitor-$(1):
$$(if $$(call detect_port),, $$(error PORT not set and no device auto-detected))
arduino-cli monitor \
--port $$(call detect_port) \
--config baudrate=$(BAUD)

# Only generate upload-data target for variants with a data/ directory
ifneq ($($(1)_DATA),)
.PHONY: upload-data-$(1)
upload-data-$(1):
$$(if $$(call detect_port),, $$(error PORT not set and no device auto-detected))
$$(if $(MKLITTLEFS),, $$(error mklittlefs not found β€” install ESP32 core first))
$$(if $(ESPTOOL),, $$(error esptool not found β€” install ESP32 core first))
@echo "Building LittleFS image from $($(1)_SKETCH)/data …"
$(MKLITTLEFS) -c $($(1)_SKETCH)/data \
-p $(LITTLEFS_PAGE) -b $(LITTLEFS_BLOCK) \
-s $(LITTLEFS_SIZE) \
$(BUILD_DIR)/$(1)/littlefs.bin
@echo "Flashing LittleFS image to $$(call detect_port) …"
$(ESPTOOL) --chip esp32 --port $$(call detect_port) --baud 921600 \
write_flash $(LITTLEFS_OFFSET) $(BUILD_DIR)/$(1)/littlefs.bin
endif

endef

# Expand targets for every variant
$(foreach v,$(VARIANTS),$(eval $(call VARIANT_TARGETS,$(v))))

# ──────────────────────────────────────────────
# Shorthand targets (use VARIANT variable)
# ──────────────────────────────────────────────
.PHONY: build upload flash monitor

build: build-$(VARIANT)
upload: upload-$(VARIANT)
flash: flash-$(VARIANT)
monitor: monitor-$(VARIANT)

# ──────────────────────────────────────────────
# Host-side unit tests (no ESP32 needed)
# ──────────────────────────────────────────────
TEST_CXX ?= clang++
TEST_CXXFLAGS := -std=c++17 -Wall -Wextra -g -O0
TEST_INCLUDES := -isystem test/mocks -I m5stack/flocksquawk_m5stick/src -I test
TEST_SRCS := test/test_main.cpp test/eventbus_impl.cpp \
test/test_detectors.cpp test/test_device_tracker.cpp \
test/test_threat_analyzer.cpp
TEST_BIN := $(BUILD_DIR)/test_runner
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)
Comment on lines +137 to +145

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetch-doctest rule downloads doctest.h directly from raw.githubusercontent.com using curl and a mutable tag URL without any checksum or signature verification, which creates a supply-chain risk if the upstream repository or delivery channel is compromised. Because this header is compiled and executed as part of the test binary, a malicious upstream change could execute arbitrary code in your CI or developer environment and exfiltrate secrets. To mitigate this, pin the dependency to an immutable identifier (e.g., commit hash or vendored copy) and add integrity verification (such as a known checksum) before using the downloaded file.

Copilot uses AI. Check for mistakes.

test: fetch-doctest
@mkdir -p $(BUILD_DIR)
$(TEST_CXX) $(TEST_CXXFLAGS) $(TEST_INCLUDES) $(TEST_SRCS) -o $(TEST_BIN)
$(TEST_BIN)

test-verbose: fetch-doctest
@mkdir -p $(BUILD_DIR)
$(TEST_CXX) $(TEST_CXXFLAGS) $(TEST_INCLUDES) $(TEST_SRCS) -o $(TEST_BIN)
$(TEST_BIN) --success

# ──────────────────────────────────────────────
# Global targets
# ──────────────────────────────────────────────
.PHONY: all clean install-deps help

all: $(foreach v,$(VARIANTS),build-$(v))

clean:
rm -rf $(BUILD_DIR)

install-deps:
arduino-cli core update-index
arduino-cli core install esp32:esp32@$(CORE_VERSION)
arduino-cli lib install \
ArduinoJson \
"NimBLE-Arduino" \
M5Unified \
U8g2 \
"Adafruit GFX Library" \
"Adafruit SSD1306"

# ──────────────────────────────────────────────
# Help (default target)
# ──────────────────────────────────────────────
.DEFAULT_GOAL := help

help:
@echo "FlockSquawk Build System"
@echo "========================"
@echo ""
@echo "Variants: $(VARIANTS)"
@echo ""
@echo "Global targets:"
@echo " make help Show this message"
@echo " make all Compile all variants"
@echo " make test Run host-side unit tests"
@echo " make test-verbose Run tests with per-assertion output"
@echo " make clean Remove build output (.build/)"
@echo " make install-deps Install ESP32 core and Arduino libraries"
@echo ""
@echo "Per-variant targets (replace <v> with a variant name above):"
@echo " make build-<v> Compile sketch"
@echo " make upload-<v> Upload firmware to device"
@echo " make flash-<v> Compile + upload"
@echo " make monitor-<v> Open serial monitor"
@echo " make upload-data-<v> Flash LittleFS data (m5fire, mini12864, oled only)"
@echo ""
@echo "Shorthand (uses VARIANT, default: $(VARIANT)):"
@echo " make build => build-\$$(VARIANT)"
@echo " make upload => upload-\$$(VARIANT)"
@echo " make flash => flash-\$$(VARIANT)"
@echo " make monitor => monitor-\$$(VARIANT)"
@echo ""
@echo "Variables:"
@echo " PORT=<path> Serial port (auto-detected if unset)"
@echo " BAUD=<rate> Monitor baud rate (default: 115200)"
@echo " VARIANT=<name> Default variant (default: m5stick)"
@echo " CORE_VERSION=<ver> ESP32 core version (default: 3.0.7)"
@echo ""
@echo "Examples:"
@echo " make build-m5stick"
@echo " make flash-oled PORT=/dev/cu.usbserial-0001"
@echo " make upload-data-mini12864 PORT=/dev/ttyUSB0"
@echo " make all"
Loading
Loading