Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25ff9b1987 |
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
title: "esp32-debug-dongle"
|
|
||||||
source: pka-assess
|
|
||||||
---
|
|
||||||
|
|
||||||
# esp32-debug-dongle
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
ESP32 dev-kit firmware turning the chip into a WiFi + Bluetooth serial-terminal dongle. Browser-based terminal uses xterm.js served from the ESP's LittleFS; also exposes a classic Bluetooth SPP port for desktop/mobile terminal apps. Multi-port (internal debug / USB / external Serial1) with on-the-fly baud-rate switching.
|
|
||||||
|
|
||||||
## Type
|
|
||||||
|
|
||||||
ESP32 PlatformIO firmware. `#embedded`, `#firmware`, `#iot`, `#service`.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- **External:** PlatformIO `espressif32` + Arduino framework, xterm.js (bundled in LittleFS), Bluetooth SPP stack (classic BT — needs a WROOM-style ESP32, NOT S2/S3/C3).
|
|
||||||
- **Internal:** complementary tool to ESP32 Sh3d projects — [[Sh3dNb]], [[Sh3dController]], [[Sh3dStick]], [[Doxy]] — when serial access is awkward.
|
|
||||||
|
|
||||||
## Notable
|
|
||||||
|
|
||||||
- Uses a `huge_app.csv` partition (3 MB app, 1 MB FS, no OTA) — swap to `min_spiffs.csv` if OTA is needed.
|
|
||||||
- Classic Bluetooth requirement is a hard constraint on target hardware.
|
|
||||||
@@ -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.
|
|
||||||
@@ -9,6 +9,7 @@ A WiFi/Bluetooth serial debugging tool for ESP32. Access serial ports via web br
|
|||||||
- **Multi-Port**: Switch between internal debug, USB serial, and external serial
|
- **Multi-Port**: Switch between internal debug, USB serial, and external serial
|
||||||
- **Virtual Serial**: Internal loopback for ESP32's own debug output
|
- **Virtual Serial**: Internal loopback for ESP32's own debug output
|
||||||
- **Configurable**: Change baud rates on the fly
|
- **Configurable**: Change baud rates on the fly
|
||||||
|
- **Robust Server**: Uses PsychicHttp - stable under load (unlike ESPAsyncWebServer)
|
||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
|
|
||||||
@@ -53,8 +54,19 @@ pio run -t uploadfs
|
|||||||
|
|
||||||
### 3. Connect
|
### 3. Connect
|
||||||
|
|
||||||
#### Via WiFi (Web Terminal)
|
#### Via WiFi (Station Mode - Default)
|
||||||
|
|
||||||
|
1. Edit `src/main.cpp` and set your WiFi credentials:
|
||||||
|
```cpp
|
||||||
|
const char* STA_SSID = "YourNetworkSSID";
|
||||||
|
const char* STA_PASSWORD = "YourPassword";
|
||||||
|
```
|
||||||
|
2. Upload and check serial monitor for the assigned IP address
|
||||||
|
3. Open browser: `http://<ip-address>`
|
||||||
|
|
||||||
|
#### Via WiFi (AP Mode Fallback)
|
||||||
|
|
||||||
|
If station mode fails, or you set `WIFI_STATION_MODE false`:
|
||||||
1. Connect to WiFi network: `ESP32-DebugDongle`
|
1. Connect to WiFi network: `ESP32-DebugDongle`
|
||||||
2. Password: `debug1234`
|
2. Password: `debug1234`
|
||||||
3. Open browser: `http://192.168.4.1`
|
3. Open browser: `http://192.168.4.1`
|
||||||
@@ -106,10 +118,20 @@ These messages appear when "Internal" port is selected.
|
|||||||
Edit `src/main.cpp` to change defaults:
|
Edit `src/main.cpp` to change defaults:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// WiFi Access Point
|
// WiFi Mode: true = connect to existing network, false = create AP
|
||||||
|
#define WIFI_STATION_MODE true
|
||||||
|
|
||||||
|
// Your WiFi network credentials (station mode)
|
||||||
|
const char* STA_SSID = "YourNetworkSSID";
|
||||||
|
const char* STA_PASSWORD = "YourPassword";
|
||||||
|
|
||||||
|
// Fallback Access Point settings
|
||||||
const char* AP_SSID = "ESP32-DebugDongle";
|
const char* AP_SSID = "ESP32-DebugDongle";
|
||||||
const char* AP_PASSWORD = "debug1234";
|
const char* AP_PASSWORD = "debug1234";
|
||||||
|
|
||||||
|
// Connection timeout before falling back to AP mode
|
||||||
|
#define WIFI_CONNECT_TIMEOUT 15000
|
||||||
|
|
||||||
// Bluetooth name
|
// Bluetooth name
|
||||||
const char* BT_NAME = "ESP32-Debug";
|
const char* BT_NAME = "ESP32-Debug";
|
||||||
|
|
||||||
@@ -122,6 +144,18 @@ const char* BT_NAME = "ESP32-Debug";
|
|||||||
#define DEFAULT_BAUD_SERIAL1 115200
|
#define DEFAULT_BAUD_SERIAL1 115200
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### WiFi Modes
|
||||||
|
|
||||||
|
**Station Mode** (`WIFI_STATION_MODE true`):
|
||||||
|
- Connects to your existing WiFi network
|
||||||
|
- Access the dongle from any device on the same network
|
||||||
|
- Falls back to AP mode if connection fails
|
||||||
|
|
||||||
|
**Access Point Mode** (`WIFI_STATION_MODE false`):
|
||||||
|
- Creates its own WiFi network
|
||||||
|
- Connect directly to the ESP32's network
|
||||||
|
- IP address: 192.168.4.1
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -237,5 +271,5 @@ MIT License - Feel free to use and modify.
|
|||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- [xterm.js](https://xtermjs.org/) - Terminal emulator
|
- [xterm.js](https://xtermjs.org/) - Terminal emulator
|
||||||
- [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) - Async web server
|
- [PsychicHttp](https://github.com/hoeken/PsychicHttp) - Robust HTTP/WebSocket server for ESP32
|
||||||
- [ArduinoJson](https://arduinojson.org/) - JSON library by Benoît Blanchon
|
- [ArduinoJson](https://arduinojson.org/) - JSON library by Benoît Blanchon
|
||||||
|
|||||||
@@ -167,8 +167,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<button onclick="clearTerminal()">Clear</button>
|
<button onclick="clearTerminal()">Clear</button>
|
||||||
<button onclick="reconnect()">Reconnect</button>
|
<button onclick="reconnect()">Reconnect</button>
|
||||||
<button id="logBtn" onclick="toggleLog()">Log: --</button>
|
|
||||||
<button onclick="toggleFiles()">Files</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
@@ -186,14 +184,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="filesPanel" style="display:none; background:#16213e; border-bottom:1px solid #0f3460; padding:10px 20px; max-height:40vh; overflow:auto;">
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
|
||||||
<strong>SD log files</strong>
|
|
||||||
<span><span id="logState" style="color:#aaa; margin-right:10px;"></span><button onclick="refreshLogs()">Refresh</button></span>
|
|
||||||
</div>
|
|
||||||
<table id="filesTable" style="width:100%; border-collapse:collapse; font-size:0.85em;"></table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="terminal-container">
|
<div class="terminal-container">
|
||||||
<div id="terminal"></div>
|
<div id="terminal"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -325,7 +315,6 @@
|
|||||||
document.getElementById('baudSelect').value = msg.baudSerial1;
|
document.getElementById('baudSelect').value = msg.baudSerial1;
|
||||||
updateStatus('btStatus', msg.btConnected);
|
updateStatus('btStatus', msg.btConnected);
|
||||||
document.getElementById('heap').textContent = `Heap: ${msg.freeHeap}`;
|
document.getElementById('heap').textContent = `Heap: ${msg.freeHeap}`;
|
||||||
updateLogUi(msg);
|
|
||||||
|
|
||||||
// Show WiFi info
|
// Show WiFi info
|
||||||
const wifiInfo = document.getElementById('wifiInfo');
|
const wifiInfo = document.getElementById('wifiInfo');
|
||||||
@@ -418,52 +407,6 @@
|
|||||||
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- SD logging controls ----
|
|
||||||
let logOn = false;
|
|
||||||
function toggleLog() {
|
|
||||||
fetch('/api/log?on=' + (logOn ? 0 : 1))
|
|
||||||
.then(r => r.json()).then(updateLogUi)
|
|
||||||
.catch(() => term.writeln('\r\n\x1b[31m[log toggle failed]\x1b[0m'));
|
|
||||||
}
|
|
||||||
function updateLogUi(s) {
|
|
||||||
if (typeof s.log === 'undefined') return;
|
|
||||||
logOn = !!s.log;
|
|
||||||
const b = document.getElementById('logBtn');
|
|
||||||
b.textContent = 'Log: ' + (logOn ? 'ON' : 'off');
|
|
||||||
b.classList.toggle('danger', logOn);
|
|
||||||
const st = document.getElementById('logState');
|
|
||||||
if (st) {
|
|
||||||
st.textContent = !s.sd ? 'no SD card'
|
|
||||||
: s.logfile ? ('file: ' + String(s.logfile).split('/').pop())
|
|
||||||
: (s.ntp ? 'idle' : 'waiting for NTP date');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function toggleFiles() {
|
|
||||||
const p = document.getElementById('filesPanel');
|
|
||||||
const show = p.style.display === 'none';
|
|
||||||
p.style.display = show ? 'block' : 'none';
|
|
||||||
if (show) refreshLogs();
|
|
||||||
}
|
|
||||||
function refreshLogs() {
|
|
||||||
fetch('/api/logs').then(r => r.json()).then(list => {
|
|
||||||
const t = document.getElementById('filesTable');
|
|
||||||
if (!list.length) { t.innerHTML = '<tr><td style="color:#666;">(no log files yet)</td></tr>'; return; }
|
|
||||||
t.innerHTML = list.map(f =>
|
|
||||||
`<tr style="border-bottom:1px solid #0f3460;">
|
|
||||||
<td style="padding:4px 0;">${f.name}${f.active ? ' <span style="color:#4ade80;">(active)</span>' : ''}</td>
|
|
||||||
<td style="text-align:right; color:#aaa;">${formatBytes(f.size)}</td>
|
|
||||||
<td style="text-align:right; padding-left:12px;"><a href="/api/logfile?name=${encodeURIComponent(f.name)}" style="color:#60a5fa;">download</a></td>
|
|
||||||
<td style="text-align:right; padding-left:12px;">${f.active ? '' : `<a href="#" onclick="deleteLog('${f.name}');return false;" style="color:#e94560;">delete</a>`}</td>
|
|
||||||
</tr>`).join('');
|
|
||||||
}).catch(() => {
|
|
||||||
document.getElementById('filesTable').innerHTML = '<tr><td style="color:#e94560;">(failed -- SD only on T3-S3 build)</td></tr>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function deleteLog(name) {
|
|
||||||
if (!confirm('Delete ' + name + '?')) return;
|
|
||||||
fetch('/api/logdelete?name=' + encodeURIComponent(name)).then(() => refreshLogs());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial connection
|
// Initial connection
|
||||||
term.writeln('\x1b[1;36m╔═══════════════════════════════════════╗\x1b[0m');
|
term.writeln('\x1b[1;36m╔═══════════════════════════════════════╗\x1b[0m');
|
||||||
term.writeln('\x1b[1;36m║ ESP32 Debug Dongle Terminal ║\x1b[0m');
|
term.writeln('\x1b[1;36m║ ESP32 Debug Dongle Terminal ║\x1b[0m');
|
||||||
|
|||||||
+10
-73
@@ -14,92 +14,29 @@
|
|||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = esp32dev
|
board = esp32dev
|
||||||
framework = arduino
|
framework = arduino
|
||||||
|
|
||||||
|
; Serial monitor settings
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_filters = esp32_exception_decoder
|
monitor_filters = esp32_exception_decoder
|
||||||
|
|
||||||
board_build.filesystem = littlefs
|
; Build flags
|
||||||
|
|
||||||
; Use a larger app partition (pick ONE):
|
|
||||||
board_build.partitions = huge_app.csv ; 3MB app, 1MB FS, no OTA
|
|
||||||
; board_build.partitions = no_ota.csv ; 2MB app, 2MB FS, no OTA
|
|
||||||
; board_build.partitions = min_spiffs.csv ; 1.9MB app + OTA, 190KB FS
|
|
||||||
|
|
||||||
build_flags =
|
build_flags =
|
||||||
-DCORE_DEBUG_LEVEL=3
|
-DCORE_DEBUG_LEVEL=3
|
||||||
-DCONFIG_BT_ENABLED=1
|
-DCONFIG_BT_ENABLED=1
|
||||||
-DCONFIG_BLUEDROID_ENABLED=1
|
-DCONFIG_BLUEDROID_ENABLED=1
|
||||||
-DHAS_BT_CLASSIC=1 ; classic SPP exists on the original ESP32
|
|
||||||
-DSTA_SSID="${sysenv.STA_SSID}"
|
; Partition scheme with space for LittleFS
|
||||||
-DSTA_PASSWORD="${sysenv.STA_PASSWORD}"
|
board_build.partitions = default.csv
|
||||||
|
board_build.filesystem = littlefs
|
||||||
|
|
||||||
; Libraries
|
; Libraries
|
||||||
lib_deps =
|
lib_deps =
|
||||||
; Async Web Server and dependencies
|
; PsychicHttp - robust HTTP server with WebSocket support
|
||||||
https://github.com/ESP32Async/ESPAsyncWebServer
|
hoeken/PsychicHttp @ ^2.1.0
|
||||||
https://github.com/ESP32Async/AsyncTCP
|
|
||||||
|
|
||||||
; ArduinoJson for configuration/commands
|
; ArduinoJson for configuration/commands
|
||||||
ArduinoJson
|
ArduinoJson @ ^7.0.0
|
||||||
|
|
||||||
; Upload settings (adjust port as needed)
|
; Upload settings (adjust port as needed)
|
||||||
; upload_port = /dev/ttyUSB0
|
; upload_port = /dev/ttyUSB0
|
||||||
; upload_speed = 921600
|
; upload_speed = 921600
|
||||||
|
|
||||||
; Extra scripts for LittleFS
|
|
||||||
extra_scripts =
|
|
||||||
pre:scripts/download_xterm.py
|
|
||||||
|
|
||||||
; ============================================================================
|
|
||||||
; LilyGo T3-S3 -- ESP32-S3 + SX127x LoRa + SSD1306 128x64 OLED + microSD.
|
|
||||||
; The remote debug bridge: UART <-> telnet(:23)/WebSocket, GPIO reset/wake,
|
|
||||||
; SD logging (NTP-dated), OLED status. NOTE: ESP32-S3 has NO Bluetooth Classic,
|
|
||||||
; so HAS_BT_CLASSIC is NOT set here (telnet + WebSocket replace SerialBT).
|
|
||||||
; Pins marked CONFIRM must be checked against your actual board wiring.
|
|
||||||
; ============================================================================
|
|
||||||
[env:t3s3]
|
|
||||||
platform = espressif32
|
|
||||||
board = esp32-s3-devkitc-1
|
|
||||||
framework = arduino
|
|
||||||
monitor_speed = 115200
|
|
||||||
monitor_filters = esp32_exception_decoder
|
|
||||||
board_build.mcu = esp32s3
|
|
||||||
board_build.flash_size = 4MB
|
|
||||||
board_upload.flash_size = 4MB
|
|
||||||
board_build.filesystem = littlefs
|
|
||||||
board_build.partitions = huge_app.csv
|
|
||||||
build_flags =
|
|
||||||
-DCORE_DEBUG_LEVEL=3
|
|
||||||
-DARDUINO_USB_CDC_ON_BOOT=1 ; console on USB CDC -> frees UART0 (43/44)
|
|
||||||
;-DSTA_SSID="${sysenv.STA_SSID}"
|
|
||||||
;-DSTA_PASSWORD="${sysenv.STA_PASSWORD}"
|
|
||||||
-DSTA_SSID="MeridenRainbow5G"
|
|
||||||
-DSTA_PASSWORD="4z8bcw5vfrs3n7dm"
|
|
||||||
-DBOARD_T3S3=1
|
|
||||||
; --- OLED (SSD1306 128x64) ---
|
|
||||||
-DT3S3_OLED_SDA=18
|
|
||||||
-DT3S3_OLED_SCL=17
|
|
||||||
-DT3S3_OLED_RST=21
|
|
||||||
-DT3S3_OLED_ADDR=0x3C
|
|
||||||
; --- 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
|
|
||||||
-DGPIO_CTRL_ACTIVE_LOW=1
|
|
||||||
; --- microSD on FSPI/SPI2 (separate bus from LoRa, which uses HSPI/SPI3) ---
|
|
||||||
-DT3S3_SD_SCK=14
|
|
||||||
-DT3S3_SD_MISO=2
|
|
||||||
-DT3S3_SD_MOSI=11
|
|
||||||
-DT3S3_SD_CS=13
|
|
||||||
-DT3S3_SD_FREQ_HZ=20000000UL
|
|
||||||
; --- timezone for NTP-dated logs (Australia/Victoria) ---
|
|
||||||
-DNTP_TZ='"AEST-10AEDT,M10.1.0,M4.1.0/3"'
|
|
||||||
lib_deps =
|
|
||||||
https://github.com/ESP32Async/ESPAsyncWebServer
|
|
||||||
https://github.com/ESP32Async/AsyncTCP
|
|
||||||
ArduinoJson
|
|
||||||
adafruit/Adafruit SSD1306
|
|
||||||
adafruit/Adafruit GFX Library
|
|
||||||
extra_scripts =
|
|
||||||
pre:scripts/download_xterm.py
|
|
||||||
|
|||||||
+291
-536
@@ -1,149 +1,94 @@
|
|||||||
/**
|
/**
|
||||||
* ESP32 Debug Dongle
|
* ESP32 Debug Dongle
|
||||||
*
|
*
|
||||||
* Remote serial debug bridge for the trough/solartrack sensors (and anything
|
* Web-based and Bluetooth serial terminal with multi-port support.
|
||||||
* with a UART). Bridges a target device's UART to:
|
|
||||||
* - Telnet (port 23) -- primary remote path; nc/minicom or an agent
|
|
||||||
* - Web terminal -- xterm.js over WebSocket
|
|
||||||
* - Bluetooth SPP -- original ESP32 only (no Classic BT on ESP32-S3)
|
|
||||||
* Plus: GPIO control to reset/wake the target, NTP-dated SD logging, and an
|
|
||||||
* OLED status page (LilyGo T3-S3).
|
|
||||||
*
|
*
|
||||||
* Telnet protocol: a line is forwarded verbatim to the target UART UNLESS it
|
* Features:
|
||||||
* starts with '~', in which case it's a dongle command:
|
* - Web terminal using xterm.js over WebSocket
|
||||||
* ~help ~status ~reset [ms] ~wake [ms] ~baud <n> ~port <int|usb|ext>
|
* - Bluetooth Classic SPP serial bridge
|
||||||
* ~log on|off ~gpio <pin> <0|1>
|
* - Switch between internal (virtual), Serial, and Serial1
|
||||||
* REST mirrors these: /api/status /api/reset /api/wake /api/baud?baud=
|
* - Configurable baud rates
|
||||||
* /api/port?port= /api/log?on= /api/gpio?pin=&val= /api/send?data=
|
|
||||||
*
|
*
|
||||||
* Boards (platformio.ini): [env:esp32dev] generic ESP32 (+ BT); [env:t3s3]
|
* Hardware connections for Serial1 (external device):
|
||||||
* LilyGo T3-S3 (OLED + SD + LoRa). Pins are build flags -- see platformio.ini.
|
* GPIO16 = RX1 (connect to external TX)
|
||||||
|
* GPIO17 = TX1 (connect to external RX)
|
||||||
|
* GND = Common ground
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <ESPAsyncWebServer.h>
|
#include <PsychicHttp.h>
|
||||||
#include <AsyncTCP.h>
|
|
||||||
#include <LittleFS.h>
|
#include <LittleFS.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include "LoopbackStream.h"
|
|
||||||
|
|
||||||
#if HAS_BT_CLASSIC
|
|
||||||
#include "BluetoothSerial.h"
|
#include "BluetoothSerial.h"
|
||||||
#endif
|
#include "LoopbackStream.h"
|
||||||
|
|
||||||
#if BOARD_T3S3
|
|
||||||
#include <time.h>
|
|
||||||
#include <Wire.h>
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <SD.h>
|
|
||||||
#include <Adafruit_GFX.h>
|
|
||||||
#include <Adafruit_SSD1306.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Configuration
|
// Configuration
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// WiFi Mode: Set to true to connect to existing network, false for AP mode
|
||||||
#define WIFI_STATION_MODE true
|
#define WIFI_STATION_MODE true
|
||||||
#define ST(A) #A
|
|
||||||
#define STR(A) ST(A)
|
|
||||||
|
|
||||||
#ifndef STA_SSID
|
// WiFi Station mode settings (connect to existing network)
|
||||||
#define STA_SSID "ExampleSSID"
|
const char* STA_SSID = "YourNetworkSSID"; // Your WiFi network name
|
||||||
#endif
|
const char* STA_PASSWORD = "YourPassword"; // Your WiFi password
|
||||||
#ifndef STA_PASSWORD
|
|
||||||
#define STA_PASSWORD "ThePassword"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
// WiFi Access Point fallback settings (if station fails or AP mode selected)
|
||||||
const char* AP_SSID = "ESP32-DebugDongle";
|
const char* AP_SSID = "ESP32-DebugDongle";
|
||||||
const char* AP_PASSWORD = "debug1234";
|
const char* AP_PASSWORD = "debug1234"; // Min 8 characters
|
||||||
|
|
||||||
|
// Station mode connection timeout (milliseconds)
|
||||||
#define WIFI_CONNECT_TIMEOUT 15000
|
#define WIFI_CONNECT_TIMEOUT 15000
|
||||||
|
|
||||||
|
// Bluetooth device name
|
||||||
const char* BT_NAME = "ESP32-Debug";
|
const char* BT_NAME = "ESP32-Debug";
|
||||||
|
|
||||||
// Target UART pins (override per board in platformio.ini).
|
// Serial1 pins (external device connection)
|
||||||
#ifndef TARGET_RX_PIN
|
#define SERIAL1_RX_PIN 16
|
||||||
#define TARGET_RX_PIN 16
|
#define SERIAL1_TX_PIN 17
|
||||||
#endif
|
|
||||||
#ifndef TARGET_TX_PIN
|
|
||||||
#define TARGET_TX_PIN 17
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// GPIO control lines to the target. -1 disables. Active-low by default on T3-S3.
|
|
||||||
#ifndef GPIO_RESET_PIN
|
|
||||||
#define GPIO_RESET_PIN -1
|
|
||||||
#endif
|
|
||||||
#ifndef GPIO_WAKE_PIN
|
|
||||||
#define GPIO_WAKE_PIN -1
|
|
||||||
#endif
|
|
||||||
#ifndef GPIO_CTRL_ACTIVE_LOW
|
|
||||||
#define GPIO_CTRL_ACTIVE_LOW 0
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
// Default baud rates
|
||||||
#define DEFAULT_BAUD_SERIAL 115200
|
#define DEFAULT_BAUD_SERIAL 115200
|
||||||
#define DEFAULT_BAUD_SERIAL1 115200
|
#define DEFAULT_BAUD_SERIAL1 115200
|
||||||
|
|
||||||
|
// Internal serial buffer size
|
||||||
#define INTERNAL_BUFFER_SIZE 2048
|
#define INTERNAL_BUFFER_SIZE 2048
|
||||||
|
|
||||||
#define TELNET_PORT 23
|
|
||||||
#define MAX_TELNET 4
|
|
||||||
#define TELNET_LINE_MAX 256
|
|
||||||
#define CTRL_PREFIX '~'
|
|
||||||
|
|
||||||
#if BOARD_T3S3
|
|
||||||
#ifndef NTP_TZ
|
|
||||||
#define NTP_TZ "UTC0"
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Global Objects
|
// Global Objects
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
AsyncWebServer server(80);
|
// PsychicHttp server on port 80
|
||||||
AsyncWebSocket ws("/ws");
|
PsychicHttpServer server;
|
||||||
|
|
||||||
#if HAS_BT_CLASSIC
|
// WebSocket handler
|
||||||
|
PsychicWebSocketHandler websocketHandler;
|
||||||
|
|
||||||
|
// Bluetooth Serial
|
||||||
BluetoothSerial SerialBT;
|
BluetoothSerial SerialBT;
|
||||||
#endif
|
|
||||||
|
|
||||||
|
// Hardware serial for external device
|
||||||
|
// HardwareSerial Serial1(1);
|
||||||
|
|
||||||
|
// Virtual/loopback serial for internal debug output
|
||||||
LoopbackStream internalSerial(INTERNAL_BUFFER_SIZE);
|
LoopbackStream internalSerial(INTERNAL_BUFFER_SIZE);
|
||||||
|
|
||||||
|
// Active serial port pointer
|
||||||
Stream* activePort = &internalSerial;
|
Stream* activePort = &internalSerial;
|
||||||
|
|
||||||
enum SerialPortId { PORT_INTERNAL = 0, PORT_USB = 1, PORT_EXTERNAL = 2 };
|
// Port identifiers
|
||||||
SerialPortId currentPort = PORT_EXTERNAL; // default: watch the wired target
|
enum SerialPortId {
|
||||||
|
PORT_INTERNAL = 0, // Virtual loopback (ESP32 debug output)
|
||||||
|
PORT_USB = 1, // Serial (USB) - shares with programming
|
||||||
|
PORT_EXTERNAL = 2 // Serial1 (GPIO pins) - external device
|
||||||
|
};
|
||||||
|
|
||||||
|
SerialPortId currentPort = PORT_INTERNAL;
|
||||||
|
|
||||||
|
// Baud rates (can be changed at runtime)
|
||||||
uint32_t baudSerial1 = DEFAULT_BAUD_SERIAL1;
|
uint32_t baudSerial1 = DEFAULT_BAUD_SERIAL1;
|
||||||
|
|
||||||
// Telnet
|
|
||||||
WiFiServer telnetServer(TELNET_PORT);
|
|
||||||
WiFiClient telnetClients[MAX_TELNET];
|
|
||||||
char telnetLine[MAX_TELNET][TELNET_LINE_MAX];
|
|
||||||
size_t telnetLineLen[MAX_TELNET] = {0};
|
|
||||||
|
|
||||||
// Counters
|
|
||||||
volatile uint32_t rxBytes = 0; // from target
|
|
||||||
volatile uint32_t txBytes = 0; // to target
|
|
||||||
|
|
||||||
// SD logging (T3-S3)
|
|
||||||
bool sdAvailable = false;
|
|
||||||
bool logEnabled = true; // ON by default once we have a date
|
|
||||||
bool ntpSynced = false;
|
|
||||||
#if BOARD_T3S3
|
|
||||||
SPIClass sdSPI(FSPI);
|
|
||||||
File logFile;
|
|
||||||
bool logAtLineStart = true;
|
|
||||||
char logPath[40] = {0};
|
|
||||||
Adafruit_SSD1306 oled(128, 64, &Wire, T3S3_OLED_RST);
|
|
||||||
bool oledOk = false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Forward decls
|
|
||||||
// ============================================================================
|
|
||||||
void broadcastFromTarget(const uint8_t* data, size_t len);
|
|
||||||
void forwardToTarget(const uint8_t* data, size_t len);
|
|
||||||
void handleCommand(const char* line, Stream* reply);
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Serial Port Management
|
// Serial Port Management
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -151,216 +96,27 @@ void handleCommand(const char* line, Stream* reply);
|
|||||||
void setActivePort(SerialPortId port) {
|
void setActivePort(SerialPortId port) {
|
||||||
currentPort = port;
|
currentPort = port;
|
||||||
switch (port) {
|
switch (port) {
|
||||||
case PORT_INTERNAL: activePort = &internalSerial; break;
|
case PORT_INTERNAL:
|
||||||
case PORT_USB: activePort = &Serial; break;
|
activePort = &internalSerial;
|
||||||
case PORT_EXTERNAL: activePort = &Serial1; break;
|
Serial.println("[System] Switched to Internal serial");
|
||||||
|
break;
|
||||||
|
case PORT_USB:
|
||||||
|
activePort = &Serial;
|
||||||
|
Serial.println("[System] Switched to USB serial");
|
||||||
|
break;
|
||||||
|
case PORT_EXTERNAL:
|
||||||
|
activePort = &Serial1;
|
||||||
|
Serial.println("[System] Switched to External serial (Serial1)");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setBaudRate(uint32_t baud) {
|
void setBaudRate(SerialPortId port, uint32_t baud) {
|
||||||
|
if (port == PORT_EXTERNAL) {
|
||||||
baudSerial1 = baud;
|
baudSerial1 = baud;
|
||||||
Serial1.end();
|
Serial1.end();
|
||||||
Serial1.begin(baud, SERIAL_8N1, TARGET_RX_PIN, TARGET_TX_PIN);
|
Serial1.begin(baud, SERIAL_8N1, SERIAL1_RX_PIN, SERIAL1_TX_PIN);
|
||||||
}
|
Serial.printf("[System] Serial1 baud rate set to %lu\n", baud);
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// GPIO control (reset / wake)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
static void gpioInit(int pin) {
|
|
||||||
if (pin < 0) return;
|
|
||||||
// Idle = inactive level.
|
|
||||||
pinMode(pin, OUTPUT);
|
|
||||||
digitalWrite(pin, GPIO_CTRL_ACTIVE_LOW ? HIGH : LOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void gpioPulse(int pin, uint32_t ms, Stream* reply, const char* name) {
|
|
||||||
if (pin < 0) { if (reply) reply->printf("[ctrl] %s pin not configured\r\n", name); return; }
|
|
||||||
if (ms == 0) ms = 200;
|
|
||||||
int active = GPIO_CTRL_ACTIVE_LOW ? LOW : HIGH;
|
|
||||||
int inactive = GPIO_CTRL_ACTIVE_LOW ? HIGH : LOW;
|
|
||||||
pinMode(pin, OUTPUT);
|
|
||||||
digitalWrite(pin, active);
|
|
||||||
delay(ms);
|
|
||||||
digitalWrite(pin, inactive);
|
|
||||||
if (reply) reply->printf("[ctrl] %s pulsed pin %d for %lu ms\r\n", name, pin, (unsigned long)ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// SD logging (T3-S3)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#if BOARD_T3S3
|
|
||||||
static void openLogIfReady() {
|
|
||||||
if (!sdAvailable || logFile) return;
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
if (now < 1700000000) return; // wait for NTP (well past 2023)
|
|
||||||
struct tm tmv; localtime_r(&now, &tmv);
|
|
||||||
SD.mkdir("/logs");
|
|
||||||
strftime(logPath, sizeof(logPath), "/logs/%Y%m%d-%H%M%S.log", &tmv);
|
|
||||||
logFile = SD.open(logPath, FILE_WRITE);
|
|
||||||
if (logFile) {
|
|
||||||
logFile.printf("# debug-dongle log opened %s\n", logPath);
|
|
||||||
logFile.flush();
|
|
||||||
logAtLineStart = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void logBytes(const uint8_t* data, size_t len) {
|
|
||||||
if (!logEnabled || !logFile) return;
|
|
||||||
for (size_t i = 0; i < len; i++) {
|
|
||||||
if (logAtLineStart) {
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
struct tm tmv; localtime_r(&now, &tmv);
|
|
||||||
char ts[16];
|
|
||||||
strftime(ts, sizeof(ts), "[%H:%M:%S] ", &tmv);
|
|
||||||
logFile.print(ts);
|
|
||||||
logAtLineStart = false;
|
|
||||||
}
|
|
||||||
logFile.write(data[i]);
|
|
||||||
if (data[i] == '\n') logAtLineStart = true;
|
|
||||||
}
|
|
||||||
logFile.flush(); // power-cut safe (it's a debug log)
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
static void openLogIfReady() {}
|
|
||||||
static void logBytes(const uint8_t*, size_t) {}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Fan-out
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// Target UART -> every sink (web, telnet, BT, SD).
|
|
||||||
void broadcastFromTarget(const uint8_t* data, size_t len) {
|
|
||||||
rxBytes += len;
|
|
||||||
ws.binaryAll((uint8_t*)data, len);
|
|
||||||
for (int i = 0; i < MAX_TELNET; i++)
|
|
||||||
if (telnetClients[i] && telnetClients[i].connected())
|
|
||||||
telnetClients[i].write(data, len);
|
|
||||||
#if HAS_BT_CLASSIC
|
|
||||||
if (SerialBT.connected()) SerialBT.write(data, len);
|
|
||||||
#endif
|
|
||||||
logBytes(data, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anything (web/telnet/BT) -> target UART.
|
|
||||||
void forwardToTarget(const uint8_t* data, size_t len) {
|
|
||||||
txBytes += len;
|
|
||||||
activePort->write(data, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Command handling (telnet ~cmds and REST share this)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
static const char* portName(SerialPortId p) {
|
|
||||||
return p == PORT_INTERNAL ? "internal" : p == PORT_USB ? "usb" : "external";
|
|
||||||
}
|
|
||||||
|
|
||||||
void printStatus(Stream* o) {
|
|
||||||
o->printf("[status] port=%s baud=%lu rx=%lu tx=%lu telnet=%d log=%s ntp=%s heap=%u\r\n",
|
|
||||||
portName(currentPort), (unsigned long)baudSerial1,
|
|
||||||
(unsigned long)rxBytes, (unsigned long)txBytes,
|
|
||||||
[]{ int n=0; for (int i=0;i<MAX_TELNET;i++) if (telnetClients[i]&&telnetClients[i].connected()) n++; return n; }(),
|
|
||||||
logEnabled ? "on" : "off", ntpSynced ? "yes" : "no",
|
|
||||||
(unsigned)ESP.getFreeHeap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// `line` is the text AFTER the CTRL_PREFIX.
|
|
||||||
void handleCommand(const char* line, Stream* reply) {
|
|
||||||
char verb[16] = {0};
|
|
||||||
int n = 0;
|
|
||||||
while (line[n] && line[n] != ' ' && n < 15) { verb[n] = line[n]; n++; }
|
|
||||||
verb[n] = '\0';
|
|
||||||
const char* args = line[n] ? line + n + 1 : line + n;
|
|
||||||
|
|
||||||
if (!strcmp(verb, "help")) {
|
|
||||||
reply->print("[help] ~status ~reset [ms] ~wake [ms] ~baud <n> "
|
|
||||||
"~port <int|usb|ext> ~log on|off ~gpio <pin> <0|1>\r\n"
|
|
||||||
" (any non-~ line is sent to the target UART)\r\n");
|
|
||||||
} else if (!strcmp(verb, "status")) {
|
|
||||||
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, "baud")) {
|
|
||||||
uint32_t b = strtoul(args, nullptr, 10);
|
|
||||||
if (b) { setBaudRate(b); reply->printf("[ctrl] baud=%lu\r\n", (unsigned long)b); }
|
|
||||||
else reply->print("[ctrl] usage: ~baud <n>\r\n");
|
|
||||||
} else if (!strcmp(verb, "port")) {
|
|
||||||
if (!strncmp(args, "int", 3)) setActivePort(PORT_INTERNAL);
|
|
||||||
else if (!strncmp(args, "usb", 3)) setActivePort(PORT_USB);
|
|
||||||
else if (!strncmp(args, "ext", 3)) setActivePort(PORT_EXTERNAL);
|
|
||||||
reply->printf("[ctrl] port=%s\r\n", portName(currentPort));
|
|
||||||
} else if (!strcmp(verb, "log")) {
|
|
||||||
#if BOARD_T3S3
|
|
||||||
if (!strncmp(args, "on", 2)) logEnabled = true;
|
|
||||||
else if (!strncmp(args, "off", 3)) logEnabled = false;
|
|
||||||
if (logEnabled) openLogIfReady();
|
|
||||||
reply->printf("[ctrl] log=%s file=%s\r\n", logEnabled ? "on" : "off",
|
|
||||||
logFile ? logPath : "(none)");
|
|
||||||
#else
|
|
||||||
reply->print("[ctrl] no SD on this build\r\n");
|
|
||||||
#endif
|
|
||||||
} else if (!strcmp(verb, "gpio")) {
|
|
||||||
int pin = -1, val = 0;
|
|
||||||
if (sscanf(args, "%d %d", &pin, &val) == 2 && pin >= 0) {
|
|
||||||
pinMode(pin, OUTPUT);
|
|
||||||
digitalWrite(pin, val ? HIGH : LOW);
|
|
||||||
reply->printf("[ctrl] gpio %d = %d\r\n", pin, val ? 1 : 0);
|
|
||||||
} else reply->print("[ctrl] usage: ~gpio <pin> <0|1>\r\n");
|
|
||||||
} else {
|
|
||||||
reply->printf("[ctrl] unknown '~%s' (try ~help)\r\n", verb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Telnet
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
void setupTelnet() { telnetServer.begin(); telnetServer.setNoDelay(true); }
|
|
||||||
|
|
||||||
void serviceTelnet() {
|
|
||||||
// Accept new clients into a free slot.
|
|
||||||
if (telnetServer.hasClient()) {
|
|
||||||
int slot = -1;
|
|
||||||
for (int i = 0; i < MAX_TELNET; i++)
|
|
||||||
if (!telnetClients[i] || !telnetClients[i].connected()) { slot = i; break; }
|
|
||||||
WiFiClient c = telnetServer.available();
|
|
||||||
if (slot >= 0) {
|
|
||||||
telnetClients[slot] = c;
|
|
||||||
telnetLineLen[slot] = 0;
|
|
||||||
telnetClients[slot].printf("[dongle] connected -- ~help for commands, "
|
|
||||||
"other lines go to the target UART\r\n");
|
|
||||||
} else {
|
|
||||||
c.println("[dongle] too many clients");
|
|
||||||
c.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Read each client line-by-line.
|
|
||||||
for (int i = 0; i < MAX_TELNET; i++) {
|
|
||||||
WiFiClient& cl = telnetClients[i];
|
|
||||||
if (!cl || !cl.connected()) continue;
|
|
||||||
while (cl.available()) {
|
|
||||||
char ch = (char)cl.read();
|
|
||||||
if (ch == '\r') continue;
|
|
||||||
if (ch == '\n') {
|
|
||||||
telnetLine[i][telnetLineLen[i]] = '\0';
|
|
||||||
if (telnetLineLen[i] > 0 && telnetLine[i][0] == CTRL_PREFIX) {
|
|
||||||
handleCommand(telnetLine[i] + 1, &cl);
|
|
||||||
} else {
|
|
||||||
// Forward the line (with newline) to the target.
|
|
||||||
forwardToTarget((const uint8_t*)telnetLine[i], telnetLineLen[i]);
|
|
||||||
const uint8_t nl = '\n';
|
|
||||||
forwardToTarget(&nl, 1);
|
|
||||||
}
|
|
||||||
telnetLineLen[i] = 0;
|
|
||||||
} else if (telnetLineLen[i] < TELNET_LINE_MAX - 1) {
|
|
||||||
telnetLine[i][telnetLineLen[i]++] = ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,300 +124,299 @@ void serviceTelnet() {
|
|||||||
// WebSocket Handlers
|
// WebSocket Handlers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t len) {
|
void handleWebSocketMessage(PsychicWebSocketClient* client, uint8_t* data, size_t len) {
|
||||||
|
// Check for command prefix (starts with 0x00)
|
||||||
if (len > 0 && data[0] == 0x00) {
|
if (len > 0 && data[0] == 0x00) {
|
||||||
|
// Command message - parse JSON
|
||||||
String cmdStr = String((char*)(data + 1)).substring(0, len - 1);
|
String cmdStr = String((char*)(data + 1)).substring(0, len - 1);
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
if (deserializeJson(doc, cmdStr)) return;
|
DeserializationError err = deserializeJson(doc, cmdStr);
|
||||||
|
|
||||||
|
if (!err) {
|
||||||
const char* cmd = doc["cmd"];
|
const char* cmd = doc["cmd"];
|
||||||
if (!cmd) return;
|
|
||||||
if (!strcmp(cmd, "setPort")) {
|
if (strcmp(cmd, "setPort") == 0) {
|
||||||
setActivePort((SerialPortId)(int)doc["port"]);
|
int port = doc["port"];
|
||||||
} else if (!strcmp(cmd, "setBaud")) {
|
setActivePort((SerialPortId)port);
|
||||||
setBaudRate((uint32_t)doc["baud"]);
|
|
||||||
} else if (!strcmp(cmd, "getStatus")) {
|
// Send confirmation
|
||||||
String response; JsonDocument r;
|
String response;
|
||||||
r["type"] = "status";
|
JsonDocument respDoc;
|
||||||
r["currentPort"] = currentPort;
|
respDoc["type"] = "portChanged";
|
||||||
r["baudSerial1"] = baudSerial1;
|
respDoc["port"] = port;
|
||||||
r["rx"] = rxBytes; r["tx"] = txBytes;
|
serializeJson(respDoc, response);
|
||||||
r["log"] = logEnabled; r["ntp"] = ntpSynced; r["sd"] = sdAvailable;
|
|
||||||
#if BOARD_T3S3
|
String msg = String((char)0x00) + response;
|
||||||
r["logfile"] = logFile ? logPath : "";
|
client->sendMessage(msg.c_str());
|
||||||
#endif
|
}
|
||||||
r["freeHeap"] = ESP.getFreeHeap();
|
else if (strcmp(cmd, "setBaud") == 0) {
|
||||||
r["wifiMode"] = (WiFi.getMode() == WIFI_STA) ? "station" : "ap";
|
int port = doc["port"];
|
||||||
r["ip"] = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
|
uint32_t baud = doc["baud"];
|
||||||
serializeJson(r, response);
|
setBaudRate((SerialPortId)port, baud);
|
||||||
client->text(String((char)0x00) + response);
|
}
|
||||||
|
else if (strcmp(cmd, "getStatus") == 0) {
|
||||||
|
String response;
|
||||||
|
JsonDocument respDoc;
|
||||||
|
respDoc["type"] = "status";
|
||||||
|
respDoc["currentPort"] = currentPort;
|
||||||
|
respDoc["baudSerial1"] = baudSerial1;
|
||||||
|
respDoc["btConnected"] = SerialBT.connected();
|
||||||
|
respDoc["freeHeap"] = ESP.getFreeHeap();
|
||||||
|
respDoc["wifiMode"] = (WiFi.getMode() == WIFI_STA) ? "station" : "ap";
|
||||||
|
respDoc["ip"] = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
|
||||||
|
if (WiFi.getMode() == WIFI_STA) {
|
||||||
|
respDoc["rssi"] = WiFi.RSSI();
|
||||||
|
}
|
||||||
|
serializeJson(respDoc, response);
|
||||||
|
|
||||||
|
String msg = String((char)0x00) + response;
|
||||||
|
client->sendMessage(msg.c_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
forwardToTarget(data, len);
|
// Regular serial data - send to active port
|
||||||
}
|
activePort->write(data, len);
|
||||||
}
|
|
||||||
|
|
||||||
void onWsEvent(AsyncWebSocket*, AsyncWebSocketClient* client,
|
|
||||||
AwsEventType type, void* arg, uint8_t* data, size_t len) {
|
|
||||||
if (type == WS_EVT_DATA) {
|
|
||||||
AwsFrameInfo* info = (AwsFrameInfo*)arg;
|
|
||||||
if (info->final && info->index == 0 && info->len == len)
|
|
||||||
handleWebSocketMessage(client, data, len);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// REST API
|
// Web Server Setup
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
static String statusJson() {
|
void setupWebServer() {
|
||||||
|
// Increase URI handler limit if needed
|
||||||
|
server.config.max_uri_handlers = 10;
|
||||||
|
|
||||||
|
// Serve static files from LittleFS
|
||||||
|
server.serveStatic("/", LittleFS, "/"); // XXX .setDefaultFile("index.html");
|
||||||
|
|
||||||
|
// API endpoint for status (can be used without WebSocket)
|
||||||
|
server.on("/api/status", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
|
||||||
|
String out;
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc["currentPort"] = currentPort;
|
doc["currentPort"] = currentPort;
|
||||||
doc["baudSerial1"] = baudSerial1;
|
doc["baudSerial1"] = baudSerial1;
|
||||||
doc["rx"] = rxBytes; doc["tx"] = txBytes;
|
doc["btConnected"] = SerialBT.connected();
|
||||||
doc["log"] = logEnabled; doc["ntp"] = ntpSynced; doc["sd"] = sdAvailable;
|
|
||||||
#if BOARD_T3S3
|
|
||||||
doc["logfile"] = logFile ? logPath : "";
|
|
||||||
#endif
|
|
||||||
doc["freeHeap"] = ESP.getFreeHeap();
|
doc["freeHeap"] = ESP.getFreeHeap();
|
||||||
doc["wifiMode"] = (WiFi.getMode() == WIFI_STA) ? "station" : "ap";
|
doc["wifiMode"] = (WiFi.getMode() == WIFI_STA) ? "station" : "ap";
|
||||||
doc["ip"] = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
|
doc["ip"] = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
|
||||||
doc["ssid"] = (WiFi.getMode() == WIFI_STA) ? WiFi.SSID() : String(AP_SSID);
|
doc["ssid"] = (WiFi.getMode() == WIFI_STA) ? WiFi.SSID() : AP_SSID;
|
||||||
if (WiFi.getMode() == WIFI_STA) doc["rssi"] = WiFi.RSSI();
|
if (WiFi.getMode() == WIFI_STA) {
|
||||||
String s; serializeJson(doc, s); return s;
|
doc["rssi"] = WiFi.RSSI();
|
||||||
}
|
}
|
||||||
|
serializeJson(doc, out);
|
||||||
void setupWebServer() {
|
return response->send(200, "application/json", out.c_str());
|
||||||
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
|
|
||||||
ws.onEvent(onWsEvent);
|
|
||||||
server.addHandler(&ws);
|
|
||||||
|
|
||||||
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest* req) {
|
|
||||||
req->send(200, "application/json", statusJson());
|
|
||||||
});
|
|
||||||
server.on("/api/reset", HTTP_GET, [](AsyncWebServerRequest* req) {
|
|
||||||
uint32_t ms = req->hasParam("ms") ? req->getParam("ms")->value().toInt() : 0;
|
|
||||||
gpioPulse(GPIO_RESET_PIN, ms, nullptr, "reset");
|
|
||||||
req->send(200, "text/plain", "reset pulsed\n");
|
|
||||||
});
|
|
||||||
server.on("/api/wake", HTTP_GET, [](AsyncWebServerRequest* req) {
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
server.on("/api/baud", HTTP_GET, [](AsyncWebServerRequest* req) {
|
|
||||||
if (req->hasParam("baud")) setBaudRate(req->getParam("baud")->value().toInt());
|
|
||||||
req->send(200, "application/json", statusJson());
|
|
||||||
});
|
|
||||||
server.on("/api/port", HTTP_GET, [](AsyncWebServerRequest* req) {
|
|
||||||
if (req->hasParam("port")) {
|
|
||||||
String p = req->getParam("port")->value();
|
|
||||||
if (p.startsWith("int")) setActivePort(PORT_INTERNAL);
|
|
||||||
else if (p.startsWith("usb")) setActivePort(PORT_USB);
|
|
||||||
else if (p.startsWith("ext")) setActivePort(PORT_EXTERNAL);
|
|
||||||
}
|
|
||||||
req->send(200, "application/json", statusJson());
|
|
||||||
});
|
|
||||||
server.on("/api/log", HTTP_GET, [](AsyncWebServerRequest* req) {
|
|
||||||
if (req->hasParam("on")) {
|
|
||||||
logEnabled = req->getParam("on")->value().toInt() != 0;
|
|
||||||
if (logEnabled) openLogIfReady();
|
|
||||||
}
|
|
||||||
req->send(200, "application/json", statusJson());
|
|
||||||
});
|
|
||||||
server.on("/api/gpio", HTTP_GET, [](AsyncWebServerRequest* req) {
|
|
||||||
if (req->hasParam("pin") && req->hasParam("val")) {
|
|
||||||
int pin = req->getParam("pin")->value().toInt();
|
|
||||||
int val = req->getParam("val")->value().toInt();
|
|
||||||
if (pin >= 0) { pinMode(pin, OUTPUT); digitalWrite(pin, val ? HIGH : LOW); }
|
|
||||||
}
|
|
||||||
req->send(200, "text/plain", "ok\n");
|
|
||||||
});
|
|
||||||
server.on("/api/send", HTTP_GET, [](AsyncWebServerRequest* req) {
|
|
||||||
if (req->hasParam("data")) {
|
|
||||||
String d = req->getParam("data")->value();
|
|
||||||
forwardToTarget((const uint8_t*)d.c_str(), d.length());
|
|
||||||
const uint8_t nl = '\n'; forwardToTarget(&nl, 1);
|
|
||||||
}
|
|
||||||
req->send(200, "text/plain", "sent\n");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
#if BOARD_T3S3
|
// WebSocket handler callbacks
|
||||||
// List SD log files: JSON [{name,size,active}].
|
websocketHandler.onOpen([](PsychicWebSocketClient* client) {
|
||||||
server.on("/api/logs", HTTP_GET, [](AsyncWebServerRequest* req) {
|
Serial.printf("[WS] Client #%u connected from %s\n",
|
||||||
JsonDocument doc; JsonArray arr = doc.to<JsonArray>();
|
client->socket(), client->remoteIP().toString().c_str());
|
||||||
File dir = SD.open("/logs");
|
|
||||||
if (dir && dir.isDirectory()) {
|
|
||||||
for (File f = dir.openNextFile(); f; f = dir.openNextFile()) {
|
|
||||||
if (f.isDirectory()) continue;
|
|
||||||
const char* n = f.name();
|
|
||||||
const char* base = strrchr(n, '/'); base = base ? base + 1 : n;
|
|
||||||
JsonObject o = arr.add<JsonObject>();
|
|
||||||
o["name"] = base;
|
|
||||||
o["size"] = (uint32_t)f.size();
|
|
||||||
o["active"] = (logFile && strcmp(logPath, n) == 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String s; serializeJson(doc, s);
|
|
||||||
req->send(200, "application/json", s);
|
|
||||||
});
|
});
|
||||||
// Download a log file. ?name=<basename> (no slashes).
|
|
||||||
server.on("/api/logfile", HTTP_GET, [](AsyncWebServerRequest* req) {
|
|
||||||
if (!req->hasParam("name")) { req->send(400, "text/plain", "name?\n"); return; }
|
|
||||||
String name = req->getParam("name")->value();
|
|
||||||
if (name.indexOf('/') >= 0 || name.indexOf("..") >= 0) { req->send(400, "text/plain", "bad name\n"); return; }
|
|
||||||
String p = "/logs/" + name;
|
|
||||||
if (!SD.exists(p)) { req->send(404, "text/plain", "not found\n"); return; }
|
|
||||||
if (logFile) logFile.flush(); // flush in-flight bytes first
|
|
||||||
req->send(SD, p, "text/plain", true); // true => download
|
|
||||||
});
|
|
||||||
// Delete a log file (refuses the currently-open one).
|
|
||||||
server.on("/api/logdelete", HTTP_GET, [](AsyncWebServerRequest* req) {
|
|
||||||
if (!req->hasParam("name")) { req->send(400, "text/plain", "name?\n"); return; }
|
|
||||||
String name = req->getParam("name")->value();
|
|
||||||
if (name.indexOf('/') >= 0 || name.indexOf("..") >= 0) { req->send(400, "text/plain", "bad name\n"); return; }
|
|
||||||
String p = "/logs/" + name;
|
|
||||||
if (logFile && strcmp(logPath, p.c_str()) == 0) { req->send(409, "text/plain", "active log -- ~log off first\n"); return; }
|
|
||||||
bool ok = SD.remove(p);
|
|
||||||
req->send(ok ? 200 : 404, "text/plain", ok ? "deleted\n" : "not found\n");
|
|
||||||
});
|
|
||||||
#endif
|
|
||||||
|
|
||||||
server.onNotFound([](AsyncWebServerRequest* req) { req->send(404, "text/plain", "Not found"); });
|
websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame) {
|
||||||
|
handleWebSocketMessage(request->client(), frame->payload, frame->len);
|
||||||
|
return ESP_OK;
|
||||||
|
});
|
||||||
|
|
||||||
|
websocketHandler.onClose([](PsychicWebSocketClient* client) {
|
||||||
|
Serial.printf("[WS] Client #%u disconnected\n", client->socket());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach WebSocket handler to /ws endpoint
|
||||||
|
server.on("/ws", &websocketHandler);
|
||||||
|
|
||||||
|
// Start the server
|
||||||
server.begin();
|
server.begin();
|
||||||
|
Serial.println("[Web] Server started");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// WiFi
|
// WiFi Setup
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
bool connectToWiFi() {
|
bool connectToWiFi() {
|
||||||
|
Serial.printf("[WiFi] Connecting to %s", STA_SSID);
|
||||||
|
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
WiFi.begin(STR(STA_SSID), STR(STA_PASSWORD));
|
WiFi.begin(STA_SSID, STA_PASSWORD);
|
||||||
unsigned long t0 = millis();
|
|
||||||
|
unsigned long startTime = millis();
|
||||||
while (WiFi.status() != WL_CONNECTED) {
|
while (WiFi.status() != WL_CONNECTED) {
|
||||||
if (millis() - t0 > WIFI_CONNECT_TIMEOUT) return false;
|
if (millis() - startTime > WIFI_CONNECT_TIMEOUT) {
|
||||||
delay(500); Serial.print(".");
|
Serial.println(" TIMEOUT");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
Serial.printf("\n[WiFi] %s IP %s %d dBm\n", STR(STA_SSID),
|
delay(500);
|
||||||
WiFi.localIP().toString().c_str(), WiFi.RSSI());
|
Serial.print(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println(" OK");
|
||||||
|
Serial.printf("[WiFi] Connected to: %s\n", STA_SSID);
|
||||||
|
Serial.printf("[WiFi] IP Address: %s\n", WiFi.localIP().toString().c_str());
|
||||||
|
Serial.printf("[WiFi] Signal strength: %d dBm\n", WiFi.RSSI());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void startAccessPoint() {
|
void startAccessPoint() {
|
||||||
WiFi.mode(WIFI_AP);
|
WiFi.mode(WIFI_AP);
|
||||||
WiFi.softAP(AP_SSID, AP_PASSWORD);
|
WiFi.softAP(AP_SSID, AP_PASSWORD);
|
||||||
Serial.printf("[WiFi] AP %s IP %s\n", AP_SSID, WiFi.softAPIP().toString().c_str());
|
|
||||||
|
IPAddress ip = WiFi.softAPIP();
|
||||||
|
Serial.println("[WiFi] Access Point started");
|
||||||
|
Serial.printf("[WiFi] SSID: %s\n", AP_SSID);
|
||||||
|
Serial.printf("[WiFi] Password: %s\n", AP_PASSWORD);
|
||||||
|
Serial.printf("[WiFi] IP Address: %s\n", ip.toString().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupWiFi() {
|
void setupWiFi() {
|
||||||
WiFi.disconnect(true); delay(100);
|
WiFi.disconnect(true); // Clear any previous connection
|
||||||
if (!WIFI_STATION_MODE || !connectToWiFi()) startAccessPoint();
|
delay(100);
|
||||||
|
|
||||||
|
if (WIFI_STATION_MODE) {
|
||||||
|
// Try to connect to existing network
|
||||||
|
if (!connectToWiFi()) {
|
||||||
|
Serial.println("[WiFi] Station mode failed, falling back to AP mode");
|
||||||
|
startAccessPoint();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Start as Access Point directly
|
||||||
|
startAccessPoint();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// T3-S3 peripherals: NTP, SD, OLED
|
// Bluetooth Setup
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
#if BOARD_T3S3
|
void setupBluetooth() {
|
||||||
void setupSD() {
|
if (!SerialBT.begin(BT_NAME)) {
|
||||||
sdSPI.begin(T3S3_SD_SCK, T3S3_SD_MISO, T3S3_SD_MOSI, T3S3_SD_CS);
|
Serial.println("[BT] Failed to initialize Bluetooth");
|
||||||
sdAvailable = SD.begin(T3S3_SD_CS, sdSPI, T3S3_SD_FREQ_HZ);
|
return;
|
||||||
Serial.printf("[SD] %s (CS=%d)\n", sdAvailable ? "mounted" : "NOT found -- check T3S3_SD_* pins",
|
|
||||||
T3S3_SD_CS);
|
|
||||||
}
|
}
|
||||||
|
Serial.printf("[BT] Bluetooth started as '%s'\n", BT_NAME);
|
||||||
void setupNTP() { configTzTime(NTP_TZ, "pool.ntp.org", "time.google.com"); }
|
Serial.println("[BT] Ready for pairing");
|
||||||
|
|
||||||
void setupOLED() {
|
|
||||||
Wire.begin(T3S3_OLED_SDA, T3S3_OLED_SCL);
|
|
||||||
oledOk = oled.begin(SSD1306_SWITCHCAPVCC, T3S3_OLED_ADDR);
|
|
||||||
if (oledOk) { oled.clearDisplay(); oled.setTextColor(SSD1306_WHITE); oled.setTextSize(1); oled.display(); }
|
|
||||||
Serial.printf("[OLED] %s\n", oledOk ? "ok" : "NOT found");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void renderOLED() {
|
|
||||||
if (!oledOk) return;
|
|
||||||
int clients = 0;
|
|
||||||
for (int i = 0; i < MAX_TELNET; i++) if (telnetClients[i] && telnetClients[i].connected()) clients++;
|
|
||||||
String ip = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
|
|
||||||
oled.clearDisplay();
|
|
||||||
oled.setCursor(0, 0); oled.println(ip);
|
|
||||||
oled.setCursor(0, 10); oled.printf("%s %ddBm\n",
|
|
||||||
(WiFi.getMode()==WIFI_STA)?WiFi.SSID().c_str():AP_SSID,
|
|
||||||
(WiFi.getMode()==WIFI_STA)?WiFi.RSSI():0);
|
|
||||||
oled.setCursor(0, 20); oled.printf("uart %d/%d %lu\n", TARGET_RX_PIN, TARGET_TX_PIN, (unsigned long)baudSerial1);
|
|
||||||
oled.setCursor(0, 30); oled.printf("rx %lu tx %lu\n", (unsigned long)rxBytes, (unsigned long)txBytes);
|
|
||||||
oled.setCursor(0, 40); oled.printf("telnet %d log %s\n", clients, (logEnabled && logFile) ? "ON" : "off");
|
|
||||||
oled.setCursor(0, 50); oled.printf("ntp %s sd %s\n", ntpSynced ? "ok" : "--", sdAvailable ? "ok" : "--");
|
|
||||||
oled.display();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Setup / Loop
|
// Debug Output Helper
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Use this function in your code to write to the internal virtual serial
|
||||||
|
// These messages will be visible when PORT_INTERNAL is selected
|
||||||
|
void debugPrint(const char* format, ...) {
|
||||||
|
char buffer[256];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vsnprintf(buffer, sizeof(buffer), format, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
internalSerial.print(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void debugPrintln(const char* format, ...) {
|
||||||
|
char buffer[256];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vsnprintf(buffer, sizeof(buffer), format, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
internalSerial.println(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Main Setup
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
|
// Initialize USB serial (for local debugging)
|
||||||
Serial.begin(DEFAULT_BAUD_SERIAL);
|
Serial.begin(DEFAULT_BAUD_SERIAL);
|
||||||
delay(500);
|
delay(1000);
|
||||||
Serial.println("\n=== ESP32 Debug Dongle ===");
|
|
||||||
|
|
||||||
Serial1.begin(baudSerial1, SERIAL_8N1, TARGET_RX_PIN, TARGET_TX_PIN);
|
Serial.println("\n\n========================================");
|
||||||
Serial.printf("[Serial1] %lu baud RX=%d TX=%d\n", baudSerial1, TARGET_RX_PIN, TARGET_TX_PIN);
|
Serial.println(" ESP32 Debug Dongle (PsychicHttp)");
|
||||||
|
Serial.println("========================================\n");
|
||||||
|
|
||||||
gpioInit(GPIO_RESET_PIN);
|
// Initialize Serial1 for external device
|
||||||
gpioInit(GPIO_WAKE_PIN);
|
Serial1.begin(baudSerial1, SERIAL_8N1, SERIAL1_RX_PIN, SERIAL1_TX_PIN);
|
||||||
setActivePort(currentPort);
|
Serial.printf("[Serial1] Initialized at %lu baud (RX=%d, TX=%d)\n",
|
||||||
|
baudSerial1, SERIAL1_RX_PIN, SERIAL1_TX_PIN);
|
||||||
|
|
||||||
if (!LittleFS.begin(true)) Serial.println("[FS] LittleFS mount failed");
|
// Initialize LittleFS
|
||||||
|
if (!LittleFS.begin(true)) {
|
||||||
#if BOARD_T3S3
|
Serial.println("[FS] LittleFS mount failed!");
|
||||||
setupOLED();
|
} else {
|
||||||
setupSD();
|
Serial.println("[FS] LittleFS mounted");
|
||||||
#endif
|
|
||||||
|
|
||||||
setupWiFi();
|
|
||||||
|
|
||||||
#if BOARD_T3S3
|
|
||||||
if (WiFi.getMode() == WIFI_STA) setupNTP();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if HAS_BT_CLASSIC
|
|
||||||
if (SerialBT.begin(BT_NAME)) Serial.printf("[BT] '%s' ready\n", BT_NAME);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
setupTelnet();
|
|
||||||
setupWebServer();
|
|
||||||
|
|
||||||
String ip = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
|
|
||||||
Serial.printf("[Ready] http://%s telnet %s 23\n", ip.c_str(), ip.c_str());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup WiFi Access Point
|
||||||
|
setupWiFi();
|
||||||
|
|
||||||
|
// Setup Bluetooth
|
||||||
|
setupBluetooth();
|
||||||
|
|
||||||
|
// Setup Web Server
|
||||||
|
setupWebServer();
|
||||||
|
|
||||||
|
Serial.println("\n[System] Ready!");
|
||||||
|
String ip = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
|
||||||
|
Serial.printf("[System] Open http://%s in browser\n", ip.c_str());
|
||||||
|
Serial.println("[System] Or connect via Bluetooth\n");
|
||||||
|
|
||||||
|
// Send initial message to internal serial
|
||||||
|
debugPrintln("ESP32 Debug Dongle initialized");
|
||||||
|
debugPrintln("Free heap: %d bytes", ESP.getFreeHeap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Main Loop
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Buffer for efficient serial reading
|
||||||
static uint8_t serialBuffer[256];
|
static uint8_t serialBuffer[256];
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// Target UART -> sinks
|
// Read from active serial port and send to WebSocket + Bluetooth
|
||||||
size_t avail = activePort->available();
|
size_t available = activePort->available();
|
||||||
if (avail > 0) {
|
if (available > 0) {
|
||||||
size_t toRead = min(avail, sizeof(serialBuffer));
|
size_t toRead = min(available, sizeof(serialBuffer));
|
||||||
size_t got = 0;
|
size_t bytesRead = 0;
|
||||||
for (size_t i = 0; i < toRead; i++) { int c = activePort->read(); if (c >= 0) serialBuffer[got++] = (uint8_t)c; }
|
|
||||||
if (got > 0) broadcastFromTarget(serialBuffer, got);
|
// Read bytes into buffer
|
||||||
|
for (size_t i = 0; i < toRead; i++) {
|
||||||
|
int c = activePort->read();
|
||||||
|
if (c >= 0) {
|
||||||
|
serialBuffer[bytesRead++] = (uint8_t)c;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if HAS_BT_CLASSIC
|
if (bytesRead > 0) {
|
||||||
while (SerialBT.available()) { int c = SerialBT.read(); if (c >= 0) { uint8_t b = (uint8_t)c; forwardToTarget(&b, 1); } }
|
// Send to all WebSocket clients (PsychicHttp uses sendAll)
|
||||||
#endif
|
websocketHandler.sendAll((char*)serialBuffer, bytesRead);
|
||||||
|
|
||||||
serviceTelnet();
|
// Send to Bluetooth if connected
|
||||||
ws.cleanupClients();
|
if (SerialBT.connected()) {
|
||||||
|
SerialBT.write(serialBuffer, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if BOARD_T3S3
|
// Read from Bluetooth and send to active serial port
|
||||||
// Once NTP lands, mark synced + open today's log.
|
while (SerialBT.available()) {
|
||||||
if (!ntpSynced && time(nullptr) > 1700000000) { ntpSynced = true; if (logEnabled) openLogIfReady(); }
|
int c = SerialBT.read();
|
||||||
static unsigned long lastOled = 0;
|
if (c >= 0) {
|
||||||
if (millis() - lastOled > 500) { lastOled = millis(); renderOLED(); }
|
activePort->write((uint8_t)c);
|
||||||
#endif
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodic internal debug messages (example)
|
||||||
|
static unsigned long lastDebug = 0;
|
||||||
|
if (millis() - lastDebug > 30000) { // Every 30 seconds
|
||||||
|
lastDebug = millis();
|
||||||
|
debugPrintln("[%lu] Heartbeat - Heap: %d, WS clients: %d",
|
||||||
|
millis() / 1000, ESP.getFreeHeap(), websocketHandler.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to prevent WDT issues
|
||||||
delay(1);
|
delay(1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user