Debug dongle test code
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
.pio
|
||||||
241
README.md
Normal file
241
README.md
Normal 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
429
data/index.html
Normal 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
214
data/index_offline.html
Normal 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
47
platformio.ini
Normal 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
62
scripts/download_xterm.py
Normal 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
140
src/LoopbackStream.h
Normal 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
433
src/main.cpp
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user