diff --git a/README.md b/README.md index 288b459..486bcd5 100644 --- a/README.md +++ b/README.md @@ -187,9 +187,19 @@ Open `http://localhost:5000`, pick your serial port from the UI, detections star --- -## Hardware +## Supported Hardware -**Board:** Seeed Studio XIAO ESP32-S3 +Five environments are included in `platformio.ini`. Pick the one that matches your board; everything else is automatic. + +| Environment | Board | Port type | Speaker / LED | Notes | +|---|---|---|---|---| +| `xiao_esp32s3` | Seeed XIAO ESP32-S3 | `/dev/tty.usbmodem*` | GPIO3 piezo + GPIO21 LED | **Original upstream board** | +| `esp32dev` | Generic ESP32 DevKit | `/dev/tty.usbserial-*` | GPIO25 piezo + GPIO2 LED | Bare dev board | +| `m5atom-echo` | M5Stack Atom Echo | `/dev/tty.usbserial-*` | NeoPixel (G27) + GPIO25 buzzer | Compact with speaker | +| `m5atom-lite` | M5Stack Atom Lite | `/dev/tty.usbserial-*` | NeoPixel (G27), no speaker | Ultra-compact | +| `m5atom-voices3r` | M5Stack Atom VoiceS3R | `/dev/tty.usbmodem*` | ES8311 I²S speaker via M5Unified | ESP32-S3, 8MB PSRAM | + +### Pin map — XIAO ESP32-S3 (original) | Pin | Function | |-----|----------| @@ -197,21 +207,110 @@ Open `http://localhost:5000`, pick your serial port from the UI, detections star | GPIO 21 | Onboard user LED (active low) | | GPIO 43 | Serial1 TX mirror (115200 baud) | -Boot sound: first 6 notes of Super Mario Bros. World 1-2 (underground). +### Pin map — Atom VoiceS3R + +| Pin | Function | +|-----|----------| +| GPIO 41 | User button | +| GPIO 18 | Speaker amp enable (NS4150_CTR, handled by M5Unified) | +| I²S (G45/G0/G48/G4/G3/G17/G11) | ES8311 codec — M5Unified drives this automatically | + +Boot sound: **Super Mario Bros. World 1-1 overworld opening riff** (E E _E_ C E G) on VoiceS3R / Atom Echo; first 6 notes of World 1-2 underground on original buzzer boards. --- ## Build and flash -Requires [PlatformIO](https://platformio.org/). +Requires [PlatformIO](https://platformio.org/) (`pip install platformio` or install the VS Code extension). + +### Quick start — any board + +```bash +# Build for your board (replace with one of the environment names above) +pio run -e + +# Build + flash +pio run -e -t upload + +# Serial monitor +pio device monitor -e +``` + +### Board-specific commands + +#### Seeed XIAO ESP32-S3 (original upstream board) +```bash +pio run -e xiao_esp32s3 -t upload +pio device monitor -e xiao_esp32s3 + +# Serial monitor (direct): +screen $(ls /dev/tty.usbmodem* | head -1) 115200 +``` + +#### Generic ESP32 DevKit +```bash +pio run -e esp32dev -t upload +pio device monitor -e esp32dev + +# Serial monitor (direct): +screen $(ls /dev/tty.usbserial-* | head -1) 115200 +``` + +#### M5Stack Atom Echo +```bash +pio run -e m5atom-echo -t upload +pio device monitor -e m5atom-echo + +# Serial monitor (direct): +screen $(ls /dev/tty.usbserial-* | head -1) 115200 +``` + +#### M5Stack Atom Lite +```bash +pio run -e m5atom-lite -t upload +pio device monitor -e m5atom-lite + +# Serial monitor (direct): +screen $(ls /dev/tty.usbserial-* | head -1) 115200 +``` + +#### M5Stack Atom VoiceS3R (ESP32-S3, native USB-CDC) +The VoiceS3R uses native USB — `pio run -t upload` works when the port is free. +If the port is busy (e.g. a serial monitor is open), kill it first: ```bash -pio run # build -pio run -t upload # flash -pio device monitor # serial output +# Kill anything holding the CDC port, then flash: +lsof -t /dev/tty.usbmodem* | xargs kill 2>/dev/null; sleep 0.5 +pio run -e m5atom-voices3r -t upload + +# Or use brew esptool directly (most reliable for ESP32-S3 native USB): +esptool --chip esp32s3 --port /dev/tty.usbmodem101 \ + --before usb-reset --after hard-reset write-flash \ + 0x0 .pio/build/m5atom-voices3r/bootloader.bin \ + 0x8000 .pio/build/m5atom-voices3r/partitions.bin \ + 0xe000 ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \ + 0x10000 .pio/build/m5atom-voices3r/firmware.bin + +# Serial monitor: +screen /dev/tty.usbmodem101 115200 +# (replace 101 with whatever number the port shows — run: ls /dev/tty.usbmodem*) ``` -`platformio.ini` and `partitions.csv` are at the root (1.9 MB SPIFFS partition, 6 MB app). No extra libraries needed beyond the Arduino-ESP32 core that ships with the espressif32 platform. +> **Tip:** After flashing the VoiceS3R, unplug and replug the USB cable once to let the firmware re-enumerate the CDC port. + +### Testing mode + +To alert on **all** WiFi frames (useful for range/hardware testing before deployment), add `-DTESTING_MODE=1` to any env's `build_flags`: + +```ini +build_flags = + ... + -DTESTING_MODE=1 +``` + +Remove it to return to normal Flock OUI–only detection. + +`platformio.ini` and `partitions_4mb.csv` are at the root. The XIAO env uses `partitions.csv` (original upstream layout); all other envs use `partitions_4mb.csv`. --- diff --git a/main.cpp b/main.cpp index 4621777..2be1872 100644 --- a/main.cpp +++ b/main.cpp @@ -4,23 +4,72 @@ #include #include #include +// NeoPixel only needed for Echo/Lite variants +#if defined(USE_M5ATOM_ECHO) || defined(USE_M5ATOM_LITE) +#include +#endif + +// M5Atom support - using NeoPixel for LED only (NO M5Atom library - it's buggy!) +#if defined(USE_M5ATOM_ECHO) || defined(USE_M5ATOM_LITE) + #define USE_M5ATOM 1 + #define LED_PIN 27 + #define NUM_LEDS 1 // Atom Lite has 1 LED (Matrix has 25) + #define BUTTON_PIN 39 // M5Atom button on GPIO39 + Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800); +#endif + +// Atom VoiceS3R (ESP32-S3-PICO-1-N8R8): native USB-CDC, no user RGB LED. +// Speaker: ES8311 codec + NS4150B amp, driven via M5Unified over I2S. +// Button: GPIO41. Amp enable: GPIO18 (NS4150_CTR — M5Unified handles it). +#if defined(USE_M5ATOM_VOICES3R) + #include + #define BUTTON_PIN 41 +#endif // ============================================================ // CONFIG // ============================================================ -#define BUZZER_PIN 3 -#define USE_BUZZER 1 +// TESTING MODE: Set to 1 to blink/alert on ANY WiFi traffic (for testing) +// Set to 0 for normal operation (Flock OUI detection only) +// Can be overridden at build time via -DTESTING_MODE=1 in platformio.ini +#ifndef TESTING_MODE + #define TESTING_MODE 0 +#endif -// Onboard user LED on Seeed XIAO ESP32-S3 is GPIO21 and is ACTIVE LOW -// (driving the pin LOW lights the LED). -#define LED_PIN 21 -#define USE_LED 1 -#define LED_ACTIVE_HIGH 0 -#define LED_FLASH_MS 120 +#if defined(USE_M5ATOM_ECHO) + // M5Atom Echo: Built-in speaker + 5x5 LED matrix + #define BUZZER_PIN 25 + #define USE_BUZZER 1 + #define USE_M5_SPEAKER 1 + #define USE_LED 1 + #define USE_LED_MATRIX 1 + #define LED_FLASH_MS 120 +#elif defined(USE_M5ATOM_LITE) + // M5Atom Lite: 5x5 LED matrix only (no speaker) + #define BUZZER_PIN 25 + #define USE_BUZZER 0 + #define USE_LED 1 + #define USE_LED_MATRIX 1 + #define LED_FLASH_MS 120 +#elif defined(USE_M5ATOM_VOICES3R) + // Atom VoiceS3R: ES8311 I2S speaker via M5Unified. No RGB LED. + #define USE_BUZZER 0 + #define USE_M5_SPEAKER 1 // M5.Speaker.tone() — requires M5Unified + #define USE_LED 0 + #define LED_FLASH_MS 0 +#else + // Standard ESP32 DevKit + #define BUZZER_PIN 25 + #define USE_BUZZER 1 + #define LED_PIN 2 + #define USE_LED 1 + #define LED_ACTIVE_HIGH 1 + #define LED_FLASH_MS 120 +#endif #define MIRROR_SERIAL 1 -#define MIRROR_TX_PIN 43 +#define MIRROR_TX_PIN 17 #define MIRROR_BAUD 115200 #define CHANNEL_MODE_FULL_HOP 0 @@ -241,7 +290,7 @@ static void dualPrintf(const char* fmt, ...) { va_end(args); if (n > 0) { Serial.write(_dualBuf, n); -#if MIRROR_SERIAL +#if MIRROR_SERIAL && !defined(USE_M5ATOM_VOICES3R) Serial1.write(_dualBuf, n); #endif } @@ -249,14 +298,22 @@ static void dualPrintf(const char* fmt, ...) { static void dualPrintln(const char* str) { Serial.println(str); -#if MIRROR_SERIAL +#if MIRROR_SERIAL && !defined(USE_M5ATOM_VOICES3R) Serial1.println(str); #endif } static inline void ledSet(bool on) { #if USE_LED -#if LED_ACTIVE_HIGH +#if defined(USE_LED_MATRIX) + // M5Atom: Use NeoPixel library for LED control + if (on) { + strip.setPixelColor(0, strip.Color(0, 0, 255)); // Blue + } else { + strip.setPixelColor(0, strip.Color(0, 0, 0)); // Off + } + strip.show(); +#elif LED_ACTIVE_HIGH digitalWrite(LED_PIN, on ? HIGH : LOW); #else digitalWrite(LED_PIN, on ? LOW : HIGH); @@ -293,6 +350,10 @@ static void newDetectChirp() { tone(BUZZER_PIN, NEW_CHIRP_LO_HZ); delay(NEW_CHIRP_NOTE_MS); noTone(BUZZER_PIN); delay(NEW_CHIRP_GAP_MS); tone(BUZZER_PIN, NEW_CHIRP_HI_HZ); delay(NEW_CHIRP_NOTE_MS); noTone(BUZZER_PIN); +#elif defined(USE_M5_SPEAKER) && USE_M5_SPEAKER + M5.Speaker.tone(NEW_CHIRP_LO_HZ, NEW_CHIRP_NOTE_MS); delay(NEW_CHIRP_NOTE_MS + NEW_CHIRP_GAP_MS); + M5.Speaker.tone(NEW_CHIRP_HI_HZ, NEW_CHIRP_NOTE_MS); delay(NEW_CHIRP_NOTE_MS); + M5.Speaker.stop(); #endif } @@ -303,6 +364,10 @@ static void heartbeatBeep() { tone(BUZZER_PIN, HB_BEEP_HZ); delay(HB_BEEP_NOTE_MS); noTone(BUZZER_PIN); delay(HB_BEEP_GAP_MS); tone(BUZZER_PIN, HB_BEEP_HZ); delay(HB_BEEP_NOTE_MS); noTone(BUZZER_PIN); +#elif defined(USE_M5_SPEAKER) && USE_M5_SPEAKER + M5.Speaker.tone(HB_BEEP_HZ, HB_BEEP_NOTE_MS); delay(HB_BEEP_NOTE_MS + HB_BEEP_GAP_MS); + M5.Speaker.tone(HB_BEEP_HZ, HB_BEEP_NOTE_MS); delay(HB_BEEP_NOTE_MS); + M5.Speaker.stop(); #endif } static void startupBeep() { @@ -316,6 +381,18 @@ static void startupBeep() { noTone(BUZZER_PIN); if (i < 5) delay(22); } +#elif defined(USE_M5_SPEAKER) && USE_M5_SPEAKER + // SMB World 1-1 overworld opening riff: E E _E_ C E G (low G) + // Frequencies: E5=659 C5=523 G5=784 G4=392 + static const uint16_t notes[] = { 659, 659, 659, 523, 659, 784, 392 }; + static const uint16_t durs[] = { 100, 100, 100, 100, 100, 300, 300 }; + static const uint8_t gaps[] = { 80, 80, 80, 0, 0, 80, 0 }; + for (size_t i = 0; i < sizeof(notes)/sizeof(notes[0]); i++) { + M5.Speaker.tone(notes[i], durs[i]); + delay(durs[i]); + if (gaps[i]) { M5.Speaker.stop(); delay(gaps[i]); } + } + M5.Speaker.stop(); #endif } @@ -846,6 +923,12 @@ static void IRAM_ATTR wifiSniffer(void* buf, wifi_promiscuous_pkt_type_t type) { uint8_t ch = (uint8_t)pkt->rx_ctrl.channel; // actual rx channel from driver +#if TESTING_MODE + // TESTING MODE: Alert on ANY WiFi traffic for verification + enqueueAlert(ALERT_OUI_ADDR2, hdr->addr2, rssi, ch, nullptr, "test"); + return; // Skip normal OUI checking in test mode +#endif + // --- OUI check: addr2 (transmitter/source) --- // // For mgmt Probe Requests (type=0 subtype=4) from a matched OUI, tighten @@ -1044,21 +1127,35 @@ static void heartbeatTick() { void setup() { Serial.begin(115200); - // Crucial for USB-optional operation: without this, Serial.write() will - // block indefinitely on an ESP32-S3 USB-CDC port when no host is attached. - Serial.setTxTimeoutMs(0); delay(300); -#if MIRROR_SERIAL - Serial1.begin(MIRROR_BAUD, SERIAL_8N1, -1, MIRROR_TX_PIN); // TX-only on GPIO43 +#if defined(USE_M5ATOM) + // Initialize NeoPixel LED (NO M5Atom library!) + strip.begin(); + strip.show(); // Initialize all pixels to 'off' + // Initialize button (GPIO39 on Echo/Lite) + pinMode(BUTTON_PIN, INPUT_PULLUP); #endif -#if USE_BUZZER +#if defined(USE_M5ATOM_VOICES3R) + // M5Unified init: configures ES8311 codec, NS4150B amp (GPIO18), and button. + auto m5cfg = M5.config(); + M5.begin(m5cfg); + M5.Speaker.setVolume(200); // 0–255; ~78% is loud but not distorted +#endif + +// Serial1 UART mirror — skip on M5Atom (NeoPixel owns that UART) and on +// VoiceS3R (USB-CDC is the main port; no spare UART mapped to GPIO17). +#if MIRROR_SERIAL && !defined(USE_M5ATOM) && !defined(USE_M5ATOM_VOICES3R) + Serial1.begin(MIRROR_BAUD, SERIAL_8N1, -1, MIRROR_TX_PIN); +#endif + +#if USE_BUZZER && !defined(USE_M5ATOM) pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW); #endif -#if USE_LED +#if USE_LED && !defined(USE_LED_MATRIX) pinMode(LED_PIN, OUTPUT); ledSet(false); #endif diff --git a/partitions_4mb.csv b/partitions_4mb.csv new file mode 100644 index 0000000..a0943b0 --- /dev/null +++ b/partitions_4mb.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x2E0000, +spiffs, data, spiffs, 0x2F0000, 0x100000, diff --git a/platformio.ini b/platformio.ini index 2b3da17..04883d3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,6 +1,7 @@ [platformio] src_dir = . +; ── Original board (upstream baseline) ────────────────────────────────────── [env:xiao_esp32s3] platform = espressif32@^6.3.0 board = seeed_xiao_esp32s3 @@ -18,3 +19,89 @@ build_src_filter = + board_build.arduino.memory_type = qio_opi board_build.partitions = partitions.csv board_build.filesystem = spiffs + +; ── Standard ESP32 DevKit ───────────────────────────────────────────────────── +[env:esp32dev] +platform = espressif32@^6.3.0 +board = esp32dev +framework = arduino +monitor_speed = 115200 +upload_speed = 921600 + +build_flags = + -DCORE_DEBUG_LEVEL=0 + +build_src_filter = + + +board_build.partitions = partitions_4mb.csv +board_build.filesystem = spiffs + +[env:m5atom-echo] +platform = espressif32@^6.3.0 +board = m5stack-atom +framework = arduino +monitor_speed = 115200 +upload_speed = 1500000 +upload_port = /dev/tty.usbserial-* + +lib_deps = + adafruit/Adafruit NeoPixel@^1.15.1 + +build_flags = + -DCORE_DEBUG_LEVEL=0 + -DUSE_M5ATOM_ECHO + +build_src_filter = + + +board_build.partitions = partitions_4mb.csv +board_build.filesystem = spiffs + +[env:m5atom-lite] +platform = espressif32@^6.3.0 +board = m5stack-atom +framework = arduino +monitor_speed = 115200 +upload_speed = 1500000 +upload_port = /dev/tty.usbserial-* + +lib_deps = + adafruit/Adafruit NeoPixel@^1.15.1 + +build_flags = + -DCORE_DEBUG_LEVEL=0 + -DUSE_M5ATOM_LITE + +build_src_filter = + + +board_build.partitions = partitions_4mb.csv +board_build.filesystem = spiffs + +; ── Atom VoiceS3R (ESP32-S3-PICO-1-N8R8, 8MB flash, 8MB PSRAM) ────────────── +; USB CDC port: /dev/tty.usbmodem* (not usbserial — it's native USB) +; Speaker: ES8311 codec via I2S — driven through M5Unified M5.Speaker.tone() +; Normal mode: Flock OUI detection only. Add -DTESTING_MODE=1 to alert on all WiFi. +[env:m5atom-voices3r] +platform = espressif32@6.7.0 +board = esp32-s3-devkitc-1 +framework = arduino +monitor_speed = 115200 +upload_port = /dev/tty.usbmodem* + +board_build.arduino.memory_type = qio_opi + +lib_deps = + m5stack/M5Unified @ ^0.2.2 + +build_flags = + -DESP32S3 + -DBOARD_HAS_PSRAM + -mfix-esp32-psram-cache-issue + -DCORE_DEBUG_LEVEL=0 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 + -DUSE_M5ATOM_VOICES3R + +build_src_filter = + + +board_build.partitions = partitions_4mb.csv +board_build.filesystem = spiffs