From fc84cfa66a5cea99adc75c3ca2552267f9d693cb Mon Sep 17 00:00:00 2001 From: Scott Penrose Date: Tue, 9 Dec 2025 18:40:35 +1100 Subject: [PATCH] Debug dongle test code --- .gitignore | 2 + README.md | 241 +++++++++++++++++++++ data/index.html | 429 +++++++++++++++++++++++++++++++++++++ data/index_offline.html | 214 +++++++++++++++++++ platformio.ini | 47 +++++ scripts/download_xterm.py | 62 ++++++ src/LoopbackStream.h | 140 ++++++++++++ src/main.cpp | 433 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 1568 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 data/index.html create mode 100644 data/index_offline.html create mode 100644 platformio.ini create mode 100644 scripts/download_xterm.py create mode 100644 src/LoopbackStream.h create mode 100644 src/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d124d4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.pio diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae15f2a --- /dev/null +++ b/README.md @@ -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: +# +# +# 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 diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000..39746de --- /dev/null +++ b/data/index.html @@ -0,0 +1,429 @@ + + + + + + ESP32 Debug Dongle + + + + + + + + + + +
+

🔌 ESP32 Debug Dongle

+
+
+ + +
+
+ + +
+ + +
+
+
+ + WebSocket +
+
+ + Bluetooth +
+ +
+
+ +
+
+
+ + + + + + diff --git a/data/index_offline.html b/data/index_offline.html new file mode 100644 index 0000000..5e7e177 --- /dev/null +++ b/data/index_offline.html @@ -0,0 +1,214 @@ + + + + + + ESP32 Debug Dongle (Offline) + + + +
+

🔌 ESP32 Debug

+ + + + + Disconnected +
+
+
+
+ + +
+
+ + + + diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..5fc3e97 --- /dev/null +++ b/platformio.ini @@ -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 diff --git a/scripts/download_xterm.py b/scripts/download_xterm.py new file mode 100644 index 0000000..7c3761a --- /dev/null +++ b/scripts/download_xterm.py @@ -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(' ') + print(' ') + 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() diff --git a/src/LoopbackStream.h b/src/LoopbackStream.h new file mode 100644 index 0000000..869bfff --- /dev/null +++ b/src/LoopbackStream.h @@ -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 +#include + +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 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..6b01cc0 --- /dev/null +++ b/src/main.cpp @@ -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 +#include +#include +#include +#include +#include +#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); +}