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
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 atflasher.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. Containssetup()andloop().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’sloop(), 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 fromplatformio.iniresolve 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):
-
MenuFunctions.h— declare the menu and its entries:extern Menu my_new_menu; -
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 }; -
WiFiScan.cpp(or wherever the new attack’s implementation lives) — definemy_attack_start()to set up state, callWiFiScan::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:
platformis alwaysespressif32(Espressif’s PlatformIO platform).boardselects the physical SoC variant —esp32-s3-devkitc-1,esp32dev(classic),esp32-s2-devkit, etc.build_flagscarry the per-board feature toggles + TFT_eSPI pin assignments.lib_depspin 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 name | Target hardware | SoC tier | Notes |
|---|---|---|---|
marauder_v6_1 | Marauder v6.1 (Koko) | ESP32-S3 | Reference platform |
marauder_v6 | Marauder v6 (Koko, prior rev) | ESP32-S3 | Pin-mapping deltas vs v6.1 |
marauder_mini | Marauder Mini (Koko) | ESP32-S3 | 3-button compact |
marauder_flipper_mini | Marauder Flipper Mini (Koko) | ESP32-S3 | Cosmetic-match Flipper variant |
marauder_dev_board_pro | Marauder Dev Board Pro (Koko, classic) | ESP32-WROOM-32 | Classic ESP32 reference; BT-classic supported |
marauder_devboard | Flipper Zero WiFi Devboard | ESP32-S2 | Headless via Flipper UART passthrough; no BLE |
cardputer_marauder | M5Stack Cardputer ADV | ESP32-S3 | QWERTY input; pure-Marauder env (Bruce is alternative) |
dstike_watch | DSTIKE Watch | Classic ESP32 | Display patches for OLED |
t_display_s3 | LilyGO T-Display-S3 | ESP32-S3 | Canonical DIY target (Vol 2 § 9) |
marauder_awok_v3 (variant) | AWOK Dual Touch V3 | Classic ESP32 × 2 | Touch 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 presenceMARAUDER_V6/MARAUDER_MINI/MARAUDER_DEV_BOARD_PRO— board-identity flags used in code for board-specific UI behaviorSCREEN_BUFFER/SCREEN_BUFFER_BIG— display-buffer size selectionBOARD_HAS_PSRAM— enable PSRAM-backed allocator for scan-result buffersMARAUDER_DEAUTH— gates 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:
- 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. - 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. - 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:
start()— bring up SoftAP with chosen SSID, start HTTP server on port 80, install DNS-spoofing UDP listener on port 53.loop()— accept and process HTTP requests; serve the captive page; on POST of credentials, append tocreds.txton SD; update display with “N captures” counter.stop()— tear down HTTP + DNS + SoftAP; restore default mode; closecreds.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:
- Initializes SPI for the SD pins (defined per-board in
configs.h). - Mounts the card.
- Ensures the canonical directory tree exists:
/marauder//marauder/pcaps//marauder/evil_portal//marauder/wordlists/
- 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_flagsinplatformio.ini(board identity, screen size, hardware feature presence). These are#defined intoconfigs.hand consumed at build time. Changing them requires a re-flash. - Runtime settings — values persisted to
/marauder/settings.txton 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 wiredHAS_BUTTONS— true if tactile buttons are wiredHAS_TOUCH— true if touchscreen input (XPT2046 etc.) is wiredHAS_GPS— true if a GPS module is wired (NMEA over UART)HAS_OLED— true if the display is OLED (SSD1306) rather than TFTHAS_BATTERY— true if a battery-monitor IC is wiredBOARD_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_WATCHetc.
Display configuration (TFT_eSPI consumption):
TFT_WIDTH/TFT_HEIGHTTFT_MOSI/TFT_MISO/TFT_SCLK/TFT_CS/TFT_DC/TFT_RST/TFT_BLST7789_DRIVER/ILI9341_DRIVER— pick oneSPI_FREQUENCY— TFT SPI clock (typically 40000000 for 40 MHz)LOAD_GLCD/LOAD_FONT2/LOAD_FONT4/LOAD_FONT6/LOAD_FONT7/LOAD_FONT8— font selectionUSER_SETUP_LOADED— tells TFT_eSPI to use the-Dflags 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 inMARAUDER_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 setCOUNTRY_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:
| Resource | Classic ESP32-WROOM-32 (e.g., DSTIKE Watch, Marauder Dev Board Pro, AWOK V3) | ESP32-S3-WROOM-1 N16R8 (Marauder v6.1, Mini, Flipper Mini) |
|---|---|---|
| Flash | 4 MB (typical) | 16 MB |
| SRAM (on-chip) | 520 KB total; ~150 KB usable after Wi-Fi stack init | 512 KB total; ~200 KB usable after Wi-Fi stack init |
| PSRAM (off-chip) | Optional; uncommon on Marauder targets | 8 MB standard on N16R8 |
| Bluetooth | BT classic + BLE 4.2 | BLE 5.0 only |
| Wi-Fi | 2.4 GHz only | 2.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 hereBluetoothScan.cpp— any BLE feature change lands hereplatformio.ini— new boards added; existing board build-flag changesMenuFunctions.cpp— UI-visible additionsconfigs.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
- Marauder repo: https://github.com/justcallmekoko/ESP32Marauder
- Mainline
platformio.ini: https://github.com/justcallmekoko/ESP32Marauder/blob/master/esp32_marauder/platformio.ini - Mainline
MenuFunctions.cpp: https://github.com/justcallmekoko/ESP32Marauder/blob/master/esp32_marauder/MenuFunctions.cpp - Mainline
WiFiScan.cpp: https://github.com/justcallmekoko/ESP32Marauder/blob/master/esp32_marauder/WiFiScan.cpp - Mainline
configs.h: https://github.com/justcallmekoko/ESP32Marauder/blob/master/esp32_marauder/configs.h - Wiki: https://github.com/justcallmekoko/ESP32Marauder/wiki
- GitHub Releases (tags + changelogs): https://github.com/justcallmekoko/ESP32Marauder/releases
Libraries
- TFT_eSPI: https://github.com/Bodmer/TFT_eSPI
- Adafruit GFX: https://github.com/adafruit/Adafruit-GFX-Library
- Adafruit SSD1306 (DSTIKE Watch OLED): https://github.com/adafruit/Adafruit_SSD1306
- NimBLE-Arduino (BLE on S3): https://github.com/h2zero/NimBLE-Arduino
- ESP-IDF Wi-Fi APIs (lower-level): https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/network/esp_wifi.html
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.