Compare commits
3 Commits
1706186727
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f13b858370 | |||
| 411864dab8 | |||
| f0cd430eb9 |
@@ -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 <n> ~port <int|usb|ext> ~log on|off
|
||||
~gpio <pin> <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(<TZ>)`; 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 <ip> 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.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
*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 <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
|
||||
```
|
||||
|
||||
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=<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 `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:
|
||||
# <link rel="stylesheet" href="/css/xterm.min.css">
|
||||
# <script src="/js/xterm.min.js"></script>
|
||||
# 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 <env> -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
|
||||
|
||||
@@ -242,6 +242,9 @@
|
||||
<option value="921600">921600</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="danger" onclick="doReset()" title="Pulse the target reset line">Reset</button>
|
||||
<button onclick="doButton()" title="Momentary press of the target button / wake line">Button</button>
|
||||
<button id="holdBtn" onclick="toggleHold()" title="Latch the button line held down (force-on) / release">Hold: off</button>
|
||||
<button onclick="clearTerminal()">Clear</button>
|
||||
<button onclick="reconnect()">Reconnect</button>
|
||||
<button id="logBtn" onclick="toggleLog()">Log: --</button>
|
||||
@@ -306,6 +309,7 @@
|
||||
// Terminal setup
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
convertEol: true, // target sends bare '\n'; treat it as CRLF so lines don't staircase
|
||||
fontSize: 14,
|
||||
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||
theme: {
|
||||
@@ -432,6 +436,7 @@
|
||||
updateStatus('btStatus', msg.btConnected);
|
||||
document.getElementById('heap').textContent = `Heap: ${msg.freeHeap}`;
|
||||
updateLogUi(msg);
|
||||
updateButtonUi(msg);
|
||||
|
||||
// Show WiFi info
|
||||
const wifiInfo = document.getElementById('wifiInfo');
|
||||
@@ -591,6 +596,34 @@
|
||||
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
// ---- Target control lines (reset / button) ----
|
||||
function pulseLine(path, label) {
|
||||
fetch(path + '?ms=200').then(r => r.text())
|
||||
.then(t => term.writeln(`\r\n\x1b[33m[${label}] ${t.trim()}\x1b[0m`))
|
||||
.catch(() => term.writeln(`\r\n\x1b[31m[${label} failed]\x1b[0m`));
|
||||
}
|
||||
function doReset() { pulseLine('/api/reset', 'reset'); }
|
||||
function doButton() {
|
||||
fetch('/api/button?ms=200').then(r => r.json()).then(s => {
|
||||
updateButtonUi(s);
|
||||
term.writeln('\r\n\x1b[33m[button] pulsed\x1b[0m');
|
||||
}).catch(() => term.writeln('\r\n\x1b[31m[button failed]\x1b[0m'));
|
||||
}
|
||||
let holdOn = false;
|
||||
function toggleHold() {
|
||||
fetch('/api/button?latch=' + (holdOn ? 'off' : 'on')).then(r => r.json()).then(s => {
|
||||
updateButtonUi(s);
|
||||
term.writeln(`\r\n\x1b[33m[button] hold ${holdOn ? 'ON' : 'off'}\x1b[0m`);
|
||||
}).catch(() => term.writeln('\r\n\x1b[31m[hold failed]\x1b[0m'));
|
||||
}
|
||||
function updateButtonUi(s) {
|
||||
if (typeof s.buttonLatch === 'undefined') return;
|
||||
holdOn = !!s.buttonLatch;
|
||||
const b = document.getElementById('holdBtn');
|
||||
b.textContent = 'Hold: ' + (holdOn ? 'ON' : 'off');
|
||||
b.classList.toggle('danger', holdOn);
|
||||
}
|
||||
|
||||
// ---- SD logging controls ----
|
||||
let logOn = false;
|
||||
function toggleLog() {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
+6
-3
@@ -83,9 +83,12 @@ build_flags =
|
||||
; --- target UART bridge (wire to the sensor's debug UART) ---
|
||||
-DTARGET_RX_PIN=44 ; CONFIRM: dongle RX <- sensor TX
|
||||
-DTARGET_TX_PIN=43 ; CONFIRM: dongle TX -> sensor RX
|
||||
; --- GPIO control lines to the target (reset / wake) ---
|
||||
-DGPIO_RESET_PIN=2 ; CONFIRM: -> sensor RST
|
||||
-DGPIO_WAKE_PIN=1 ; CONFIRM: -> sensor control/wake pin
|
||||
; --- GPIO control lines to the target (reset out / button out) ---
|
||||
; GPIO38 + GPIO39 are free pins on the T3-S3 RIGHT header, right next to the
|
||||
; UART pins TXD=43/RXD=44 -- so reset/button/TX/RX/GND all come off one row.
|
||||
; (GPIO2=SD MISO and GPIO1=battery ADC; GPIO4/12 are not broken out at all.)
|
||||
-DGPIO_RESET_PIN=38 ; -> sensor RST (active-low pulse)
|
||||
-DGPIO_WAKE_PIN=39 ; -> sensor button/wake (active-low pulse)
|
||||
-DGPIO_CTRL_ACTIVE_LOW=1
|
||||
; --- microSD on FSPI/SPI2 (separate bus from LoRa, which uses HSPI/SPI3) ---
|
||||
-DT3S3_SD_SCK=14
|
||||
|
||||
+37
-7
@@ -125,6 +125,9 @@ size_t telnetLineLen[MAX_TELNET] = {0};
|
||||
volatile uint32_t rxBytes = 0; // from target
|
||||
volatile uint32_t txBytes = 0; // to target
|
||||
|
||||
// Button line held active (latched) rather than momentary-pulsed.
|
||||
bool buttonLatched = false;
|
||||
|
||||
// SD logging (T3-S3)
|
||||
bool sdAvailable = false;
|
||||
bool logEnabled = true; // ON by default once we have a date
|
||||
@@ -190,6 +193,17 @@ static void gpioPulse(int pin, uint32_t ms, Stream* reply, const char* name) {
|
||||
if (reply) reply->printf("[ctrl] %s pulsed pin %d for %lu ms\r\n", name, pin, (unsigned long)ms);
|
||||
}
|
||||
|
||||
// Hold a control line active (on) or release it (off) -- a latched "press".
|
||||
static void gpioLatch(int pin, bool on, Stream* reply, const char* name) {
|
||||
if (pin < 0) { if (reply) reply->printf("[ctrl] %s pin not configured\r\n", name); return; }
|
||||
int active = GPIO_CTRL_ACTIVE_LOW ? LOW : HIGH;
|
||||
int inactive = GPIO_CTRL_ACTIVE_LOW ? HIGH : LOW;
|
||||
pinMode(pin, OUTPUT);
|
||||
digitalWrite(pin, on ? active : inactive);
|
||||
if (reply) reply->printf("[ctrl] %s latch=%s (pin %d %s)\r\n",
|
||||
name, on ? "on" : "off", pin, on ? "held" : "released");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SD logging (T3-S3)
|
||||
// ============================================================================
|
||||
@@ -364,7 +378,7 @@ void handleCommand(const char* line, Stream* reply) {
|
||||
const char* args = line[n] ? line + n + 1 : line + n;
|
||||
|
||||
if (!strcmp(verb, "help")) {
|
||||
reply->print("[help] ~status ~reset [ms] ~wake [ms] ~baud <n> "
|
||||
reply->print("[help] ~status ~reset [ms] ~button [ms|on|off] ~baud <n> "
|
||||
"~port <int|usb|ext> ~log on|off ~gpio <pin> <0|1>\r\n"
|
||||
" ~mesh [on|off] ~psk <base64key> ~chan <name> <base64key> ~msg <text>\r\n"
|
||||
" (any non-~ line is sent to the target UART)\r\n");
|
||||
@@ -372,8 +386,10 @@ void handleCommand(const char* line, Stream* reply) {
|
||||
printStatus(reply);
|
||||
} else if (!strcmp(verb, "reset")) {
|
||||
gpioPulse(GPIO_RESET_PIN, (uint32_t)strtoul(args, nullptr, 10), reply, "reset");
|
||||
} else if (!strcmp(verb, "wake")) {
|
||||
gpioPulse(GPIO_WAKE_PIN, (uint32_t)strtoul(args, nullptr, 10), reply, "wake");
|
||||
} else if (!strcmp(verb, "wake") || !strcmp(verb, "button")) {
|
||||
if (!strncmp(args, "on", 2)) { gpioLatch(GPIO_WAKE_PIN, true, reply, "button"); buttonLatched = true; }
|
||||
else if (!strncmp(args, "off", 3)) { gpioLatch(GPIO_WAKE_PIN, false, reply, "button"); buttonLatched = false; }
|
||||
else { gpioPulse(GPIO_WAKE_PIN, (uint32_t)strtoul(args, nullptr, 10), reply, "button"); buttonLatched = false; }
|
||||
} else if (!strcmp(verb, "baud")) {
|
||||
uint32_t b = strtoul(args, nullptr, 10);
|
||||
if (b) { setBaudRate(b); reply->printf("[ctrl] baud=%lu\r\n", (unsigned long)b); }
|
||||
@@ -495,6 +511,7 @@ void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t
|
||||
r["baudSerial1"] = baudSerial1;
|
||||
r["rx"] = rxBytes; r["tx"] = txBytes;
|
||||
r["log"] = logEnabled; r["ntp"] = ntpSynced; r["sd"] = sdAvailable;
|
||||
r["buttonLatch"] = buttonLatched;
|
||||
#if BOARD_T3S3
|
||||
r["logfile"] = logFile ? logPath : "";
|
||||
#endif
|
||||
@@ -539,6 +556,7 @@ static String statusJson() {
|
||||
doc["baudSerial1"] = baudSerial1;
|
||||
doc["rx"] = rxBytes; doc["tx"] = txBytes;
|
||||
doc["log"] = logEnabled; doc["ntp"] = ntpSynced; doc["sd"] = sdAvailable;
|
||||
doc["buttonLatch"] = buttonLatched;
|
||||
#if BOARD_T3S3
|
||||
doc["logfile"] = logFile ? logPath : "";
|
||||
#endif
|
||||
@@ -563,11 +581,23 @@ void setupWebServer() {
|
||||
gpioPulse(GPIO_RESET_PIN, ms, nullptr, "reset");
|
||||
req->send(200, "text/plain", "reset pulsed\n");
|
||||
});
|
||||
server.on("/api/wake", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
// Momentary pulse (?ms=), or latch the line held (?latch=on|off|1|0).
|
||||
auto doButton = [](AsyncWebServerRequest* req) {
|
||||
if (req->hasParam("latch")) {
|
||||
String v = req->getParam("latch")->value();
|
||||
bool on = (v == "1" || v.startsWith("on") || v == "true");
|
||||
gpioLatch(GPIO_WAKE_PIN, on, nullptr, "button");
|
||||
buttonLatched = on;
|
||||
req->send(200, "application/json", statusJson());
|
||||
return;
|
||||
}
|
||||
uint32_t ms = req->hasParam("ms") ? req->getParam("ms")->value().toInt() : 0;
|
||||
gpioPulse(GPIO_WAKE_PIN, ms, nullptr, "wake");
|
||||
req->send(200, "text/plain", "wake pulsed\n");
|
||||
});
|
||||
gpioPulse(GPIO_WAKE_PIN, ms, nullptr, "button");
|
||||
buttonLatched = false;
|
||||
req->send(200, "application/json", statusJson());
|
||||
};
|
||||
server.on("/api/button", HTTP_GET, doButton);
|
||||
server.on("/api/wake", HTTP_GET, doButton); // back-compat alias
|
||||
server.on("/api/baud", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
if (req->hasParam("baud")) setBaudRate(req->getParam("baud")->value().toInt());
|
||||
req->send(200, "application/json", statusJson());
|
||||
|
||||
Reference in New Issue
Block a user