Debug dongle test code

This commit is contained in:
2025-12-09 18:40:35 +11:00
commit fc84cfa66a
8 changed files with 1568 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.pio

241
README.md Normal file
View File

@@ -0,0 +1,241 @@
# ESP32 Debug Dongle
A WiFi/Bluetooth serial debugging tool for ESP32. Access serial ports via web browser or Bluetooth terminal.
## 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
## 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 for External Serial (Serial1)
| ESP32 Pin | Function | Connect To |
|-----------|----------|------------|
| GPIO16 | RX1 | External device TX |
| GPIO17 | TX1 | External device RX |
| GND | Ground | External device GND |
## Quick Start
### 1. Install PlatformIO
```bash
# Install PlatformIO CLI (if not already installed)
pip install platformio
# Or use VS Code with PlatformIO IDE extension
```
### 2. Build and Upload
```bash
# Clone/copy this project
cd esp32-debug-dongle
# Build the firmware
pio run
# Upload firmware to ESP32
pio run -t upload
# Upload web files to LittleFS
pio run -t uploadfs
```
### 3. Connect
#### Via WiFi (Web Terminal)
1. Connect to WiFi network: `ESP32-DebugDongle`
2. Password: `debug1234`
3. Open browser: `http://192.168.4.1`
#### Via Bluetooth
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`
## Usage
### 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
### Serial Ports
| 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 |
### Using Internal Debug Output
In your ESP32 code, use the provided helper functions:
```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");
```
These messages appear when "Internal" port is selected.
## Configuration
Edit `src/main.cpp` to change defaults:
```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
```
esp32-debug-dongle/
├── platformio.ini # PlatformIO configuration
├── src/
│ └── main.cpp # Main ESP32 firmware
├── data/
│ └── index.html # Web interface (uploaded to LittleFS)
├── scripts/
│ └── download_xterm.py # Optional: download xterm.js locally
└── 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
## License
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

429
data/index.html Normal file
View File

@@ -0,0 +1,429 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Debug Dongle</title>
<!-- xterm.js from CDN -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #1a1a2e;
color: #eee;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: #16213e;
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #0f3460;
}
.header h1 {
margin: 0;
font-size: 1.2em;
color: #e94560;
}
.controls {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
.control-group label {
font-size: 0.85em;
color: #aaa;
}
select, button {
background: #0f3460;
color: #fff;
border: 1px solid #1a1a2e;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
select:hover, button:hover {
background: #1a4a7a;
}
button.danger {
background: #e94560;
}
button.danger:hover {
background: #ff6b6b;
}
.status {
display: flex;
gap: 15px;
font-size: 0.8em;
}
.status-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #666;
}
.status-dot.connected {
background: #4ade80;
}
.status-dot.disconnected {
background: #ef4444;
}
.terminal-container {
flex: 1;
padding: 10px;
overflow: hidden;
}
#terminal {
height: 100%;
}
.footer {
background: #16213e;
padding: 8px 20px;
font-size: 0.75em;
color: #666;
border-top: 1px solid #0f3460;
display: flex;
justify-content: space-between;
}
/* Responsive */
@media (max-width: 768px) {
.controls {
gap: 10px;
}
.control-group label {
display: none;
}
}
</style>
</head>
<body>
<div class="header">
<h1>🔌 ESP32 Debug Dongle</h1>
<div class="controls">
<div class="control-group">
<label>Port:</label>
<select id="portSelect">
<option value="0">Internal (Debug)</option>
<option value="1">USB Serial</option>
<option value="2">External (Serial1)</option>
</select>
</div>
<div class="control-group">
<label>Baud:</label>
<select id="baudSelect">
<option value="9600">9600</option>
<option value="19200">19200</option>
<option value="38400">38400</option>
<option value="57600">57600</option>
<option value="115200" selected>115200</option>
<option value="230400">230400</option>
<option value="460800">460800</option>
<option value="921600">921600</option>
</select>
</div>
<button onclick="clearTerminal()">Clear</button>
<button onclick="reconnect()">Reconnect</button>
</div>
<div class="status">
<div class="status-item">
<span class="status-dot" id="wsStatus"></span>
<span>WebSocket</span>
</div>
<div class="status-item">
<span class="status-dot" id="btStatus"></span>
<span>Bluetooth</span>
</div>
<div class="status-item" id="wifiInfo" style="display:none;">
<span id="wifiIcon">📶</span>
<span id="wifiText">WiFi</span>
</div>
</div>
</div>
<div class="terminal-container">
<div id="terminal"></div>
</div>
<div class="footer">
<span id="rxCount">RX: 0 bytes</span>
<span id="txCount">TX: 0 bytes</span>
<span id="heap">Heap: --</span>
</div>
<script>
// Terminal setup
const term = new Terminal({
cursorBlink: true,
fontSize: 14,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
theme: {
background: '#1a1a2e',
foreground: '#eee',
cursor: '#e94560',
cursorAccent: '#1a1a2e',
selection: 'rgba(233, 69, 96, 0.3)',
black: '#1a1a2e',
red: '#e94560',
green: '#4ade80',
yellow: '#fbbf24',
blue: '#60a5fa',
magenta: '#c084fc',
cyan: '#22d3d3',
white: '#eee',
brightBlack: '#666',
brightRed: '#ff6b6b',
brightGreen: '#86efac',
brightYellow: '#fcd34d',
brightBlue: '#93c5fd',
brightMagenta: '#d8b4fe',
brightCyan: '#67e8f9',
brightWhite: '#fff'
}
});
// Addons
const fitAddon = new FitAddon.FitAddon();
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
term.loadAddon(fitAddon);
term.loadAddon(webLinksAddon);
// Open terminal
term.open(document.getElementById('terminal'));
fitAddon.fit();
// Resize handler
window.addEventListener('resize', () => {
fitAddon.fit();
});
// Stats
let rxBytes = 0;
let txBytes = 0;
// WebSocket connection
let ws = null;
let reconnectTimer = null;
function connect() {
const wsUrl = `ws://${window.location.host}/ws`;
term.writeln(`\x1b[33mConnecting to ${wsUrl}...\x1b[0m`);
ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
term.writeln('\x1b[32mConnected!\x1b[0m\r\n');
updateStatus('wsStatus', true);
// Request initial status
sendCommand({ cmd: 'getStatus' });
};
ws.onclose = () => {
term.writeln('\r\n\x1b[31mDisconnected\x1b[0m');
updateStatus('wsStatus', false);
// Auto-reconnect after 3 seconds
if (!reconnectTimer) {
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
connect();
}, 3000);
}
};
ws.onerror = (err) => {
term.writeln('\r\n\x1b[31mWebSocket error\x1b[0m');
console.error('WebSocket error:', err);
};
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const data = new Uint8Array(event.data);
// Check for command response (starts with 0x00)
if (data.length > 0 && data[0] === 0x00) {
const jsonStr = new TextDecoder().decode(data.slice(1));
try {
const msg = JSON.parse(jsonStr);
handleResponse(msg);
} catch (e) {
console.error('JSON parse error:', e);
}
} else {
// Regular serial data
rxBytes += data.length;
updateStats();
// Write to terminal
const text = new TextDecoder().decode(data);
term.write(text);
}
}
};
}
function handleResponse(msg) {
switch (msg.type) {
case 'status':
document.getElementById('portSelect').value = msg.currentPort;
document.getElementById('baudSelect').value = msg.baudSerial1;
updateStatus('btStatus', msg.btConnected);
document.getElementById('heap').textContent = `Heap: ${msg.freeHeap}`;
// Show WiFi info
const wifiInfo = document.getElementById('wifiInfo');
const wifiText = document.getElementById('wifiText');
wifiInfo.style.display = 'flex';
if (msg.wifiMode === 'station' && msg.rssi) {
const signal = msg.rssi > -50 ? '📶' : msg.rssi > -70 ? '📶' : '📶';
wifiText.textContent = `${msg.rssi}dBm`;
} else {
wifiText.textContent = 'AP Mode';
}
break;
case 'portChanged':
document.getElementById('portSelect').value = msg.port;
const portNames = ['Internal', 'USB Serial', 'External'];
term.writeln(`\r\n\x1b[33m[Switched to ${portNames[msg.port]}]\x1b[0m\r\n`);
break;
}
}
function sendCommand(cmd) {
if (ws && ws.readyState === WebSocket.OPEN) {
const json = JSON.stringify(cmd);
const data = new Uint8Array(json.length + 1);
data[0] = 0x00; // Command prefix
for (let i = 0; i < json.length; i++) {
data[i + 1] = json.charCodeAt(i);
}
ws.send(data);
}
}
function sendData(data) {
if (ws && ws.readyState === WebSocket.OPEN) {
const bytes = new TextEncoder().encode(data);
txBytes += bytes.length;
updateStats();
ws.send(bytes);
}
}
// Terminal input handler
term.onData(data => {
sendData(data);
});
// UI handlers
document.getElementById('portSelect').addEventListener('change', (e) => {
sendCommand({ cmd: 'setPort', port: parseInt(e.target.value) });
});
document.getElementById('baudSelect').addEventListener('change', (e) => {
const port = parseInt(document.getElementById('portSelect').value);
sendCommand({ cmd: 'setBaud', port: port, baud: parseInt(e.target.value) });
});
function clearTerminal() {
term.clear();
rxBytes = 0;
txBytes = 0;
updateStats();
}
function reconnect() {
if (ws) {
ws.close();
}
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
setTimeout(connect, 100);
}
function updateStatus(elementId, connected) {
const el = document.getElementById(elementId);
el.classList.toggle('connected', connected);
el.classList.toggle('disconnected', !connected);
}
function updateStats() {
document.getElementById('rxCount').textContent = `RX: ${formatBytes(rxBytes)}`;
document.getElementById('txCount').textContent = `TX: ${formatBytes(txBytes)}`;
}
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
}
// Initial connection
term.writeln('\x1b[1;36m╔═══════════════════════════════════════╗\x1b[0m');
term.writeln('\x1b[1;36m║ ESP32 Debug Dongle Terminal ║\x1b[0m');
term.writeln('\x1b[1;36m╚═══════════════════════════════════════╝\x1b[0m');
term.writeln('');
term.writeln('Ports:');
term.writeln(' • Internal: ESP32 debug output (virtual serial)');
term.writeln(' • USB Serial: Main UART (shared with USB)');
term.writeln(' • External: Serial1 (GPIO16=RX, GPIO17=TX)');
term.writeln('');
connect();
// Periodic status update
setInterval(() => {
sendCommand({ cmd: 'getStatus' });
}, 5000);
</script>
</body>
</html>

214
data/index_offline.html Normal file
View File

@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Debug Dongle (Offline)</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: monospace;
background: #1a1a2e;
color: #eee;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: #16213e;
padding: 10px 15px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
border-bottom: 1px solid #0f3460;
}
.header h1 { font-size: 1em; color: #e94560; }
select, button, input {
background: #0f3460;
color: #fff;
border: 1px solid #1a1a2e;
padding: 5px 10px;
border-radius: 3px;
font-family: monospace;
}
.terminal-wrap {
flex: 1;
display: flex;
flex-direction: column;
padding: 10px;
overflow: hidden;
}
#output {
flex: 1;
background: #0d0d1a;
border: 1px solid #333;
padding: 10px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
font-size: 14px;
line-height: 1.4;
}
#inputLine {
display: flex;
margin-top: 5px;
}
#input {
flex: 1;
background: #0d0d1a;
border: 1px solid #333;
color: #4ade80;
padding: 8px;
font-size: 14px;
}
#input:focus { outline: 1px solid #e94560; }
.status { font-size: 0.8em; color: #666; }
.status.connected { color: #4ade80; }
.green { color: #4ade80; }
.red { color: #ef4444; }
.yellow { color: #fbbf24; }
.cyan { color: #22d3d3; }
</style>
</head>
<body>
<div class="header">
<h1>🔌 ESP32 Debug</h1>
<select id="port">
<option value="0">Internal</option>
<option value="1">USB</option>
<option value="2">External</option>
</select>
<select id="baud">
<option value="9600">9600</option>
<option value="115200" selected>115200</option>
<option value="921600">921600</option>
</select>
<button onclick="clear_()">Clear</button>
<button onclick="reconnect()">Reconnect</button>
<span id="status" class="status">Disconnected</span>
</div>
<div class="terminal-wrap">
<div id="output"></div>
<div id="inputLine">
<input type="text" id="input" placeholder="Type and press Enter..." autofocus>
<button onclick="sendInput()">Send</button>
</div>
</div>
<script>
const output = document.getElementById('output');
const input = document.getElementById('input');
const status = document.getElementById('status');
let ws = null;
let history = [];
let histIdx = 0;
function log(msg, cls='') {
const span = document.createElement('span');
if(cls) span.className = cls;
span.textContent = msg;
output.appendChild(span);
output.scrollTop = output.scrollHeight;
}
function connect() {
log('Connecting...\\n', 'yellow');
ws = new WebSocket('ws://' + location.host + '/ws');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
log('Connected!\\n', 'green');
status.textContent = 'Connected';
status.className = 'status connected';
sendCmd({cmd:'getStatus'});
};
ws.onclose = () => {
log('Disconnected\\n', 'red');
status.textContent = 'Disconnected';
status.className = 'status';
setTimeout(connect, 3000);
};
ws.onmessage = (e) => {
const data = new Uint8Array(e.data);
if(data[0] === 0) {
// Command response
try {
const json = JSON.parse(new TextDecoder().decode(data.slice(1)));
if(json.type === 'status') {
document.getElementById('port').value = json.currentPort;
document.getElementById('baud').value = json.baudSerial1;
} else if(json.type === 'portChanged') {
log('[Port changed]\\n', 'yellow');
}
} catch(e) {}
} else {
output.appendChild(document.createTextNode(new TextDecoder().decode(data)));
output.scrollTop = output.scrollHeight;
}
};
}
function sendCmd(cmd) {
if(ws && ws.readyState === 1) {
const json = JSON.stringify(cmd);
const arr = new Uint8Array(json.length + 1);
arr[0] = 0;
for(let i=0; i<json.length; i++) arr[i+1] = json.charCodeAt(i);
ws.send(arr);
}
}
function sendData(str) {
if(ws && ws.readyState === 1) {
ws.send(new TextEncoder().encode(str));
}
}
function sendInput() {
const val = input.value;
if(val) {
history.push(val);
histIdx = history.length;
sendData(val + '\\r\\n');
log('> ' + val + '\\n', 'cyan');
input.value = '';
}
}
function clear_() {
output.innerHTML = '';
}
function reconnect() {
if(ws) ws.close();
}
input.addEventListener('keydown', (e) => {
if(e.key === 'Enter') sendInput();
else if(e.key === 'ArrowUp' && histIdx > 0) {
histIdx--;
input.value = history[histIdx];
} else if(e.key === 'ArrowDown' && histIdx < history.length) {
histIdx++;
input.value = history[histIdx] || '';
}
});
document.getElementById('port').onchange = (e) => {
sendCmd({cmd:'setPort', port:parseInt(e.target.value)});
};
document.getElementById('baud').onchange = (e) => {
const port = parseInt(document.getElementById('port').value);
sendCmd({cmd:'setBaud', port:port, baud:parseInt(e.target.value)});
};
log('ESP32 Debug Dongle - Offline Version\\n', 'cyan');
log('(Lightweight terminal without xterm.js)\\n\\n');
connect();
</script>
</body>
</html>

47
platformio.ini Normal file
View File

@@ -0,0 +1,47 @@
; PlatformIO Project Configuration File
; ESP32 Debug Dongle - Web Serial Terminal with Bluetooth
;
; Build and upload:
; pio run -t upload
;
; Upload filesystem (LittleFS with web files):
; pio run -t uploadfs
;
; Monitor serial output:
; pio device monitor
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
board_build.filesystem = littlefs
; 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 =
-DCORE_DEBUG_LEVEL=3
-DCONFIG_BT_ENABLED=1
-DCONFIG_BLUEDROID_ENABLED=1
; Libraries
lib_deps =
; Async Web Server and dependencies
https://github.com/ESP32Async/ESPAsyncWebServer
https://github.com/ESP32Async/AsyncTCP
; ArduinoJson for configuration/commands
ArduinoJson
; Upload settings (adjust port as needed)
; upload_port = /dev/ttyUSB0
; upload_speed = 921600
; Extra scripts for LittleFS
extra_scripts =
pre:scripts/download_xterm.py

62
scripts/download_xterm.py Normal file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""
Pre-build script for PlatformIO
Downloads xterm.js files for local hosting (optional)
To use local files instead of CDN, run:
python scripts/download_xterm.py --local
Then update data/index.html to use local paths instead of CDN URLs.
"""
import os
import sys
# This script runs before build but doesn't do anything by default
# The HTML uses CDN links which work fine when you have internet
def main():
# Check if --local flag was passed
if '--local' in sys.argv:
try:
import urllib.request
data_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data')
js_dir = os.path.join(data_dir, 'js')
css_dir = os.path.join(data_dir, 'css')
os.makedirs(js_dir, exist_ok=True)
os.makedirs(css_dir, exist_ok=True)
files = [
('https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js',
os.path.join(js_dir, 'xterm.min.js')),
('https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css',
os.path.join(css_dir, 'xterm.min.css')),
('https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js',
os.path.join(js_dir, 'xterm-addon-fit.min.js')),
('https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js',
os.path.join(js_dir, 'xterm-addon-web-links.min.js')),
]
for url, path in files:
if not os.path.exists(path):
print(f'Downloading {url}...')
urllib.request.urlretrieve(url, path)
print(f' -> {path}')
print('\nLocal files downloaded!')
print('Update index.html to use local paths:')
print(' <link rel="stylesheet" href="/css/xterm.min.css">')
print(' <script src="/js/xterm.min.js"></script>')
print(' etc.')
except Exception as e:
print(f'Warning: Could not download xterm.js files: {e}')
print('The HTML will use CDN links instead.')
# Normal build - do nothing
pass
if __name__ == '__main__':
main()

140
src/LoopbackStream.h Normal file
View File

@@ -0,0 +1,140 @@
/**
* LoopbackStream.h
*
* A simple Arduino Stream implementation with a ring buffer.
* Data written with write() can be read back with read().
*
* Perfect for creating virtual serial ports for internal debug output.
*/
#ifndef LOOPBACK_STREAM_H
#define LOOPBACK_STREAM_H
#include <Arduino.h>
#include <Stream.h>
class LoopbackStream : public Stream {
public:
/**
* Create a loopback stream with specified buffer size
* @param bufferSize Size of the ring buffer in bytes
*/
LoopbackStream(size_t bufferSize = 256) : _bufferSize(bufferSize) {
_buffer = new uint8_t[bufferSize];
_head = 0;
_tail = 0;
_count = 0;
}
~LoopbackStream() {
delete[] _buffer;
}
// Stream methods (reading)
int available() override {
return _count;
}
int read() override {
if (_count == 0) {
return -1;
}
uint8_t c = _buffer[_tail];
_tail = (_tail + 1) % _bufferSize;
_count--;
return c;
}
int peek() override {
if (_count == 0) {
return -1;
}
return _buffer[_tail];
}
// Print methods (writing)
size_t write(uint8_t c) override {
if (_count >= _bufferSize) {
// Buffer full - drop oldest byte (overwrite mode)
_tail = (_tail + 1) % _bufferSize;
_count--;
}
_buffer[_head] = c;
_head = (_head + 1) % _bufferSize;
_count++;
return 1;
}
size_t write(const uint8_t* buffer, size_t size) override {
size_t written = 0;
for (size_t i = 0; i < size; i++) {
write(buffer[i]);
written++;
}
return written;
}
// Additional utility methods
/**
* Clear all data in the buffer
*/
void clear() {
_head = 0;
_tail = 0;
_count = 0;
}
/**
* Check if buffer is empty
*/
bool isEmpty() const {
return _count == 0;
}
/**
* Check if buffer is full
*/
bool isFull() const {
return _count >= _bufferSize;
}
/**
* Get current number of bytes in buffer
*/
size_t count() const {
return _count;
}
/**
* Get total buffer capacity
*/
size_t capacity() const {
return _bufferSize;
}
/**
* Get available space for writing
*/
int availableForWrite() override {
return _bufferSize - _count;
}
/**
* Flush is a no-op for loopback stream
*/
void flush() override {
// Nothing to flush - data is immediately available
}
private:
uint8_t* _buffer;
size_t _bufferSize;
size_t _head; // Write position
size_t _tail; // Read position
size_t _count; // Number of bytes in buffer
};
#endif // LOOPBACK_STREAM_H

433
src/main.cpp Normal file
View File

@@ -0,0 +1,433 @@
/**
* ESP32 Debug Dongle
*
* Web-based and Bluetooth serial terminal with multi-port support.
*
* Features:
* - Web terminal using xterm.js over WebSocket
* - Bluetooth Classic SPP serial bridge
* - Switch between internal (virtual), Serial, and Serial1
* - Configurable baud rates
*
* Hardware connections for Serial1 (external device):
* GPIO16 = RX1 (connect to external TX)
* GPIO17 = TX1 (connect to external RX)
* GND = Common ground
*/
#include <Arduino.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "BluetoothSerial.h"
#include "LoopbackStream.h"
// ============================================================================
// Configuration
// ============================================================================
// WiFi Mode: Set to true to connect to existing network, false for AP mode
#define WIFI_STATION_MODE true
// WiFi Station mode settings (connect to existing network)
const char* STA_SSID = "MeridenRainbow5G"; // Your WiFi network name
const char* STA_PASSWORD = "4z8bcw5vfrs3n7dm"; // Your WiFi password
// WiFi Access Point fallback settings (if station fails or AP mode selected)
const char* AP_SSID = "ESP32-DebugDongle";
const char* AP_PASSWORD = "debug1234"; // Min 8 characters
// Station mode connection timeout (milliseconds)
#define WIFI_CONNECT_TIMEOUT 15000
// Bluetooth device name
const char* BT_NAME = "ESP32-Debug";
// Serial1 pins (external device connection)
#define SERIAL1_RX_PIN 16
#define SERIAL1_TX_PIN 17
// Default baud rates
#define DEFAULT_BAUD_SERIAL 115200
#define DEFAULT_BAUD_SERIAL1 115200
// Internal serial buffer size
#define INTERNAL_BUFFER_SIZE 2048
// ============================================================================
// Global Objects
// ============================================================================
// Web server on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
// Bluetooth Serial
BluetoothSerial SerialBT;
// Hardware serial for external device
// HardwareSerial Serial1(1);
// Virtual/loopback serial for internal debug output
LoopbackStream internalSerial(INTERNAL_BUFFER_SIZE);
// Active serial port pointer
Stream* activePort = &internalSerial;
// Port identifiers
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;
// ============================================================================
// Serial Port Management
// ============================================================================
void setActivePort(SerialPortId port) {
currentPort = port;
switch (port) {
case PORT_INTERNAL:
activePort = &internalSerial;
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(SerialPortId port, uint32_t baud) {
if (port == PORT_EXTERNAL) {
baudSerial1 = baud;
Serial1.end();
Serial1.begin(baud, SERIAL_8N1, SERIAL1_RX_PIN, SERIAL1_TX_PIN);
Serial.printf("[System] Serial1 baud rate set to %lu\n", baud);
}
}
// ============================================================================
// WebSocket Handlers
// ============================================================================
void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t len) {
// Check for command prefix (starts with 0x00)
if (len > 0 && data[0] == 0x00) {
// Command message - parse JSON
String cmdStr = String((char*)(data + 1)).substring(0, len - 1);
JsonDocument doc;
DeserializationError err = deserializeJson(doc, cmdStr);
if (!err) {
const char* cmd = doc["cmd"];
if (strcmp(cmd, "setPort") == 0) {
int port = doc["port"];
setActivePort((SerialPortId)port);
// Send confirmation
String response;
JsonDocument respDoc;
respDoc["type"] = "portChanged";
respDoc["port"] = port;
serializeJson(respDoc, response);
client->text(String((char)0x00) + response);
}
else if (strcmp(cmd, "setBaud") == 0) {
int port = doc["port"];
uint32_t baud = doc["baud"];
setBaudRate((SerialPortId)port, baud);
}
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);
client->text(String((char)0x00) + response);
}
}
} else {
// Regular serial data - send to active port
activePort->write(data, len);
}
}
void onWsEvent(AsyncWebSocket* server, AsyncWebSocketClient* client,
AwsEventType type, void* arg, uint8_t* data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("[WS] Client #%u connected from %s\n",
client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("[WS] Client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA: {
AwsFrameInfo* info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len) {
handleWebSocketMessage(client, data, len);
}
break;
}
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
// ============================================================================
// Web Server Setup
// ============================================================================
void setupWebServer() {
// Serve static files from LittleFS
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
// WebSocket handler
ws.onEvent(onWsEvent);
server.addHandler(&ws);
// API endpoint for status (can be used without WebSocket)
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest* request) {
String response;
JsonDocument doc;
doc["currentPort"] = currentPort;
doc["baudSerial1"] = baudSerial1;
doc["btConnected"] = SerialBT.connected();
doc["freeHeap"] = ESP.getFreeHeap();
doc["wifiMode"] = (WiFi.getMode() == WIFI_STA) ? "station" : "ap";
doc["ip"] = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
doc["ssid"] = (WiFi.getMode() == WIFI_STA) ? WiFi.SSID() : AP_SSID;
if (WiFi.getMode() == WIFI_STA) {
doc["rssi"] = WiFi.RSSI();
}
serializeJson(doc, response);
request->send(200, "application/json", response);
});
// 404 handler
server.onNotFound([](AsyncWebServerRequest* request) {
request->send(404, "text/plain", "Not found");
});
server.begin();
Serial.println("[Web] Server started");
}
// ============================================================================
// WiFi Setup
// ============================================================================
bool connectToWiFi() {
Serial.printf("[WiFi] Connecting to %s", STA_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(STA_SSID, STA_PASSWORD);
unsigned long startTime = millis();
while (WiFi.status() != WL_CONNECTED) {
if (millis() - startTime > WIFI_CONNECT_TIMEOUT) {
Serial.println(" TIMEOUT");
return false;
}
delay(500);
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;
}
void startAccessPoint() {
WiFi.mode(WIFI_AP);
WiFi.softAP(AP_SSID, AP_PASSWORD);
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() {
WiFi.disconnect(true); // Clear any previous connection
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();
}
}
// ============================================================================
// Bluetooth Setup
// ============================================================================
void setupBluetooth() {
if (!SerialBT.begin(BT_NAME)) {
Serial.println("[BT] Failed to initialize Bluetooth");
return;
}
Serial.printf("[BT] Bluetooth started as '%s'\n", BT_NAME);
Serial.println("[BT] Ready for pairing");
}
// ============================================================================
// 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() {
// Initialize USB serial (for local debugging)
Serial.begin(DEFAULT_BAUD_SERIAL);
delay(1000);
Serial.println("\n\n========================================");
Serial.println(" ESP32 Debug Dongle");
Serial.println("========================================\n");
// Initialize Serial1 for external device
Serial1.begin(baudSerial1, SERIAL_8N1, SERIAL1_RX_PIN, SERIAL1_TX_PIN);
Serial.printf("[Serial1] Initialized at %lu baud (RX=%d, TX=%d)\n",
baudSerial1, SERIAL1_RX_PIN, SERIAL1_TX_PIN);
// Initialize LittleFS
if (!LittleFS.begin(true)) {
Serial.println("[FS] LittleFS mount failed!");
} else {
Serial.println("[FS] LittleFS mounted");
}
// 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];
void loop() {
// Read from active serial port and send to WebSocket + Bluetooth
size_t available = activePort->available();
if (available > 0) {
size_t toRead = min(available, sizeof(serialBuffer));
size_t bytesRead = 0;
// 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 (bytesRead > 0) {
// Send to all WebSocket clients
ws.binaryAll(serialBuffer, bytesRead);
// Send to Bluetooth if connected
if (SerialBT.connected()) {
SerialBT.write(serialBuffer, bytesRead);
}
}
}
// Read from Bluetooth and send to active serial port
while (SerialBT.available()) {
int c = SerialBT.read();
if (c >= 0) {
activePort->write((uint8_t)c);
}
}
// Cleanup disconnected WebSocket clients
ws.cleanupClients();
// 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(), ws.count());
}
// Small delay to prevent WDT issues
delay(1);
}