ESP32 Debug Dongle
A remote serial-debug bridge for ESP32 targets. It bridges a target device's UART to a web terminal, telnet, and (on the original ESP32) Bluetooth SPP — and adds GPIO reset/button control, NTP-dated SD logging, an OLED status page, and a MeshCore LoRa comms panel for sniffing/sending channel traffic while you debug.
Web UI: serial terminal (left), SD log files (top), and the MeshCore comms panel (right).
Features
- Web terminal — browser serial terminal (xterm.js) over WebSocket
- Telnet (port 23) — primary remote path for
nc/minicom or an agent - Bluetooth SPP — Classic Bluetooth serial, original ESP32 only
- Multi-port — switch between internal-debug loopback, USB serial, and the external target UART
- Target control — pulse reset, pulse/latch a button (wake / force-on) from web, telnet, and REST
- SD logging (T3-S3) — NTP-dated logs of UART and MeshCore traffic, with a self-describing header
- MeshCore panel (T3-S3 + LoRa) — program a channel PSK at runtime, watch received messages, send messages
- OLED status (T3-S3) — IP, WiFi, UART, byte counts, log/SD/NTP state
Build variants
The board and optional features are selected by PlatformIO environment (platformio.ini):
| Env | Board | SD | OLED | MeshCore LoRa | Bluetooth |
|---|---|---|---|---|---|
esp32dev |
generic ESP32 | – | – | – | ✅ Classic SPP |
t3s3 |
LilyGo T3-S3 (ESP32-S3) | ✅ | ✅ | – | – |
t3s3_mesh |
LilyGo T3-S3 (ESP32-S3 + SX1276) | ✅ | ✅ | ✅ | – |
ESP32-S3 has no Classic Bluetooth — telnet + the web terminal replace SerialBT there.
t3s3_mesh extends t3s3 and just adds the radio (-DUSE_MESHCORE=1); everything mesh-related
compiles to no-ops in the other builds, so the firmware and web UI are identical across all three.
Hardware
Pin connections — LilyGo T3-S3 (t3s3 / t3s3_mesh)
Wire the target to the right-hand header — reset, TX, RX, and button are the top pins, with GND a few pins down:
| T3-S3 GPIO | Function | Connect to target |
|---|---|---|
| GPIO38 | Reset out (active-low pulse) | target RST |
| GPIO43 | TX | target RX |
| GPIO44 | RX | target TX |
| GPIO39 | Button out (active-low pulse/latch) | target button / wake |
| GND | Ground | target GND |
Onboard peripherals (already wired on the board, listed for reference): SD card on HSPI
(SCK 14, MISO 2, MOSI 11, CS 13); OLED on I2C (SDA 18, SCL 17); and in t3s3_mesh the SX1276
on FSPI (NSS 7, RST 8, DIO0 9, DIO1 33, SCLK 5, MISO 3, MOSI 6, RXEN 21, TXEN 10). The reset/
button polarity is active-low (GPIO_CTRL_ACTIVE_LOW=1); change it in platformio.ini if your
target is active-high.
Pin connections — generic ESP32 (esp32dev)
External serial defaults to RX=GPIO16, TX=GPIO17, plus a common GND. No SD/OLED/LoRa.
Quick Start
1. Install PlatformIO
pip install platformio # or use the VS Code PlatformIO IDE extension
2. Build & upload
Pick your environment with -e:
cd esp32-debug-dongle
# Firmware
pio run -e t3s3_mesh -t upload # or: -e t3s3 / -e esp32dev
# Web files (LittleFS) -- needed on first flash and after any data/ change
pio run -e t3s3_mesh -t uploadfs
# Serial monitor
pio device monitor
MeshCore note:
t3s3_meshpulls the MeshCore library from a local checkout (symlink:///home/scottp/github/MeshCore) plus RadioLib / Crypto / RTClib / base64 / ed25519. Adjust that path inplatformio.inito wherever your MeshCore checkout lives.
3. Connect
The dongle joins your WiFi in station mode (SSID/password are build flags in platformio.ini).
If it can't join, it falls back to an access point:
- AP SSID:
ESP32-DebugDonglePassword:debug1234→ openhttp://192.168.4.1
On a successful station join, the device prints its IP on the USB serial monitor and on the OLED:
[Ready] http://10.0.1.241 telnet 10.0.1.241 23
Bluetooth (esp32dev only)
Pair with ESP32-Debug, then use any BT serial terminal (Android "Serial Bluetooth Terminal",
Windows PuTTY on the COM port, Linux rfcomm, macOS /dev/tty.ESP32-Debug).
Usage
Web interface
- Port — Internal (debug loopback) / USB Serial / External (target UART)
- Baud — 9600…921600
- Reset / Button — momentary pulse of the target reset / button lines
- Hold — latch the button line held active (force-on) until released
- Clear / Reconnect — terminal + WebSocket
- Log / Files — toggle SD logging and browse/download/delete log files (T3-S3)
- MeshCore panel — program a channel + PSK, view RX/TX messages, send a message
Serial ports
| Port | Description | Use case |
|---|---|---|
| Internal | Virtual loopback buffer | the dongle's own debug output |
| USB Serial | UART0 (shared with USB) | console |
| External | Serial1 (target UART pins) | the device under test |
Telnet (port 23)
Any line is forwarded verbatim to the target UART unless it starts with ~, in which case
it's a dongle command:
~help list commands
~status port/baud/counters/log/ntp/heap
~reset [ms] pulse the reset line (default 200 ms)
~button [ms|on|off] pulse the button line, or latch it on/off (force-on)
~baud <n> set target baud
~port <int|usb|ext> select the active port
~log on|off SD logging (T3-S3)
~gpio <pin> <0|1> drive an arbitrary GPIO
~mesh [on|off] mesh status / toggle echo of mesh msgs to telnet
~psk <base64key> reprogram the user channel's PSK (16- or 32-byte key)
~chan <name> <base64key> set channel name + PSK together
~msg <text> send a message on the user channel
REST API
Mirrors the telnet commands:
/api/status /api/reset?ms= /api/button?ms= | ?latch=on|off
/api/baud?baud= /api/port?port=int|usb|ext
/api/log?on=0|1 /api/gpio?pin=&val= /api/send?data=
/api/logs /api/logfile?name= /api/logdelete?name= (T3-S3)
MeshCore comms panel (t3s3_mesh)
The right-hand panel talks to a MeshCore BaseChatMesh node on the SX1276 radio. It listens on
the well-known Public channel plus one user channel (default SensorsTest). You can:
- Program a new channel name + PSK at runtime (base64-encoded 16- or 32-byte key). The PSK is persisted to NVS, so it survives reboots, and the change is recorded in the SD log.
- Watch received channel messages (sender, text, RSSI/SNR).
- Send a message on the user channel.
The same actions are available over telnet (~psk, ~chan, ~msg, ~mesh). LoRa PHY defaults
are Australia-narrow (916.575 MHz, BW 62.5, SF7, CR8, 20 dBm) — change them in platformio.ini.
SD logging (T3-S3)
Logs are written to NTP-dated files under /logs/ on the SD card. Each file opens with a
self-describing header and captures both the target UART stream and MeshCore RX/TX, timestamped
per line:
# debug-dongle log opened /logs/20260616-142348.log
# uart: port=external baud=115200 rx=44 tx=43
# mesh: up node=dongle channel=SensorsPH psk=<base64> freq=916.575 bw=62.5 sf=7 cr=8 tx=20
[14:24:15] [mesh rx batcave] device=ScottTrailer batt=13.11 ...
[14:26:36] sensor boot #1 ...
This is a low-level debugger, so the channel PSK is logged in plaintext on purpose — the card
carries the key. Browse/download/delete logs from the Files panel or the /api/logs*
endpoints. Toggle with the Log button or ~log on|off.
WebSocket protocol
Endpoint: ws://<device-ip>/ws
- Binary frames — raw target serial data (both directions).
- Text frames — control/event JSON, prefixed with a
0x00byte. Events carry atype:status,mesh(a received/sent message), ormeshcfg(channel/PSK state).
Browser → device commands (sent as 0x00 + JSON):
{ "cmd": "setPort", "port": 2 } // 0=Internal, 1=USB, 2=External
{ "cmd": "setBaud", "baud": 115200 }
{ "cmd": "getStatus" }
{ "cmd": "meshSend", "text": "hello" }
{ "cmd": "meshPsk", "name": "SensorsTest", "psk": "PNtgMxiq9R7eQ3IleHoL3g==" }
{ "cmd": "meshGet" }
Configuration
Most settings are build flags in platformio.ini (per environment): WiFi STA_SSID /
STA_PASSWORD, target UART pins (TARGET_RX_PIN / TARGET_TX_PIN), control pins
(GPIO_RESET_PIN / GPIO_WAKE_PIN / GPIO_CTRL_ACTIVE_LOW), SD/OLED/LoRa pins, the LoRa PHY,
and the default mesh channel (SENSORS_CHANNEL_NAME / SENSORS_CHANNEL_PSK_B64). The AP SSID,
Bluetooth name, and default bauds live near the top of src/main.cpp.
Project structure
esp32-debug-dongle/
├── platformio.ini # build envs: esp32dev / t3s3 / t3s3_mesh
├── src/
│ ├── main.cpp # bridge: UART <-> web/telnet/BT, control, SD log, OLED
│ ├── meshcore_link.{h,cpp} # MeshCore node facade (no-op unless USE_MESHCORE)
│ └── LoopbackStream.{h,cpp} # internal virtual serial
├── data/
│ └── index.html # web UI (uploaded to LittleFS)
├── scripts/
│ └── download_xterm.py # optional: host xterm.js locally instead of CDN
└── README.md
Troubleshooting
- Web page won't load — upload the filesystem (
pio run -e <env> -t uploadfs); usehttp://, nothttps. - No serial data — check baud, swap TX/RX, ensure a common ground.
- SD "not found" / CRC errors — confirm the SD pins, and on
t3s3_meshthat the radio is on FSPI and SD on HSPI (they must be different SPI peripherals). - MeshCore: nothing received — confirm the channel PSK matches the sender's, and the LoRa PHY (freq/BW/SF/CR) matches the fleet.
- Terminal lines "staircase" — already handled (
convertEol); the raw SD log keeps bare\n. - Bluetooth won't pair — original ESP32 only (
esp32dev); not on S3.
License
MIT License — feel free to use and modify.
Credits
- xterm.js — terminal emulator
- ESPAsyncWebServer — async web server
- ArduinoJson — JSON library by Benoît Blanchon
- MeshCore — LoRa mesh + RadioLib radio drivers
