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