# 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. ![ESP32 Debug Dongle web interface](debug_dongle.png) *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 ```bash pip install platformio # or use the VS Code PlatformIO IDE extension ``` ### 2. Build & upload Pick your environment with `-e`: ```bash 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_mesh` pulls the MeshCore library from a local checkout > (`symlink:///home/scottp/github/MeshCore`) plus RadioLib / Crypto / RTClib / base64 / ed25519. > Adjust that path in `platformio.ini` to 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-DebugDongle` **Password:** `debug1234` → open `http://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 set target baud ~port select the active port ~log on|off SD logging (T3-S3) ~gpio <0|1> drive an arbitrary GPIO ~mesh [on|off] mesh status / toggle echo of mesh msgs to telnet ~psk reprogram the user channel's PSK (16- or 32-byte key) ~chan set channel name + PSK together ~msg 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= 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:///ws` - **Binary frames** — raw target serial data (both directions). - **Text frames** — control/event JSON, prefixed with a `0x00` byte. Events carry a `type`: `status`, `mesh` (a received/sent message), or `meshcfg` (channel/PSK state). Browser → device commands (sent as `0x00` + JSON): ```javascript { "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 -t uploadfs`); use `http://`, not `https`. - **No serial data** — check baud, swap TX/RX, ensure a common ground. - **SD "not found" / CRC errors** — confirm the SD pins, and on `t3s3_mesh` that 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](https://xtermjs.org/) — terminal emulator - [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) — async web server - [ArduinoJson](https://arduinojson.org/) — JSON library by Benoît Blanchon - [MeshCore](https://github.com/meshcore-dev/MeshCore) — LoRa mesh + RadioLib radio drivers