Flash ESP32 firmware

draft

Targets

Two product lines share the same repo (nSealr/esp32):

LinePrimary boardStatus
ESP32 USB/NIP-46 signerLILYGO T-Display S3 (no camera)display+button drivers under work
ESP32 Stateless QR VaultT-Display S3 Pro OV5640 (camera+display)pending hardware target

Both build from the same source tree; the target flag selects firmware features.

Prerequisites

  • ESP-IDF v5+ installed and on your PATH. Follow ESP-IDF setup.
  • esptool.py (ships with ESP-IDF; otherwise pip install esptool).
  • A USB-C cable that does data, not just power (cheap charge-only cables are a common pitfall).
  • macOS / Linux workstation. Windows works with WSL2 — pass the USB port through with usbipd-win.

Quick check:

idf.py --version    # → ESP-IDF v5.x.x
esptool.py version  # → esptool.py vX.Y

Clone & build

git clone https://github.com/nSealr/esp32
cd esp32
make setup
make build TARGET=esp32s3-tdisplay

The TARGET flag corresponds to a board profile in boards/. Other profiles: esp32s3-tdisplay-pro-ov5640 (QR vault candidate), esp32-classic-ttgo (compatibility target).

Build outputs land in build/<target>/ — the relevant file is firmware.bin.

Identify the serial port

OSTypical port
macOS/dev/cu.usbserial-XXXX or /dev/cu.usbmodemXXXX
Linux/dev/ttyUSB0 / /dev/ttyACM0

Plug the board in and run ls /dev/cu.* (macOS) or dmesg | tail (Linux) to confirm.

Flash

make flash TARGET=esp32s3-tdisplay PORT=/dev/cu.usbserial-XXXX

This wraps esptool.py with the right offsets. Typical output:

Connecting....
Chip is ESP32-S3 (revision v0.2)
Writing at 0x00010000... (100 %)
Hash of data verified.
Leaving...
Hard resetting via RTS pin...

If Connecting... hangs, hold the BOOT button on the dev board while flashing (or strap GPIO0 to GND for breakouts that don’t have a button).

Smoke evidence

With the firmware flashed, run the companion smoke tests from your workstation:

# 1. Companion ↔ device serial bring-up
nsealr serial-line exchange \
  --port /dev/cu.usbserial-XXXX \
  --request ../specs/vectors/requests/request-v0.json \
  --out response.json
# → request-bound capture checks pass

# 2. Shared fixture verification
nsealr fixture verify --specs ../specs
# → ✓ approval_digest matches reviewed material
# → ✓ deterministic "signing_disabled" decision

You should see these in the firmware protocol evidence and Unicode fallback tracking in nSealr/esp32. They are the smoke evidence — not a production claim.

Recovery

If the firmware bricks the device, drop to bootloader:

esptool.py --port /dev/cu.usbserial-XXXX erase_flash
make flash TARGET=esp32s3-tdisplay PORT=/dev/cu.usbserial-XXXX

Hold BOOT, tap RESET, release BOOT — board is in bootloader mode and will accept any firmware.

Troubleshooting

SymptomLikely causeFix
serial.serialutil.SerialExceptionWrong port or permissionsudo usermod -a -G dialout $USER on Linux; pick the right /dev/cu.* on macOS
Failed to connectCable is charge-only, or BOOT not heldSwap to a data cable; hold BOOT while flashing
Display flickers / wrong colorsSPI clock too high for the panel revisionLower LCD_SPI_FREQ_HZ in boards/<target>/Kconfig
signing_disabled despite gates allegedly passingYou’re on dev firmware; production target not built yetVerify the target — esp32s3-tdisplay is always signing-disabled in this tree
Companion can’t see the deviceSome USB hubs drop the CDC ACM interfacePlug directly into the host

Where to go next

Last updated 2026-05-20