ESP32 Marauder · Volume 3

ESP32 Marauder Firmware Volume 3 — Firmware Architecture

Repo layout, menu dispatcher, per-board build matrix, scan/attack module pattern, display + SD abstractions

Contents

SectionTopic
1About this volume
2Repository layout
· 2.1Top-level tree
· 2.2esp32_marauder/ — the firmware tree
· 2.3data/ — Evil Portal templates and web flasher
3The menu dispatcher (MenuFunctions.cpp)
· 3.1The Menu struct
· 3.2The main-loop pattern
· 3.3Adding a menu entry
4The per-board build matrix (platformio.ini)
· 4.1Anatomy of an environment block
· 4.2Documented environments
· 4.3Cross-environment build flags
5The scan/attack module pattern
· 5.1Lifecycle: start()loop()stop()
· 5.2WiFiScan.cpp — the canonical example
· 5.3BluetoothScan.cpp — the BLE/BT-classic variant
· 5.4EvilPortal.cpp — the most stateful module
6Display abstraction (TFT_eSPI / Adafruit_GFX)
7SD interface (SDInterface.cpp)
8Settings persistence (Settings.cpp)
9Build flags inventory
10Memory map — classic ESP32 vs ESP32-S3
11Release cadence and how to read mainline activity
12Resources

1. About this volume

Vol 3 walks the firmware architecture at code-reading depth. The target reader has cloned github.com/justcallmekoko/ESP32Marauder locally and wants to know what file does what, how the menu dispatcher routes a button press, where to add a new attack, and how the per-board build matrix in platformio.ini covers ~15 documented hardware targets. The companion volume is Vol 10 (build toolchain + custom development) — this volume is the map of the codebase; Vol 10 is the how to build and modify it.

A note on code-reading depth: the descriptions below are pattern-level, not line-level. The Marauder code style is straightforward Arduino-flavored C++ — class declarations in .h, definitions in .cpp, setup() and loop() in the top-level .ino file. Specific function names and class names cited below are the canonical ones at mainline v1.12.x; minor names may have shifted in later refactors. When in doubt, fetch the current source: git clone --depth=1 https://github.com/justcallmekoko/ESP32Marauder.git and grep for the symbol.

The firmware is GPLv3-licensed, so any downstream fork that ships binaries must publish source. The data/ directory (Evil Portal templates, web flasher pages) is mixed-licensed — most of data/ is the user’s content (modify freely); a couple of files are upstream-authored and inherit GPLv3.


2. Repository layout

2.1 Top-level tree

A fresh clone of github.com/justcallmekoko/ESP32Marauder produces roughly:

ESP32Marauder/
├── README.md
├── LICENSE                                    GPLv3
├── esp32_marauder/                            ← the firmware tree (see § 2.2)
│   ├── ESP32Marauder.ino                      ← Arduino-style entry point
│   ├── platformio.ini                         ← per-board build matrix (see § 4)
│   ├── *.cpp / *.h                            ← source files (see § 2.2)
│   └── data/                                  ← SPIFFS / LittleFS payload (Evil Portal templates etc.)
├── flasher/                                   ← web-flasher source (esptool-js + UI)
├── images/                                    ← README + wiki figure assets
└── docs/                                      ← supplementary docs (sometimes empty; wiki is canonical)

The two directories that matter day-to-day:

  • esp32_marauder/ — what gets built and flashed. This is where the source lives.
  • flasher/ — the web flasher (covered in Vol 10 § 4). If you only ever use the hosted version at flasher.marauder.maurersystems.com, you never touch this folder.

2.2 esp32_marauder/ — the firmware tree

Roughly 30-50 source files at the top level (no per-feature subdirectories; flat layout is the project’s longstanding convention). The important ones, grouped by role:

Entry point:

  • ESP32Marauder.ino — Arduino-style top-level. Contains setup() and loop(). setup() initializes hardware (display, SD, Wi-Fi stack), instantiates the global manager objects, splash-screens the boot. loop() is the main scheduler — dispatches to the active scan/attack module’s loop(), polls buttons / touch / serial, refreshes the display.

Menu and UI:

  • MenuFunctions.cpp + MenuFunctions.h — the menu dispatcher (§ 3 below).
  • Settings.cpp + Settings.h — settings load/save and run-time config (§ 8).
  • Display.cpp + Display.h — the display abstraction (§ 6).
  • Buffer.cpp + Buffer.h — the line-buffer that scrolls scan output onto the TFT.

Scan and attack modules:

  • WiFiScan.cpp + WiFiScan.h — Wi-Fi sniffer + attack catalogue (§ 5.2).
  • BluetoothScan.cpp + BluetoothScan.h — BLE + BT-classic (§ 5.3).
  • EvilPortal.cpp + EvilPortal.h — captive portal harness (§ 5.4).
  • WiFiNetwork.cpp + WiFiNetwork.h — AP/client list management.

Hardware glue:

  • SDInterface.cpp + SDInterface.h — mount + read + write (§ 7).
  • LedInterface.cpp + LedInterface.h — RGB LED control (per-board variability).
  • BatteryInterface.cpp + BatteryInterface.h — battery-voltage measurement + percent-remaining UI.

Build-time configuration:

  • configs.h — global build-flag aggregator. Per-board flags from platformio.ini resolve into #defines used here.

Data:

  • data/index.html — default Evil Portal template (covered in Vol 8 § 5).
  • data/connect_wifi.html, data/portal/*.html — alternative portal templates.

This list is not exhaustive — additional small modules ship for hardware-specific features (GPS support, RGB-pattern animations, specific-board button-mapping helpers). The 10-12 files above carry 80%+ of the firmware logic.

2.3 data/ — Evil Portal templates and web flasher

The data/ subdirectory is bundled as a SPIFFS (older) or LittleFS (newer) image at build time and flashed to the ESP32’s flash partition. At runtime, the firmware mounts it as a read-only filesystem and serves files out of it for the Evil Portal feature.

User customization of Evil Portal HTML happens by editing files in data/ (or, more commonly, by editing evil_portal/index.html on the SD card — which Marauder reads in preference to the SPIFFS-bundled version when present). Vol 8 § 5 walks the priority order and HTML authoring conventions.


3. The menu dispatcher (MenuFunctions.cpp)

3.1 The Menu struct

The menu system is built around a simple Menu struct holding a name, a list of child entries, a render function, and (for leaf entries) an action callback. Each menu has a parent pointer — the dispatcher uses this to handle the Back button. The top-level menu has parent nullptr.

Roughly (paraphrased from the canonical MenuFunctions.h):

struct Menu {
    const char* name;            // displayed in the menu UI
    std::vector<MenuEntry> list; // child entries; empty for action-leaf menus
    void (*onSelect)();          // optional callback fired when this menu is entered
    Menu* parent;                // for Back-button traversal; nullptr at top-level
};

struct MenuEntry {
    const char* label;          // text shown in the list
    Menu* child;                // submenu to enter; nullptr if this is a leaf
    void (*action)();           // callback fired on Select for a leaf
};

A “scan” or “attack” menu entry has child = nullptr and an action pointing at the corresponding module’s start() method. Hitting Select fires the action, which puts the firmware into “scan running” state. The dispatcher then routes loop() calls to the running module until the user hits Back / Stop.

3.2 The main-loop pattern

In ESP32Marauder.ino’s loop():

loop():
    poll input (button / touch / serial)
    if scan/attack is running:
        scan->loop()           // give the module CPU time
        refresh display from buffer
    else:
        menu->render()         // draw the current menu
        on Select:  enter child, or fire action
        on Back:    pop to parent

The scheduler is cooperative — every running module is expected to return from its loop() quickly (target: << 10 ms per call) so the menu dispatcher can stay responsive to inputs. Modules that need long-running RF tasks (Wi-Fi scan, deauth flood) drive ESP-IDF’s lower-level Wi-Fi APIs in interrupt context and use their loop() only for state-machine progression and display updates.

The architecture does not use FreeRTOS tasks for the scan modules — everything runs on the main Arduino task. This is the canonical Arduino-style “single big loop” pattern.

3.3 Adding a menu entry

Adding a new menu item is a three-file change (covered fully in Vol 10 § 5; capsule here):

  1. MenuFunctions.h — declare the menu and its entries:

    extern Menu my_new_menu;
  2. MenuFunctions.cpp — define the menu and wire its action:

    Menu my_new_menu = {
        "My New Attack",
        { { "Start", nullptr, &my_attack_start } },
        nullptr,                                 // no on-select callback
        &wifi_attack_menu                        // parent menu
    };
  3. WiFiScan.cpp (or wherever the new attack’s implementation lives) — define my_attack_start() to set up state, call WiFiScan::startSomething(), and return.

Wire the new menu into a parent menu by appending an entry to that parent’s list. Recompile, flash, verify the menu appears.

Worked example in Vol 10 § 5: a hypothetical “channel-survey CSV dumper” feature added end-to-end.


4. The per-board build matrix (platformio.ini)

4.1 Anatomy of an environment block

A single platformio.ini environment block looks like:

[env:marauder_v6_1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
upload_speed = 921600
monitor_speed = 115200
build_flags =
    -DBOARD_HAS_PSRAM
    -DARDUINO_USB_CDC_ON_BOOT=1
    -DHAS_SCREEN
    -DHAS_BUTTONS
    -DSCREEN_BUFFER
    -DMARAUDER_V6
    -DNEOPIXEL_PIN=21
    -DUSER_SETUP_LOADED=1
    [... more TFT_eSPI pin assignments ...]
lib_deps =
    bodmer/TFT_eSPI@^2.5.0
    SPI
    SD
    https://github.com/h2zero/NimBLE-Arduino

The pattern is consistent across environments:

  • platform is always espressif32 (Espressif’s PlatformIO platform).
  • board selects the physical SoC variant — esp32-s3-devkitc-1, esp32dev (classic), esp32-s2-devkit, etc.
  • build_flags carry the per-board feature toggles + TFT_eSPI pin assignments.
  • lib_deps pin library versions; mainline locks TFT_eSPI to a known-good major.

4.2 Documented environments

As of mainline at v1.12.x, the per-board environments include (this list rotates as boards age in/out; check the current platformio.ini):

Env nameTarget hardwareSoC tierNotes
marauder_v6_1Marauder v6.1 (Koko)ESP32-S3Reference platform
marauder_v6Marauder v6 (Koko, prior rev)ESP32-S3Pin-mapping deltas vs v6.1
marauder_miniMarauder Mini (Koko)ESP32-S33-button compact
marauder_flipper_miniMarauder Flipper Mini (Koko)ESP32-S3Cosmetic-match Flipper variant
marauder_dev_board_proMarauder Dev Board Pro (Koko, classic)ESP32-WROOM-32Classic ESP32 reference; BT-classic supported
marauder_devboardFlipper Zero WiFi DevboardESP32-S2Headless via Flipper UART passthrough; no BLE
cardputer_marauderM5Stack Cardputer ADVESP32-S3QWERTY input; pure-Marauder env (Bruce is alternative)
dstike_watchDSTIKE WatchClassic ESP32Display patches for OLED
t_display_s3LilyGO T-Display-S3ESP32-S3Canonical DIY target (Vol 2 § 9)
marauder_awok_v3 (variant)AWOK Dual Touch V3Classic ESP32 × 2Touch UI; dual-board firmware

(The above are typical; the exact env names rotate. The canonical source is whatever the current platformio.ini says.)

4.3 Cross-environment build flags

Some flags are common across all environments and gate features at compile-time. Inventory of the consequential ones (covered fully in § 9 below):

  • HAS_SCREEN / HAS_BUTTONS / HAS_TOUCH / HAS_GPS — feature presence
  • MARAUDER_V6 / MARAUDER_MINI / MARAUDER_DEV_BOARD_PRO — board-identity flags used in code for board-specific UI behavior
  • SCREEN_BUFFER / SCREEN_BUFFER_BIG — display-buffer size selection
  • BOARD_HAS_PSRAM — enable PSRAM-backed allocator for scan-result buffers
  • MARAUDER_DEAUTHgates compilation of the deauth attack (some mainline builds disable; rebuild from source with the flag enabled to turn on)
  • COUNTRY_US / COUNTRY_ANY — channel-plan region

5. The scan/attack module pattern

5.1 Lifecycle: start()loop()stop()

Every scan or attack module follows the same lifecycle:

  1. Menu action calls Module::start(mode_id) — sets up state, opens output files on SD if needed, configures the Wi-Fi or Bluetooth stack for the relevant mode, returns control to the dispatcher with the module marked active.
  2. Main loop calls Module::loop() repeatedly — module checks if anything new has arrived (e.g., a captured packet), updates the display buffer, advances any state machine. Returns quickly.
  3. User hits Back / Select-Stop, which calls Module::stop() — tears down state, closes SD files, restores default Wi-Fi/BLE mode.

start() / loop() / stop() are public methods on each module’s class. The dispatcher uses a global active-module pointer to route loop() calls.

5.2 WiFiScan.cpp — the canonical example

WiFiScan is the busiest module — it handles probe-request capture, beacon sniff, EAPOL/PMKID capture, deauth, beacon spam, probe spam, and Evil Portal launch (which then hands off to EvilPortal).

Sketch of the class:

class WiFiScan {
public:
    void main();                                  // initial setup
    void RunSetup();
    int currentScanMode;                          // which scan is active
    void StartScan(uint8_t scan_mode, uint16_t color);
    void StopScan(uint8_t scan_mode);
    void main(uint32_t currentTime);              // the per-loop tick (poorly named — second overload)
    bool shutdownWiFi();
    void displayProbeRequests();
    void displayBeacons();
    // ... ~25 more public methods
private:
    // result buffers, channel-hop state, target MAC, etc.
};

Modes are integer-coded — WIFI_SCAN_PROBE_REQ, WIFI_SCAN_BEACON, WIFI_SCAN_EAPOL, WIFI_ATTACK_DEAUTH, etc. The currentScanMode member tracks which is active; main(currentTime) is the per-loop tick that all modes share, with mode-specific branches inside.

The actual RF work happens inside ESP-IDF’s esp_wifi_* and esp_promiscuous_* APIs, with a registered packet-callback that fires in IRQ context. The callback appends to a ring buffer; main(currentTime) drains the ring buffer onto SD and the display.

5.3 BluetoothScan.cpp — the BLE/BT-classic variant

BluetoothScan parallels WiFiScan for the Bluetooth side. On ESP32-S3 (BLE-only) it uses NimBLE-Arduino for advertising-packet capture; on classic ESP32 it uses BluetoothSerial (or the ESP-IDF BT-classic APIs directly) for the BT-classic discovery path.

The BLE-spam attacks (Sour Apple, Swiftpair — in forks, not mainline) live here in the forks; mainline’s BluetoothScan is sniff-only.

5.4 EvilPortal.cpp — the most stateful module

EvilPortal is the most complex module because it wraps a full ESP32 SoftAP + HTTP server + DNS-spoof (to redirect all DNS to the captive page) + LittleFS-served HTML + SD-card credential log + UI feedback. Lifecycle:

  1. start() — bring up SoftAP with chosen SSID, start HTTP server on port 80, install DNS-spoofing UDP listener on port 53.
  2. loop() — accept and process HTTP requests; serve the captive page; on POST of credentials, append to creds.txt on SD; update display with “N captures” counter.
  3. stop() — tear down HTTP + DNS + SoftAP; restore default mode; close creds.txt.

Vol 5 § 5 walks the Evil Portal feature end-to-end from the user’s perspective; this section is the implementation glimpse.


6. Display abstraction (TFT_eSPI / Adafruit_GFX)

Mainline’s primary display library is TFT_eSPI by Bodmer (github.com/Bodmer/TFT_eSPI). It supports ST7789, ILI9341, ILI9488, and most other 16-bit-color TFT controllers. Per-board pin assignments and controller selection live in the TFT_eSPI User_Setup.h equivalent — but mainline doesn’t use the library’s setup-file mechanism; instead, it uses build_flags in platformio.ini to pass all configuration via -D defines, which TFT_eSPI consumes when its USER_SETUP_LOADED flag is set.

This is why most platformio.ini environment blocks have a long tail of -D TFT_* defines — they’re configuring TFT_eSPI without an external setup file.

For the DSTIKE Watch (0.96″ OLED), the path is different: TFT_eSPI doesn’t support that OLED controller (SSD1306), so the firmware uses Adafruit_GFX + Adafruit_SSD1306 instead, with a thin abstraction layer in Display.cpp that picks the right driver based on HAS_SCREEN + HAS_OLED build flags.

Display calls in feature code (WiFiScan, BluetoothScan, EvilPortal) go through Display::draw*() helper functions, not directly to TFT_eSPI methods. This is the abstraction layer. In practice the abstraction occasionally leaks — a few helper functions take TFT_eSPI-specific color constants — but cross-driver swaps mostly work.


7. SD interface (SDInterface.cpp)

SDInterface wraps Arduino-ESP32’s SD.h (the FAT32 driver). At boot, it:

  1. Initializes SPI for the SD pins (defined per-board in configs.h).
  2. Mounts the card.
  3. Ensures the canonical directory tree exists:
    • /marauder/
    • /marauder/pcaps/
    • /marauder/evil_portal/
    • /marauder/wordlists/
  4. Logs success/failure to serial.

If SD mount fails (no card / wrong format / hardware issue), the firmware boots normally but most scan-capture features no-op silently. Vol 8 § 3 covers the SD card formatting requirements (FAT32 mandatory; exFAT explicitly not supported).

Writes go through SDInterface::open() returning a File object, the standard Arduino pattern. The capture modules buffer in RAM and flush to SD periodically rather than per-packet — flushing per-packet is too slow and locks out the main loop.

A subtle point: the SPI bus is shared between the SD card and the TFT on most boards (Vol 2 § 4.2). A long SD write blocks display updates. Capture modules schedule writes between display refreshes to keep the UI responsive.


8. Settings persistence (Settings.cpp)

Settings.cpp handles two distinct categories of state:

  • Compile-time settings — values set via build_flags in platformio.ini (board identity, screen size, hardware feature presence). These are #defined into configs.h and consumed at build time. Changing them requires a re-flash.
  • Runtime settings — values persisted to /marauder/settings.txt on SD. These can be changed from the menu UI and survive reboots. Examples: default channel for static-channel mode, Evil Portal SSID, brightness, color theme.

The runtime settings file is a simple key=value text format:

EvilPortalSSID:CompanyGuest
ChannelHop:1
StaticChannel:6
Brightness:75
ColorTheme:1

Loading happens at boot (after SD mount, before main menu draw). Saving happens whenever the user exits a settings sub-menu.

Vol 8 § 6 has the full settings.txt schema.


9. Build flags inventory

The consequential build flags, grouped by purpose. Pass via -D FLAG=value in platformio.ini’s build_flags:

Feature presence:

  • HAS_SCREEN — true if a display is wired
  • HAS_BUTTONS — true if tactile buttons are wired
  • HAS_TOUCH — true if touchscreen input (XPT2046 etc.) is wired
  • HAS_GPS — true if a GPS module is wired (NMEA over UART)
  • HAS_OLED — true if the display is OLED (SSD1306) rather than TFT
  • HAS_BATTERY — true if a battery-monitor IC is wired
  • BOARD_HAS_PSRAM — true if ESP32-S3 has external PSRAM

Board identity (used for board-specific code branches):

  • MARAUDER_V6 / MARAUDER_V6_1 / MARAUDER_MINI / MARAUDER_DEV_BOARD_PRO / MARAUDER_FLIPPER_MINI / MARAUDER_T_DISPLAY_S3 / MARAUDER_DEVBOARD / MARAUDER_DSTIKE_WATCH etc.

Display configuration (TFT_eSPI consumption):

  • TFT_WIDTH / TFT_HEIGHT
  • TFT_MOSI / TFT_MISO / TFT_SCLK / TFT_CS / TFT_DC / TFT_RST / TFT_BL
  • ST7789_DRIVER / ILI9341_DRIVER — pick one
  • SPI_FREQUENCY — TFT SPI clock (typically 40000000 for 40 MHz)
  • LOAD_GLCD / LOAD_FONT2 / LOAD_FONT4 / LOAD_FONT6 / LOAD_FONT7 / LOAD_FONT8 — font selection
  • USER_SETUP_LOADED — tells TFT_eSPI to use the -D flags rather than a setup file

Feature gates:

  • MARAUDER_DEAUTH — compile the deauth attack in (some mainline builds default off; rebuild from source to enable). The single most-asked-about build flag.
  • MARAUDER_PROBE_SPAM — compile probe-spam in
  • MARAUDER_BEACON_SPAM — compile beacon-spam in (defaults on across all builds)
  • MARAUDER_EVIL_PORTAL — compile Evil Portal in (defaults on)

Region:

  • COUNTRY_US — restrict channels to 1-11; defaults if no other COUNTRY_* flag set
  • COUNTRY_DE / COUNTRY_JP / COUNTRY_ANY — alternative channel plans

USB:

  • ARDUINO_USB_CDC_ON_BOOT=1 — native USB-CDC on boot for ESP32-S3 (no UART bridge)
  • ARDUINO_USB_MODE=1 — native USB mode

Memory:

  • MAX_AP_RESULTS / MAX_PROBE_RESULTS / MAX_BLE_RESULTS — scan-result buffer sizes. Boards with PSRAM bump these higher; classic-ESP32 boards run smaller buffers.

The full inventory is in configs.h (commented). When debugging “why doesn’t this feature work on my custom build” issues, check the build flags first.


10. Memory map — classic ESP32 vs ESP32-S3

The ESP32-S3 platforms have substantially more flexibility:

ResourceClassic ESP32-WROOM-32 (e.g., DSTIKE Watch, Marauder Dev Board Pro, AWOK V3)ESP32-S3-WROOM-1 N16R8 (Marauder v6.1, Mini, Flipper Mini)
Flash4 MB (typical)16 MB
SRAM (on-chip)520 KB total; ~150 KB usable after Wi-Fi stack init512 KB total; ~200 KB usable after Wi-Fi stack init
PSRAM (off-chip)Optional; uncommon on Marauder targets8 MB standard on N16R8
BluetoothBT classic + BLE 4.2BLE 5.0 only
Wi-Fi2.4 GHz only2.4 GHz only

The PSRAM delta is the most consequential. Scan-result buffers (AP list, probe-request list, BLE-advertising list) are bigger on S3 — common defaults in mainline:

  • Classic ESP32: MAX_AP_RESULTS = 200, MAX_PROBE_RESULTS = 200, MAX_BLE_RESULTS = 200
  • S3 with PSRAM: MAX_AP_RESULTS = 1000, MAX_PROBE_RESULTS = 5000, MAX_BLE_RESULTS = 1000

Exceeding the buffer cap causes the oldest results to be evicted (FIFO ring-buffer) — not a crash, but quietly losing data. For long scans (hours), the S3 with PSRAM is materially better.

Brownout posture also differs. The classic ESP32 has a more aggressive brownout detector that trips at ~2.7 V, and the AWOK V3 platform (dual classic-ESP32) is the most-likely to show brownout resets under sustained TX-spam on a weak battery. Vol 11 § 4 has the full power discussion.


11. Release cadence and how to read mainline activity

Marauder mainline ships tagged releases via GitHub Releases (visible at github.com/justcallmekoko/ESP32Marauder/releases). The current pattern is roughly:

  • Major version bumps for substantial feature additions or architectural changes (rare; 4-6 per year)
  • Minor version bumps for smaller features and important bug-fixes (monthly cadence typical)
  • Patch version bumps for build-flag changes, per-board fixes (irregular)

As of 2026-05-13 the current tag is v1.12.x. The mainline master branch typically runs slightly ahead of the latest tag — for the bleeding edge, clone master and build; for stability, build a tagged release.

Reading the commit log (git log --oneline) is informative — the commit message format is descriptive enough that you can spot feature additions vs cleanup vs board-additions without reading the diffs.

Notable file-watch list when tracking mainline changes:

  • WiFiScan.cpp — any new attack will land here
  • BluetoothScan.cpp — any BLE feature change lands here
  • platformio.ini — new boards added; existing board build-flag changes
  • MenuFunctions.cpp — UI-visible additions
  • configs.h — new build flags (signals an upcoming feature)
  • data/portal/ — new Evil Portal templates

For passive monitoring, GitHub’s “Watch → Releases only” subscription is enough — every release ships with a changelog summary in the release notes.


12. Resources

Upstream

Libraries

Forward references in this series

  • Building from source + adding custom attacks: Vol 10
  • Per-attack implementation details: Vols 4 (Wi-Fi scan), 5 (Wi-Fi attack), 6 (Bluetooth)
  • Fork comparison (mainline vs Ghost ESP vs Bruce vs Bad Pinguino code-divergence): Vol 7
  • SD layout + Evil Portal HTML format: Vol 8

This is Volume 3 of a twelve-volume series. Next: Vol 4 walks the Wi-Fi scanning subsystems in operational depth — probe-request sniffer, beacon sniffer, PMKID capture, EAPOL 4-way handshake capture, AP+client mapping, channel hopping, output formats, end-to-end workflow recipes.