How to port SecureGen firmware to a new ESP32 board.
Target audience: Arduino enthusiasts and embedded developers.
- Hardware Requirements
- Architecture Overview
- Step-by-Step: Adding a New Board
- Which
#ifdefto Touch — and Which Not - Porting Checklist
- Common Mistakes & Anti-Patterns
| Requirement | Reason |
|---|---|
| ESP32 or ESP32-S3 SoC | mbedTLS, LittleFS, BLE HID stack |
| SPI or Parallel TFT display | UI rendering (TFT_eSPI / LGFX) |
| ≥ 4 MB Flash | Firmware + LittleFS partition |
| ≥ 2 physical buttons | Navigation + factory reset (both held 5s) |
| Hardware entropy source | mbedTLS CTR_DRBG — available on all ESP32 variants |
| Requirement | Reason |
|---|---|
| 8 MB PSRAM | Large JSON documents, password vault; required for long payloads |
| USB HID support (ESP32-S3) | Type passwords/TOTP codes as keyboard |
| DS3231 RTC module | TOTP in AP/Offline mode without NTP |
- ESP8266 — no hardware entropy, no BLE, insufficient RAM
- ESP32-C3 / C6 single-core — untested, BLE HID stack behaviour unknown
- Boards without a display — UI is display-first, no headless mode
Understanding these layers saves you from breaking security when porting.
┌─────────────────────────────────────────────┐
│ platformio.ini │
│ defines: ARDUINO_LILYGO_T_DISPLAY_S3 │ ← compile-time board flag
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ include/boards/board_*.h │ ← board constants (pins, dims)
│ BOARD_HAS_USB_HID, LCD_POWER_ON_PIN, … │ ← capability flags derived here
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ Source files │
│ Use BOARD_HAS_USB_HID, not raw board flag │ ← feature flags, not board names
└─────────────────────────────────────────────┘
Rule: source files should know about features, not board names.
#ifdef BOARD_HAS_USB_HID ✅ — #ifdef ARDUINO_LILYGO_T_DISPLAY_S3 in business logic
Copy the closest existing header and adapt it:
cp include/boards/board_t_display_s3.h include/boards/board_myboard.hMandatory constants to define:
// include/boards/board_myboard.h
#pragma once
// --- Display ---
#define BOARD_TFT_WIDTH 240 // physical pixels
#define BOARD_TFT_HEIGHT 135
#define BOARD_TFT_BL_PIN 4 // backlight GPIO
// --- Buttons ---
#define BOARD_BTN_TOP 35 // top button GPIO
#define BOARD_BTN_BOTTOM 0 // bottom button GPIO (also deep sleep wake pin)
// --- Power ---
#define LCD_POWER_ON_PIN 10 // -1 if not present
// --- Capability flags ---
#define BOARD_HAS_USB_HID 0 // 1 only for ESP32-S3 with native USB
#define BOARD_HAS_PSRAM 0 // 1 if PSRAM available and initializedAdd a new build environment:
[env:myboard]
platform = espressif32
board = <your_board_id>
build_flags =
${common.build_flags}
-DMYBOARD_FLAG=1 ; your unique identifier
-DSECURE_LAYER_ENABLED=1 ; always keep this
framework = arduinoDo not remove
-DSECURE_LAYER_ENABLED=1.
This enables AES-256-GCM encryption, ECDH key exchange, and tunnel decryption.
Without it the web interface sends all data in plaintext.
In include/boards/board_myboard.h add the detection block, then include it from the main board selector (if one exists), or add detection directly:
// At the top of board_myboard.h
#ifdef MYBOARD_FLAGThe bottom button (GPIO0 on reference boards) wakes the device from deep sleep.
ESP32 and ESP32-S3 use different APIs — this is already abstracted in main.cpp and pin_manager.cpp via ARDUINO_LILYGO_T_DISPLAY_S3. For a new board you add another branch:
#ifdef MYBOARD_FLAG
esp_sleep_enable_ext0_wakeup(GPIO_NUM_X, 0); // adjust GPIO
#elif defined(ARDUINO_LILYGO_T_DISPLAY_S3)
esp_sleep_enable_ext1_wakeup((1ULL << GPIO_NUM_0), ESP_EXT1_WAKEUP_ANY_LOW);
#else
esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);
#endifTip: This pattern repeats 3 times in
main.cpp(:1208, :1533, :1599) and once inpin_manager.cpp(:218). Change all four.
UI layout uses tft->height() and tft->width() dynamically in most places, but some offsets are hardcoded per board. Search for display-specific blocks:
grep -n "ARDUINO_LILYGO_T_DISPLAY_S3" \
src/display_manager.cpp src/splash_manager.cpp src/main.cppAdd your board's geometry in the same #ifdef chain where needed.
If your board has native USB:
// in board_myboard.h
#define BOARD_HAS_USB_HID 1All USB HID code is already guarded by #ifdef BOARD_HAS_USB_HID in web_server.cpp and main.cpp. No other changes needed — the flag does the work.
If your board does not have USB HID:
#define BOARD_HAS_USB_HID 0Zero is enough — do not delete or comment out the #ifdef BOARD_HAS_USB_HID blocks.
Check partitions_16mb.csv matches your flash size. If your board has less than 16 MB flash, create a matching partition table. Minimum recommended:
| Partition | Size |
|---|---|
| app0 | 2 MB |
| app1 | 2 MB |
| littlefs | 8 MB |
LittleFS stores all encrypted data — do not shrink it below 4 MB.
| Location | What it guards | Action |
|---|---|---|
main.cpp — sleep wake pin |
ext0 vs ext1 API | Add #elif defined(MYBOARD_FLAG) |
display_manager.cpp — geometry |
pixel offsets | Add #elif with your dimensions |
splash_manager.cpp — splash size |
image resolution | Add #elif with your resolution |
pin_manager.cpp :146 — star/selector Y |
UI layout | Add #elif with your offsets |
web_server.cpp :3881, :5899, :7650 — board name |
JSON "board" field |
Add #elif with your board name |
| Flag | Reason |
|---|---|
SECURE_LAYER_ENABLED |
Always must be 1 in production. Removing #else branches breaks nothing but removing the flag disables all encryption. |
BOARD_HAS_USB_HID |
Already correct abstraction — set it in board_*.h, never override in source. |
DEBUG_BUILD |
Dev-only. Never define in production platformio.ini. |
ARDUINO_USB_CDC_ON_BOOT |
Low-level ESP-IDF flag, managed by board definition. |
// ✅ Correct — feature flag in source
#ifdef BOARD_HAS_USB_HID
usbHIDManager.typeString(password);
#endif
// ⚠️ Acceptable — board name only for identity/display
#ifdef ARDUINO_LILYGO_T_DISPLAY_S3
doc["board"] = "T-Display S3";
#elif defined(MYBOARD_FLAG)
doc["board"] = "My Board Name";
#else
doc["board"] = "T-Display ESP32";
#endif
// ❌ Wrong — board name used to gate a feature
#ifdef ARDUINO_LILYGO_T_DISPLAY_S3
usbHIDManager.typeString(password); // should be BOARD_HAS_USB_HID instead
#endif- ESP32 or ESP32-S3 SoC confirmed
- Flash ≥ 4 MB (≥ 16 MB recommended)
- Two physical buttons available and mapped
- Display connected and TFT_eSPI / LGFX driver working
- Deep sleep wake pin identified (bottom button GPIO)
- PSRAM available? → set
BOARD_HAS_PSRAM - Native USB? → set
BOARD_HAS_USB_HID
-
include/boards/board_myboard.hcreated with all constants - New environment added to
platformio.iniwith-DSECURE_LAYER_ENABLED=1 - Deep sleep wake pin added in
main.cpp(3 locations) andpin_manager.cpp(1 location) - Display geometry offsets checked in
display_manager.cpp,splash_manager.cpp,main.cpp - Board name added to JSON responses in
web_server.cpp(3 locations) - Factory reset tested (both buttons held 5 seconds on boot)
-
-DSECURE_LAYER_ENABLED=1present in build flags -
-DDEBUG_BUILDabsent from production build flags - First boot creates PIN and generates device key (check serial log)
- Web interface — ECDH key exchange completes (no
KeyExchange failedin console) - AES-256-GCM active — response body unreadable in Wireshark
- CSRF token verified — requests without
X-CSRF-Tokenreturn 403
[INFO] CryptoManager: CTR_DRBG initialized
[INFO] Main: Initializing Secure Layer Manager...
[INFO] Main: Secure Layer Manager initialized successfully
[INFO] WebServer: Server started
If Secure Layer Manager initialized successfully is missing — ECDH will fail and all requests will return 400 Decryption failed.
; WRONG
build_flags = -DDEBUG_BUILD -DSECURE_LAYER_ENABLED=1DEBUG_BUILD exposes test pages and debug endpoints. They are compiled out in production for a reason.
; WRONG — disables AES-GCM, ECDH, tunnel decryption
build_flags = -DBOARD_HAS_USB_HID=0
; without -DSECURE_LAYER_ENABLED=1The web interface will still load but all API responses will be plaintext and all encrypted requests will fail with 400 Decryption failed.
// WRONG
#ifdef ARDUINO_LILYGO_T_DISPLAY_S3
usbHIDManager.begin();
#endif
// CORRECT
#ifdef BOARD_HAS_USB_HID
usbHIDManager.begin();
#endifUsing the board name to gate features means your port won't enable USB HID even if your board supports it.
The deep sleep wake pin configuration repeats in 4 places. Missing even one causes the device to not wake from deep sleep on your board:
main.cpp :1208 — pseudo-sleep path
main.cpp :1533 — auto-lock deep sleep
main.cpp :1599 — manual sleep
pin_manager.cpp :218 — sleep after PIN lockout
If BOARD_TFT_HEIGHT is wrong, PIN entry stars and selector will render outside the screen. Always verify with the actual tft->height() at runtime:
// Temporary debug — remove after verification
LOG_INFO("Board", "Display: " + String(tft->width()) + "x" + String(tft->height()));If you add a new API endpoint during porting, it must be registered in all six locations (see system documentation). Missing even one causes either plaintext exposure in traffic or 400 Decryption failed.
If your port is working and stable, consider contributing it back:
- Add
docs/development/boards/myboard.mdwith hardware pinout and known limitations - Add your board to the build matrix in
platformio.ini - Open a PR with
[port]prefix in the title
Please include in the PR:
- Serial log of successful first boot (PIN creation)
- Confirmation that ECDH key exchange completes in browser console
- Wireshark capture showing encrypted responses (bodies unreadable)
Based on SecureGen firmware audit — May 2026
See also: docs/development/security/SECURITY_OVERVIEW.md, docs/development/ENDPOINTS.md
For internal multi-board development rules, see multi-board.md.