ESP32 Marauder · Volume 10

ESP32 Marauder Firmware Volume 10 — Build Toolchain and Custom Development

PlatformIO setup, building from source, adding a custom attack, forking strategy, the web flasher

Contents

SectionTopic
1About this volume
2PlatformIO setup
· 2.1CLI-only path
· 2.2VS Code + PlatformIO extension
· 2.3Cloning the Marauder repo
3Building for a specific board
· 3.1Choosing the env
· 3.2pio run invocation
· 3.3Flashing the built binary
· 3.4Enabling deauth at build time
4The web flasher path
· 4.1How the web flasher works
· 4.2When the web flasher is the right answer
· 4.3When local build is required
5Adding a custom attack
· 5.1Worked example — channel-survey CSV dumper
· 5.2Step 1: declare the menu entry
· 5.3Step 2: define the attack module
· 5.4Step 3: wire into the dispatcher and build
· 5.5Testing the new attack
6Forking and downstream maintenance
· 6.1When to fork vs PR upstream
· 6.2GitHub fork mechanics
· 6.3Rebasing against mainline
· 6.4GPLv3 obligations
7Building your own web flasher
8Releasing a fork — versioning, changelogs, distribution
9Common build errors
10Resources

1. About this volume

Vol 10 is for the user who wants to build Marauder from source — to flash a custom fork, add a feature, or maintain a downstream. The companion volume is Vol 3 (firmware architecture); read Vol 3 first if you haven’t yet — it’s the map of the code you’re about to edit.

Coverage:

  • PlatformIO setup (§ 2)
  • Building for a specific board (§ 3)
  • The web flasher as an alternative for end-users (§ 4)
  • A worked example of adding a custom attack (§ 5)
  • Forking strategy for downstream maintenance (§ 6)
  • Common build errors and their causes (§ 9)

By the end of this volume, the operator should be able to: build mainline from source for the AWOK V3 (or any documented PlatformIO env), enable deauth via build flag, add a new menu entry that fires a custom scan, and maintain that custom version as a personal fork.


2. PlatformIO setup

2.1 CLI-only path

The minimal install:

# Python required (3.9+)
python3 --version

# Install platformio
pip install platformio

# Verify
pio --version  # Should report 6.x.x or newer

For local development on a single-user machine, the global pip install is fine. For multi-user / production setups, a venv or pipx install isolates the toolchain.

# Recommended: pipx for isolated tool install
pipx install platformio

PlatformIO auto-downloads the ESP32 toolchain (espressif32 platform) on first build — typically 200-400 MB on first invocation. This goes to ~/.platformio/.

2.2 VS Code + PlatformIO extension

For an IDE-driven workflow:

  1. Install VS Code from https://code.visualstudio.com/.
  2. Open Extensions panel, search “PlatformIO IDE”, install.
  3. Restart VS Code.
  4. PlatformIO sidebar appears; “Open PlatformIO Home” → browse / clone projects.

The VS Code path adds: integrated build buttons, serial monitor, debugger, library manager UI. For a one-off build the CLI is faster; for ongoing development the IDE is better.

2.3 Cloning the Marauder repo

git clone https://github.com/justcallmekoko/ESP32Marauder.git
cd ESP32Marauder/esp32_marauder    # Note: the build root is the subdir, not the top-level

The build root is esp32_marauder/, not the repository root. This is a long-standing source-tree quirk (see Vol 3 § 2.1). PlatformIO commands must run from esp32_marauder/.


3. Building for a specific board

3.1 Choosing the env

The PlatformIO environments are documented in platformio.ini (Vol 3 § 4.2). Common choices:

Hardware targetEnv nameSoC tier
Marauder v6.1marauder_v6_1ESP32-S3
Marauder Minimarauder_miniESP32-S3
Marauder Dev Board Promarauder_dev_board_proClassic ESP32
Flipper WiFi Devboardmarauder_devboardESP32-S2
LilyGO T-Display-S3 (DIY)t_display_s3ESP32-S3
M5Stack Cardputer ADVcardputer_marauderESP32-S3
DSTIKE Watchdstike_watchClassic ESP32
AWOK Dual Touch V3marauder_awok_v3 (variant)Classic ESP32 × 2

To list available envs:

cd esp32_marauder
pio project config | grep -E "^\[env:"

3.2 pio run invocation

# Compile only
pio run -e marauder_v6_1

# Compile + upload (auto-detects USB port)
pio run -e marauder_v6_1 -t upload

# Specify USB port if auto-detect fails
pio run -e marauder_v6_1 -t upload --upload-port /dev/ttyUSB0

# Clean and rebuild
pio run -e marauder_v6_1 -t clean
pio run -e marauder_v6_1

Build output lands in .pio/build/<env_name>/:

  • firmware.bin — the user app
  • bootloader.bin — bootloader
  • partitions.bin — partition table

For a USB flash, the bootloader + partitions go to addresses 0x0 and 0x8000; firmware goes to 0x10000. PlatformIO handles the addresses automatically with -t upload.

3.3 Flashing the built binary

Three flash paths:

  1. PlatformIO’s -t upload (most common): handles bootloader + partitions + firmware in one go.

  2. esptool.py manually (when troubleshooting):

    esptool.py --chip esp32s3 -p /dev/ttyUSB0 -b 460800 write_flash \
        0x0 .pio/build/marauder_v6_1/bootloader.bin \
        0x8000 .pio/build/marauder_v6_1/partitions.bin \
        0x10000 .pio/build/marauder_v6_1/firmware.bin
  3. Web flasher (§ 4): drop the built .bin files into a web-flasher front-end. Useful for distributing a custom fork.

Bootloader-mode entry varies by board:

  • ESP32-S3 with native USB: hold BOOT button during plug-in, or trigger the firmware’s “Reboot to Bootloader” menu, or let PlatformIO’s auto-reset-via-DTR/RTS do it (the default for most boards).
  • Classic ESP32 with UART bridge (CP210x / CH340): the bridge chip’s DTR/RTS lines drive BOOT and EN; PlatformIO uses them to enter bootloader automatically.

If auto-reset fails (it does on a small set of boards), the manual hold-BOOT-during-plug-in always works.

3.4 Enabling deauth at build time

The most-asked-about build-time customization. Edit platformio.ini, find your env, add -DMARAUDER_DEAUTH=1 to build_flags:

[env:marauder_v6_1]
; ... existing settings ...
build_flags =
    -DBOARD_HAS_PSRAM
    -DARDUINO_USB_CDC_ON_BOOT=1
    -DHAS_SCREEN
    -DHAS_BUTTONS
    -DMARAUDER_V6
    -DMARAUDER_DEAUTH=1                          ; ← ADD THIS LINE
    -DMARAUDER_BEACON_SPAM=1
    -DMARAUDER_EVIL_PORTAL=1
    ; ... rest of build flags ...

Rebuild and flash. The WiFi → Attack menu now shows the Deauth entry.


4. The web flasher path

4.1 How the web flasher works

flasher.marauder.maurersystems.com is a static-hosted web page that uses Web Serial API + esptool-js (github.com/espressif/esptool-js) to flash binaries directly from the browser to an ESP32 connected via USB. No PlatformIO, no toolchain install — just Chrome (or Edge), a USB cable, and the target device.

Mechanism:

  1. The page hosts pre-built firmware.bin / bootloader.bin / partitions.bin for each documented board variant. These are produced by the project maintainers via the standard PlatformIO build (§ 3.2) and uploaded to the page’s static-asset bucket.
  2. User selects a board variant from a dropdown.
  3. Browser uses Web Serial API to connect to the ESP32 USB port.
  4. esptool-js drives the same protocol esptool.py uses — chip identification, flash erase, write, verify.
  5. Done. Take the USB out, the device reboots into the freshly flashed firmware.

Total time: ~2 minutes for a typical board.

4.2 When the web flasher is the right answer

  • First-time users who haven’t installed PlatformIO and don’t want to.
  • Quick firmware refresh between known versions (mainline tag bumps).
  • End-of-life forks where the binaries are static archives.
  • Air-gapped flashing if the web flasher’s content is mirrored locally (see § 7).
  • Fork developers distributing a custom version — host the binaries + the flasher page on the project’s website.

4.3 When local build is required

  • Custom build flags beyond what the web flasher exposes (e.g., enabling MARAUDER_DEAUTH if the hosted binary was built with it off).
  • Custom attacks added via source modification (§ 5).
  • New board envs that aren’t in the maintainer’s build matrix.
  • Bleeding-edge mainline (the web flasher typically lags master by tag releases).
  • Forks whose maintainer doesn’t host a flasher.

For tjscientist’s AWOK V3 specifically: the official web flasher doesn’t list AWOK as a board target; AWOK community ships pre-built binaries on Discord. The local-build path is the alternative — clone, build, flash via PlatformIO or esptool.py.


5. Adding a custom attack

5.1 Worked example — channel-survey CSV dumper

Goal: add a new menu entry WiFi → Sniffer → Channel Survey that scans each 2.4 GHz channel for 5 seconds, counts how many beacons + probe requests are observed per channel, and dumps the per-channel counts to SD as channel_survey_<timestamp>.csv.

The pattern is the 3-file change from Vol 3 § 3.3. Walked end-to-end below.

5.2 Step 1: declare the menu entry

Edit MenuFunctions.h to add the menu entry declaration:

// Add to the existing block of Menu externs
extern Menu wifi_sniffer_menu;          // existing — the parent
extern Menu channel_survey_menu;        // ADD THIS LINE

5.3 Step 2: define the attack module

Create two new files: ChannelSurvey.h and ChannelSurvey.cpp in esp32_marauder/.

ChannelSurvey.h:

#ifndef CHANNEL_SURVEY_H
#define CHANNEL_SURVEY_H

#include <Arduino.h>
#include <SD.h>

class ChannelSurvey {
public:
    void start();
    void loop();
    void stop();

private:
    int current_channel;
    uint32_t channel_start_ms;
    uint32_t beacon_counts[14];      // channels 1-14 (index 0 unused)
    uint32_t probe_counts[14];
    File output_file;
    bool running;

    void advance_channel();
    void write_csv_line(int ch);
};

extern ChannelSurvey channel_survey;

#endif

ChannelSurvey.cpp:

#include "ChannelSurvey.h"
#include "configs.h"
#include "WiFiScan.h"     // reuse the existing promiscuous-mode plumbing

#define DWELL_MS 5000     // 5 seconds per channel

ChannelSurvey channel_survey;

static void promisc_cb(void* buf, wifi_promiscuous_pkt_type_t type) {
    if (type != WIFI_PKT_MGMT) return;
    wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf;
    uint8_t* payload = pkt->payload;
    uint8_t subtype = (payload[0] & 0xF0) >> 4;
    int ch = channel_survey.get_current_channel();   // (you'd add this accessor)
    if (subtype == 8) channel_survey.bump_beacon(ch);
    else if (subtype == 4) channel_survey.bump_probe(ch);
}

void ChannelSurvey::start() {
    // Open output file with timestamp
    char fname[64];
    snprintf(fname, sizeof(fname), "/marauder/channel_survey_%lu.csv", millis());
    output_file = SD.open(fname, FILE_WRITE);
    if (!output_file) return;
    output_file.println("channel,beacons,probes");

    // Reset counts
    memset(beacon_counts, 0, sizeof(beacon_counts));
    memset(probe_counts, 0, sizeof(probe_counts));

    // Configure radio for promiscuous, start at channel 1
    esp_wifi_set_promiscuous(true);
    esp_wifi_set_promiscuous_rx_cb(promisc_cb);
    current_channel = 1;
    esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE);
    channel_start_ms = millis();
    running = true;
}

void ChannelSurvey::loop() {
    if (!running) return;
    if (millis() - channel_start_ms < DWELL_MS) return;

    // Time to advance: record this channel's counts, move to next
    write_csv_line(current_channel);
    current_channel++;
    if (current_channel > 14) {
        // Survey complete
        stop();
        return;
    }
    esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE);
    channel_start_ms = millis();
}

void ChannelSurvey::stop() {
    esp_wifi_set_promiscuous(false);
    if (output_file) {
        output_file.close();
    }
    running = false;
}

void ChannelSurvey::write_csv_line(int ch) {
    if (output_file) {
        output_file.printf("%d,%u,%u\n", ch, beacon_counts[ch], probe_counts[ch]);
        output_file.flush();
    }
}

This is abridged — the real implementation would need to handle the per-pkt callback’s channel access more carefully (the callback fires in IRQ context, can’t call user methods directly), include the extern glue, and integrate with Marauder’s existing channel-management. But the structure is canonical.

5.4 Step 3: wire into the dispatcher and build

Edit MenuFunctions.cpp:

  1. Add an entry to the wifi_sniffer_menu list:

    Menu wifi_sniffer_menu = {
        "WiFi Sniffer",
        {
            { "Probe Request", nullptr, &probe_req_start },
            { "Beacon", nullptr, &beacon_start },
            { "EAPOL / PMKID", nullptr, &eapol_start },
            { "Channel Survey", nullptr, &channel_survey_start },    // ADD
        },
        nullptr,
        &wifi_menu
    };
  2. Add the action function:

    void channel_survey_start() {
        channel_survey.start();
        // Switch UI into "scan running" state
        current_active_module = &channel_survey;   // hook into your dispatcher pattern
    }
  3. Update the dispatcher’s loop() to route to channel_survey.loop() when active.

Edit platformio.ini if your env doesn’t already include ChannelSurvey.cpp in the source list (most envs use src_filter defaults that auto-include all *.cpp — typically no change needed).

Rebuild: pio run -e marauder_v6_1. Flash: pio run -e marauder_v6_1 -t upload.

5.5 Testing the new attack

After flash:

  1. Navigate to WiFi → Sniffer → Channel Survey. Verify the menu entry appears.

  2. Select. Display should show “Channel 1: scanning…” then advance through channels 2-14.

  3. Total scan time: 14 channels × 5 seconds = 70 seconds.

  4. After completion, eject SD and view channel_survey_*.csv on host:

    channel,beacons,probes
    1,42,18
    2,3,0
    3,1,0
    4,0,0
    5,2,1
    6,87,34
    ...

Iterate: tune the IRQ callback timing, the dwell duration, the display feedback. The whole development cycle is “edit + pio run -t upload + test” — typically a 60-90 second loop on modern hardware.


6. Forking and downstream maintenance

6.1 When to fork vs PR upstream

PR upstream when:

  • The change is a bug fix that affects all users.
  • The change adds a new board env (PlatformIO env block + per-board pin mappings).
  • The change improves an existing feature without changing its scope.
  • The change is broadly applicable, well-tested, and you’re willing to maintain it through code review.

Fork when:

  • The change adds a feature mainline has explicitly declined (BLE-spam, AirTag detection — see Vol 7 § 3.2).
  • The change is hardware-specific to a board the upstream doesn’t maintain.
  • The change is part of a larger downstream effort (e.g., Ghost ESP, Bruce).
  • You want full editorial control over the codebase for an extended period.

Discussion before authoring usually saves rework — open an issue or chat in the Marauder Discord before a big PR. Smaller fixes can land directly.

6.2 GitHub fork mechanics

  1. Click “Fork” on github.com/justcallmekoko/ESP32Marauder. Now you have github.com/<your-username>/ESP32Marauder.

  2. Clone your fork locally:

    git clone https://github.com/<your-username>/ESP32Marauder.git
    cd ESP32Marauder
    git remote add upstream https://github.com/justcallmekoko/ESP32Marauder.git
  3. Make changes on a branch (don’t work directly on master — leave master tracking upstream for easy rebase):

    git checkout -b my-feature-branch
    # ... edit code ...
    git commit -am "Add channel-survey feature"
    git push -u origin my-feature-branch
  4. (For PR upstream): open a PR from your fork’s branch to upstream’s master.

6.3 Rebasing against mainline

When mainline ships a new release, pull into your fork:

git fetch upstream
git checkout master
git merge upstream/master --ff-only         # fast-forward your master
git push origin master

# Now rebase your feature branch onto the new master
git checkout my-feature-branch
git rebase master
# Resolve conflicts if any
git push --force-with-lease origin my-feature-branch

The --force-with-lease is safer than --force — it refuses to overwrite remote changes you don’t have locally.

6.4 GPLv3 obligations

Marauder mainline is GPLv3. If you fork and distribute binaries (web flasher hosting, Tindie sales, anything beyond your own personal use), you must:

  1. Make the modified source code available to recipients (or link to it).
  2. License your modifications under GPLv3 (or compatible).
  3. Preserve the original copyright + license notices in source files.
  4. Document the modifications you made.

Distributing only the compiled binary without source is a GPL violation. If you’re shipping commercially, talk to a lawyer; the GPLv3 has substantial enforcement history.

Note that Ghost ESP uses AGPLv3 for some components — even network-served users are entitled to the source. Bruce is similar. The license-by-fork choice matters for compliance.

For tjscientist’s personal use (build, flash, run privately), GPLv3 imposes no obligations. The obligations only attach to distribution.


7. Building your own web flasher

If you’re distributing a custom fork, hosting your own web flasher is a polished end-user path. Mechanics:

  1. Fork esp32_marauder/flasher/ (the Marauder web-flasher repo, or any other esptool-js-based flasher).
  2. Build the production assets (typically npm install && npm run build for the typical flasher repo).
  3. Replace the bundled firmware.bin / bootloader.bin / partitions.bin with your fork’s compiled outputs.
  4. Update the board-selector dropdown to list your supported envs.
  5. Host on GitHub Pages, Vercel, Netlify, or a personal web server (must serve HTTPS — Web Serial API requires secure context).

Total setup time for an experienced web dev: 1-2 hours. Maintenance is per-release rebuild-and-redeploy.

The official Marauder web flasher source is in the main repo’s flasher/ directory (or a separate companion repo, depending on era). Pattern-match against it; you don’t need to start from scratch.


8. Releasing a fork — versioning, changelogs, distribution

For maintained forks (more than one user):

Versioning: use semantic versioning (major.minor.patch). Tag each release with vX.Y.Z:

git tag v1.0.0
git push origin v1.0.0

GitHub auto-creates a Release page for each tag; attach the compiled binaries (firmware.bin + bootloader.bin + partitions.bin per board variant).

Changelog: maintain a CHANGELOG.md in the repo root. Format: keep-a-changelog convention (https://keepachangelog.com). Per release, document Added / Changed / Fixed / Removed.

Binary distribution:

  • GitHub Releases page (per-tag binaries) — the canonical pattern
  • A hosted web flasher for one-click flashing — adds polish
  • A Tindie product page (if selling hardware pre-flashed)
  • Discord / Telegram for support; project-specific channels

Documentation:

  • README.md with quick-start
  • Wiki for deeper docs (per-board build instructions, feature details)
  • Per-feature .md files for complex features

Ghost ESP and Bruce both follow this pattern. Marauder itself follows it. Bad Pinguino is more lightweight (no formal changelog, less-frequent tags).


9. Common build errors

Triage table for build errors that bite operators (in rough order of frequency):

SymptomLikely causeFix
multiple definition of 'tft' linker errorTwo .cpp files instantiate TFT_eSPI; the TFT_eSPI library expects only one instanceVerify only Display.cpp (or equivalent) instantiates TFT_eSPI; remove duplicate from custom code
error: 'X' was not declared in this scopeMissing #include or undefined-symbol from older arduino-esp32Update framework-arduinoespressif32 in PlatformIO; pio platform update espressif32
Build hangs at “compiling” for >10 minAnti-virus scanning the toolchain; or first-time compile downloading depsWait it out for first build; whitelist ~/.platformio in AV
Flash succeeds, device boots into BLACK SCREENTFT_eSPI pin mappings wrong for the boardVerify build_flags TFT_MOSI / TFT_SCLK / TFT_CS / TFT_DC / TFT_RST / TFT_BL match physical board
Flash succeeds, device reboot-loops with “Brownout detector triggered”Power-supply inadequate during TXBetter USB cable, faster charger, or set CONFIG_ESP_BROWNOUT_DET_LVL_SEL_5 to less aggressive in sdkconfig (advanced)
Build OOMs with arm-none-eabi-cc1: out of memoryInsufficient RAM during compile (small VMs, low-RAM machines)Add -j1 to limit parallelism; close other apps
Error: The current upload protocol "esptool" is not supportedPlatformIO platform version conflictpio platform install espressif32 --skip-default-package-sync; pio platform update espressif32
Display works but corrupted colorsWrong color-order setting (TFT_RGB_ORDER)Toggle 0x00 / 0x08 in the build flag
Display works but rotated 90/180/270Wrong rotation settingAdjust TFT_ROT in build flags
ESP32-S3 with OPI PSRAM crashes on bootMissing -DBOARD_HAS_PSRAM + -DCONFIG_SPIRAM_MODE_OCT build flagsAdd both flags; rebuild
SD card not mountingWrong SD pin assignments in build flagsVerify SD_MISO / SD_MOSI / SD_SCK / SD_CS match board
USB Serial output empty on ESP32-S3Missing -DARDUINO_USB_CDC_ON_BOOT=1 flagAdd to build_flags; rebuild

The Marauder Discord is the fastest path for board-specific build issues — the community has hit most of them and the answers are searchable.


10. Resources

Tools

Marauder build references

Libraries used by Marauder

Web flasher hosting

License references

Forward / cross references in this series

  • Firmware architecture (what the code is structured like): Vol 3
  • Per-board build env catalog: Vol 3 § 4.2
  • Per-feature implementation patterns to copy: Vols 4 § 2 + Vol 5 + Vol 6 § 5
  • Operational posture for custom firmware (don’t ship malicious): Vol 11

This is Volume 10 of a twelve-volume series. Next: Vol 11 covers operational posture — detection signatures, regional / legal considerations, power profile, thermal under continuous TX, RF discipline, chain-of-custody, and the cases when not to use Marauder.