diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 68b1b02..0000000 --- a/PLAN.md +++ /dev/null @@ -1,69 +0,0 @@ -# esp32-debug-dongle → LilyGo T3-S3 debug bridge - -## Why -USB serial to the trough/solartrack sensors drops on every deep sleep, so we -can't watch boots/crashes/wakes. This dongle becomes an always-connected box on -the LAN that bridges a sensor's UART to telnet + a web terminal, controls GPIO -to reset/wake the target, logs to SD, and (Phase 2) sniffs the MeshCore LoRa -mesh — so we can debug remotely and stop flying blind. - -## Decisions (confirmed with user) -1. **Board: LilyGo T3-S3** (ESP32-S3 + SX127x LoRa + SSD1306 128×64 OLED + SD). - Chosen over TTGO for onboard SD and LoRa (mesh monitoring). -2. **Extend `esp32-debug-dongle` in place** (not a new Sh3dNb app). Keep the - existing `esp32dev` env working; add a `t3s3` env. -3. **Both telnet (:23) and the WebSocket terminal.** Telnet is the primary - remote control path (nc/minicom + an agent driving it); GPIO reset/wake and - port/baud are exposed as **both** telnet `~commands` and REST endpoints. -4. **SD logging in Phase 1, ON by default** once NTP (built-in `configTzTime`) - gives today's date — log file named/stamped from real time. -5. **Power monitoring: out of scope.** -6. **Phase 2: MeshCore monitor** over the onboard LoRa (passive RX of mesh - packets, surfaced on telnet/web/SD). - -## Board facts that shape the build -- ESP32-S3 has **no Bluetooth Classic** → `BluetoothSerial`/SerialBT is guarded - behind `HAS_BT_CLASSIC` (esp32dev only). On T3-S3, telnet + WebSocket replace it. -- USB CDC on boot (`-DARDUINO_USB_CDC_ON_BOOT=1`) → UART0 (GPIO43/44) is free for - the target bridge. -- Pins (from `Sh3dNb/apps_oglas/lilygot3s3_basic`): OLED SDA18/SCL17/RST21@0x3C; - LoRa SCK5/MISO3/MOSI6/CS7/RST8/DIO0:9; BTN0. - -## Phase 1 (this build) — extend src/main.cpp + platformio.ini -- **platformio.ini**: keep `[env:esp32dev]` (+ `-DHAS_BT_CLASSIC=1`); add - `[env:t3s3]` (esp32-s3-devkitc-1, USB CDC on boot, huge_app, littlefs, - Adafruit SSD1306+GFX). All pins via build flags (target UART, GPIO reset/wake, - SD, OLED) so they're config, not hardcoded. -- **Target UART bridge**: Serial1 on `TARGET_RX/TX` pins ↔ fan-out to WebSocket - (existing), **telnet clients**, and **SD log**. RX/TX byte+line counters. -- **Telnet server :23** with line protocol: non-`~` lines forward to the target - UART; `~` lines are commands: - `~help ~status ~reset [ms] ~wake [ms] ~baud ~port ~log on|off - ~gpio <0|1>`. Lets an operator (or agent via `nc`) drive a remote board. -- **REST** (mirror of the commands): `/api/status /api/reset /api/wake - /api/baud?baud= /api/log?on= /api/gpio?pin=&val= /api/send?data=`. -- **GPIO control**: configurable `reset`/`wake` pins (active-low option), pulse. -- **NTP + SD logging**: `configTzTime()`; once time syncs, open - `/logs/YYYYMMDD-HHMMSS.log` and append timestamped serial. Logging ON by - default; `~log off` / REST toggles. Graceful no-op if SD absent/pins wrong. -- **OLED**: IP/SSID/RSSI, target pins+baud, RX/TX counts, telnet client count, - logging on/off, NTP synced y/n. - -## Phase 2 — MeshCore monitor (later) -- Bring up the T3-S3 LoRa radio (confirm chip: SX1276 vs SX1262 variant) with - the mesh's PHY (freq/SF/BW/CR matching the sensors), RX-only. -- Decode/forward MeshCore channel frames to telnet/web/SD so we see beacons - (e.g. count trough beacons to confirm the sleep-crash fix) without a sensor. - -## Pins to CONFIRM against the actual board (set as build flags) -- **SD**: SCK/MISO/MOSI/CS for the T3-S3 SD slot (may share the LoRa SPI bus + - own CS — verify). Logging silently disables if wrong. -- **Target UART bridge** RX/TX (default UART0 43/44, free under USB CDC). -- **GPIO reset/wake** pins to whatever lines you wire to the sensor's RST/control. -- **LoRa chip variant** (Phase 2). - -## Verify -- `pio run -e t3s3` compiles; `pio run -e esp32dev` still compiles (BT intact). -- Flash T3-S3: OLED shows IP; `nc 23` streams a wired sensor's UART; - `~reset` reboots it (seen on the same stream); a dated file appears under - `/logs` on the SD and grows. diff --git a/README.md b/README.md index ae15f2a..48422df 100644 --- a/README.md +++ b/README.md @@ -1,241 +1,254 @@ # ESP32 Debug Dongle -A WiFi/Bluetooth serial debugging tool for ESP32. Access serial ports via web browser or Bluetooth terminal. +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-based serial terminal using xterm.js -- **Bluetooth SPP**: Classic Bluetooth serial port for desktop/mobile apps -- **Multi-Port**: Switch between internal debug, USB serial, and external serial -- **Virtual Serial**: Internal loopback for ESP32's own debug output -- **Configurable**: Change baud rates on the fly +- **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 -### Requirements -- ESP32 DevKit v1 (or compatible ESP32 board with Classic Bluetooth) -- **Note**: ESP32-S2, S3, C3 do NOT support Classic Bluetooth SPP +### Pin connections — LilyGo T3-S3 (`t3s3` / `t3s3_mesh`) -### Pin Connections for External Serial (Serial1) +Wire the target to the **right-hand header** — reset, TX, RX, and button are the top pins, with +GND a few pins down: -| ESP32 Pin | Function | Connect To | -|-----------|----------|------------| -| GPIO16 | RX1 | External device TX | -| GPIO17 | TX1 | External device RX | -| GND | Ground | External device GND | +| 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 -# Install PlatformIO CLI (if not already installed) -pip install platformio - -# Or use VS Code with PlatformIO IDE extension +pip install platformio # or use the VS Code PlatformIO IDE extension ``` -### 2. Build and Upload +### 2. Build & upload + +Pick your environment with `-e`: ```bash -# Clone/copy this project cd esp32-debug-dongle -# Build the firmware -pio run +# Firmware +pio run -e t3s3_mesh -t upload # or: -e t3s3 / -e esp32dev -# Upload firmware to ESP32 -pio run -t upload +# Web files (LittleFS) -- needed on first flash and after any data/ change +pio run -e t3s3_mesh -t uploadfs -# Upload web files to LittleFS -pio run -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 -#### Via WiFi (Web Terminal) +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: -1. Connect to WiFi network: `ESP32-DebugDongle` -2. Password: `debug1234` -3. Open browser: `http://192.168.4.1` +- **AP SSID:** `ESP32-DebugDongle` **Password:** `debug1234` → open `http://192.168.4.1` -#### Via Bluetooth +On a successful station join, the device prints its IP on the USB serial monitor and on the OLED: -1. Pair with device: `ESP32-Debug` -2. Use any Bluetooth serial terminal app: - - **Android**: "Serial Bluetooth Terminal" by Kai Morich - - **Windows**: PuTTY (use assigned COM port after pairing) - - **Linux**: `rfcomm connect 0 XX:XX:XX:XX:XX:XX` then use `/dev/rfcomm0` - - **macOS**: Pair in System Preferences, use `/dev/tty.ESP32-Debug` +``` +[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 +### Web interface -The web terminal provides: -- **Port Selection**: Choose between Internal, USB Serial, or External -- **Baud Rate**: Configure serial speed (9600 - 921600) -- **Clear**: Clear terminal screen -- **Reconnect**: Re-establish WebSocket connection +- **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 +### Serial ports -| Port | Description | Use Case | +| Port | Description | Use case | |------|-------------|----------| -| Internal | Virtual loopback buffer | ESP32's own debug output | -| USB Serial | UART0 (USB connection) | Shared with programming | -| External | Serial1 (GPIO16/17) | External device debugging | +| 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 | -### Using Internal Debug Output +### Telnet (port 23) -In your ESP32 code, use the provided helper functions: +Any line is forwarded verbatim to the target UART **unless** it starts with `~`, in which case +it's a dongle command: -```cpp -// Write to internal virtual serial -debugPrint("Sensor value: %d", sensorValue); -debugPrintln("Status: OK"); - -// Or write directly to the loopback stream -internalSerial.println("Debug message"); +``` +~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 ``` -These messages appear when "Internal" port is selected. +### 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 -Edit `src/main.cpp` to change defaults: +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`. -```cpp -// WiFi Access Point -const char* AP_SSID = "ESP32-DebugDongle"; -const char* AP_PASSWORD = "debug1234"; - -// Bluetooth name -const char* BT_NAME = "ESP32-Debug"; - -// Serial1 pins -#define SERIAL1_RX_PIN 16 -#define SERIAL1_TX_PIN 17 - -// Default baud rates -#define DEFAULT_BAUD_SERIAL 115200 -#define DEFAULT_BAUD_SERIAL1 115200 -``` - -## Project Structure +## Project structure ``` esp32-debug-dongle/ -├── platformio.ini # PlatformIO configuration +├── platformio.ini # build envs: esp32dev / t3s3 / t3s3_mesh ├── src/ -│ └── main.cpp # Main ESP32 firmware +│ ├── 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 interface (uploaded to LittleFS) +│ └── index.html # web UI (uploaded to LittleFS) ├── scripts/ -│ └── download_xterm.py # Optional: download xterm.js locally +│ └── download_xterm.py # optional: host xterm.js locally instead of CDN └── README.md ``` -## xterm.js Setup - -The web interface uses xterm.js loaded from CDN. If you need offline operation: - -```bash -# Download files locally -python scripts/download_xterm.py --local - -# Then edit data/index.html to use local paths: -# -# -# etc. -``` - -## WebSocket Protocol - -The WebSocket endpoint is `ws://192.168.4.1/ws` - -### Data Format - -- **Regular serial data**: Raw bytes sent/received directly -- **Commands**: JSON prefixed with `0x00` byte - -### Commands - -```javascript -// Switch serial port -{ "cmd": "setPort", "port": 0 } // 0=Internal, 1=USB, 2=External - -// Set baud rate -{ "cmd": "setBaud", "port": 2, "baud": 115200 } - -// Get status -{ "cmd": "getStatus" } -``` - -### JavaScript Example - -```javascript -const ws = new WebSocket('ws://192.168.4.1/ws'); -ws.binaryType = 'arraybuffer'; - -// Send serial data -ws.send(new TextEncoder().encode('Hello\r\n')); - -// Send command -function sendCommand(cmd) { - const json = JSON.stringify(cmd); - const data = new Uint8Array(json.length + 1); - data[0] = 0x00; - new TextEncoder().encodeInto(json, data.subarray(1)); - ws.send(data); -} - -// Receive data -ws.onmessage = (e) => { - const data = new Uint8Array(e.data); - if (data[0] === 0x00) { - // Command response - const json = JSON.parse(new TextDecoder().decode(data.slice(1))); - console.log('Response:', json); - } else { - // Serial data - console.log('Serial:', new TextDecoder().decode(data)); - } -}; -``` - ## Troubleshooting -### Can't connect to WiFi -- Ensure you're connecting to `ESP32-DebugDongle` network -- Password is `debug1234` (case-sensitive) -- Try resetting the ESP32 - -### Web page won't load -- Make sure you uploaded the filesystem: `pio run -t uploadfs` -- Check serial monitor for errors -- Try `http://192.168.4.1` (not https) - -### Bluetooth won't pair -- Only works on original ESP32 (not S2, S3, C3) -- Delete existing pairing and try again -- Check that Bluetooth is enabled in build flags - -### No serial data -- Verify baud rate matches your device -- Check TX/RX connections (try swapping them) -- Ensure common ground connection - -### Build errors -- Ensure you have the ESP32 board package installed in PlatformIO -- Library dependencies should auto-install on first build +- **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. +MIT License — feel free to use and modify. ## Credits -- [xterm.js](https://xtermjs.org/) - Terminal emulator -- [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) - Async web server -- [ArduinoJson](https://arduinojson.org/) - JSON library by Benoît Blanchon +- [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 diff --git a/debug_dongle.png b/debug_dongle.png new file mode 100644 index 0000000..39945f3 Binary files /dev/null and b/debug_dongle.png differ