Compare commits

1 Commits

3 changed files with 94 additions and 86 deletions

View File

@@ -9,6 +9,7 @@ A WiFi/Bluetooth serial debugging tool for ESP32. Access serial ports via web br
- **Multi-Port**: Switch between internal debug, USB serial, and external serial - **Multi-Port**: Switch between internal debug, USB serial, and external serial
- **Virtual Serial**: Internal loopback for ESP32's own debug output - **Virtual Serial**: Internal loopback for ESP32's own debug output
- **Configurable**: Change baud rates on the fly - **Configurable**: Change baud rates on the fly
- **Robust Server**: Uses PsychicHttp - stable under load (unlike ESPAsyncWebServer)
## Hardware ## Hardware
@@ -53,8 +54,19 @@ pio run -t uploadfs
### 3. Connect ### 3. Connect
#### Via WiFi (Web Terminal) #### Via WiFi (Station Mode - Default)
1. Edit `src/main.cpp` and set your WiFi credentials:
```cpp
const char* STA_SSID = "YourNetworkSSID";
const char* STA_PASSWORD = "YourPassword";
```
2. Upload and check serial monitor for the assigned IP address
3. Open browser: `http://<ip-address>`
#### Via WiFi (AP Mode Fallback)
If station mode fails, or you set `WIFI_STATION_MODE false`:
1. Connect to WiFi network: `ESP32-DebugDongle` 1. Connect to WiFi network: `ESP32-DebugDongle`
2. Password: `debug1234` 2. Password: `debug1234`
3. Open browser: `http://192.168.4.1` 3. Open browser: `http://192.168.4.1`
@@ -106,10 +118,20 @@ These messages appear when "Internal" port is selected.
Edit `src/main.cpp` to change defaults: Edit `src/main.cpp` to change defaults:
```cpp ```cpp
// WiFi Access Point // WiFi Mode: true = connect to existing network, false = create AP
#define WIFI_STATION_MODE true
// Your WiFi network credentials (station mode)
const char* STA_SSID = "YourNetworkSSID";
const char* STA_PASSWORD = "YourPassword";
// Fallback Access Point settings
const char* AP_SSID = "ESP32-DebugDongle"; const char* AP_SSID = "ESP32-DebugDongle";
const char* AP_PASSWORD = "debug1234"; const char* AP_PASSWORD = "debug1234";
// Connection timeout before falling back to AP mode
#define WIFI_CONNECT_TIMEOUT 15000
// Bluetooth name // Bluetooth name
const char* BT_NAME = "ESP32-Debug"; const char* BT_NAME = "ESP32-Debug";
@@ -122,6 +144,18 @@ const char* BT_NAME = "ESP32-Debug";
#define DEFAULT_BAUD_SERIAL1 115200 #define DEFAULT_BAUD_SERIAL1 115200
``` ```
### WiFi Modes
**Station Mode** (`WIFI_STATION_MODE true`):
- Connects to your existing WiFi network
- Access the dongle from any device on the same network
- Falls back to AP mode if connection fails
**Access Point Mode** (`WIFI_STATION_MODE false`):
- Creates its own WiFi network
- Connect directly to the ESP32's network
- IP address: 192.168.4.1
## Project Structure ## Project Structure
``` ```
@@ -237,5 +271,5 @@ MIT License - Feel free to use and modify.
## Credits ## Credits
- [xterm.js](https://xtermjs.org/) - Terminal emulator - [xterm.js](https://xtermjs.org/) - Terminal emulator
- [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) - Async web server - [PsychicHttp](https://github.com/hoeken/PsychicHttp) - Robust HTTP/WebSocket server for ESP32
- [ArduinoJson](https://arduinojson.org/) - JSON library by Benoît Blanchon - [ArduinoJson](https://arduinojson.org/) - JSON library by Benoît Blanchon

View File

@@ -14,36 +14,29 @@
platform = espressif32 platform = espressif32
board = esp32dev board = esp32dev
framework = arduino framework = arduino
; Serial monitor settings
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = esp32_exception_decoder monitor_filters = esp32_exception_decoder
board_build.filesystem = littlefs ; Build flags
; Use a larger app partition (pick ONE):
board_build.partitions = huge_app.csv ; 3MB app, 1MB FS, no OTA
; board_build.partitions = no_ota.csv ; 2MB app, 2MB FS, no OTA
; board_build.partitions = min_spiffs.csv ; 1.9MB app + OTA, 190KB FS
build_flags = build_flags =
-DCORE_DEBUG_LEVEL=3 -DCORE_DEBUG_LEVEL=3
-DCONFIG_BT_ENABLED=1 -DCONFIG_BT_ENABLED=1
-DCONFIG_BLUEDROID_ENABLED=1 -DCONFIG_BLUEDROID_ENABLED=1
-DSTA_SSID="${sysenv.STA_SSID}"
-DSTA_PASSWORD="${sysenv.STA_PASSWORD}" ; Partition scheme with space for LittleFS
board_build.partitions = default.csv
board_build.filesystem = littlefs
; Libraries ; Libraries
lib_deps = lib_deps =
; Async Web Server and dependencies ; PsychicHttp - robust HTTP server with WebSocket support
https://github.com/ESP32Async/ESPAsyncWebServer hoeken/PsychicHttp @ ^2.1.0
https://github.com/ESP32Async/AsyncTCP
; ArduinoJson for configuration/commands ; ArduinoJson for configuration/commands
ArduinoJson ArduinoJson @ ^7.0.0
; Upload settings (adjust port as needed) ; Upload settings (adjust port as needed)
; upload_port = /dev/ttyUSB0 ; upload_port = /dev/ttyUSB0
; upload_speed = 921600 ; upload_speed = 921600
; Extra scripts for LittleFS
extra_scripts =
pre:scripts/download_xterm.py

View File

@@ -17,8 +17,7 @@
#include <Arduino.h> #include <Arduino.h>
#include <WiFi.h> #include <WiFi.h>
#include <ESPAsyncWebServer.h> #include <PsychicHttp.h>
#include <AsyncTCP.h>
#include <LittleFS.h> #include <LittleFS.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "BluetoothSerial.h" #include "BluetoothSerial.h"
@@ -31,17 +30,9 @@
// WiFi Mode: Set to true to connect to existing network, false for AP mode // WiFi Mode: Set to true to connect to existing network, false for AP mode
#define WIFI_STATION_MODE true #define WIFI_STATION_MODE true
#define ST(A) #A
#define STR(A) ST(A)
// WiFi Station mode settings (connect to existing network) // WiFi Station mode settings (connect to existing network)
#ifndef STA_SSID const char* STA_SSID = "YourNetworkSSID"; // Your WiFi network name
#define STA_SSID "ExampleSSID" const char* STA_PASSWORD = "YourPassword"; // Your WiFi password
#endif
#ifndef STA_PASSWORD
#define STA_PASSWORD "ThePassword"
#endif
// WiFi Access Point fallback settings (if station fails or AP mode selected) // WiFi Access Point fallback settings (if station fails or AP mode selected)
const char* AP_SSID = "ESP32-DebugDongle"; const char* AP_SSID = "ESP32-DebugDongle";
@@ -68,9 +59,11 @@ const char* BT_NAME = "ESP32-Debug";
// Global Objects // Global Objects
// ============================================================================ // ============================================================================
// Web server on port 80 // PsychicHttp server on port 80
AsyncWebServer server(80); PsychicHttpServer server;
AsyncWebSocket ws("/ws");
// WebSocket handler
PsychicWebSocketHandler websocketHandler;
// Bluetooth Serial // Bluetooth Serial
BluetoothSerial SerialBT; BluetoothSerial SerialBT;
@@ -131,7 +124,7 @@ void setBaudRate(SerialPortId port, uint32_t baud) {
// WebSocket Handlers // WebSocket Handlers
// ============================================================================ // ============================================================================
void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t len) { void handleWebSocketMessage(PsychicWebSocketClient* client, uint8_t* data, size_t len) {
// Check for command prefix (starts with 0x00) // Check for command prefix (starts with 0x00)
if (len > 0 && data[0] == 0x00) { if (len > 0 && data[0] == 0x00) {
// Command message - parse JSON // Command message - parse JSON
@@ -152,7 +145,9 @@ void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t
respDoc["type"] = "portChanged"; respDoc["type"] = "portChanged";
respDoc["port"] = port; respDoc["port"] = port;
serializeJson(respDoc, response); serializeJson(respDoc, response);
client->text(String((char)0x00) + response);
String msg = String((char)0x00) + response;
client->sendMessage(msg.c_str());
} }
else if (strcmp(cmd, "setBaud") == 0) { else if (strcmp(cmd, "setBaud") == 0) {
int port = doc["port"]; int port = doc["port"];
@@ -173,7 +168,9 @@ void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t
respDoc["rssi"] = WiFi.RSSI(); respDoc["rssi"] = WiFi.RSSI();
} }
serializeJson(respDoc, response); serializeJson(respDoc, response);
client->text(String((char)0x00) + response);
String msg = String((char)0x00) + response;
client->sendMessage(msg.c_str());
} }
} }
} else { } else {
@@ -182,47 +179,20 @@ void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t
} }
} }
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 // Web Server Setup
// ============================================================================ // ============================================================================
void setupWebServer() { void setupWebServer() {
// Serve static files from LittleFS // Increase URI handler limit if needed
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html"); server.config.max_uri_handlers = 10;
// WebSocket handler // Serve static files from LittleFS
ws.onEvent(onWsEvent); server.serveStatic("/", LittleFS, "/"); // XXX .setDefaultFile("index.html");
server.addHandler(&ws);
// API endpoint for status (can be used without WebSocket) // API endpoint for status (can be used without WebSocket)
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest* request) { server.on("/api/status", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
String response; String out;
JsonDocument doc; JsonDocument doc;
doc["currentPort"] = currentPort; doc["currentPort"] = currentPort;
doc["baudSerial1"] = baudSerial1; doc["baudSerial1"] = baudSerial1;
@@ -234,15 +204,29 @@ void setupWebServer() {
if (WiFi.getMode() == WIFI_STA) { if (WiFi.getMode() == WIFI_STA) {
doc["rssi"] = WiFi.RSSI(); doc["rssi"] = WiFi.RSSI();
} }
serializeJson(doc, response); serializeJson(doc, out);
request->send(200, "application/json", response); return response->send(200, "application/json", out.c_str());
}); });
// 404 handler // WebSocket handler callbacks
server.onNotFound([](AsyncWebServerRequest* request) { websocketHandler.onOpen([](PsychicWebSocketClient* client) {
request->send(404, "text/plain", "Not found"); Serial.printf("[WS] Client #%u connected from %s\n",
client->socket(), client->remoteIP().toString().c_str());
}); });
websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame) {
handleWebSocketMessage(request->client(), frame->payload, frame->len);
return ESP_OK;
});
websocketHandler.onClose([](PsychicWebSocketClient* client) {
Serial.printf("[WS] Client #%u disconnected\n", client->socket());
});
// Attach WebSocket handler to /ws endpoint
server.on("/ws", &websocketHandler);
// Start the server
server.begin(); server.begin();
Serial.println("[Web] Server started"); Serial.println("[Web] Server started");
} }
@@ -252,10 +236,10 @@ void setupWebServer() {
// ============================================================================ // ============================================================================
bool connectToWiFi() { bool connectToWiFi() {
Serial.printf("[WiFi] Connecting to %s", STR(STA_SSID)); Serial.printf("[WiFi] Connecting to %s", STA_SSID);
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.begin(STR(STA_SSID), STR(STA_PASSWORD)); WiFi.begin(STA_SSID, STA_PASSWORD);
unsigned long startTime = millis(); unsigned long startTime = millis();
while (WiFi.status() != WL_CONNECTED) { while (WiFi.status() != WL_CONNECTED) {
@@ -268,7 +252,7 @@ bool connectToWiFi() {
} }
Serial.println(" OK"); Serial.println(" OK");
Serial.printf("[WiFi] Connected to: %s\n", STR(STA_SSID)); Serial.printf("[WiFi] Connected to: %s\n", STA_SSID);
Serial.printf("[WiFi] IP Address: %s\n", WiFi.localIP().toString().c_str()); Serial.printf("[WiFi] IP Address: %s\n", WiFi.localIP().toString().c_str());
Serial.printf("[WiFi] Signal strength: %d dBm\n", WiFi.RSSI()); Serial.printf("[WiFi] Signal strength: %d dBm\n", WiFi.RSSI());
return true; return true;
@@ -350,7 +334,7 @@ void setup() {
delay(1000); delay(1000);
Serial.println("\n\n========================================"); Serial.println("\n\n========================================");
Serial.println(" ESP32 Debug Dongle"); Serial.println(" ESP32 Debug Dongle (PsychicHttp)");
Serial.println("========================================\n"); Serial.println("========================================\n");
// Initialize Serial1 for external device // Initialize Serial1 for external device
@@ -407,8 +391,8 @@ void loop() {
} }
if (bytesRead > 0) { if (bytesRead > 0) {
// Send to all WebSocket clients // Send to all WebSocket clients (PsychicHttp uses sendAll)
ws.binaryAll(serialBuffer, bytesRead); websocketHandler.sendAll((char*)serialBuffer, bytesRead);
// Send to Bluetooth if connected // Send to Bluetooth if connected
if (SerialBT.connected()) { if (SerialBT.connected()) {
@@ -425,15 +409,12 @@ void loop() {
} }
} }
// Cleanup disconnected WebSocket clients
ws.cleanupClients();
// Periodic internal debug messages (example) // Periodic internal debug messages (example)
static unsigned long lastDebug = 0; static unsigned long lastDebug = 0;
if (millis() - lastDebug > 30000) { // Every 30 seconds if (millis() - lastDebug > 30000) { // Every 30 seconds
lastDebug = millis(); lastDebug = millis();
debugPrintln("[%lu] Heartbeat - Heap: %d, WS clients: %d", debugPrintln("[%lu] Heartbeat - Heap: %d, WS clients: %d",
millis() / 1000, ESP.getFreeHeap(), ws.count()); millis() / 1000, ESP.getFreeHeap(), websocketHandler.count());
} }
// Small delay to prevent WDT issues // Small delay to prevent WDT issues