Hardware Developer Documentation: ESP32-S3 Pocket Pentest & Weather Tool
1. Device Overview
This device is a highly integrated, keyring-sized hardware platform designed for cybersecurity (pentesting), network analysis, and environmental monitoring. It is built around the ESP32-S3 Super Mini (FH4R2), utilizing a dual-SPI bus architecture to maintain high-framerate UI rendering while concurrently reading and writing to external modules or SD storage.
1.1 Core Hardware Specifications
- Microcontroller: ESP32-S3 (System-on-Chip)
- Architecture: Xtensa 32-bit LX7 dual-core @ 240 MHz
- Memory: 4 MB embedded QSPI flash + 2 MB integrated QSPI PSRAM (FH4R2 uses quad, not octal, PSRAM)
- Wireless: 2.4 GHz Wi-Fi (802.11 b/g/n) and Bluetooth 5 (LE)
- Display: 0.85-inch IPS LCD (ST7735 / GC9107 controller), 128 × 128 resolution, 65K colors
- Storage: MicroSD card slot (hardware SPI)
- Sensors: AHT20 (temperature/humidity) + BMP280 (temperature/pressure/altitude)
- Indicator LED: Adafruit NeoPixel RGB LED on GPIO 48
- Power: 500 mAh 3.7 V lithium-polymer (LiPo) battery
- Accessories: 7-pin exposed header for power-off-swappable SPI, I2C, and UART modules
Silicon note: Espressif's hardware design guide identifies ESP32-S3FH4R2 as 4 MB in-package quad-SPI flash plus 2 MB in-package quad-SPI PSRAM. GPIO33-37 are consumed by memory only on octal S3R8/S3R8V variants, so the FH4R2 display mapping on GPIO35-37 is valid. See the ESP32-S3 Hardware Design Guidelines.
2. Master GPIO Pin Mapping
The physical wiring has been specifically routed to eliminate bus contention. AI coding agents must adhere strictly to these pin mappings when generating hardware abstraction layers (HALs) or device trees.
2.1 User Inputs (Right Edge)
All buttons are wired active-low (connected to GND). Software must configure these pins with INPUT_PULLUP.
GPIO3 is an ESP32-S3 strapping pin. Do not hold the UP button while powering on or resetting the device; after boot it operates normally as an active-low input.
| Subsystem | Physical Action | ESP32-S3 GPIO | Logic State |
|---|---|---|---|
| D-Pad | UP | GPIO 3 | Active-low (INPUT_PULLUP) |
| D-Pad | DOWN | GPIO 5 | Active-low (INPUT_PULLUP) |
| D-Pad | LEFT | GPIO 4 | Active-low (INPUT_PULLUP) |
| D-Pad | RIGHT | GPIO 6 | Active-low (INPUT_PULLUP) |
| Action | OK / SELECT | GPIO 1 | Active-low (INPUT_PULLUP) |
| Action | BACK | GPIO 2 | Active-low (INPUT_PULLUP) |
2.2 Indicators & Sensors
| Component | Function | ESP32-S3 GPIO | Notes |
|---|---|---|---|
| Status LED | CPU activity / heartbeat | GPIO 45 | Digital output (HIGH = ON). Requires a current-limiting resistor. GPIO45 selects VDD_SPI when that strap is not eFuse-overridden; this 3.3 V-flash design must not be pulled high during reset. An active-high LED wired from GPIO45 through its resistor to ground is the safe orientation. |
| Battery monitor | LiPo terminal voltage | GPIO 7 | ADC input connected to the midpoint of a 100k/100k divider from B+ to B- (2:1 voltage scaling). |
| NeoPixel RGB LED | Addressable status / activity indicator | GPIO 48 | Adafruit NeoPixel data input. Requires the appropriate NeoPixel library and 5 V-compatible power handling. |
| AHT20 + BMP280 | I2C SDA (data) | GPIO 8 | Shared hardware I2C bus. |
| AHT20 + BMP280 | I2C SCL (clock) | GPIO 9 | Shared hardware I2C bus. |
2.3 Display Bus (Isolated Dedicated SPI / HSPI)
The display utilizes the internal surface-mount pads on the back of the ESP32-S3. This isolates UI drawing traffic from the SD card and expansion bus.
| LCD Pin | ESP32-S3 GPIO | Function |
|---|---|---|
| MOSI (DIN) | GPIO 35 | SPI data out |
| SCK (CLK) | GPIO 36 | SPI clock |
| CS | GPIO 37 | Chip select |
| DC | GPIO 38 | Data / command toggle |
| RST | GPIO 39 | Hardware reset |
| BL | GPIO 40 | Backlight control; supports PWM via analogWrite |
2.4 Multi-protocol accessory header
The header exposes the primary hardware SPI bus plus two independently usable GPIOs. Firmware can therefore route SPI, I2C, or UART without assuming every accessory is an SPI radio.
| Bus Signal | ESP32-S3 GPIO | Connected To |
|---|---|---|
| MOSI | GPIO 11 | MicroSD MOSI + expansion header |
| MISO | GPIO 13 | MicroSD MISO + expansion header |
| SCK | GPIO 12 | MicroSD CLK + expansion header |
| CS (SD) | GPIO 10 | MicroSD CS (dedicated) |
| CS (EXT) / GPS RX | GPIO 14 | Expansion chip select or UART receive from module TX |
| AUX / CE / GPS TX | GPIO 15 | Expansion GPIO/IRQ/CE or UART transmit to module RX |
The physical header is shown left-to-right on every Accessories status screen as
15, 14, 12, 13, 11, GND, 3V3; the 3.3 V end is on the screen's right side.
The row beneath it changes with the detected module: nRF24L01 uses
CE, CSN, SCK, MISO, MOSI, GND, VCC, while CC1101 uses
GDO0, CSN, SCK, MISO, MOSI, GND, VCC.
Firmware 3.8 also provides explicit mappings and diagnostics for SX127x LoRa, MFRC522 RFID, JEDEC SPI flash, MAX6675, ADXL345, W5500, BMP/BME280, MPU6050, SSD1306, generic I2C discovery, NMEA GPS modules, and a guarded RGB LED light-response experiment.
- I2C: GPIO15 → module SCL and GPIO14 → module SDA. Module boards must include pull-ups or use external 4.7 kΩ pull-ups to 3.3 V.
- UART/GPS: GPIO14 is ESP RX and connects to module TX; GPIO15 is ESP TX and connects to module RX. The GPS system defaults to 9600 baud, 8-N-1 and matches the tested ATGM336H sketch.
- GPIO14 remains a high-impedance input with a weak pull-up while no SPI accessory is selected, including deep sleep. This prevents electrical contention with a powered GPS module that continues transmitting NMEA.
- GPIO12/13/11 remain unused by I2C/UART accessories, preventing contention with the mounted SD card.
Common-anode RGB LED sensor: connect the red cathode to GPIO11, green
cathode to GPIO12, common anode to GPIO13, and blue cathode to GPIO14. In the
physical header row this appears as NC, BLUE, GREEN, COMMON, RED, NC, NC under
15, 14, 12, 13, 11, GND, 3V3, so jumper wires are required; these leads are
not four adjacent numerical GPIOs. Add a 4.7 kΩ to 10 kΩ series resistor to
each cathode for fault-current protection. Never connect the common anode to
3.3 V in sensor mode.
GPIO11/12/13 are also the microSD SPI signals, and GPIO11-14 are ADC2 channels. The firmware therefore refuses to start the experiment while an SD card is mounted, stops SD polling, pauses Wi-Fi for the complete sensing session, and restores both only after the screen asks the user to disconnect the LED. Do not boot or operate the device with both the LED assembly and an SD card connected. The raw die responses are useful for relative ambient-light and color-change experiments only; LED spectral sensitivity is broad and unequal, so the values are not calibrated lux, RGB colorimetry, or a true spectrometer result.
Only one accessory should assert the external chip select at a time. Always power the device off before changing a module, and never apply 5 V logic to the header.
3. Subsystem Implementation Details
3.1 Display (Adafruit ST7735 / GFX)
-
Driver initialization: The 0.85-inch panel uses the GC9107 controller, which is compatible with the ST7735 driver but requires specific initialization parameters to prevent color inversion and clipping.
-
Mandatory initialization sequence:
tft.initR(INITR_GREENTAB); tft.setRotation(0); tft.invertDisplay(true); // CRITICAL: Required for accurate RGB rendering on this panel -
Backlight:
GPIO 40must be pulledHIGH(or driven via PWM from 0–255) for the screen to be visible.
3.2 MicroSD Card (Hardware SPI)
-
Formatting: Cards must be formatted to FAT32. exFAT and NTFS will fail to mount via the standard
SD.hlibrary. -
Initialization: The SPI bus must be explicitly routed before calling
SD.begin().SPI.begin(12, 13, 11, 10); // SCK, MISO, MOSI, CS if (!SD.begin(10, SPI)) { // Handle failure }
3.3 Environmental Sensors (I2C)
- Addresses:
- AHT20: default address
0x38. - BMP280: usually
0x76or0x77. Firmware must attempt to initialize both addresses to guarantee a connection.
- AHT20: default address
- Logic: The BMP280 temperature reading is typically slightly elevated due to chip self-heating. Software should prioritize the AHT20 for ambient temperature and humidity, using the BMP280 strictly for atmospheric pressure and altitude calculation.
3.4 Power Management
- Battery: 500 mAh LiPo cell directly soldered to the
B+andB-pads. - Charging: Handled by the onboard LTH7 (LTC4054) IC. When plugged in via USB-C, it charges at a regulated ~100 mA.
- Battery sensing: GPIO 7 reads the midpoint of two 100 kOhm resistors placed in series across the battery. Firmware doubles the calibrated ADC millivolts, filters the high-impedance source, maps the result through a LiPo discharge curve, and infers charging from a rapid positive voltage edge. Espressif recommends a 0.1 uF filter capacitor from an ADC pin to ground to improve accuracy; it is particularly useful with this divider's 50 kOhm Thevenin impedance.
- Charging indication limitation: The charger status output is not wired to the MCU, so the lightning icon is an inference. It latches after a rapid rise and clears after a sustained falling edge; load changes can occasionally resemble a plug/unplug event.
- Soft power: Holding BACK for two seconds opens a Power dialog. Power off stops Wi-Fi/BLE, display/backlight, LEDs, SD traffic, and accessory operations before entering ESP32-S3 deep sleep. GPIO 2 is configured as the active-low RTC wake input. GPIO40 and the other power-sensitive outputs use pad hold plus global deep-sleep hold so pull-ups cannot relight the backlight.
- Soft wake: The initial BACK press wakes a minimal gate because deep-sleep hardware can only react to a pin level. Startup proceeds only if BACK remains held for two seconds; an early release re-enters deep sleep. This is not electrical disconnection—the regulator and any accessory powered from
3V3still have leakage or quiescent current. Use the physical battery switch for true zero-power storage. - Voltage levels: Native operating logic is 3.3 V. The 5 V pin should strictly be used for USB power input, not for powering external 5 V modules unless actively tethered to USB.
3.5 NeoPixel RGB LED
- Data pin: GPIO 48.
- Library: Use the Adafruit NeoPixel library or an equivalent addressable-LED driver.
- Power: Confirm the LED’s supply voltage and current requirements before connecting it directly to the board. Set the NeoPixel brightness conservatively when operating from the LiPo battery.
4. Compilation & IDE Configuration
Because the ESP32-S3 utilizes native USB-to-JTAG/CDC for flashing (with no external CP2102/CH340 chip), incorrect compiler settings can cause the device to enter a boot loop and drop the USB COM port.
4.1 Arduino IDE Settings
When flashing firmware, the following configurations are mandatory:
- Board:
ESP32S3 Dev Module - USB Mode:
Hardware CDC and JTAG - USB CDC On Boot:
Enabled— crucial; failure to enable this disables the Serial Monitor and can block future uploads without a manual hardware reset. - Flash Size:
4MB (32Mb) - PSRAM:
QSPI PSRAM(required for the FH4R2's 2 MB integrated quad-SPI RAM)
4.2 PlatformIO Configuration (platformio.ini)
For advanced framework development, use the following environment setup:
[env:esp32-s3-supermini-v2]
platform = espressif32
board = esp32-s3-supermini-fh4r2 ; Local manifest in boards/
framework = arduino
board_build.mcu = esp32s3
board_build.f_cpu = 240000000L
board_build.flash_mode = qio
board_upload.flash_size = 4MB
board_build.partitions = partitions/doomsday_recovery.csv
board_build.arduino.memory_type = qio_qspi ; QIO flash + FH4R2 QSPI PSRAM
build_flags =
-D BOARD_HAS_PSRAM
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
lib_deps =
adafruit/Adafruit ST7735 and ST7789 Library
adafruit/Adafruit GFX Library
adafruit/Adafruit AHTX0
adafruit/Adafruit BMP280 Library
4.3 Phase 3.13 recovery and SD update layout
The 4 MB target cannot hold two copies of the full current firmware. Phase 3.13
therefore uses a 2,424,832-byte main partition at 0x10000 and an independent
655,360-byte recovery partition at 0x260000. The recovery program keeps the
TFT backlight low, mounts the SD card, revalidates the staged image, and writes
main. It selects main only after full image validation, so a power interruption
cannot make a partial main image the selected boot target.
This flash layout must be installed once over USB:
.\tools\install_ota_recovery.ps1 -Port COM5
Substitute the board's actual COM port. Thereafter, Settings → Firmware Update
accepts only a main application .bin from /OTA-Firmware-Update/; never
place the merged factory image or the recovery image in that SD directory.
Changing partition offsets or sizes is a hardware-release migration and must be
reflected in the installer, OTA validator, release images, and maintainer guide.
4.4 Hardware Rescue Procedure
If an upload fails with PermissionError(13, 'Access is denied.') or the board is boot-looping:
- Ensure all Serial Monitors and terminals are completely closed.
- Hold the BOOT button on the physical board.
- Tap the RST button once.
- Release the BOOT button.
- The device is now in factory Download Mode. Re-flash the firmware with
USB CDC On Boot: Enabled.
5. Example Code: Master Hardware Validation Sketch
The following sketch initializes and tests all onboard peripherals—display, SD card, I2C sensors, buttons, and LED—based on the confirmed GPIO mapping above.
#include <SPI.h>
#include <SD.h>
#include <FS.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <Adafruit_AHTX0.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_NeoPixel.h>
// ==========================================
// PIN DEFINITIONS
// ==========================================
// Display Pins (Isolated SPI)
#define TFT_MOSI 35
#define TFT_SCK 36
#define TFT_CS 37
#define TFT_DC 38
#define TFT_RST 39
#define TFT_BL 40
// SD Card Pins (Hardware SPI)
#define SD_MOSI 11
#define SD_MISO 13
#define SD_SCK 12
#define SD_CS 10
// I2C Weather Sensors
#define I2C_SDA 8
#define I2C_SCL 9
// Buttons & LED (Updated Mapping)
#define BTN_UP 3
#define BTN_DOWN 5
#define BTN_LEFT 4
#define BTN_RIGHT 6
#define BTN_OK 1
#define BTN_BACK 2
#define LED_PIN 45
#define BATTERY_ADC_PIN 7
#define NEOPIXEL_PIN 48
#define NEOPIXEL_COUNT 1
// ==========================================
// GLOBAL OBJECTS
// ==========================================
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCK, TFT_RST);
Adafruit_AHTX0 aht;
Adafruit_BMP280 bmp(&Wire);
Adafruit_NeoPixel pixel(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
// State tracking variables
bool sd_ok = false;
bool aht_ok = false;
bool bmp_ok = false;
unsigned long lastSensorRead = 0;
bool ledState = false;
unsigned long lastLedBlink = 0;
String lastButton = "NONE ";
void setup() {
Serial.begin(115200);
// 1. Initialize LED & Buttons
pinMode(LED_PIN, OUTPUT);
pinMode(BATTERY_ADC_PIN, INPUT);
analogReadResolution(12);
analogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(BTN_LEFT, INPUT_PULLUP);
pinMode(BTN_RIGHT, INPUT_PULLUP);
pinMode(BTN_OK, INPUT_PULLUP);
pinMode(BTN_BACK, INPUT_PULLUP);
// Initialize NeoPixel RGB LED
pixel.begin();
pixel.setBrightness(32);
pixel.clear();
pixel.show();
// 2. Initialize Display
pinMode(TFT_BL, OUTPUT);
digitalWrite(TFT_BL, HIGH);
tft.initR(INITR_GREENTAB);
tft.setRotation(0);
tft.invertDisplay(true);
tft.fillScreen(ST77XX_BLACK);
// Draw Static UI Elements
tft.setTextColor(ST77XX_CYAN);
tft.setTextSize(1);
tft.setCursor(20, 5);
tft.print("SYSTEM TEST");
tft.drawLine(0, 15, 128, 15, ST77XX_WHITE);
// 3. Initialize SD Card
SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
if (SD.begin(SD_CS, SPI)) {
sd_ok = true;
}
// 4. Initialize I2C Sensors
Wire.begin(I2C_SDA, I2C_SCL);
if (aht.begin(&Wire)) {
aht_ok = true;
}
if (bmp.begin(0x76) || bmp.begin(0x77)) {
bmp_ok = true;
}
// Draw Initial Status
drawStatus();
}
void loop() {
unsigned long now = millis();
// --- BUTTON TESTING ---
// Using simple digitalRead (LOW means pressed due to INPUT_PULLUP)
bool btnPressed = false;
if (!digitalRead(BTN_UP)) { lastButton = "UP "; btnPressed = true; }
if (!digitalRead(BTN_DOWN)) { lastButton = "DOWN "; btnPressed = true; }
if (!digitalRead(BTN_LEFT)) { lastButton = "LEFT "; btnPressed = true; }
if (!digitalRead(BTN_RIGHT)) { lastButton = "RIGHT "; btnPressed = true; }
if (!digitalRead(BTN_OK)) { lastButton = "OK "; btnPressed = true; }
if (!digitalRead(BTN_BACK)) { lastButton = "BACK "; btnPressed = true; }
if (btnPressed) {
tft.setTextColor(ST77XX_YELLOW, ST77XX_BLACK); // Background color overwrites old text
tft.setCursor(4, 110);
tft.print("Btn: ");
tft.print(lastButton);
}
// --- SENSOR POLLING (Every 2 seconds) ---
if (now - lastSensorRead > 2000) {
lastSensorRead = now;
updateSensors();
}
// --- LED BLINKING (Heartbeat every 500ms) ---
if (now - lastLedBlink > 500) {
lastLedBlink = now;
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
pixel.setPixelColor(0, ledState ? pixel.Color(0, 32, 0) : pixel.Color(0, 0, 0));
pixel.show();
}
}
// ==========================================
// HELPER FUNCTIONS
// ==========================================
void drawStatus() {
tft.setTextSize(1);
// SD Card Status
tft.setCursor(4, 25);
tft.setTextColor(ST77XX_WHITE);
tft.print("SD Card: ");
if (sd_ok) {
tft.setTextColor(ST77XX_GREEN);
tft.print("MOUNTED");
} else {
tft.setTextColor(ST77XX_RED);
tft.print("FAILED ");
}
// AHT20 Status
tft.setCursor(4, 40);
tft.setTextColor(ST77XX_WHITE);
tft.print("AHT20: ");
tft.setTextColor(aht_ok ? ST77XX_GREEN : ST77XX_RED);
tft.print(aht_ok ? "DETECTED" : "MISSING ");
// BMP280 Status
tft.setCursor(4, 55);
tft.setTextColor(ST77XX_WHITE);
tft.print("BMP280: ");
tft.setTextColor(bmp_ok ? ST77XX_GREEN : ST77XX_RED);
tft.print(bmp_ok ? "DETECTED" : "MISSING ");
tft.drawLine(0, 70, 128, 70, ST77XX_WHITE);
}
void updateSensors() {
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK); // Use text background to prevent flicker
if (aht_ok) {
sensors_event_t humidity, temp;
aht.getEvent(&humidity, &temp);
tft.setCursor(4, 78);
tft.print("Temp: ");
tft.print(temp.temperature, 1);
tft.print(" C ");
tft.setCursor(4, 90);
tft.print("Hum: ");
tft.print(humidity.relative_humidity, 1);
tft.print(" % ");
}
if (bmp_ok && !aht_ok) {
// Fallback to BMP temp if AHT is missing
tft.setCursor(4, 78);
tft.print("Temp: ");
tft.print(bmp.readTemperature(), 1);
tft.print(" C ");
}
if (bmp_ok) {
tft.setCursor(4, 102); // Just below humidity
tft.print("Pres: ");
tft.print(bmp.readPressure() / 100.0F, 1);
tft.print(" hPa ");
}
}