diff --git a/README.md b/README.md index ae15f2a..a3fb961 100644 --- a/README.md +++ b/README.md @@ -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 - **Virtual Serial**: Internal loopback for ESP32's own debug output - **Configurable**: Change baud rates on the fly +- **Robust Server**: Uses PsychicHttp - stable under load (unlike ESPAsyncWebServer) ## Hardware @@ -53,8 +54,19 @@ pio run -t uploadfs ### 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://` + +#### Via WiFi (AP Mode Fallback) + +If station mode fails, or you set `WIFI_STATION_MODE false`: 1. Connect to WiFi network: `ESP32-DebugDongle` 2. Password: `debug1234` 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: ```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_PASSWORD = "debug1234"; +// Connection timeout before falling back to AP mode +#define WIFI_CONNECT_TIMEOUT 15000 + // Bluetooth name const char* BT_NAME = "ESP32-Debug"; @@ -122,6 +144,18 @@ const char* BT_NAME = "ESP32-Debug"; #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 ``` @@ -237,5 +271,5 @@ 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 +- [PsychicHttp](https://github.com/hoeken/PsychicHttp) - Robust HTTP/WebSocket server for ESP32 - [ArduinoJson](https://arduinojson.org/) - JSON library by BenoƮt Blanchon diff --git a/platformio.ini b/platformio.ini index 5fc3e97..cc055aa 100644 --- a/platformio.ini +++ b/platformio.ini @@ -14,34 +14,29 @@ platform = espressif32 board = esp32dev framework = arduino + +; Serial monitor settings 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 build_flags = -DCORE_DEBUG_LEVEL=3 -DCONFIG_BT_ENABLED=1 -DCONFIG_BLUEDROID_ENABLED=1 +; Partition scheme with space for LittleFS +board_build.partitions = default.csv +board_build.filesystem = littlefs + ; Libraries lib_deps = - ; Async Web Server and dependencies - https://github.com/ESP32Async/ESPAsyncWebServer - https://github.com/ESP32Async/AsyncTCP + ; PsychicHttp - robust HTTP server with WebSocket support + hoeken/PsychicHttp @ ^2.1.0 ; ArduinoJson for configuration/commands - ArduinoJson + ArduinoJson @ ^7.0.0 ; 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/src/main.cpp b/src/main.cpp index 6b01cc0..99deb91 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,8 +17,7 @@ #include #include -#include -#include +#include #include #include #include "BluetoothSerial.h" @@ -32,8 +31,8 @@ #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 +const char* STA_SSID = "YourNetworkSSID"; // Your WiFi network name +const char* STA_PASSWORD = "YourPassword"; // Your WiFi password // WiFi Access Point fallback settings (if station fails or AP mode selected) const char* AP_SSID = "ESP32-DebugDongle"; @@ -60,9 +59,11 @@ const char* BT_NAME = "ESP32-Debug"; // Global Objects // ============================================================================ -// Web server on port 80 -AsyncWebServer server(80); -AsyncWebSocket ws("/ws"); +// PsychicHttp server on port 80 +PsychicHttpServer server; + +// WebSocket handler +PsychicWebSocketHandler websocketHandler; // Bluetooth Serial BluetoothSerial SerialBT; @@ -123,7 +124,7 @@ void setBaudRate(SerialPortId port, uint32_t baud) { // 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) if (len > 0 && data[0] == 0x00) { // Command message - parse JSON @@ -144,7 +145,9 @@ void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t respDoc["type"] = "portChanged"; respDoc["port"] = port; 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) { int port = doc["port"]; @@ -165,7 +168,9 @@ void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t respDoc["rssi"] = WiFi.RSSI(); } serializeJson(respDoc, response); - client->text(String((char)0x00) + response); + + String msg = String((char)0x00) + response; + client->sendMessage(msg.c_str()); } } } else { @@ -174,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 // ============================================================================ void setupWebServer() { - // Serve static files from LittleFS - server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html"); + // Increase URI handler limit if needed + server.config.max_uri_handlers = 10; - // WebSocket handler - ws.onEvent(onWsEvent); - server.addHandler(&ws); + // Serve static files from LittleFS + server.serveStatic("/", LittleFS, "/"); // XXX .setDefaultFile("index.html"); // API endpoint for status (can be used without WebSocket) - server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest* request) { - String response; + server.on("/api/status", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { + String out; JsonDocument doc; doc["currentPort"] = currentPort; doc["baudSerial1"] = baudSerial1; @@ -226,15 +204,29 @@ void setupWebServer() { if (WiFi.getMode() == WIFI_STA) { doc["rssi"] = WiFi.RSSI(); } - serializeJson(doc, response); - request->send(200, "application/json", response); + serializeJson(doc, out); + return response->send(200, "application/json", out.c_str()); }); - // 404 handler - server.onNotFound([](AsyncWebServerRequest* request) { - request->send(404, "text/plain", "Not found"); + // WebSocket handler callbacks + websocketHandler.onOpen([](PsychicWebSocketClient* client) { + 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(); Serial.println("[Web] Server started"); } @@ -342,7 +334,7 @@ void setup() { delay(1000); Serial.println("\n\n========================================"); - Serial.println(" ESP32 Debug Dongle"); + Serial.println(" ESP32 Debug Dongle (PsychicHttp)"); Serial.println("========================================\n"); // Initialize Serial1 for external device @@ -399,8 +391,8 @@ void loop() { } if (bytesRead > 0) { - // Send to all WebSocket clients - ws.binaryAll(serialBuffer, bytesRead); + // Send to all WebSocket clients (PsychicHttp uses sendAll) + websocketHandler.sendAll((char*)serialBuffer, bytesRead); // Send to Bluetooth if connected if (SerialBT.connected()) { @@ -417,15 +409,12 @@ void loop() { } } - // 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()); + millis() / 1000, ESP.getFreeHeap(), websocketHandler.count()); } // Small delay to prevent WDT issues