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
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:
- Install VS Code from https://code.visualstudio.com/.
- Open Extensions panel, search “PlatformIO IDE”, install.
- Restart VS Code.
- 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 target | Env name | SoC tier |
|---|---|---|
| Marauder v6.1 | marauder_v6_1 | ESP32-S3 |
| Marauder Mini | marauder_mini | ESP32-S3 |
| Marauder Dev Board Pro | marauder_dev_board_pro | Classic ESP32 |
| Flipper WiFi Devboard | marauder_devboard | ESP32-S2 |
| LilyGO T-Display-S3 (DIY) | t_display_s3 | ESP32-S3 |
| M5Stack Cardputer ADV | cardputer_marauder | ESP32-S3 |
| DSTIKE Watch | dstike_watch | Classic ESP32 |
| AWOK Dual Touch V3 | marauder_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 appbootloader.bin— bootloaderpartitions.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:
-
PlatformIO’s
-t upload(most common): handles bootloader + partitions + firmware in one go. -
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 -
Web flasher (§ 4): drop the built
.binfiles 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:
- The page hosts pre-built
firmware.bin/bootloader.bin/partitions.binfor 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. - User selects a board variant from a dropdown.
- Browser uses Web Serial API to connect to the ESP32 USB port.
- esptool-js drives the same protocol esptool.py uses — chip identification, flash erase, write, verify.
- 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_DEAUTHif 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:
-
Add an entry to the
wifi_sniffer_menulist: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 }; -
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 } -
Update the dispatcher’s
loop()to route tochannel_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:
-
Navigate to WiFi → Sniffer → Channel Survey. Verify the menu entry appears.
-
Select. Display should show “Channel 1: scanning…” then advance through channels 2-14.
-
Total scan time: 14 channels × 5 seconds = 70 seconds.
-
After completion, eject SD and view
channel_survey_*.csvon 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
-
Click “Fork” on
github.com/justcallmekoko/ESP32Marauder. Now you havegithub.com/<your-username>/ESP32Marauder. -
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 -
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 -
(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:
- Make the modified source code available to recipients (or link to it).
- License your modifications under GPLv3 (or compatible).
- Preserve the original copyright + license notices in source files.
- 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:
- Fork
esp32_marauder/flasher/(the Marauder web-flasher repo, or any other esptool-js-based flasher). - Build the production assets (typically
npm install && npm run buildfor the typical flasher repo). - Replace the bundled
firmware.bin/bootloader.bin/partitions.binwith your fork’s compiled outputs. - Update the board-selector dropdown to list your supported envs.
- 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):
| Symptom | Likely cause | Fix |
|---|---|---|
multiple definition of 'tft' linker error | Two .cpp files instantiate TFT_eSPI; the TFT_eSPI library expects only one instance | Verify only Display.cpp (or equivalent) instantiates TFT_eSPI; remove duplicate from custom code |
error: 'X' was not declared in this scope | Missing #include or undefined-symbol from older arduino-esp32 | Update framework-arduinoespressif32 in PlatformIO; pio platform update espressif32 |
| Build hangs at “compiling” for >10 min | Anti-virus scanning the toolchain; or first-time compile downloading deps | Wait it out for first build; whitelist ~/.platformio in AV |
| Flash succeeds, device boots into BLACK SCREEN | TFT_eSPI pin mappings wrong for the board | Verify 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 TX | Better 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 memory | Insufficient RAM during compile (small VMs, low-RAM machines) | Add -j1 to limit parallelism; close other apps |
Error: The current upload protocol "esptool" is not supported | PlatformIO platform version conflict | pio platform install espressif32 --skip-default-package-sync; pio platform update espressif32 |
| Display works but corrupted colors | Wrong color-order setting (TFT_RGB_ORDER) | Toggle 0x00 / 0x08 in the build flag |
| Display works but rotated 90/180/270 | Wrong rotation setting | Adjust TFT_ROT in build flags |
| ESP32-S3 with OPI PSRAM crashes on boot | Missing -DBOARD_HAS_PSRAM + -DCONFIG_SPIRAM_MODE_OCT build flags | Add both flags; rebuild |
| SD card not mounting | Wrong SD pin assignments in build flags | Verify SD_MISO / SD_MOSI / SD_SCK / SD_CS match board |
| USB Serial output empty on ESP32-S3 | Missing -DARDUINO_USB_CDC_ON_BOOT=1 flag | Add 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
- PlatformIO: https://platformio.org/
- PlatformIO docs: https://docs.platformio.org/
- VS Code: https://code.visualstudio.com/
- esptool.py: https://github.com/espressif/esptool
- esptool-js (web flasher engine): https://github.com/espressif/esptool-js
Marauder build references
- Marauder repo: https://github.com/justcallmekoko/ESP32Marauder
- platformio.ini (canonical per-board env source): https://github.com/justcallmekoko/ESP32Marauder/blob/master/esp32_marauder/platformio.ini
- Marauder wiki — build from source: https://github.com/justcallmekoko/ESP32Marauder/wiki
Libraries used by Marauder
- TFT_eSPI: https://github.com/Bodmer/TFT_eSPI
- Adafruit GFX: https://github.com/adafruit/Adafruit-GFX-Library
- NimBLE-Arduino (BLE on S3): https://github.com/h2zero/NimBLE-Arduino
- Arduino-ESP32 core: https://github.com/espressif/arduino-esp32
Web flasher hosting
- GitHub Pages: https://pages.github.com/
- Netlify: https://www.netlify.com/
- Vercel: https://vercel.com/
License references
- GPLv3 full text: https://www.gnu.org/licenses/gpl-3.0.en.html
- “Keep a Changelog”: https://keepachangelog.com/
- Semantic Versioning: https://semver.org/
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.