Bruce Firmware · Volume 4
Bruce Firmware Volume 4 — Architecture & the HAL

4.1 About this Volume
This volume is the engineering tour of how Bruce is put together as software: why it runs on dozens of physically different ESP32 boards from one codebase, how a board is described to the firmware, how features are switched on and off at compile time, where the runtime settings live, and how an operator actually talks to a running device. It is the companion to the hardware material in Vol 2 (the supported-device matrix) and Vol 3 (Bruce’s own PCBs); it feeds directly into Vol 5 (build, flash and OTA), because the thing being built in Vol 5 is exactly the per-board artifact described here.
The organising idea is simple and worth stating up front: Bruce treats “which board am I” as a build-time question, not a runtime one. There is no universal binary that probes its surroundings and reconfigures itself. Instead, each supported board is a named build target with its own pin map and its own set of enabled features, and the web flasher (Vol 5) simply hands the browser the right pre-built image for the board selected. Everything in this volume follows from that decision.
The firmware is licensed AGPL-3.0; the hardware designs that accompany it (Vol 3) are CERN-OHL-P-2.0, and third-party components are catalogued in the repository’s THIRD_PARTY.md. The architecture described here is the work of the core contributor @bmorcelli, who authored the “new core” / hardware-abstraction layer and ported many of the devices, building on the original project by pr3y (now hosted under the BruceDevices organisation at BruceDevices/firmware, with pr3y/Bruce redirecting there).
Voice note. This is a reference account of Bruce’s architecture assembled from the official repository, site and wiki. Nothing here is a record of hands-on bring-up on any specific board.
4.2 The “new core” — what the HAL rewrite changed
Early ESP32 “multi-tool” firmwares were, almost without exception, written against one board. The screen driver, the button pins, the SD chip-select, the presence or absence of an RGB LED — all of it was baked into the source. Supporting a second board meant forking the project and editing those constants, which is exactly how the ESP32 firmware landscape ended up with so many near-identical, mutually incompatible forks.
Bruce’s new core is the refactor that broke that pattern. Rather than a formal driver-model HAL in the operating-system sense, it is a disciplined separation between application code (the menus, the attack logic, the interpreter, the file managers) and board description (what pins things are on, what peripherals exist, what the display is). The application code is written against an abstract notion of “the display,” “the buttons,” “the SD slot,” “the RGB LED” and so on; each board supplies the concrete wiring. The practical consequence, stated plainly in the wiki’s porting guide, is that adding a device is adding a board description, not forking the application.
Bruce application layer
(menus · WiFi/BLE/RF/RFID/IR/FM subsystems · JS interpreter ·
file managers · WebUI server · headless command parser)
│
abstract peripheral interface (the "new core")
display │ buttons/keyboard │ SD │ LittleFS │ RGB LED │
battery/fuel-gauge │ speaker │ mic │ external-module pins
│
┌──────────────┬──────────────┬──────────────┬──────────────┐
Cardputer ADV M5StickC CYD-2432S028 T-Embed CC1101 RF REAPER …
(board config: PLUS2 (board config) (board config) (board config)
pins + flags) (board config)
The payoff is visible in the project’s reach. One application codebase drives M5Stack sticks and cores, LILYGO T-Embed / T-Deck / T-Display boards, the cheap “CYD” (Cheap Yellow Display) ESP32-2432S028 family, Elecrow panels, and Bruce’s own PCBs — each with a different screen, a different button layout, and a different peripheral complement — without those differences leaking into the feature code. It is the single biggest reason Bruce is the broadest-hardware entry in this class of firmware (the positioning argument is developed in Vol 1 and Vol 10).
4.3 One board, one PlatformIO environment
Bruce is built with PlatformIO (the Arduino-framework toolchain for ESP32), and the unit of “a board” in PlatformIO terms is an environment (env). Each supported device corresponds to exactly one PlatformIO environment; building that environment produces that board’s flashable image. The set of environments that CI builds in parallel is declared in the repository’s PlatformIO configuration.
; platformio.ini (schematic — the real file aggregates many per-board.ini files)
[platformio]
default_envs =
m5stack-cardputer
m5stack-cardputer-adv
m5stick-c-plus2
CYD-2432S028
; … one entry per supported board …
[env]
platform = espressif32
framework = arduino
monitor_speed = 115200
Two things follow from “one board = one env.” First, the complete list of what Bruce supports is, almost literally, the list of environments — the supported-device matrix in Vol 2 is the human-readable rendering of that list. Second, because each board is a separate build, each board can carry a different feature set (the subject of §5): the environment is where the compile-time switches get set.
Table 1 — 3. One board, one PlatformIO environment
| PlatformIO concept | Bruce meaning |
|---|---|
Environment (env) | One supported board/device |
default_envs | The full build matrix CI compiles each release |
platform = espressif32 | The ESP32 Arduino core (all targets are ESP32-family) |
| Build flags / defines | Per-board capability switches (§5) |
boards/… tree | Per-board pin maps and interface code (§4) |
Build targets therefore map one-to-one onto the Releases page: a release ships one Bruce-<device>.bin per environment, and the web flasher’s board picker is the same list again.
4.4 The board-config file set — pins, flags, interface
Porting (or just understanding) a board means touching a small, well-defined set of files. The wiki’s “Porting to Devices” page enumerates them; they are reproduced here because they are the architecture — this file set is the whole contract between a board and the application.
Table 2 — 4. The board-config file set — pins, flags, interface
| File | Role |
|---|---|
platformio.ini (default_envs) | Register the new environment so it builds |
boards/_boards_json/[board].json | PlatformIO board JSON (flash size, MCU variant, partition defaults) |
boards/pinouts/pins_arduino.h | Pin definitions and the board’s capability flags |
boards/[board]/interface.cpp | Board-specific setup / glue code |
boards/[board].ini | The PlatformIO env block for the device (build flags live here) |
.github/workflows/buil_parallel.yml | Add the env + bootloader offset so CI builds it |
A couple of details are worth drawing out for an engineer reading the tree for the first time:
pins_arduino.hcarries both pins and flags. This is where “this board has an RGB LED on GPIO N” and “this board has no SD slot” live together. The capability flags that gate features (next section) are defined alongside the physical pin numbers, which is why the feature matrix and the wiring are two views of the same file.- The
.iniper board is the feature switchboard. Build flags (-D NAME) set inboards/[board].iniare what turn a generic ESP32-S3 build into, say, a CYD LITE build. A headless, display-less board is handled the same way — the referenceboards/ESP-General/ESP-General.initargetsesp32-s3-devkitc-1and “should work with any headless ESP32-S3 board,” with control deferred to the WebUI or Serial (§8). - The CI workflow filename is literally
buil_parallel.yml(the typo is upstream). Each board’s line also sets the bootloader flash offset, which matters in Vol 5:0x0000for most ESP32-S3/C targets,0x1000for classic ESP32. Flashing an image at the wrong offset is the single most common “it won’t boot” mistake, and the offset is a per-board property precisely because it is declared here.
Design callout — porting is additive. Because a port adds files under
boards/and one line in two config files, it almost never conflicts with unrelated changes. That is what lets a community of contributors land new boards continuously (the project carries thousands of commits and a large fork count) without the application code churning underneath them.
4.5 Compile-time capability flags and the feature matrix
The per-device feature columns in Vol 2’s compatibility matrix — CC1101, NRF24, FM Radio, PN532 (NFC), Mic, BadUSB, RGB LED, Audio/Speaker, Screen, ESP-NOW, Battery/fuel-gauge, Flash, PSRAM, LITE_VERSION — are not runtime probes. They are, for the most part, build-time defines. When the matrix says a board “has CC1101 support,” what that means concretely is that the board’s .ini/pins_arduino.h enables the CC1101 code path and assigns its SPI pins at compile time; a board without it is built without that code at all.
build define set for a board → resulting firmware
───────────────────────────── ──────────────────
(CC1101 pins) + (NRF24 pins) + full sub-GHz + 2.4GHz +
(PN532) + (BadUSB) + (RGB) + … NFC + HID + LED menus present
LITE_VERSION defined → size-reduced build:
Telnet, SSH, WireGuard,
ScanHosts, RawSniffer,
Brucegotchi, BLEBacon,
BLEScan, JS Interpreter
compiled OUT (see §7, Vol 5)
This compile-time gating is what keeps a resource-thin board usable. A device with a small flash partition cannot hold the entire feature set, so the build simply omits features it either can’t fit or has no hardware for. The clearest worked example is LITE_VERSION, carried by the JCZN CYD-2432S028 build (and other Launcher-targeted builds): it is a single define that drops a named list of heavier features to fit under a size ceiling. Vol 5 treats the LITE build in full, including exactly what it removes and why; the point here is architectural — LITE is not a separate fork or a runtime mode, it is one build flag on the same source tree.
Table 3 — 5. Compile-time capability flags and the feature matrix
| Flag class | Example columns | What “present” means |
|---|---|---|
| Peripheral-present | CC1101, NRF24, PN532, Mic, Speaker, RGB LED | The code path for that chip is compiled in and pinned |
| Platform-capacity | PSRAM, Flash size | Larger builds / buffers enabled where the silicon allows |
| HID capability | BadUSB | Native USB-HID (S3/C) vs. limited/absent (classic ESP32, footnoted on Core/CYD/StickC) |
| Build-size profile | LITE_VERSION | A curated set of features compiled out for Launcher fit |
The practical reading rule for the Vol 2 matrix, then, is: a checkmark is a promise the binary for that board contains the feature, not a promise the running firmware will discover it. This is also why two different builds for physically similar boards can differ — the difference is in the defines, not the hardware alone.
4.6 bruce.conf — the runtime configuration file
Everything in §3–§5 is fixed when the image is built. What is mutable at runtime lives in a single file, bruce.conf, a JSON document stored on LittleFS, or on the SD card when one is present (SD takes priority). The firmware creates a default bruce.conf on first boot if none exists, and — importantly for the Launcher story in §7 — the file persists across reboots and across firmware swaps performed by Launcher, so settings survive an OTA update to a different build.
{
"priColor": "a80f",
"secColor": "880f",
"bgColor": "0",
"bright": 100,
"dimmerSet": 10,
"tmz": 0,
"soundEnabled": 1,
"soundVolume": 100,
"wifiAtStartup": 0,
"instantBoot": 0,
"ledBright": 50,
"ledColor": "960064",
"webUI": { "user": "admin", "pwd": "bruce" },
"wifiAp": { "ssid": "BruceNet", "pwd": "brucenet" },
"wifi": {},
"evilWifiNames": [],
"mifareKeys": [],
"startupApp": "",
"disabledMenus": [],
"devMode": 0
}
The documented keys group into a handful of concerns:
Table 4 — The documented keys group into a handful of concerns
| Group | Keys (selected) | Purpose |
|---|---|---|
| Appearance / UI | priColor, secColor, bgColor, themeFile, themeOnSd, colorInverted, bright, dimmerSet | Theme colours, brightness, dimmer, display inversion |
| RGB LED | ledBright, ledColor, ledBlinkEnabled, ledEffect, ledEffectSpeed, ledEffectDirection | LED behaviour on boards that have one |
| Audio / time | soundEnabled, soundVolume, tmz | Beeps, volume, timezone offset for the clock |
| Boot behaviour | wifiAtStartup, instantBoot, startupApp | Bring Wi-Fi up at boot, skip the splash, auto-launch an app (e.g. WebUI) |
| WebUI / AP | webUI (user/pwd), webUISessions, wifiAp (ssid/pwd), wifiMAC, wifi | WebUI credentials, soft-AP identity, saved station creds, MAC override |
| Feature state | evilWifiNames, evilWifiEndpoints, evilWifiPasswordMode, mifareKeys, qrCodes, disabledMenus, badUSBBLEKeyboardLayout, badUSBBLEKeyDelay, wigleBasicToken | Persisted per-feature settings and saved lists |
| Developer | devMode | Developer/diagnostic mode toggle |
Security callout — credentials are stored in cleartext. The default WebUI login is
admin/bruce, and the soft-AP isBruceNet/brucenet. The configuration file stores these, and any saved station passwords, in plain text; the upstream file even carries the note “TODO: Don’t store pwd in cleartext.” Two operational consequences follow: change the WebUI and AP defaults before putting a device on any network that matters, and treat the SD card / LittleFS image as sensitive — a lifted SD card exposes those secrets. This is a posture point, not a feature; it belongs in the operating-discipline discussion in Vol 10 as well.
The disabledMenus array is architecturally interesting: it lets an operator hide menu categories at runtime without rebuilding. It is a soft, cosmetic control — it does not recompile the feature out the way LITE_VERSION does — but it is the one place where “which menus exist” becomes a runtime rather than a build-time question.
4.6.1 Themes and the customizable UI layer
A notable share of bruce.conf is about appearance, and that is deliberate: Bruce exposes a themeable UI layer so the same firmware looks different per operator preference without touching code. The priColor / secColor / bgColor keys are 16-bit RGB565 colour words (hence compact strings like "a80f"), and a full theme can be loaded from a file via themeFile, with themeOnSd selecting whether that theme lives on the SD card or on LittleFS. The wiki’s Customize UI → Themes / Create Theme pages document the theme format.
Architecturally this matters for two reasons. First, it is another instance of the firmware/config split (§7): the look is data on the filesystem, not compiled-in constants, so it survives updates and launcher swaps. Second, it reinforces that the UI is a genuine layer over the new-core display abstraction (§2) rather than a hard-coded screen — the same theme description renders across the many different panels Bruce supports (a 240×135 stick screen, a Cardputer’s wide LCD, a CYD’s 2.8–3.5″ panel) because the application draws against the abstract display, and the board config supplies the panel geometry.
Table 5 — 6.1 Themes and the customizable UI layer
| Appearance key | Meaning |
|---|---|
priColor / secColor / bgColor | Primary / secondary / background colours (RGB565 hex) |
themeFile | Path to a theme file to load, if any |
themeOnSd | Whether the theme file is on SD (1) or LittleFS (0) |
colorInverted | Invert the palette (some panels render inverted) |
bright / dimmerSet | Backlight brightness and auto-dim timeout |
4.6.2 The configuration lifecycle
Pulling §6 and §7 together, the lifecycle of a Bruce device’s state is worth seeing whole:
first boot ──► no bruce.conf found ──► defaults written (SD if present, else LittleFS)
│
▼
operator changes settings (UI / WebUI) ──► bruce.conf updated in place
│
▼
reboot / power cycle ──────────────► settings restored from bruce.conf
│
▼
OTA / Launcher swaps the app ───────► bruce.conf UNTOUCHED (lives outside app partition)
│
▼
erase_flash (manual recovery) ──────► LittleFS cleared; SD-resident bruce.conf survives
The single rule that explains every arrow: configuration lives on a filesystem, the firmware lives in the app partition, and the two have independent lifecycles. That is why settings survive updates, why a launcher can hot-swap Bruce in and out, and why the one way to lose settings is an on-chip erase of a LittleFS-only config (Vol 5 covers the flash/erase mechanics in full).
4.7 Launcher compatibility and why it shapes the architecture
Bruce is built to run as an app under a launcher — specifically Launcher (the app launcher by @bmorcelli, the same author as Bruce’s core) and the M5-world M5Launcher. A launcher is a small resident firmware that lives in flash, lists installable apps, downloads a chosen app’s binary over the air, and flashes it into the app partition. Under that model Bruce is one selectable, OTA-managed app among several.
┌─────────────────────────────────────────────┐
│ Launcher / M5Launcher (resident firmware) │
│ • lists apps • OTA download • flash app │
└───────────────┬─────────────────────────────┘
│ selects + OTA-installs
▼
┌─────────────────────────────────────────────┐
│ Bruce (one app in the launcher's catalog) │
│ must fit the app partition → LITE build │
│ bruce.conf survives the swap (SD/LittleFS) │
└─────────────────────────────────────────────┘
This relationship is the concrete reason the LITE_VERSION build exists (§5, Vol 5). The launcher’s app partition imposes a size ceiling; a full-feature Bruce image can exceed it on some boards. Rather than deny those boards launcher use, Bruce ships a LITE build that compiles out the heaviest features to fit. “For Launcher Compatibility” is the phrase the project uses, and it is the honest explanation: LITE is a size compromise driven by the launcher partition, not a crippled or demo edition.
The persistence design dovetails with this. Because bruce.conf lives on SD/LittleFS rather than in the app partition, swapping Bruce in and out via the launcher — or OTA-updating it to a newer build — does not wipe the operator’s settings. The firmware and the configuration have deliberately separate lifecycles.
4.8 Control surfaces — UI, WebUI, Serial, Headless
A running Bruce exposes four ways to drive it. They are not alternatives bolted on after the fact; the application was structured so the same command surface is reachable through each, which is what makes display-less boards (§4) first-class citizens.
┌───────────── Bruce application (command surface) ─────────────┐
│ │
on-device UI WebUI (browser) Serial Headless Mode
screen + buttons/ http://bruce.local or USB CDC / no screen; driven
keyboard; keyboard device IP; Navigator, UART command entirely via WebUI
shortcuts file mgr, editor, Serial reference Navigator or Serial
Cmd, settings
On-device UI. The native interface: the board’s screen plus whatever input it has (the Cardputer’s QWERTY, a stick’s buttons, a T-Deck’s trackball-and-keyboard). Keyboard shortcuts accelerate navigation on boards with a keyboard. This is the default surface and the one the menu categories (Vols 6–9) are described against.
WebUI. A browser front end served by the device. It is started from the on-device Files → WebUI, then either joins the current network (“My Network”) or raises its own soft-AP (“AP Mode”); the browser then reaches it at http://bruce.local or the IP shown on screen. Default login is admin / bruce (change it — see §6). What it exposes is substantial: a Navigator (a live remote mirror of the device screen, controllable from the PC keyboard), a file manager over both LittleFS and the SD card (upload, download, create, rename, delete, run), a text editor (CTRL+S to save, ALT+ENTER to run on the device), the ability to run .ir / .sub / .js files on the device, a Serial Cmd box that forwards serial commands, and Settings (credentials, overlay toggle, reboot). The WebUI can be set as the startupApp so a headless board comes up straight into it.
Serial. A text command interface over the USB/UART link. It is both a standalone control surface and the back end that the WebUI’s Serial Cmd box talks to — the wiki points to the Serial command reference as the canonical list. This is the surface of choice for scripting a device from a host (Vol 9’s interpreter can also be driven this way) and for automation.
Headless Mode. For boards with no display or buttons (the generic ESP32-S3 devkit profile, custom builds, embedded deployments). The device runs normally but all interaction is via the WebUI Navigator or Serial. Headless is not a reduced feature set — it is the same firmware with the input/output assumption removed, which is exactly the kind of thing the new-core abstraction (§2) makes cheap.
Table 6 — 8. Control surfaces — UI, WebUI, Serial, Headless
| Surface | Needs a screen? | Typical use | Back-end shared with |
|---|---|---|---|
| On-device UI | Yes | Field use, standalone operation | — |
| WebUI | No | Remote control, file transfer, running scripts | Serial (via Serial Cmd) |
| Serial | No | Host automation, scripting, debugging | WebUI Serial Cmd |
| Headless | No (by definition) | Display-less / embedded boards | WebUI + Serial |
4.9 Storage — SD and LittleFS
Bruce uses two filesystems, and the distinction matters for everything from captures to scripts to configuration.
LittleFS is the on-chip flash filesystem — a partition carved out of the ESP32’s own flash. It always exists (no card required), it is modest in size, and it holds the things the firmware needs to find reliably on any board: bruce.conf when no SD is present, themes, small assets, and the WebUI’s on-board file area. Because it is on-chip, it survives a missing or removed SD card.
SD card is the bulk, removable store. When a card is present it takes priority for bruce.conf, and it is where large and operator-generated artifacts live: packet captures (.pcap), sub-GHz captures (.sub), IR files (.ir), JavaScript scripts (.js), wordlists, GPS/wardriving logs, and so on. Both filesystems are exposed through the same file-manager abstraction, so the on-device file browser and the WebUI file manager each present LittleFS and SD side by side.
ESP32 flash
┌───────────────────────────────────────────────┐
│ bootloader │ app partition (Bruce / LITE) │ … │ LittleFS │
└───────────────────────────────────────────────┘ │
├─ bruce.conf (if no SD)
├─ themes / assets
└─ WebUI onboard files
microSD (optional, takes priority)
┌───────────────────────────────────────────────┐
│ bruce.conf · *.pcap · *.sub · *.ir · *.js · │
│ wordlists · wardrive logs · captures │
└───────────────────────────────────────────────┘
The division is deliberate and has a clean rule of thumb: LittleFS for what the firmware must always find; SD for what the operator generates and wants to carry off. It is also why the Launcher persistence story (§7) works — bruce.conf has a home that outlives the app partition on either filesystem.
4.10 External modules and wiring
Several of Bruce’s subsystems depend on chips that are not on every board and must be wired to free GPIOs. The wiki documents these under “External Modules” and “Wiring Diagrams,” and the architecture treats each as an optional, pin-mapped peripheral behind the new-core abstraction (§2): the application asks for “the CC1101,” and the board config says where it is — or that it isn’t present.
Table 7 — 10. External modules and wiring
| External module | Subsystem it enables | Covered in |
|---|---|---|
| CC1101 (sub-GHz transceiver) | RF 433/315/868/915 capture/replay (Vol 8) | Wiring: CYD/ESP32-S3/StickC diagrams |
| NRF24L01+ (2.4 GHz) | NRF24 spectrum/jammer (Vol 7) | Per-board wiring |
| PN532 (NFC reader) | RFID/NFC HF read/clone (Vol 9) | Per-board wiring |
| CH9329 (USB-HID bridge) | BadUSB on boards without native USB-HID (Vol 7/9) | CH9329 USB HID page |
| GPS module | Wardriving location tagging (Vol 6) | GPS page |
| W5500 (Ethernet) | Wired networking (Vol 6) | W5500 page |
| SD card module | Bulk storage (§9) | SD Card Module page |
On Bruce’s own PCBs (Vol 3 — RF REAPER, Bruce PCB V2) these modules are integrated on-board and pre-wired, which is the whole value proposition of those designs; on a bare dev board or a CYD they are soldered or jumpered to the documented pins. Either way, the firmware sees them through the same board-config contract described in §4 — the wiring diagram and the pins_arduino.h entries are the two ends of the same mapping.
4.11 Reference
- Repository:
https://github.com/pr3y/Bruce(=https://github.com/BruceDevices/firmware) —platformio.ini,boards/tree,THIRD_PARTY.md. - Wiki:
https://wiki.bruce.computer— Configuration → bruce.conf; Development → Porting to Devices; Controlling Device → User Interface / WebUI / Serial / Headless Mode / Keyboard Shortcuts; External Modules; Wiring Diagrams. - License: firmware AGPL-3.0; hardware CERN-OHL-P-2.0; third parties in
THIRD_PARTY.md. - Core architecture credit: @bmorcelli (new core / HAL + many device ports), on pr3y’s original project.
- Related volumes: Vol 2 (supported-device matrix — the human-readable env list), Vol 3 (Bruce’s own hardware), Vol 5 (build, flash & OTA, including the LITE build in full), Vols 6–9 (the feature subsystems named in §5 and §10).
- Legal/ethics baseline: see
../_shared/legal_ethics.md— applies to every capability these subsystems expose.
Comments (0)