M5Stack Cardputer ADV · Volume 10
M5Stack Cardputer ADV Volume 10 — Custom Firmware Development
Worked example, source modifications, fork strategy, contribution workflow
Contents
1. About this volume
Vol 10 is for the user who wants to modify firmware, not just use it. Five concrete worked examples — adding a Bruce module, writing a MicroHydra app, modifying M5Launcher, building a custom Wi-Fi scan from scratch, cherry-picking features across forks. Plus fleet-flash patterns, self-hosted catalog setup, and the contribution-upstream workflow.
Cross-reference: this volume parallels ../../../ESP32 Marauder Firmware/03-outputs/ESP32_Marauder_Firmware_Complete.html Vol 10 — the build-system mechanics are identical (both are PlatformIO-based, both target ESP32-S3, both use Arduino framework). The Cardputer-ADV-specific deltas are: M5Cardputer library (not raw arduino-esp32), TCA8418 keyboard scanner (not buttons), ST7789V2 display (not generic TFT), specific GPIO assignments per Vol 3.
2. Adding a custom payload module to Bruce
Goal: add a new attack/utility module to Bruce that’s not in the upstream catalog.
Process:
- Fork Bruce on GitHub:
github.com/BruceDevices/firmware. - Clone locally:
git clone https://github.com/<your-username>/firmware.git. - Open in PlatformIO (VS Code recommended).
- Source tree:
firmware/
├── platformio.ini
├── src/
│ ├── main.cpp
│ ├── core/
│ │ ├── menu.cpp ← top-level menu registry
│ │ └── settings.cpp
│ └── modules/
│ ├── wifi/ ← Wi-Fi attack modules
│ ├── ble/ ← BLE attack modules
│ ├── ir/ ← IR modules
│ │ ├── tv_b_gone.cpp ← template for new IR module
│ │ └── ...
│ ├── subghz/ ← Sub-GHz modules
│ ├── rfid/ ← RFID modules
│ └── badusb/ ← BadUSB modules
├── data/ ← Evil Portal templates, etc.
└── include/ ← headers
- Add a new module: copy
src/modules/ir/tv_b_gone.cpptosrc/modules/ir/apple_tv_off.cpp. Edit the code list — for Apple TV NEC remote (address 0x77E1):
const uint16_t apple_tv_codes[] = {
0x77E1, 0x40, // Address + Power command (NEC 32-bit)
0x77E1, 0x05, // Address + Menu command
0x77E1, 0xA0, // Address + Select command
// ... add more codes as needed
};
- Register in menu: edit
src/core/menu.cpp:
// In the IR submenu definition:
MenuItem ir_menu[] = {
{"TV-B-Gone", &tv_b_gone_start, IR_ICON},
{"AC-B-Gone", &ac_b_gone_start, IR_ICON},
{"Apple TV Off", &apple_tv_off_start, IR_ICON}, // ← NEW ENTRY
// ...
};
- Build:
pio run -e cardputer-adv -t upload. - Test: device boots into Bruce, IR submenu shows new “Apple TV Off” entry.
- PR upstream (if generally useful) or maintain personal fork.
3. Writing your first MicroHydra app
Goal: create a custom MicroPython app that runs under MicroHydra’s app-switcher.
Process:
- Format SD with FAT32. Create
/apps/HelloWorld/directory on SD. - Create
__init__.pyin that directory:
from lib.hydra.app import App
from lib.display import Display
from lib.keyboard import Keyboard
import time
class HelloWorld(App):
name = "Hello World"
icon = "rocket"
description = "My first MicroHydra app"
def __init__(self):
super().__init__()
self.display = Display()
self.keyboard = Keyboard()
self.counter = 0
def main(self):
self.display.clear()
while not self.exit_pressed():
self.display.text(f"Hello! Counter: {self.counter}", 10, 10)
self.counter += 1
# Read keyboard
key = self.keyboard.read()
if key == 'r':
self.counter = 0 # Reset on 'r' key
time.sleep_ms(100)
self.tick() # Let MicroHydra handle background tasks
- Restart MicroHydra: pull power, replace SD, power on.
- Navigate to your app: home screen → scroll to “Hello World” → select.
- App runs: screen shows incrementing counter; pressing ‘r’ resets; pressing Esc exits.
Edit-test loop with mpremote (faster than SD card swap):
# After initial deployment, edit live
mpremote connect /dev/ttyACM0 cp __init__.py :/apps/HelloWorld/__init__.py
mpremote connect /dev/ttyACM0 exec "import machine; machine.reset()"
Iterate in 2-3 seconds. Once the app stabilizes, copy to SD permanently.
4. Modifying M5Launcher itself
Common modifications and their files:
| Mod | File | Change |
|---|---|---|
| Default Wi-Fi credentials | src/settings.cpp | Edit setupSDCard() defaults |
| Custom catalog URL | include/sd_functions.h | Change CATALOG_URL macro |
| Default color theme | src/themes/default_theme.h | Edit RGB565 hex palette (bg / fg / accent / highlight) |
| Add a top-level menu item | src/core/menu.cpp + add icon BMP to src/data/icons/ | Add MenuItem entry + icon |
| Add a Tools submenu entry | src/core/tools.cpp | Add switch-case enum + handler |
| Add a language (i18n) | src/i18n/ | Add xx_XX.h with string table + register in i18n.cpp |
| Disable BLE (save ~80 KB binary) | platformio.ini | Drop -DUSE_BLE flag |
| Boot straight into a specific app | src/main.cpp | Patch the boot_picker function |
Build + flash: pio run -e m5stack-cardputer-adv -t upload. Test. Iterate.
RGB565 color picker: M5Launcher themes use RGB565 16-bit color. Convert RGB888 to RGB565:
def rgb888_to_rgb565(r, g, b):
return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
# Example: orange (#FF6B35) = 0xFB67
print(hex(rgb888_to_rgb565(0xFF, 0x6B, 0x35)))
Online RGB888→RGB565 converters are also fine.
5. Worked example — Wi-Fi channel survey on Cardputer ADV
Goal: write a Cardputer ADV firmware from scratch that scans each 2.4 GHz Wi-Fi channel for 5 seconds, counts beacons + probe requests per channel, dumps the counts to SD as a CSV. Demonstrate the full Arduino + PlatformIO + M5Cardputer development loop.
Setup:
# Create project
mkdir cardputer-channel-survey
cd cardputer-channel-survey
pio init -b esp32-s3-devkitc-1 -d .
# Edit platformio.ini
platformio.ini:
[env:cardputer-adv]
platform = espressif32@6.7.0
board = esp32-s3-devkitc-1
framework = arduino
upload_speed = 1500000
monitor_speed = 115200
board_build.partitions = default_8MB.csv
build_flags =
-DBOARD_HAS_PSRAM=0
-DARDUINO_USB_CDC_ON_BOOT=1
-DARDUINO_USB_MODE=1
lib_deps =
https://github.com/m5stack/M5Cardputer.git
https://github.com/m5stack/M5Unified.git
src/main.cpp:
#include <Arduino.h>
#include <M5Cardputer.h>
#include <SD.h>
#include <esp_wifi.h>
// Per-channel statistics
struct ChannelStats {
uint32_t beacons;
uint32_t probe_requests;
};
ChannelStats stats[15]; // Indices 1-14
volatile int current_channel = 1;
// Promiscuous-mode callback (runs in IRQ context)
void IRAM_ATTR 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 frame_type = (payload[0] >> 2) & 0x03;
uint8_t subtype = (payload[0] >> 4) & 0x0F;
if (frame_type != 0) return; // Not a management frame
int ch = current_channel;
if (ch >= 1 && ch <= 14) {
if (subtype == 8) stats[ch].beacons++; // Beacon
else if (subtype == 4) stats[ch].probe_requests++; // Probe request
}
}
void setup() {
auto cfg = M5.config();
M5Cardputer.begin(cfg, true);
M5Cardputer.Display.fillScreen(BLACK);
M5Cardputer.Display.setTextSize(2);
M5Cardputer.Display.setCursor(10, 10);
M5Cardputer.Display.println("Channel Survey");
M5Cardputer.Display.setTextSize(1);
// Wi-Fi promiscuous mode
nvs_flash_init();
wifi_init_config_t cfg_wifi = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg_wifi);
esp_wifi_set_storage(WIFI_STORAGE_RAM);
esp_wifi_set_mode(WIFI_MODE_NULL);
esp_wifi_start();
esp_wifi_set_promiscuous(true);
esp_wifi_set_promiscuous_rx_cb(&promisc_cb);
// SD
SD.begin(12);
}
void loop() {
// Walk channels 1-14, 5 seconds each
for (int ch = 1; ch <= 14; ch++) {
current_channel = ch;
esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE);
M5Cardputer.Display.setCursor(10, 50);
M5Cardputer.Display.printf("Channel %2d beacons:%6u probes:%6u ",
ch, stats[ch].beacons, stats[ch].probe_requests);
delay(5000);
}
// After full pass, write to SD
char fname[64];
snprintf(fname, sizeof(fname), "/survey_%lu.csv", millis());
File f = SD.open(fname, FILE_WRITE);
if (f) {
f.println("channel,beacons,probe_requests");
for (int ch = 1; ch <= 14; ch++) {
f.printf("%d,%u,%u\n", ch, stats[ch].beacons, stats[ch].probe_requests);
}
f.close();
M5Cardputer.Display.setCursor(10, 100);
M5Cardputer.Display.printf("Wrote %s\n", fname);
} else {
M5Cardputer.Display.setCursor(10, 100);
M5Cardputer.Display.println("SD write failed");
}
delay(60000); // Pause 1 minute before next sweep
}
Build: pio run -e cardputer-adv. ~30 seconds first build (libraries fetched + compiled).
Flash: pio run -e cardputer-adv -t upload. ~10 seconds.
Test:
- Cardputer ADV boots, shows “Channel Survey” on top.
- Counter at bottom updates as the promiscuous callback fires.
- After ~70 seconds (14 channels × 5 sec), a full pass completes.
- SD card now has
/survey_<millis>.csvwith per-channel counts. - Eject SD, view on host:
channel,beacons,probe_requests
1,42,18
2,3,0
3,1,0
4,0,0
5,2,1
6,87,34
7,4,1
8,1,0
9,2,0
10,5,2
11,68,29
12,0,0
13,0,0
14,0,0
(Channels 1, 6, 11 dominate as expected in a US environment.)
Iteration loop: edit main.cpp → pio run -t upload → test. ~60-90 seconds per cycle on modern hardware.
This is the full development loop. The pattern scales to substantially larger firmware (Bruce, Marauder, NEMO are all built this way).
6. Cherry-picking features across forks
Goal: take a feature from one firmware and port it to another.
Example: NEMO is minimal; Bruce has an Apple TV-Off feature you want in NEMO.
Process:
- Locate the feature in Bruce source:
src/modules/ir/apple_tv_off.cpp(and its.hif present). - Identify dependencies: what headers does it include? What M5Cardputer / M5GFX / Arduino APIs does it call?
- Copy
.cpp+.hto NEMO’s source tree at the equivalent path:src/modules/ir/. - Add
#includes at the top of the new file:#include <M5Cardputer.h> // (NEMO uses this too) #include "menu_helper.h" // NEMO's abstraction layer - API differences: Bruce uses
M5Cardputer.Display.*directly. NEMO sometimes usesmenu_helper.habstraction. Search-replaceM5Cardputer.Display.fillScreen→menu_helper.clear_screen(or whatever NEMO’s pattern is). ~80% of changes are mechanical search-replace; ~20% require manual fixup. - Register in NEMO’s menu: edit NEMO’s
src/main.cppor equivalent, add the new module to the menu enum. - Build + test:
pio run -e cardputer-adv -t upload.
Time budget: 30-90 minutes for a typical feature port, depending on dependency depth.
Cross-fork code sharing is a real thing in the Cardputer ADV community — Bruce’s Sour Apple emerged from spec exploration that started in Marauder; NEMO’s BadUSB Hunter inspired similar features elsewhere. Stay legal-compatible (license headers, attribution).
7. Fleet-flash with compile-time defaults
Goal: flash 20+ Cardputer ADVs with the same Wi-Fi credentials + custom catalog URL + theme. Avoid paste-NVS-on-each-boot tedium.
Process:
- Patch M5Launcher’s compile-time defaults: edit
src/settings.cpploadConfigs():
void loadConfigs() {
nvs.begin("launcher", false);
#ifdef FLEET_DEFAULTS
// Pre-populate fields if NVS is empty
if (!nvs.contains("wifi_ssid")) {
nvs.putString("wifi_ssid", "Engagement_AP");
nvs.putString("wifi_pass", "secret_password");
nvs.putString("catalog_url", "https://my-private-catalog.example.com/catalog.json");
}
#endif
// ... existing loadConfigs logic ...
}
- Add the flag to
platformio.ini:
[env:fleet-cardputer-adv]
extends = env:m5stack-cardputer-adv
build_flags =
${env.build_flags}
-DFLEET_DEFAULTS
- Build:
pio run -e fleet-cardputer-adv. Produces a fleet-specific binary. - Flash each device:
pio run -e fleet-cardputer-adv -t upload --upload-port /dev/ttyACM0. Or use M5Burner with the produced.bin.
Result: every fresh device boots with the same defaults. No per-device configuration needed.
8. Self-hosting an M5Launcher catalog
Goal: serve your own M5Launcher catalog from a private URL — fleet distribution without depending on bmorcelli’s catalog (rate limits, customization, internal-tools control).
Catalog schema:
{
"version": 4,
"categories": [
{"id": "pentest", "name": "Pentest", "icon": "skull"},
{"id": "internal", "name": "My Tools", "icon": "wrench"}
],
"firmwares": [
{
"name": "My Custom Bruce",
"author": "tjscientist",
"description": "Bruce with internal tooling",
"icon": "https://my-cdn.example.com/icons/custom-bruce.bmp",
"category": "internal",
"versions": [
{
"tag": "v1.0.0",
"url": "https://my-releases.example.com/bruce-custom-v1.0.0-cardputer-adv.bin",
"sha256": "abc123...",
"size_bytes": 2147483
}
]
}
]
}
Hosting:
- GitHub Pages: free, HTTPS, easy. Put
catalog.jsonin a public repo, enable Pages. - Netlify / Vercel / Cloudflare Pages: same simplicity, custom domain support.
- Private HTTPS server: nginx serving static
catalog.json+ binary files.
M5Launcher configuration:
- M5Launcher → Config → Catalog URL →
https://my-private-catalog.example.com/catalog.json. - Restart Catalog menu — M5Launcher fetches new URL.
- Custom firmwares appear in your private category.
Useful for:
- Fleet distribution (don’t risk bmorcelli’s catalog changing)
- Internal-tools-only firmwares not for public release
- Controlled-update environments (HIPAA / PCI / classified)
- Rate-limit avoidance (your own server has no rate limit)
9. Contributing upstream
Bruce (github.com/BruceDevices/firmware):
- AGPLv3 license — your contributions inherit.
- Active maintainers respond to PRs typically within a week.
- High merge bar for well-described PRs with tests.
- Process: fork → branch → develop → PR → respond to review → merge.
- Discord for design discussion before authoring a big feature.
NEMO (github.com/n0xa/m5stick-nemo):
- MIT license.
- Responsive maintainer.
- Smaller community — less competition for attention; PRs land faster.
M5Launcher (github.com/bmorcelli/Launcher):
- Bmorcelli is selective — discuss in Discord or issue tracker first.
- License: MIT for the launcher core; some bundled apps have different licenses.
MicroHydra, CardputerLoRaChat, Meshtastic:
- Each has its own contribution model. Read CONTRIBUTING.md in the repo first.
General etiquette:
- Sign your commits.
- Match the existing code style.
- Test on Cardputer ADV (not just compile-clean).
- Write descriptive PR titles + bodies.
- Be patient with review feedback.
10. Common build errors
Triage table for the errors that most frequently bite Cardputer ADV firmware development:
| Symptom | Likely cause | Fix |
|---|---|---|
multiple definition of 'tft' linker error | Two .cpp files instantiate M5GFX / TFT_eSPI | Ensure only Display.cpp (or equivalent) instantiates the display |
error: 'X' was not declared in this scope | arduino-esp32 version mismatch | pio platform update espressif32 |
| Build hangs at “compiling” for >10 min | Antivirus scanning ~/.platformio toolchain | Whitelist ~/.platformio in your AV |
| Flash succeeds but BLACK SCREEN on boot | Wrong TFT pin mapping for Cardputer ADV | Verify build flags match Vol 3 § 2 (G33-G38 LCD bus etc.) |
| Reboot loop with “Brownout detector triggered” | Inadequate USB power during boot | Better USB cable; check battery; relax brownout threshold (Vol 11 § 5) |
Build OOMs with arm-none-eabi-cc1: out of memory | Insufficient RAM during compile | 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 in M5GFX init | Toggle M5.Display.setSwapBytes(true/false) |
| Display works but rotated 90/180/270 | Wrong rotation setting | M5.Display.setRotation(N) with N=0-3 |
| SD card not mounting | Wrong SD pin assignment | Verify SD.begin(12) for Cardputer ADV (CS = G12) |
| USB Serial output empty | Missing -DARDUINO_USB_CDC_ON_BOOT=1 build flag | Add to build_flags; rebuild |
Wire and Wire1 confusion | Grove I²C on G1/G2 secondary bus vs primary G8/G9 | Use Wire1.begin(2, 1) for Grove I²C |
| Compile fails on M5Cardputer library | Library version mismatch with arduino-esp32 | Pin specific M5Cardputer version in lib_deps |
The Cardputer Discord is the fastest path for Cardputer-ADV-specific build issues — the community has hit most of them, and answers are searchable.
11. Resources
Repos
- Bruce: https://github.com/BruceDevices/firmware
- NEMO: https://github.com/n0xa/m5stick-nemo
- M5Launcher: https://github.com/bmorcelli/Launcher
- MicroHydra: https://github.com/echo-lalia/MicroHydra
- M5Cardputer library: https://github.com/m5stack/M5Cardputer
- M5Unified: https://github.com/m5stack/M5Unified
Build tools
- PlatformIO docs: https://docs.platformio.org/
- “Keep a Changelog” convention: https://keepachangelog.com/
- Semantic Versioning: https://semver.org/
Cross-references
- Parallel build-toolchain coverage: Marauder Firmware Vol 10 at
../../../ESP32 Marauder Firmware/03-outputs/ESP32_Marauder_Firmware_Complete.html - Programming environment setup: Vol 7
- GPIO assignments for custom code: Vol 3 § 2
- Cardputer Discord (firmware development support): linked from
cardputer.wiki
This is Volume 10 of a twelve-volume series. Next: Vol 11 covers operational posture — detection, regulatory, legal, RF safety, LiPo handling, charging gotchas, when-NOT-to-use.