feat: add M5Atom Echo/Lite and Atom VoiceS3R (ESP32-S3) board support + multi-board READMEMain - #44
Open
simeononsecurity wants to merge 71 commits into
Open
simeononsecurity wants to merge 71 commits into
simeononsecurity wants to merge 71 commits into
Conversation
Adds Michael / DeFlockJoplin's high-precision detection method on top of the NitekryDPaul baseline: a Flock camera is flagged when it transmits a Probe Request (type=0 subtype=4) with a wildcard SSID IE (tag 0 len 0) AND its addr2 matches the OUI list. Drive-test in Joplin: 11/12 cameras caught with only 2 false positives. - New AlertType ALERT_WILDCARD_PROBE, emitted as detection_method 'wifi_wildcard_probe' (high-precision class) - Wildcard-probe hits suppress the addr2 broad alert for the same frame to prevent double counting; non-probe OUI matches still emit as 'wifi_oui_addr2' - IE parser returns tri-state (1=wildcard / 0=directed / -1=no SSID IE), with FCS-trailer retry only on the -1 no-IE case - addr1 receiver-side sleeper-catch and the optional addr3 + SSID paths are unchanged — wildcard is purely additive - 31st OUI 82:6b:f2 added to target_ouis[] and to the dataset doc; it's the OUI of the 12th camera in Michael's drive-test that the original 30 didn't catch - README explains the wildcard-probe method, credits Michael with a link to github.com/DeflockJoplin/flock-you, and bumps Acknowledgments Source: https://github.com/DeflockJoplin/flock-you
…cious-dev Promiscious dev
Hardware support added - M5Atom Echo (USE_M5ATOM_ECHO): NeoPixel LED + GPIO25 buzzer - M5Atom Lite (USE_M5ATOM_LITE): NeoPixel LED, no buzzer - Atom VoiceS3R (USE_M5ATOM_VOICES3R): ESP32-S3-PICO-1-N8R8, ES8311 I2S speaker via M5Unified, no RGB LED, button on GPIO41 platformio.ini - [env:m5atom-echo]: Adafruit NeoPixel, usbserial port, 1.5Mbaud upload - [env:m5atom-lite]: same board, no buzzer flag - [env:m5atom-voices3r]: espressif32@6.7.0, esp32-s3-devkitc-1, qio_opi PSRAM, ARDUINO_USB_CDC_ON_BOOT, M5Unified@^0.2.2 main.cpp - TESTING_MODE guarded with #ifndef so build flag can override it - Adafruit NeoPixel include guarded for Echo/Lite only (no NeoPixel on S3R) - M5Unified speaker: M5.Speaker.tone() used for all audio on VoiceS3R - Startup: SMB World 1-1 overworld riff (E E _E_ C E G) - Detection chirp: two ascending tones (2000→2800 Hz) - Heartbeat beep: two monotone 1500 Hz pulses - Serial1 UART mirror skipped on VoiceS3R (USB-CDC only) - setup(): M5.begin() + Speaker.setVolume(200) for VoiceS3R New scripts - flash_atom.sh: auto-detects /dev/tty.usbserial-*, venv auto-load - flash_voices3r.sh: targets /dev/tty.usbmodem* (ESP32-S3 native USB-CDC), kills port-holding processes, uses brew esptool with --before usb-reset - Both scripts activate venv automatically (searches .venv/venv/env/api/venv and ~/.platformio/penv in priority order) - partitions_4mb.csv: custom partition table for all supported boards Add -DTESTING_MODE=1 to any env's build_flags to alert on all WiFi frames.
…g mode docs - Restored [env:xiao_esp32s3] (Seeed XIAO ESP32-S3, original upstream board) - Added Supported Hardware table: xiao_esp32s3 / esp32dev / m5atom-echo / m5atom-lite / m5atom-voices3r with port types, audio/LED notes - Replaced generic 'pio run' section with board-specific flash + monitor commands for all five environments - Added VoiceS3R section: kill-port-holder one-liner + esptool fallback for ESP32-S3 native USB-CDC (fixes 'port busy' upload errors) - Added Testing mode section: -DTESTING_MODE=1 build flag instructions - Pin maps for XIAO ESP32-S3 (original) and Atom VoiceS3R - Removed flash_atom.sh and flash_voices3r.sh (commands in README instead)
…38/38)
Core firmware (main.cpp):
- Three OUI confidence tiers: HIGH(32)/MFR(6)/SoundThinking(1) — PR#39
- ALERT_LAA_SSID: detects 'Flock Camera net.' locally-administered-MAC cameras
- checkSeqMac(): :DE/:DF sequential last-byte pair heuristic for dual-band LAA cams
- Confidence score 0-100 per detection, stored in SPIFFS + emitted in JSON
- BLE_COEX_MODE=1: ESP-IDF SW coexistence — continuous NimBLE scan, no promisc pause
- BLE_COEX_MODE=0: fallback manual time-mux (5s pause/60s) for boards with issues
- Fixed BLE mfr-ID: 0x09C8 (XUNTONG/Flock) was incorrectly 0x05A7 (Assa Abloy)
- Full 128-bit Raven GATT service UUIDs (GainSec research, 8 UUIDs)
- CHIRP_MIN_CONFIDENCE=30: mfr-OUI alone (20pts) logs silently, no false beeps
- channelBand(ch): JSON protocol field is band-aware (wifi_2_4ghz / wifi_5ghz)
- 5 GHz guard: documents ESP32-C5 path, prevents ch149/157 on 2.4GHz-only hw
- addr3 CHECK_ADDR3=1 (was 0): BSSID fallback always active
- ENABLE_SSID_MATCH=1 (was 0): required for LAA-MAC camera class
Detection patterns (fy_detect.h — new shared header):
- fy_oui_high[]: 32 exclusively-Flock OUIs (NitekryDPaul + DeFlockJoplin + dougborg/PR#39)
- fy_oui_mfr[]: 6 Liteon/USI contract-manufacturer OUIs
- fy_oui_soundthinking[]: 1 SoundThinking/ShotSpotter OUI (avenstewart/PR#39)
- fy_ble_mfr_ids[]: {0x09C8} XUNTONG (corrected)
- fy_raven_uuids[]: 8 full 128-bit Raven GATT service UUIDs
- Pure matching functions: fyCheckFlockHighMAC/MfrMAC/SoundThinkingMAC/BLEName/BLEMfrID
- fyCheckRavenUUIDFromStrings(): hardware-independent UUID matching
- fyEstimateRavenFW(): firmware version heuristic from UUID categories
platformio.ini:
- 10 environments: 5 WiFi-only + 5 BLE-COEX twins for all supported boards
- [env:native]: host Unity test runner (pio test -e native)
- All -ble envs: -DENABLE_BLE_SCAN=1 -DBLE_COEX_MODE=1
Native unit tests (test/) — 38/38 PASS:
- test_ble_matching: all 32 high-OUIs, 6 mfr-OUIs, SoundThinking isolation,
BLE name substring, mfr-ID 0x09C8 match + reject old 0x05A7 (22 tests)
- test_uuid_matching: all 8 Raven UUIDs, case-insensitive, fw version
estimation, old short-form UUID rejection (16 tests)
New files:
- fy_detect.h: shared detection pattern library
- test/: native Unity test suite
- flash.sh: unified multi-board flasher with device identification
- flash_atom.sh: M5Atom Lite quick-flash script
- flash_voices3r.sh: Atom VoiceS3R two-stage USB CDC flasher
- DETECTION_IMPROVEMENTS.md: full design rationale, score tables, examples
- SOLDERLESS_BUILD_GUIDE.md: breadboard build (-11, no soldering)
- CASE_DESIGN.md: 3D-printable enclosure design guide
- hardware/: PCB schematic, BOM, OpenSCAD case, renders
.gitignore:
- Added SPIFFS runtime files: session.json, session.tmp, prev_session.json
Build sizes (all within 4MB limits):
- WiFi-only (esp32dev): Flash 26%, RAM 19%
- BLE COEX (esp32dev-ble): Flash 34%, RAM 22%
- M5Atom VoiceS3R BLE: Flash 40%, RAM 23%
Auto-discovers /dev/cu.usbserial-* (Atom Lite/Echo/Voice FTDI) and /dev/cu.usbmodem* (VoiceS3R native USB CDC) every 2 seconds. - Color-coded, labeled output per device (6 ANSI colors, stable) - Auto-reconnects when a device is unplugged / replugged / reflashed - Backs off when esptool/pio/platformio holds the port (lsof check) - Non-exclusive port open so flash tools can take over at any time - PID management via /tmp/fymon.PID/ temp dir (bash 3.2 compatible) - Ctrl-C kills all background monitor subprocesses cleanly Usage: ./monitor_all.sh [baud] (default 115200)
On macOS, 'while read < /dev/cu.*' opens the character device non-blockingly and read() returns EOF immediately if no data is waiting — causing the connect/disconnect bounce seen in testing. 'cat $port | while IFS= read -r line' correctly blocks on the serial fd and only exits when the port disappears or is reset, giving live streaming output as intended.
Root cause: re-opening /dev/cu.* with cat or bash '<' redirect each
iteration pulsed DTR on the FTDI chip, which triggered the ESP32
auto-reset circuit (DTR→EN capacitor). The ESP32 reset, the
bootloader output arrived on the still-open fd, then the fd got a
hangup/EOF → 'disconnected' loop with no visible output.
Fix:
exec 3<>"$port" # O_RDWR open — DTR asserted once, stays up
stty -f "$port" ... clocal -hupcl # configure in-place, no retrigger
while IFS= read -r line <&3 # VMIN=1 raw read blocks correctly
line="${line%$'\r'}" # strip trailing CR from ESP32 Serial.println()
Also documents firmware output format in header:
[flockyou] log lines (startup, heartbeat, detections)
{"event":"detection",...} JSON lines (one per alert, for Flask)
…evice' prompt
After a successful flash, show_boot_output() opens the port with
exec 3<>$port (O_RDWR — same trick as monitor_all.sh) and streams
serial lines until:
• '[flockyou] scanning' heartbeat arrives → '✅ Firmware confirmed running'
• 15 s elapses with no firmware output → warning printed
This gives a visual confirmation that the freshly-flashed firmware
actually booted correctly before you swap in the next device.
Implementation details:
• BOOT_PORT global set by flash_device() — usbmodem gets RESTART_PORT,
FTDI gets the same port (stays up through pio upload reset)
• stty -f $port 115200 raw cs8 -cstopb -parenb clocal -hupcl
configures in-place on the already-open fd (no DTR re-pulse)
• read -r -t 2 line <&3 — 2 s blocking read per line, outer loop
checks elapsed time against 15 s deadline
• Trailing \r stripped from ESP32 Serial.println() \r\n output
• Graceful skip if port is unavailable (prints warning, continues)
HEARTBEAT_MS=30000 means the scanning heartbeat is 30 s away after
boot — too long to wait. '[flockyou] OUIs:' is the last startup
banner line printed right before scanning begins and appears within
~2 s of boot. Exit on that instead.
Also increase fallback timeout 15s→35s and clean up the timeout
message ('Firmware running.' instead of 'heartbeat not yet received').
- .github/workflows/build-firmware.yml: builds 5 BLE envs on push to main,
generates per-board manifest JSONs, deploys to gh-pages via
JamesIves/github-pages-deploy-action
- docs/index.html: board selector dropdown + esp-web-install-button,
Chrome/Edge Web Serial API, dark theme, hardware reference table
Boards: esp32dev-ble, m5atom-lite-ble, m5atom-echo-ble,
m5atom-voice-ble, m5atom-voices3r-ble (ESP32-S3)
To enable: repo Settings → Pages → Source: gh-pages branch
Adds a 'Verify build artifacts' step between builds and packaging that lists .pio/build/ and explicitly checks each firmware.bin exists, providing clear diagnostics if any build silently failed.
- platformio.ini: new envs lilygo-t-dongle-c5 and lilygo-t-dongle-c5-ble - main.cpp: USE_LILYGO_T_DONGLE_C5 hardware config block (GPIO8 LED) - workflow: build + manifest for lilygo-t-dongle-c5-ble (RISC-V, bootloader at 0x0) - docs/index.html: T-Dongle C5 option in board dropdown + chip table row
- platformio.ini: pin all envs to espressif32@6.7.0 (was ^6.3.0 causing potential platform version conflicts across envs in the same job) - platformio.ini: add boards_dir = boards; C5 envs now use custom board JSON (boards/lilygo-t-dongle-c5.json) instead of unknown esp32-c5-devkitm-1 - workflow: remove restore-keys (prevents stale partial-platform cache hits) - workflow: lilygo-t-dongle-c5-ble step gets continue-on-error: true (C5 experimental) - workflow: verify step only fails on required envs; C5 is advisory only - workflow: C5 firmware copy and manifest generation are conditional
…witch clean wiping builds
…ocation clean wiping builds
- c5_display.h: new helper for LILYGO T-Dongle C5 hardware - ST7735S 80x160 TFT via Adafruit_ST7735 (SPI: GPIO5/6/4/2/3, BL=GPIO1) - WS2812B RGB LED on GPIO11 via Adafruit_NeoPixel - c5DisplayInit(), c5DisplayScanning(), c5DisplayDetection(), c5DisplayScore() - LED colour coding: idle=green, caution=amber, alert=red - main.cpp: wire up C5 display in all call sites - USE_C5_DISPLAY=1 replaces plain GPIO8 LED for T_DONGLE_C5 builds - c5DisplayInit() called in setup() - c5DisplayScanning(ch, detCount) called from printHeartbeat() - c5DisplayDetection(type, mac, conf, rssi, ch) called from drainAlertQueue() - platformio.ini: add display lib_deps to both C5 envs - Adafruit ST7735/ST7789 + GFX + NeoPixel libraries - -DUSE_C5_DISPLAY=1 build flag for both lilygo-t-dongle-c5 envs
- Add lilygo-t-dongle-c5 and lilygo-t-dongle-c5-ble to environment table - New '📺 LILYGO T-Dongle C5 — Display & RGB LED' section with: - TFT display state/colour table (startup, scanning, detection tiers) - Full pin reference (TFT + RGB LED + BOOT button) - Flash commands for both C5 environments - Experimental status note
- Tab switcher: Flash Firmware | Live Dashboard
- Dashboard uses Web Serial API (Chrome/Edge) to parse JSON detections
from firmware: {event,detection_method,mac_address,oui,rssi,channel,confidence,...}
- Browser Geolocation API for GPS wardriving (no extra hardware needed)
- Leaflet.js map with color-coded markers (green=low/yellow=med/red=high conf)
- Real-time detection table + sidebar feed
- localStorage persistence across page reloads
- JSON / CSV / KML export
- Firmware selector expanded: Standard, T-Dongle C5, ATOM, Voices3r
…tiles
- Fix CSS specificity bug: #pane-flash ID rule was overriding .tab-pane{display:none}
causing flash pane to always be visible behind dashboard
- Dashboard pane now uses position:fixed (top:45px/left:right:bottom:0) so it
fills the viewport as a true overlay without page scrolling
- Switch map tiles from OSM (referrer issues) to CartoDB Dark Matter which
loads reliably from any domain including GitHub Pages
- Replace broken manifest.json/manifest-c5.json/manifest-atom.json refs with correct CI-generated names: manifest-esp32dev-ble.json, manifest-m5atom-lite-ble.json, manifest-m5atom-echo-ble.json, manifest-m5atom-voice-ble.json, manifest-m5atom-voices3r-ble.json, manifest-lilygo-t-dongle-c5-ble.json - Redesign Flash tab: numbered wizard steps 1→2→3 (Connect→Select→Flash) - Show all 6 board variants with chip family badges - Add Open Serial Monitor button that connects and switches to Dashboard - Modern top nav with brand, tab switcher, GitHub/Docs links - Preserve full Live Dashboard tab unchanged
- main.cpp: wrap SPIFFS.begin(true) with esp_log_level_set("SPIFFS",
ESP_LOG_NONE) before and ESP_LOG_WARN after, so the expected
'mount failed, -10025' on freshly-erased flash is suppressed.
- sdkconfig.defaults (new): CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y
prevents the ESP-IDF init-time 'No core dump partition found!'
warning that fires ~500 ms before setup() on every boot.
- platformio.ini: add board_build.sdkconfig_defaults = sdkconfig.defaults
to all 12 hardware environments so the overlay is applied at build time.
…ye-spy) Add tryOpenPort() helper — same as eye-spy fix. Handles the rare case where a USB CDC board causes a DTR-triggered reset during port.open(), causing Chrome to throw 'Failed to open serial port'.
- LED_FLASH_MS: 120ms → 30000ms for Atom Lite, Atom Voice, LilyGo T-Dongle-C5
(120ms was imperceptible; camera now holds red LED for 30 seconds on detection)
- HB_DEVICE_ACTIVE_MS: 3000ms → 120000ms
(was shorter than HB_BEEP_INTERVAL_MS=10000ms so heartbeat beep could never fire)
- flash.sh FTDI menu options 1/2/3 now default to BLE variants:
1) m5atom-lite-ble (Atom Lite + BLE)
2) m5atom-echo-ble (Atom Echo + BLE)
3) m5atom-voice-ble (Atom Voice + BLE)
Default (*) also falls through to m5atom-lite-ble
- LICENSE: Apache 2.0 with 'Copyright 2024 SimeonOnSecurity' footer - NOTICE: attribution requirement for redistributors / derivative works - main.cpp: SPDX-License-Identifier + SPDX-FileCopyrightText header - README.md: license badge + author badge Any redistribution or derivative work must retain the NOTICE file and credit SimeonOnSecurity (https://github.com/simeononsecurity).
- m5basic_display.h: full display driver using M5Unified (LovyanGFX)
- Scanning screen: channel, mode, OUI counts, runtime, SPIFFS status
- Detection screen: method (large), MAC, RSSI, channel, SSID, confidence
bar with label, time since last detection, session count
- Button A: force SPIFFS session save
- Button B: cycle brightness (40/160/255)
- Button C: force channel hop + clear alert screen
- main.cpp: USE_M5BASIC guards for config, M5Unified init, display hooks
in printHeartbeat() + drainAlertQueue() + loop()
- platformio.ini: m5stack-basic + m5stack-basic-ble environments
(board=m5stack-core-esp32, lib=m5stack/M5Unified@^0.2.2)
…t loop) Symptom (reported on Atom Echo, real hardware): E (444) esp_core_dump_flash: No core dump partition found! ets Jun 8 2016 00:22:57 rst:0x7 (TG0WDT_SYS_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) flash read err, 1000 ets_main.c 371 [repeats forever] Root cause: all m5atom-* environments (echo, echo-ble, lite, lite-ble, voice, voice-ble) used upload_speed = 1500000, matching the m5stack-atom board's aggressive default. Many Atom Lite/Echo/Voice units' onboard USB-serial bridge (CH9102/CP210x, varies by production batch/cable) can't reliably sustain 1.5 Mbaud. A flaky upload at that speed can silently corrupt bytes written to the bootloader or app partition; the ROM bootloader then fails to read a valid second-stage image on the next boot, producing the endless TG0/TG1WDT_SYS_RESET + "flash read err, 1000" loop shown above -- this happens before any application code (or even the second-stage bootloader) gets a chance to run, so it's a flashing transport problem, not a firmware logic bug. Fix: lower upload_speed to 921600 for all six m5atom-* environments -- the same conservative rate already used successfully by every other environment in this file (esp32dev, esp32dev-ble, lilygo-t-dongle-c5[-ble]). Verified m5atom-echo and m5atom-echo-ble still build successfully (upload_speed only affects the flashing step, not compilation). If a unit is already stuck in this boot loop from a prior bad flash at 1.5 Mbaud, a full flash erase before reflashing clears any corrupted residual bootloader/partition data: pio run -e m5atom-echo -t erase pio run -e m5atom-echo -t upload
… err boot loop)" This reverts commit 495023d.
Follow-up to the reverted "lower upload_speed" attempt. Real hardware
testing showed baud-rate reliability on M5Atom's USB-serial bridges
(CH9102/CP210x, varies by production batch) does not scale simply with
speed:
- At the board-default 1500000 baud, upload completes ("Flash
complete!") but the device then boot-loops with repeated
"flash read err, 1000" (corrupted bootloader/app image from an
unstable high-speed write).
- At a lower 921600 baud, upload itself fails outright with
"Unable to verify flash chip connection (No serial data
received.)" right as esptool switches speeds -- worse, not better.
Since neither a fixed higher nor fixed lower upload_speed is
universally correct across different physical units/cables, reverted
the platformio.ini upload_speed change entirely (back to the vendor
default 1500000 for all m5atom-* environments) and instead made
flash.sh's flash_device() resilient: if the primary `pio run -t upload`
attempt fails, it now automatically retries once with a full flash
erase followed by an upload at a different, conservative fallback baud
(460800, via PLATFORMIO_UPLOAD_SPEED env var override -- no
platformio.ini changes needed). If that also fails, it prints an
explicit manual fallback command using 115200 baud.
This fixes flaky units without touching the config for units that
already flash reliably at the default speed, and without introducing
Yet Another regression from guessing a single "correct" fixed speed.
Verified: `bash -n flash.sh` (syntax check only -- no physical hardware
available to exercise the actual retry path in this environment).
Follow-up to f392346 (the erase+fallback-baud retry). Real hardware testing surfaced a second bug: after the first upload attempt fails, the device can be mid-reset for a moment (or, rarely, macOS re-enumerates it under a different /dev/cu.usbserial-XXXX ID). The retry logic was blindly reusing the original $port variable for both the erase and fallback-baud upload steps, so it failed immediately with: A fatal error occurred: Could not open /dev/cu.usbserial-XXXX, the port doesn't exist Fix: added wait_for_usbserial_port(), which polls for up to 8s for the expected port to reappear, falling back to whatever usbserial port IS currently present if the device re-enumerated under a new ID. This is now called before both the erase step and the fallback upload step (erase can itself trigger another reset, so the port is re-checked a second time immediately before the final upload attempt too). Verified: bash -n flash.sh (syntax only -- no physical hardware available in this environment to exercise the retry path end-to-end).
ROOT CAUSE FOUND after exhaustive on-device bisection (2 physical M5Atom
Echo units): the call
Serial1.begin(MIRROR_BAUD, SERIAL_8N1, -1, MIRROR_TX_PIN);
in setup() reliably corrupts the ESP32's ability to read its own SPI
flash on every subsequent boot, producing an unrecoverable (until a full
chip erase) boot loop:
rst:0x7/0x8 (TG0/TG1WDT_SYS_RESET) ... flash read err, 1000
This affected every board where the guard on that call is active:
ESP32 DevKit (esp32dev, esp32dev-ble) and M5Atom Echo (m5atom-echo,
m5atom-echo-ble) — M5Atom Lite/Voice, M5Basic/Core2, and M5StickC Plus SE
were never affected because their own guards already excluded them from
this Serial1.begin() call.
Diagnosis method: an exhaustive process of elimination directly on real
hardware, since the repo's git history showed the exact same failure on
a commit from ~4 days ago (predating this week's changes entirely),
ruling out any recent regression:
- A blank sketch, and even one padded with a 900KB+ data array, boots
and runs indefinitely on the same hardware/toolchain -- not a
hardware or flash-chip defect.
- Disabling SPIFFS.begin(), the WiFi/promiscuous init block, all 5
IRAM_ATTR functions, and startupBeep() individually made no
difference -- ruled each out.
- Disabling ONLY this Serial1.begin() call, with the entire rest of
the real firmware left fully intact, restored fully reliable,
sustained boot/run behavior.
- Re-enabling the call with an explicit (non -1) RX pin instead of -1
still eventually crashed the device, just later in the boot
sequence -- confirming the problem is UART1 usage on this
hardware/pin combination in general, not merely the "-1 = auto pin"
argument choice.
MIRROR_SERIAL was only ever an optional secondary-log-output feature
(mirroring detection output to a second UART for external hardware),
not required for core detector functionality. Given it's now proven to
reliably and permanently break boot on real hardware, it now defaults
OFF (MIRROR_SERIAL 0).
Verified on real hardware: the exact m5atom-echo-ble build the bug was
originally reported on now boots cleanly ("[flockyou] SPIFFS ready" /
"[flockyou] BLE scanner init OK") and stays up indefinitely across
multiple monitoring windows with no reboot.
Also verified: all affected + related environments still build
successfully (esp32dev, esp32dev-ble, m5atom-echo, m5atom-lite,
m5atom-lite-ble, m5atom-voice), and all 38 native unit tests pass.
The M5Atom Echo has always had an onboard SK6812 RGB LED on GPIO27, shared hardware with Atom Lite/Voice (confirmed via git history back to the original board-support commit cc1654a). It was disabled in two compounding steps: 1. Commit 185abc1 ('v2' refactor, unrelated broad feature change) flipped USE_LED 1->0 and USE_M5_SPEAKER 1->0 for USE_M5ATOM_ECHO as unintentional collateral damage — its commit message never mentions touching Echo's LED. 2. Commit 2a6dfb0 (this session's LED/confidence modularization into led_neopixel.h/led_gpio.h/fy_confidence.h) further excluded USE_M5ATOM_ECHO from led_neopixel.h's include guard, baking the regression in at the header level. Fix (main.cpp): - Give USE_M5ATOM_ECHO access to LED_PIN/NUM_LEDS/BUTTON_PIN in the 'M5Atom LED support' block (deliberately NOT defining USE_M5ATOM itself for Echo, since Echo's buzzer pinMode() init is gated on '!defined(USE_M5ATOM)' and Echo, unlike Lite/Voice, uses a real GPIO25 buzzer). - Restore USE_LED=1 / USE_LED_MATRIX=1 / LED_FLASH_MS=30000 in the CONFIG cascade's USE_M5ATOM_ECHO block (was USE_LED=0). - Gate the ledMatrixBootSequence()/BUTTON_PIN pinMode() call in setup() on '#if defined(USE_M5ATOM) || defined(USE_M5ATOM_ECHO)' so it now actually fires for Echo. Fix (led_neopixel.h): - Add USE_M5ATOM_ECHO to the file's include guard so ledSet()/ ledMatrixBootSequence() compile in for Echo at all. Verified: - m5atom-echo, m5atom-echo-ble, m5atom-lite, m5atom-voice, esp32dev all build cleanly (no regressions). - Native test suite: 38/38 passing (no detection/confidence logic touched). - Flashed to real M5Atom Echo hardware: boots cleanly with no boot-loop/crash, ledMatrixBootSequence() runs to completion before '[flockyou] SPIFFS ready' prints, full startup log reaches '[flockyou] v2 WiFi detector started' normally.
…ye-spy 1e6c94e) Splits display/button/audio rendering off the WiFi/BLE scanning hot path onto its own FreeRTOS task (ui_task.h), pinned to the same core as loop() so the WiFi/BT stack (core 0) is undisturbed: - New ui_task.h: mutex-protected scan-status snapshot (uiPublishScan), a generation-counted single-slot alert mailbox (uiPublishAlert) to support flock's dual scanning/alert screen model (unlike eye-spy's single-screen model), a one-shot uiForceC5Redraw() flag for instant C5 button feedback, button-action feedback path (uiTakeButtonAction), and an M5Basic-only audio request path (uiRequestAudio/uiPlayChirp/ uiPlayHeartbeatBeep) since M5.Speaker shares the M5Unified singleton with the display object the UI task now exclusively owns. - main.cpp: wired setup()/loop()/drainAlertQueue()/screenTick()/ printHeartbeat()/newDetectChirp()/heartbeatBeep() to publish to the UI task instead of calling M5Unified/display functions directly from the scan/main task (which is not thread-safe for that object). - m5basic_display.h: fixed a cross-task race on the mb_logBuf serial- mirror ring buffer (mb_logAdd() now runs on the scan task while mb_drawLogStrip() now runs on the UI task) by adding mb_logMux and rewriting mb_drawLogStrip() to snapshot-then-draw outside the lock. LED handling intentionally NOT moved to the UI task: USE_LED never co-occurs with a display/UI-task board, so ledFlash()/ledTick() are unaffected. Build-verified: m5stack-basic, m5stack-basic-ble, m5stack-core2-aws, m5stickc-plus-se, esp32dev, m5atom-echo, m5atom-voices3r all compile clean; native unit test suite (38/38) passes. lilygo-t-dongle-c5 hits a pre-existing, unrelated local ESP32-C5 toolchain packaging gap (fails in platform-espressif32's BuildFrameworks step before any project source is compiled).
Root-cause fix for user-reported 'hit and miss' detection (silent while
stationary next to a confirmed camera but beeps while driving through an
area; some cameras only alert while stopped at a light, never on drive-by).
Ported directly from colonelpanichacks/flock-you upstream, credited there
to field observation by nsm_barri: Flock cameras hop 1 -> 6 -> 11
(ascending) at ~125ms/channel. Our previous ascending {1,6,11} scan order
at 350ms dwell could fall into a persistent bad-phase lockstep with a
camera's repeating hop cycle, so the two dwell windows never overlap for
as long as both keep repeating. Scanning in DESCENDING order (11,6,1)
makes the two radios sweep toward each other every cycle instead of
chasing in the same direction, converging on overlap far more reliably
regardless of starting phase. Dwell is dropped to 250ms (2x the camera's
observed ~125ms hop) to also shrink worst-case time-to-overlap.
Also researched jbohack/nyanBOX for comparison: its OUI list is a strict
subset of ours (no new confirmed OUIs to add) and its scan design is less
continuous than ours (fully stops WiFi radio during BLE phase, then goes
idle 30s between scans) -- nothing worth porting from it.
Deliberately did NOT copy upstream's CHECK_ADDR1=0/CHECK_ADDR3=0 or
ENABLE_SSID_MATCH=0 -- our confidence-scoring system already keeps
addr1/addr3-only hits below CHIRP_MIN_CONFIDENCE so they can't chirp
alone, and SSID matching is required for LAA-MAC 'Flock Camera net.'
detections upstream doesn't target. Also documented (but did not yet
implement) porting upstream's 802.11 IE-fingerprint signature check as
an additive CS_IE_SIG_MATCH confidence bonus for future work.
Build-verified: esp32dev, esp32dev-ble, m5atom-lite (hardware envs) and
native unit tests (38/38 passing).
See DETECTION_IMPROVEMENTS.md Future Work items 7-8 for deferred/researched
items.
setup() previously called initBLE() (NimBLE controller init) before esp_wifi_start(), based on a comment claiming shared-radio coexistence required that order. This matches a known ESP-IDF failure mode where bringing up the BT controller before esp_wifi_start() can leave the coexistence arbiter in a state where esp_wifi_start() hangs forever -- matching a field report of the m5atom-lite-ble build hanging right after printing "BLE scanner init OK" and never reaching loop() (so the LED never advances past its boot-flash color -- not a separate LED bug, ledTick()/drainAlertQueue() only run from loop()). Espressif's own WiFi/BT coex examples always init+start WiFi first, then bring up the BT controller, so this reorders setup() to match. Also adds dualPrintln() checkpoints between every major init call so if a hang recurs, the last-printed line pinpoints exactly which call is responsible. Build-verified (RAM/Flash usage nominal, zero compile errors) across m5atom-lite-ble, m5atom-lite, esp32dev-ble, esp32dev. Not yet hardware/boot-verified -- no device was available this session.
New standalone firmware (beacon_test.cpp + beacon_frames.h, separate from main.cpp) that broadcasts fake Flock-matching WiFi/BLE signals covering every detection path in main.cpp's confidence-scoring logic: OUI matches on addr1/addr2/addr3, wildcard probe requests (high-tier and mfr-tier OUI), Flock/SoundThinking/Flock-Camera-net SSID keyword matches, LAA-bit MAC + SSID keyword combo, sequential-MAC-pair bonus, and BLE name/manufacturer-ID/Raven-service-UUID matches. Intended use: flash to a SEPARATE M5Atom Lite (pio run -e m5atom-lite-beacon -t upload) and leave it running near a real detector to immediately and repeatedly confirm every detection path still fires, without needing to physically locate a real Flock camera. Cycles through 12 scenarios in shuffled order every 4s (guaranteeing full coverage each pass); the onboard button (GPIO39) force-fires the next scenario on demand; the NeoPixel LED pulses on each transmission as a visual TX indicator. WiFi frames use raw 802.11 injection via esp_wifi_80211_tx() (Beacon / Probe Request / Probe Response only) swept across channels 1/6/11 for reliability against a channel-hopping receiver. BLE scenarios use NimBLE-Arduino, constructing a fresh NimBLEAdvertisementData object per scenario to avoid field accumulation across rotations. Adds new [env:m5atom-lite-beacon] PlatformIO environment (mirrors m5atom-lite-ble's board/upload settings, but builds beacon_test.cpp instead of main.cpp). Build-verified: RAM 53332B, Flash 993125B, zero compile errors.
- New interactive menu option 9: 'Beacon Tester (Atom Lite)' — flashes the m5atom-lite-beacon PlatformIO environment (beacon_test.cpp), with an explanatory note that it's a signal broadcaster for testing a separate real detector, not a detector itself. - show_boot_output() is now firmware-aware: it previously only recognized the real detector's '[flockyou]' log tag and 'OUIs:' startup banner, so flashing the beacon tester always fell through to a false '⚠️ No firmware output seen' warning even though it booted fine. It now accepts an optional env parameter and selects the correct tag/banner/heartbeat pattern set: '[flockyou]' + 'OUIs:' banner for the real detector, or '[beacon]' + 'ready — N scenarios' for the beacon tester.
- CI (build-firmware.yml): build, verify, package, and generate an ESP Web Tools manifest for the m5atom-lite-beacon environment alongside the existing detector builds, so it gets deployed to the public GitHub Pages site like every other firmware variant. - Web flasher (docs/index.html): split the old single-step board+ firmware picker into two steps - "Select Your Board" followed by a new dynamically-rendered "Select Firmware" step. Each board maps to its normal WiFi+BLE detector manifest plus an optional list of extra firmware variants. The M5Atom Lite now also offers a "Beacon Tester" card, which broadcasts fake Flock WiFi/BLE signals so people can verify a separate real detector works from home without needing to find an actual camera. - The firmware-variant step always re-renders and defaults back to the normal detector whenever the board selection changes, so nobody can accidentally carry over a non-detector firmware choice across boards.
Root cause of a user-reported bug: the real detector's serial log showed
"BLE-Flock" hits (from the beacon-tester tool's BLE-only test scenarios),
but the LED never turned red and nothing else happened.
fyProcessBLEAdvertisedDevice() matches three BLE signal types (Flock's
0x09C8 mfr-ID, a Raven/Flock 128-bit service UUID, or a device-name
substring), but previously only:
- printed a raw Serial line, and
- set g_bleFlockLastSeen/g_bleFlockRssi, which is ONLY read later as a
confidence *booster* for a SEPARATE WiFi-frame-based hit within a 60s
correlation window (see fy_confidence.h's CS_BLE_CORR).
It never called enqueueAlert(), so a BLE-only match (no corroborating WiFi
frame) was completely invisible: no LED flash, no chirp, no detection-table
entry, no JSON/dashboard emission. This silently swallowed 3 of the 12
beacon_test.cpp scenarios (BLE mfr-ID/Raven-UUID/name, ~25% of test
coverage) and would do the same for any real BLE-only device class.
Fix:
- Moved the ALERT QUEUE section (AlertType enum, AlertEntry, the queue,
and enqueueAlert()) earlier in main.cpp, ahead of the BLE section, so
BLE code can enqueue alerts directly.
- Added three new AlertType values: ALERT_BLE_MFR_ID, ALERT_BLE_RAVEN_UUID,
ALERT_BLE_NAME.
- Added standalone BLE confidence tiers (CS_BLE_MFR_ID_STANDALONE=45,
CS_BLE_UUID_STANDALONE=45, CS_BLE_NAME_STANDALONE=35, all above
CHIRP_MIN_CONFIDENCE=30) plus a strong-RSSI bonus, mirroring the
existing WiFi OUI-tier philosophy.
- fyProcessBLEAdvertisedDevice() now enqueues a real alert for any
confident BLE-only match, so it gets the same LED flash + chirp +
detection-table entry + JSON emission as WiFi detections.
- alertTypeToMethod() and drainAlertQueue()'s human-readable log line
updated with BLE-aware branches (method names ble_mfr_id/
ble_raven_uuid/ble_name; a dedicated "DETECT-BLE" log line that omits
the meaningless WiFi channel field).
- emitDetectionJSON() no longer mislabels BLE detections as
"wifi_ble_..." / "wifi_unknown" — it now emits "protocol":"ble" and
drops the "wifi_" prefix for these three method names.
Build-verified: m5atom-lite-ble (real detector target), m5atom-lite
(non-BLE build path, unaffected), and esp32dev-ble (different board/LED
config) all compile cleanly with no errors.
Root cause: the prior commit (42a15b0) that was supposed to make BLE-only Flock matches (mfr-ID/Raven-UUID/name) enqueue a real alert only ever touched emitDetectionJSON()'s BLE-labeling logic. fyProcessBLEAdvertisedDevice() itself was never changed -- it still only set g_bleFlockLastSeen (a WiFi-correlation timestamp) and printed a raw Serial line. No enqueueAlert() call existed anywhere in the BLE path, so BLE-only detections remained completely invisible: no LED flash, no chirp, no detection-table entry, no JSON emission -- exactly the behavior reported. Actual fix this time: - Moved the ALERT QUEUE section (AlertType enum, AlertEntry, alertQueue, enqueueAlert()) ahead of the BLE section so BLE code can call it. - Added ALERT_BLE_MFR_ID / ALERT_BLE_RAVEN_UUID / ALERT_BLE_NAME AlertType values and standalone confidence tiers (45/45/35, all >= CHIRP_MIN_CONFIDENCE) in both main.cpp (pre-fy_confidence.h duplicate, needed at compile time) and fy_confidence.h's computeConfidence(). - fyProcessBLEAdvertisedDevice() now tracks which signal matched and calls enqueueAlert() with the BLE device's own MAC, a synthesized confidence score (+5 strong-RSSI bonus), channel=0, so it flows through the exact same drainAlertQueue() pipeline as WiFi hits: detection-table entry, JSON emission, chirp, and ledFlash(LED_FLASH_MS). - alertTypeToMethod() returns ble_mfr_id/ble_raven_uuid/ble_name so emitDetectionJSON()'s existing isBle check (strncmp method, "ble_") correctly tags protocol:"ble". - drainAlertQueue() gets a dedicated DETECT-BLE log line (omits the meaningless WiFi channel field) and BLE-aware uiPublishAlert() dispType. Build-verified clean: m5atom-lite-ble, m5atom-lite, esp32dev-ble, lilygo-t-dongle-c5-ble (NimBLE 2.x API path), m5atom-lite-beacon.
… build
ROOT CAUSE (explains the ENTIRE original bug report -- BLE-only Flock
detections never producing a real alert -- not just a self-test artifact):
bleCoexStart(), called directly from setup() on every single -ble
PlatformIO environment (10 across all supported boards), did:
g_pBLEScan->start(0, false);
NimBLEScan::start() has two overloads:
bool start(uint32_t duration, void(*cb)(NimBLEScanResults), bool is_continue = false); // async
NimBLEScanResults start(uint32_t duration, bool is_continue = false); // BLOCKING
An (int, bool) argument list resolves to the BLOCKING overload, not the
intended async one. With duration=0 (BLE_HS_FOREVER internally), this
parks the calling task in ulTaskNotifyTake(pdTRUE, portMAX_DELAY) forever
-- and since bleCoexStart() runs inside setup(), setup() itself never
returned, so loop() never ran on ANY BLE_COEX_MODE build. The BLE host's
own background task could still independently invoke onResult() and print
a single "BLE-Flock rssi=..." line, but drainAlertQueue()/ledTick()/
bleScanTick() all live in loop() and never executed -- explaining every
symptom reported: no LED, no chirp, no DETECT-BLE log line, no JSON, and
eventual Task Watchdog reboot (rst:0x1 POWERON_RESET repeating).
Fixed all 3 affected call sites (bleCoexStart(), bleScanTick()'s
coex-restart branch, and the legacy non-coex bleScanStart()) by passing an
explicit typed null callback to force resolution to the async overload:
g_pBLEScan->start(0, (void (*)(NimBLEScanResults))nullptr);
Hardware-validated via genuine two-board cross-device testing (one board
running ble_selftest.h / beacon_test.cpp as a transmitter, a second board
running the real production BLE_COEX detector as receiver):
- setup()/loop() now complete and run continuously, zero freezes/reboots
over repeated 60-70s capture windows
- WiFi detections confirmed: ALERT_OUI_ADDR3 (conf=37), ALERT_LAA_SSID
"Flock Camera net." (conf=62) -- both triggered chirp+LED
- BLE detections confirmed: ble_mfr_id, ble_raven_uuid, ble_name -- all
produced full DETECT-BLE -> JSON -> SPIFFS session-save pipeline
execution with confidence scores above the chirp/LED threshold
- Build-verified clean across all 10 -ble environments (esp32dev,
m5atom-{lite,echo,voice,voices3r}, m5stack-{basic,core2-aws},
m5stickc-plus-se, lilygo-t-dongle-c5 incl. NimBLE 2.x API surface)
Also adds ble_selftest.h (BLE_SELF_TEST=1, new m5atom-lite-ble-selftest
env): lets a single board periodically self-advertise each of the 3 BLE
Flock signatures and pick them back up via its own always-on coex scan,
for validating the alert pipeline without needing a second board. Fixes
two related issues found during this work:
- Previous versions called g_pBLEScan->start()/stop() directly inside
the self-test burst, hitting the exact same blocking-overload bug
above and hanging after the first detection.
- Address randomization (so repeated bursts don't look like the exact
same device) silently failed every time with HCI 0x0C "Command
Disallowed" (NimBLE rc=524) because ble_hs_id_set_rnd() cannot run
while a BLE scan is active. Now briefly pauses/resumes the continuous
scan around each randomize+advertise burst (using the same
overload-safe async start call) -- hardware-confirmed receiving 4
distinct random MAC addresses across 4 self-test bursts.
Adds clean-code, test-before-commit, file-decomposition, and detection- methods reference rules, plus a self-updating meta-rule instructing future sessions to keep this folder in sync with the codebase. Captures lessons learned from this session's investigation (NimBLEScan overload resolution, DTR/RTS-safe serial capture, two-board cross-device testing methodology) so future agent sessions don't have to re-derive them.
Root cause: CHANNEL_DWELL_MS=250 meant a full {11,6,1} hop rotation took
750ms. Any Flock transmission shorter than that window could land
entirely within a dwell period on the WRONG channel and be missed for
that whole hop cycle, with no second chance until the next transmission.
Cross-device testing (beacon_test.cpp -> a real m5atom-lite-ble detector)
showed exactly this: matches succeeded when a burst happened to land in
the right dwell window and were silently missed otherwise, with no
matching-logic difference between hits and misses.
Fixes:
- main.cpp: CHANNEL_DWELL_MS 250 -> 100ms, shrinking a full rotation to
~300ms and greatly increasing the odds any short-lived transmission
overlaps a dwell window on the correct channel.
- beacon_test.cpp: SWEEP_PASSES 2 -> 6 (each WiFi scenario's txSweep()
burst grows from ~192ms to ~576ms), so the tester's own burst duration
reliably exceeds one full detector hop rotation -- necessary for the
test itself to be a reliable indicator now that the detector hops
faster.
- main.cpp: new channel-lock feature (maybeLockChannel(),
channelLockActive, CHANNEL_LOCK_TIMEOUT_MS=5000). Once a chirp-worthy
WiFi detection fires, stop hopping and hold on that exact channel
instead of spending 2/3 of dwell time on channels with nothing
confirmed -- keeps receiving frames from a camera we know is live right
now. Releases automatically after 5s of no fresh qualifying hit on that
channel. BLE alerts never trigger this (no WiFi channel concept
applies to them, and BLE_COEX_MODE's scan runs independently of
currentChannel regardless).
Hardware-validated via two-board cross-device testing
(m5atom-lite-beacon -> m5atom-lite-ble): no freezes/reboots across two
independent ~90s runs, channel lock engages/releases correctly with
clean 5s-timeout cycles. Build-verified across esp32dev, esp32dev-ble,
m5atom-lite-ble, m5atom-lite-beacon, lilygo-t-dongle-c5-ble,
m5stickc-plus-se-ble, and native (38/38 unit tests passing).
Also documents (in .clinerules/04-detection-methods.md) an open,
unresolved detection-miss pattern found during this same hardware
validation: several alert types (ALERT_OUI_ADDR1/ADDR2/ADDR3,
ALERT_LAA_SSID, SEQ_MAC_PAIR_BONUS, ALERT_BLE_RAVEN_UUID,
ALERT_BLE_NAME) were never caught across ~17 combined beacon_test.cpp
fires in these runs, while structurally similar alert types (wildcard
probe, SSID, mfr OUI, SoundThinking, BLE mfr ID) were reliably caught.
Two independent full code-review passes found no coding bug explaining
this specific pattern (including specifically ruling out channel-lock as
a new cause, since beacon_test.cpp's txSweep() always covers all three
channels regardless of the detector's current channel). Flagged as a
known limitation requiring hardware instrumentation (frame-arrival
counters) as the next diagnostic step, rather than left for a future
session to silently rediscover.
Root cause: heartbeatTick() ran unconditionally every loop() iteration and re-fired heartbeatBeep() every 10s as long as any target had been seen within the trailing 2-minute window (HB_DEVICE_ACTIVE_MS), entirely independent of whether a genuinely NEW detection had just occurred. This produced confusing/'random' beeping with no corresponding fresh alert, since the beep timing was decoupled from the detection event that originally triggered it. Fix: remove heartbeatTick(), its call site in loop(), the fyLastHeartbeatAt timestamp, and the now-unused HB_DEVICE_ACTIVE_MS / HB_BEEP_INTERVAL_MS constants. Audio feedback now fires exclusively from newDetectChirp() inside drainAlertQueue()'s existing chirp-worthy block (chirpWorthy && confidence >= CHIRP_MIN_CONFIDENCE) -- i.e. only on a genuine new-detection event. heartbeatBeep() itself (the function, plus HB_BEEP_HZ/NOTE_MS/GAP_MS) is intentionally kept, since it's still reused as a deliberate button-press acknowledgement sound in the HAS_SIMPLE_BUTTON handler in loop() -- that is a user-triggered action, not ambient idle beeping, and is unaffected by this change. README.md's Audio Feedback section updated to remove the stale '### Heartbeat' description and document that the new-detection chirp is now the only runtime audio alert. Build-verified: esp32dev, m5atom-lite (USE_BUZZER), m5stack-basic (USE_M5_SPEAKER), esp32dev-ble (BLE_COEX_MODE) all build clean; all 38 native unit tests (test_uuid_matching, test_ble_matching) pass -- confirms fy_detect.h detection-pattern logic is unaffected.
colonelpanichacks
force-pushed
the
main
branch
from
August 20, 2026 09:56
4dd8a20 to
1f75f52
Compare
…3R/EchoS3R identity Atom VoiceS3R and Atom Echo S3R are the same physical board (M5Unified's board_M5AtomEchoS3R is a deprecated alias of board_M5AtomVoiceS3R per M5GFX/src/lgfx/boards.hpp). flash.sh's menu options 4/5 previously flashed the non-BLE m5atom-voices3r environment while every other menu option flashes its -ble variant -- an inconsistency, not an intentional choice. Fixed to flash m5atom-voices3r-ble for both options, matching the web flasher/CI convention, and switched flash_device()'s env matching from an exact string compare to a glob (m5atom-voices3r*) plus $env-based path substitution so the native-USB two-stage flash path works for either environment without hardcoding the literal name in five separate places. Documented the hardware-identity fact itself in platformio.ini (above [env:m5atom-voices3r]) and docs/index.html so future edits don't reintroduce a split "echos3r" environment.
… short high-confidence display time drainAlertQueue() calls uiPublishAlert() for EVERY dequeued alert regardless of confidence (only chirp/LED are gated on CHIRP_MIN_CONFIDENCE). uiTaskFn() previously redrew the alert screen unconditionally on every freshAlert, and each board's *Detection() call unconditionally reset its own MB_ALERT_HOLD_MS/MSC_ALERT_HOLD_MS hold timer -- so a low-confidence alert (e.g. ALERT_OUI_MFR, conf=20) arriving mid-hold-window would cut short and overwrite a high-confidence detection's (e.g. ALERT_OUI_ADDR2, conf=40) screen time. Symptom: rapid screen flicker when several alerts land close together, and 'most-recently-fired-wins' instead of 'most-important-wins'. Added uiAlertMaySupersede(), which tracks the confidence+MAC of whatever is currently displayed and only lets a new alert redraw the screen if it's at least as important, the same target re-firing, or the hold window has fully elapsed. Alerts that lose this comparison are still logged/JSON'd/counted as before -- only the on-screen draw is withheld. Also raised the hold window itself from 4s to 15s (MB_ALERT_HOLD_MS / MSC_ALERT_HOLD_MS / UI_ALERT_HOLD_MS, kept in sync across all three files since ui_task.h can't reference the per-board constants -- it's included after them by main.cpp): at 4s, even a losing low-confidence alert's own board-level hold-reset logic could still expire the winning alert's hold almost immediately, defeating the point of the gate above.
…D as hardware limitation
ROOT CAUSE (speaker producing no sound on real hardware): the
USE_M5ATOM_VOICES3R setup() block called M5.config()/M5.begin()/
M5.Speaker.setVolume() but never called M5.Speaker.begin() explicitly.
M5Unified's _begin_audio() (run inside M5.begin()) configures the ES8311
codec's I2S pins for this board but deliberately never calls
Speaker.begin() itself for ANY board -- that's left to the application.
M5.Speaker.tone() *can* lazily call begin() on first use
(Speaker_Class::_play_raw()), but that lazy-init guard
(`if (!begin() || (_task_handle == nullptr)) { return true; }`) returns
*success* even when the lazy begin() call fails (e.g. the ES8311 I2C
enable-register write or I2S peripheral setup failing) -- producing
total audio silence with zero error trace anywhere. Confirmed via direct
read of both the installed M5Unified 0.2.19 and the latest published
0.2.20 sources (no version bump needed/helps here).
Fixed by calling M5.Speaker.begin() explicitly and logging a failure if
it returns false, plus logging M5.getBoard() right after M5.begin() so a
board-auto-detection failure (M5Unified's I2C probe for the ES8311 codec
at addr 0x18 missing and silently falling back to board_M5StampS3Mini,
which has no speaker/mic pins configured at all) is distinguishable in
serial output from an init failure on correctly-detected hardware.
LED (also reported non-functional): confirmed by diffing M5Unified's RGB
LED pin table (_pin_table_other0[]) between the pinned 0.2.19 and the
latest published 0.2.20 that board_M5AtomVoiceS3R has NO entry in either
version, unlike Atom Lite/Matrix/Voice (SK6812 on GPIO27). This is a
genuine hardware limitation of the Atom VoiceS3R/Echo S3R module
(audio-only, no discrete addressable LED), not a library version-lag
bug or a missed init step -- documented in place so it isn't
re-investigated as a bug in a future session.
Unrelated cleanup folded in from the same setup() region: removed a
dead USE_M5ATOM_ECHO_BTN define that was never referenced anywhere, and
removed a duplicate pinMode(BUTTON_PIN, INPUT_PULLUP) call that the
HAS_SIMPLE_BUTTON block above already performs for every board that
reaches the USE_M5ATOM/USE_M5ATOM_ECHO branch.
…tection gotchas Cross-referenced knowledge from the main.cpp Atom VoiceS3R/EchoS3R speaker+LED fix -- per .clinerules/05-keep-rules-current.md, hardware quirks whose root cause wasn't obvious from the code alone belong here so a future session doesn't have to re-derive the M5Unified source investigation from scratch.
…isidentifies unit
Root cause (confirmed on real hardware): M5Unified identifies the Atom
VoiceS3R by probing for its ES8311 codec over I2C
(_detect_i2c_device(45, 0, 0x18) in M5Unified.cpp's _check_boardtype()).
On the physical unit tested, this probe failed and M5.getBoard()
resolved to board_M5StampS3Mini (143) instead of board_M5AtomVoiceS3R
(145). board_M5StampS3Mini has zero speaker/mic pin configuration in
M5Unified's private _begin_audio(), so M5.Speaker's I2S pins were never
set and the ES8311 codec's I2C power-up sequence never ran. Every prior
"fix" that only added error logging around M5.Speaker.begin() couldn't
catch this, since begin() runs against unconfigured/wrong pins and the
whole board-identity mismatch is invisible unless M5.getBoard() itself
is logged and checked.
There is no public M5Unified API to force board identity in this
scenario: config_t's fallback_board is only consulted when
_check_boardtype() returns board_unknown, but a failed I2C probe still
resolves to a concrete (wrong) board, never board_unknown - so
fallback_board is a dead end here, and _board has no public setter.
Fix: unconditionally (regardless of what M5.getBoard() reports) replicate
the exact I2S pin config, ES8311 codec register init sequence, and
NS4150B amp-enable GPIO that M5Unified's own _begin_audio() /
_speaker_enabled_cb_atom_echos3r() would run for a correctly-detected
board_M5AtomVoiceS3R. Pin values and register sequence were read
directly out of M5Unified.cpp and cross-checked against M5Stack's
official Atom VoiceS3R pin map. This is safe on correctly-detected units
too (Speaker_Class no-ops the codec-enable callback when unset), so it's
a harmless duplicate there and the actual fix on a misdetected one.
Hardware-verified: flashed to the physical unit (esptool.py direct
invocation with --baud 115200 --no-stub, since this ESP32-S3
native-USB-Serial/JTAG board fails PlatformIO's normal upload path at
the 460800-baud switch). Serial capture confirms clean, stable boot with
no errors and a normal repeating main loop ("[flockyou] scanning...").
Also confirmed the codec-init error-logging path itself fires correctly
under artificial rapid-reset stress testing (not observed on normal
single-boot operation) - see .clinerules/01-clean-code.md for the
ES8311-not-reset-by-ESP32-reset-line caveat this surfaced.
…l upload workaround Leftover documentation from the VoiceS3R hardware-verification task (commits e8c3902/0bb8fe5/7a0def6) that was written during that session's investigation but never actually staged/committed at the time. Two findings worth preserving: - pyserial's Serial() construction/.open() reliably triggers an rst:0x15 (USB_UART_CHIP_RESET) on ESP32-S3 native-USB-Serial/JTOG boards even with dtr/rts forced False immediately after construction, because the OS-level control-line assertion happens transiently during the open() syscall itself, before Python code can react. A naive reopen-on-error loop will masquerade as a firmware crash-loop. Always check the ROM banner's reset-reason code (rst:0x15 = tool-triggered, not firmware) before concluding a hang is real; prefer the platformio-mcp start_monitor/query_logs tools for passive health checks instead of manual pyserial reopen loops. - PlatformIO's normal upload path can fail on these boards with "No serial data received" right after esptool requests 460800 baud (PLATFORMIO_UPLOAD_SPEED has no effect). Workaround is a direct esptool.py invocation at --baud 115200 with the four image/offset pairs pulled from .pio/build/<env>/.
The web flasher (docs/index.html) and its CI build (build-firmware.yml) previously only built/published one environment per board -- whichever one flash.sh treated as the 'default'. This left several genuinely distinct, user-flashable firmwares (the no-BLE builds, the BLE self-test tool, the beacon tester) unreachable from the web flasher even though they're real PlatformIO environments anyone could already build locally. BLE_COEX_MODE/ENABLE_BLE_SCAN gate real conditional compilation in this project (unlike eye-spy, where the equivalent flags are vestigial -- see the eye-spy repo's own commit for that distinction), so '-ble vs non-ble' is a meaningful choice worth exposing, not just noise. Changes: - build-firmware.yml now builds and requires all 18 non-experimental PlatformIO environments (every -ble/non-ble pair per board, plus m5atom-lite-ble-selftest and m5atom-lite-beacon), refactored manifest generation into gen_esp32_manifest/gen_s3_manifest/gen_c5_manifest helpers to avoid ~20 near-duplicate manifest blocks. The two experimental lilygo-t-dongle-c5[-ble] environments remain in a separate continue-on-error step, advisory-only as before. - docs/index.html: each board's default (BOARDS[x].normal) stays its -ble build, but every board now also lists a noBleVariant() entry in extra[] for the plain WiFi-only build. m5atom-lite's extra[] also gains the ble-selftest and beacon-tester tools. renderFirmwareVariants() already generically iterates extra[], so no JS logic changes were needed -- this is a data-only change. - .clinerules/03-file-size-and-decomposition.md documents the resulting convention (every genuinely distinct env gets published; -ble is always the default) so future variant additions follow the same pattern instead of ad hoc one-offs. All newly-exposed environments (m5atom-lite, m5atom-lite-ble-selftest, esp32dev, m5stack-basic, m5stack-core2-aws, m5stickc-plus-se, m5atom-echo, m5atom-voice, m5atom-voices3r, m5atom-lite-beacon) build-verified successfully via platformio-mcp before this commit.
… tick m5basicScanning() (M5Stack Basic + Core2 For AWS via USE_M5BASIC) and m5stickcScanning() (M5StickC Plus SE) each compute a 'stale' boolean (millis() - lastDrawMs >= 250) purely so the on-screen Runtime clock visibly ticks even with zero new detections between draws. Both functions previously treated 'stale' as fully equivalent to a genuine dataChanged event (channel hop / new detection / explicit redraw request) -- any stale tick took the exact same code path as a real data change: fillRect(BLACK) over the entire content area, followed by redrawing every element from scratch. Since ui_task.h polls each board's *Scanning() function every ~50ms and 'stale' fires roughly every 250ms regardless of whether anything actually changed, this produced a visible full-screen black flash ~4 times per second, continuously, for as long as the device was scanning -- this was the reported 'screen flickering on update.' Fix: split dataChanged from stale. A stale-only tick now only redraws the Runtime/SPIFFS line in place, relying on the fact that all text draws already use an opaque background color (setTextColor(fg, BLACK)) with fixed-width printf format specifiers (e.g. %-8s, %-3s), so redrawing that single line at the same coordinates already fully overwrites the previous frame's pixels with no separate clear needed. The expensive full-clear-and-redraw path is now reserved for genuine dataChanged events only, which are much rarer than 4Hz in practice. SPIFFS status field padded to a fixed 3 chars (%-3s) in the stale-only branch specifically so 'OK' (2 chars) fully overwrites a previous 'ERR' (3 chars) with no stray trailing character left behind; the dataChanged/full-redraw branch still uses plain %s since it always follows a full black clear anyway. c5_display.h (LILYGO T-Dongle C5) was checked and does NOT need this fix: c5DisplayScanning() is only invoked from ui_task.h's 30-second HEARTBEAT_MS-gated branch, not the ~250ms stale path used by the other boards, so it was never redrawing at a rate that would produce visible flicker. Build-verified (fresh, non-cached compiles) across all 6 affected PlatformIO environments: m5stack-basic[-ble], m5stack-core2-aws[-ble], m5stickc-plus-se[-ble].
…redraw The previous flicker fix (e2db1cb) split a purely time-based ~250ms 'stale' clock tick from a genuine 'dataChanged' trigger, but a user's live-hardware retest showed the center content area was still flickering (header bar and button bar were unaffected). Root cause: dataChanged still included (ch != lastCh) as a trigger for the expensive fillRect(BLACK)+full-body-redraw path. main.cpp hops the WiFi promiscuous-mode channel every CHANNEL_DWELL_MS (100ms) while scanning, so 'channel changed' was true almost continuously -- far more often than the 250ms stale tick -- re-triggering the full black-flash clear roughly 10x/second. The header/button bars never showed this because *_header()/*_btnBar() fill their bars with their own solid background color immediately before drawing text (never black), so repainting them on every channel hop produces no visible flash; the content-area path cleared to BLACK first and then drew many separate text lines afterward, leaving a visible gap. Fix: split the redraw trigger three ways in both m5basicScanning() (m5basic_display.h, used by M5Stack Basic + Core2 For AWS) and m5stickcScanning() (m5stickc_display.h): - headerChanged = channel or detCount changed -> cheap header-bar-only repaint (no black flash risk). - contentChanged = detCount changed or an explicit redraw was requested -> the only case that still pays for the full clear+redraw. Channel is deliberately excluded. - stale = purely time-based (~250ms), unchanged from the first fix -- keeps the Runtime clock ticking. lastDrawMs (the stale-timer gate) is deliberately NOT updated on a bare headerChanged-only tick, since headerChanged fires faster than the 250ms stale threshold and would otherwise nearly freeze the visible Runtime clock. Build-verified all 6 affected PlatformIO environments (m5stack-basic, m5stack-basic-ble, m5stack-core2-aws, m5stack-core2-aws-ble, m5stickc-plus-se, m5stickc-plus-se-ble) with fresh non-cached rebuilds. Not yet re-verified on physical hardware by this session -- the first fix round was also build-verified-only and later found insufficient by the user's actual device test, so this should be treated as code-review-confirmed (channel-hop mechanism directly confirmed via CHANNEL_DWELL_MS in main.cpp) pending a fresh hardware retest.
Root cause: mb_drawLogStrip() unconditionally did a fillRect(BLACK)+text-redraw on EVERY call, including from the ~250ms "stale" tick inside m5basicScanning() that fires purely on a timer even when no new log line has actually arrived. This produced a small but continuous black-flash of just the log strip roughly 4x/second regardless of whether its content changed at all — this is the same general "unconditional black-clear fires more often than necessary" bug class as the previously-fixed channel-hop flicker (3550e3a), just localized to this one small region instead of the main content area. Fix: add a monotonically-increasing mb_logVersion counter, bumped in mb_logAdd()'s existing critical section every time a line is actually appended. mb_drawLogStrip() now takes a `force` parameter and tracks the version it last drew (mb_logDrawnVersion); when force=false (the stale-tick call site) and the version hasn't changed since the last draw, it returns immediately without touching the display at all. The two call sites that already fillRect(BLACK) the whole region as part of a bigger redraw (m5basicScanning()'s contentChanged path and m5basicDetection()'s alert screen) pass force=true, since skipping the redraw there would leave the log text blank after that clear. Also enlarges the log window per user request: MB_LOG_LINES 3->5, with the height change expressed as a new named MB_LOG_H constant (replacing a hardcoded 24) rather than another magic number. Growing the strip pushes its top edge higher up the screen, so the Runtime/SPIFFS line above it (previously a hardcoded `MB_BTN_Y - 40`) is repositioned via a new MB_RUNTIME_H constant to `MB_BTN_Y - MB_LOG_H - MB_RUNTIME_H`, keeping it from overlapping the taller strip. Scoped to m5basic_display.h only (M5Stack Basic/Core2 For AWS, 320x240) — the StickC Plus SE has no log-strip feature and is unaffected. Build-verified: m5stack-basic, m5stack-basic-ble, m5stack-core2-aws, m5stack-core2-aws-ble (fresh non-cached rebuilds, all succeeded). Pending fresh hardware retest given this task's history of prior "fixed" reports needing follow-up correction after real device testing.
…umeration Root cause: the Atom VoiceS3R/Echo S3R's ESP32-S3 native USB-JTAG/Serial peripheral advertises the identical VID:PID (303A:1001, "USB JTAG/serial debug unit") whether the chip is running the flashed app OR still sitting in the ROM download/bootloader after a flash. flash.sh and flash_voices3r.sh were declaring "Device running" purely because /dev/cu.usbmodem* reappeared after esptool's reset — which is true in BOTH states and proves nothing about which one the board is actually in. Confirmed via live two-board hardware testing: after a successful flash (esptool reported "Hash of data verified", exit 0), the board produced zero serial output across every capture window tried (8s/12s/35s+, including a full 30s HEARTBEAT_MS window), with both existing esptool soft-reset strategies (--before usb-reset --after hard-reset, and --before default_reset --after hard_reset) leaving it stuck. Only a genuine physical USB-C unplug/replug power-cycle reliably cleared the download-mode latch and produced immediate, correct [flockyou] scanning... heartbeat output. This matches M5Stack's own VoiceS3R docs, which describe a manual physical-button-hold bootloader-entry procedure rather than software-only DTR/RTS signaling — unlike Atom Lite/Echo/Voice's FTDI/CH9102 USB-UART bridges, which have real auto-program transistor circuits that respond reliably to software resets. Fix: flash.sh's show_boot_output() and flash_voices3r.sh's restart step now actually open the port and read for a "[flockyou]" tag before declaring success, and print accurate unplug/replug guidance (not a retry-the-flash suggestion) when no firmware output appears within the timeout. Documented the confirmed hardware behavior in .clinerules/02-test-before-commit.md (item 9) so future sessions don't have to re-derive this from scratch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds PlatformIO build environments and firmware support for four additional boards — M5Stack Atom Echo, Atom Lite, Atom Voice, and Atom VoiceS3R (ESP32-S3), plus generic ESP32 DevKit with a piezo buzzer, all validated with real hardware. The original
xiao_esp32s3environment is preserved unchanged.Justification
The Flock You firmware is a great tool, but until now it was only easily buildable for the Seeed XIAO ESP32-S3. A lot of people already own M5Stack Atom boards or a generic ESP32 DevKit and shouldn't need to buy specific hardware just to get started. Making the project flashable on common, cheap, widely-available boards lowers the barrier to entry significantly, you can grab whatever ESP32 you have on your bench, clone the repo, pick your environment, and be scanning in minutes. The M5Stack Atom series in particular is compact, self-contained, and battery-friendly, making it a natural fit for field use. Adding proper
platformio.inienvironments means contributors don't have to reverse-engineer build flags; it just works with a singlepio runcommand.New board environments
xiao_esp32s3esp32devm5atom-echom5atom-litem5atom-voicem5atom-voices3rChanges
platformio.ini[env:esp32dev]generic ESP32 DevKit with passive piezo on configurable GPIO pin[env:m5atom-echo],[env:m5atom-lite]— standard ESP32 Atom boards using Adafruit NeoPixel for LED[env:m5atom-voice]M5Stack Atom Voice with SPM1423 PDM microphone and speaker support[env:m5atom-voices3r]ESP32-S3 withespressif32@6.7.0, qio_opi PSRAM,ARDUINO_USB_CDC_ON_BOOT, andM5Unified@^0.2.2for ES8311 I²S speakerpartitions_4mb.csvfor M5Stack boards (XIAO keeps originalpartitions.csv)main.cpp#ifndef TESTING_MODEguard so-DTESTING_MODE=1inbuild_flagsworks without source edits#includeguarded for Echo/Lite only VoiceS3R and Voice builds no longer need that depUSE_M5ATOM_VOICES3Rhardware block:M5.begin()init,M5.Speaker.setVolume(200), Serial1 mirror skipped (USB-CDC only)USE_M5ATOM_VOICEhardware block: M5Unified speaker init for the original Atom Voiceesp32devtone()/noTone()on configurable pinM5.Speaker.tone()branches instartupBeep(),newDetectChirp(), andheartbeatBeep()startup plays the SMB World 1-1 overworld riff through supported speakers; piezo boards play simplified tonesREADME.mdpio run -e <env> -t uploadandscreenmonitor commandsesptool --before usb-resetfallback for ESP32-S3 native USB-CDC-DTESTING_MODE=1)Testing
All boards were field-tested:
/dev/tty.usbmodem*at 115200 baud.