1
0
Эх сурвалжийг харах

Added WebSerialLite: a lightweight high-performance version of WebSerial (#78)

* Fix warnings
On Arduino 3, type definition has changed. It is recommended to use PRIu32 / PRId32 / PRIu16 / PRI16 / etc for printf formats

* Review implemented signatures

* Support configuring WS max clients

* Add onMessage(WSLStringMessageHandler recv) to support String callback

* PONG Optimization

* Simplify _write_row_packet by precomputing header and lengths and make it static

* Added WebSerialLite: a lightweight high-performance version of WebSerial
- WebSerialLite is able to stream in real-time at a speed of more than 20 messages per second with a low memory footprint
- There is no synchronization or mutex in place (be careful when 2 cores are writing)
- There is no buffering by default, so write(c), print(c), printf(...) and variants with only 1 character are not allowed
- WebSerialLite can use a special line buffering mode to only send complete lines to the browser - suitable for redirecting Arduino logs to the browser
Mathieu Carbou 9 сар өмнө
parent
commit
07181ff087

+ 14 - 1
.github/workflows/ci.yml

@@ -72,6 +72,12 @@ jobs:
       - name: Build Demo_AP
         run: arduino-cli compile --library . --warnings none -b ${{ matrix.board }} "examples/Demo_AP/Demo_AP.ino"
 
+      - name: Build Logging
+        run: arduino-cli compile --library . --warnings none -b ${{ matrix.board }} "examples/Logging/Logging.ino"
+
+      - name: Build HighPerf
+        run: arduino-cli compile --library . --warnings none -b ${{ matrix.board }} "examples/HighPerf/HighPerf.ino"
+
   platformio:
     name: pio ${{ matrix.name }}
     runs-on: ubuntu-latest
@@ -123,4 +129,11 @@ jobs:
       - run: platformio platform install ${{ matrix.platform }}
 
       - run: platformio ci "examples/Demo/Demo.ino" -l '.' -b ${{ matrix.board }} ${{ matrix.opts }}
-      - run: platformio ci "examples/Demo_AP/Demo_AP.ino" -l '.' -b ${{ matrix.board }} ${{ matrix.opts }}
+      - run: platformio ci "examples/Demo_AP/Demo_AP.ino" -l '.' -b ${{ matrix.board }} ${{ matrix.opts }}
+      - run: platformio ci "examples/Logging/Logging.ino" -l '.' -b ${{ matrix.board }} ${{ matrix.opts }}
+      - run: platformio ci "examples/HighPerf/HighPerf.ino" -l '.' -b ${{ matrix.board }} ${{ matrix.opts }}
+
+      - run: PLATFORMIO_BUILD_FLAGS="-DWSL_HIGH_PERF" platformio ci "examples/Demo/Demo.ino" -l '.' -b ${{ matrix.board }} ${{ matrix.opts }}
+      - run: PLATFORMIO_BUILD_FLAGS="-DWSL_HIGH_PERF" platformio ci "examples/Demo_AP/Demo_AP.ino" -l '.' -b ${{ matrix.board }} ${{ matrix.opts }}
+      - run: PLATFORMIO_BUILD_FLAGS="-DWSL_HIGH_PERF" platformio ci "examples/Logging/Logging.ino" -l '.' -b ${{ matrix.board }} ${{ matrix.opts }}
+      - run: PLATFORMIO_BUILD_FLAGS="-DWSL_HIGH_PERF" platformio ci "examples/HighPerf/HighPerf.ino" -l '.' -b ${{ matrix.board }} ${{ matrix.opts }}

+ 1 - 1
examples/Demo/Demo.ino

@@ -81,7 +81,7 @@ void loop() {
     WebSerial.print(F("IP address: "));
     WebSerial.println(WiFi.localIP());
     WebSerial.printf("Uptime: %lums\n", millis());
-    WebSerial.printf("Free heap: %u\n", ESP.getFreeHeap());
+    WebSerial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap());
     last_print_time = millis();
   }
 

+ 1 - 1
examples/Demo_AP/Demo_AP.ino

@@ -74,7 +74,7 @@ void loop() {
     WebSerial.print(F("IP address: "));
     WebSerial.println(WiFi.softAPIP());
     WebSerial.printf("Uptime: %lums\n", millis());
-    WebSerial.printf("Free heap: %u\n", ESP.getFreeHeap());
+    WebSerial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap());
     last_print_time = millis();
   }
 

+ 69 - 0
examples/HighPerf/HighPerf.ino

@@ -0,0 +1,69 @@
+/*
+ * This example shows how to use WebSerial variant to send data to the browser when timing, speed and latency are important.
+ * WebSerial focuses on reducing latency and increasing speed by enqueueing messages and sending them in a single packet.
+ *
+ * The responsibility is left to the caller to ensure that the messages sent are not too large or not too small and frequent.
+ * For example, use of printf(), write(c), print(c), etc are not recommended.
+ *
+ * This variant can allow WebSerial to support a high speed of more than 20 messages per second like in this example.
+ *
+ * It can be used to log data, debug, or send data to the browser in real-time without any delay.
+ *
+ * You might want to look at the Logging variant to see how to better use WebSerial for streaming logging.
+ *
+ * You might want to control these flags to control the async library performance:
+ *  -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128
+ *  -D CONFIG_ASYNC_TCP_RUNNING_CORE=1
+ *  -D WS_MAX_QUEUED_MESSAGES=128
+ */
+#include <Arduino.h>
+#if defined(ESP8266)
+#include <ESP8266WiFi.h>
+#include <ESPAsyncTCP.h>
+#elif defined(ESP32)
+#include <AsyncTCP.h>
+#include <WiFi.h>
+#endif
+#include <DNSServer.h>
+#include <ESPAsyncWebServer.h>
+#include <WString.h>
+#include <WebSerial.h>
+
+AsyncWebServer server(80);
+
+static const char* dict = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890";
+static uint32_t last = millis();
+static uint32_t count = 0;
+
+void setup() {
+  Serial.begin(115200);
+
+  WiFi.softAP("WSLDemo");
+  Serial.print("IP Address: ");
+  Serial.println(WiFi.softAPIP().toString());
+
+  WebSerial.onMessage([](const String& msg) { Serial.println(msg); });
+  WebSerial.begin(&server);
+
+  server.onNotFound([](AsyncWebServerRequest* request) { request->redirect("/webserial"); });
+  server.begin();
+}
+
+void loop() {
+  if (millis() - last > 50) {
+    count++;
+    long r = random(10, 250) + 15;
+    String buffer;
+    buffer.reserve(r);
+    buffer += count;
+    while (buffer.length() < 10) {
+      buffer += " ";
+    }
+    buffer += "";
+    for (int i = 0; i < r; i++) {
+      buffer += dict[random(0, 62)];
+    }
+    WebSerial.print(buffer);
+    last = millis();
+  }
+}

+ 55 - 0
examples/Logging/Logging.ino

@@ -0,0 +1,55 @@
+/*
+ * This example shows how to use WebSerial variant to send logging data to the browser.
+ *
+ * Before using this example, make sure to look at the WebSerial example before and its description.\
+ *
+ * You might want to control these flags to control the async library performance:
+ *  -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128
+ *  -D CONFIG_ASYNC_TCP_RUNNING_CORE=1
+ *  -D WS_MAX_QUEUED_MESSAGES=128
+ */
+#include <Arduino.h>
+#if defined(ESP8266)
+#include <ESP8266WiFi.h>
+#include <ESPAsyncTCP.h>
+#elif defined(ESP32)
+#include <AsyncTCP.h>
+#include <WiFi.h>
+#endif
+#include <DNSServer.h>
+#include <ESPAsyncWebServer.h>
+#include <WString.h>
+#include <WebSerial.h>
+
+AsyncWebServer server(80);
+
+static uint32_t last = millis();
+static uint32_t count = 0;
+
+void setup() {
+  Serial.begin(115200);
+
+  WiFi.softAP("WSLDemo");
+  Serial.print("IP Address: ");
+  Serial.println(WiFi.softAPIP().toString());
+
+  WebSerial.onMessage([](const String& msg) { Serial.println(msg); });
+  WebSerial.begin(&server);
+  WebSerial.setBuffer(100);
+
+  server.onNotFound([](AsyncWebServerRequest* request) { request->redirect("/webserial"); });
+  server.begin();
+}
+
+void loop() {
+  if (millis() - last > 1000) {
+    count++;
+
+    WebSerial.print(F("IP address: "));
+    WebSerial.println(WiFi.softAPIP());
+    WebSerial.printf("Uptime: %lums\n", millis());
+    WebSerial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap());
+
+    last = millis();
+  }
+}

+ 8 - 1
platformio.ini

@@ -4,6 +4,10 @@ build_flags =
   -Wall -Wextra
   -D CONFIG_ARDUHAL_LOG_COLORS
   -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
+  -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128
+  -D CONFIG_ASYNC_TCP_RUNNING_CORE=1
+  -D WS_MAX_QUEUED_MESSAGES=128
+  -D WSL_HIGH_PERF
 lib_deps = 
   mathieucarbou/Async TCP @ ^3.1.4
   mathieucarbou/ESP Async WebServer @ 2.10.4
@@ -13,8 +17,10 @@ monitor_filters = esp32_exception_decoder, log2file
 
 [platformio]
 lib_dir = .
-src_dir = examples/Demo
+; src_dir = examples/Demo
 ; src_dir = examples/Demo_AP
+; src_dir = examples/HighPerf
+src_dir = examples/Logging
 
 [env:arduino]
 platform = espressif32
@@ -30,6 +36,7 @@ platform_packages=
   platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.0
   platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/arduino-esp32/releases/download/3.0.0/esp32-arduino-libs-3.0.0.zip
 board = esp32-s3-devkitc-1
+; board = esp32dev
 
 [env:esp8266]
 platform = espressif8266

+ 140 - 39
src/WebSerial.cpp

@@ -15,6 +15,27 @@ typedef enum {
   WSL_PONG = 0x04,
 } WSLPacketType;
 
+static const uint8_t WSL_PONG_MSG[] = {WSL_MAGIC_BYTE_1, WSL_MAGIC_BYTE_2, WSLPacketType::WSL_PONG};
+static const size_t WSL_PONG_MSG_LEN = sizeof(WSL_PONG_MSG) / sizeof(WSL_PONG_MSG[0]);
+
+static const uint8_t WSL_HEAD[] = {
+  WSL_MAGIC_BYTE_1,             // Magic Bytes
+  WSL_MAGIC_BYTE_2,             // Magic Bytes
+  WSLPacketType::WSL_WRITE_ROW, // Packet Type (1 byte)
+  0x00,                         // Reserved
+  0x00,                         // Reserved
+  0x00,                         // Reserved
+  0x00,                         // Reserved
+  0x00,                         // Padding
+  0x00,                         // Padding
+  0x00,                         // Padding
+  0x00,                         // Padding
+  0x00                          // Reserved
+};
+static const size_t WSL_HEAD_LEN = sizeof(WSL_HEAD) / sizeof(WSL_HEAD[0]);
+
+static const size_t WSL_MSG_SIZE_LEN = sizeof(uint16_t);
+
 void WebSerialClass::setAuthentication(const String& username, const String& password){
   _username = username;
   _password = password;
@@ -48,6 +69,10 @@ void WebSerialClass::begin(AsyncWebServer *server, const char* url) {
     // if(type == WS_EVT_CONNECT){
     // } else if(type == WS_EVT_DISCONNECT){
     // } else if(type == WS_EVT_DATA){
+    if (type == WS_EVT_CONNECT) {
+      client->setCloseClientOnQueueFull(false);
+      return;
+    }
     if(type == WS_EVT_DATA){
       // Detect magic bytes
       if (data[0] == WSL_MAGIC_BYTE_1 && data[1] == WSL_MAGIC_BYTE_2) {
@@ -60,8 +85,7 @@ void WebSerialClass::begin(AsyncWebServer *server, const char* url) {
           }
         } else if (data[2] == WSLPacketType::WSL_PING) {
           // Send pong
-          uint8_t pong[] = {WSL_MAGIC_BYTE_1, WSL_MAGIC_BYTE_2, WSLPacketType::WSL_PONG};
-          client->binary(pong, sizeof(pong) / sizeof(pong[0]));
+          client->binary(WSL_PONG_MSG, WSL_PONG_MSG_LEN);
         }
       }
     }
@@ -76,14 +100,74 @@ void WebSerialClass::onMessage(WSLMessageHandler recv) {
   _recv = recv;
 }
 
+void WebSerialClass::onMessage(WSLStringMessageHandler callback) {
+  _recv = [&](uint8_t *data, size_t len) {
+    if(data && len) {
+#ifdef ESP8266
+      String msg;
+      msg.reserve(len);
+      msg.concat((char*)data, len);
+      callback(msg);
+#else
+      callback(String((char*)data, len));
+#endif
+    }
+  };
+}
+
 // Print func
 size_t WebSerialClass::write(uint8_t m) {
+  if (!_ws)
+    return 0;
+
+#ifdef WSL_HIGH_PERF
+  // We do not support non-buffered write on webserial for the HIGH_PERF version
+  // we fail with a stack trace allowing the user to change the code to use write(const uint8_t* buffer, size_t size) instead
+  if(!_initialBufferCapacity) {
+#ifdef ESP8266
+    ets_printf("Non-buffered write is not supported. Please use write(const uint8_t* buffer, size_t size) instead.");
+#else
+    log_e("Non-buffered write is not supported. Please use write(const uint8_t* buffer, size_t size) instead.");
+#endif
+    assert(false);
+    return 0;
+  }
+#endif // WSL_HIGH_PERF
+
   write(&m, 1);
   return(1);
 }
 
 // Println / Printf / Write func
-size_t WebSerialClass::write(uint8_t* buffer, size_t size) {
+size_t WebSerialClass::write(const uint8_t* buffer, size_t size) {
+  if (!_ws || size == 0)
+    return 0;
+
+#ifdef WSL_HIGH_PERF
+  // No buffer, send directly (i.e. use case for log streaming)
+  if (!_initialBufferCapacity) {
+    size = buffer[size - 1] == '\n' ? size - 1 : size;
+    _send(buffer, size);
+    return size;
+  }
+
+  // fill the buffer while sending data for each EOL
+  size_t start = 0, end = 0;
+  while (end < size) {
+    if (buffer[end] == '\n') {
+      if (end > start) {
+        _buffer.concat(reinterpret_cast<const char*>(buffer + start), end - start);
+      }
+      _send(reinterpret_cast<const uint8_t*>(_buffer.c_str()), _buffer.length());
+      start = end + 1;
+    }
+    end++;
+  }
+  if (end > start) {
+    _buffer.concat(reinterpret_cast<const char*>(buffer + start), end - start);
+  }
+  return size;
+#else
   loop();
   _wait_for_print_mutex();
   _print_buffer_mutex = true;
@@ -99,8 +183,33 @@ size_t WebSerialClass::write(uint8_t* buffer, size_t size) {
   _print_buffer_mutex = false;
   _last_print_buffer_write_time = micros();
   return(size);
+#endif
+}
+
+#ifdef WSL_HIGH_PERF
+void WebSerialClass::_send(const uint8_t* buffer, size_t size) {
+  if (_ws && size > 0) {
+    _ws->cleanupClients(WSL_MAX_WS_CLIENTS);
+    if (_ws->count()) {
+      if (size > UINT16_MAX)
+        size = UINT16_MAX;
+      AsyncWebSocketMessageBuffer* wsbuffer = _ws->makeBuffer(WSL_HEAD_LEN + WSL_MSG_SIZE_LEN + size);
+      _write_row_packet(wsbuffer->get(), buffer, size);
+      _ws->binaryAll(wsbuffer);
+    }
+  }
+
+  // if buffer grew too much, free it, otherwise clear it
+  if (_initialBufferCapacity) {
+    if (_buffer.length() > _initialBufferCapacity) {
+      setBuffer(_initialBufferCapacity);
+    } else {
+      _buffer.clear();
+    }
+  }
 }
 
+#else // WSL_HIGH_PERF
 void WebSerialClass::_wait_for_global_mutex() {
   // Wait for mutex to be released
   if (_buffer_mutex) {
@@ -124,40 +233,6 @@ bool WebSerialClass::_has_enough_space(size_t size) {
   return (_buffer_offset + WSL_CALC_LOG_PACKET_SIZE(size) > WSL_BUFFER_SIZE);
 }
 
-size_t WebSerialClass::_write_row_packet(__unused uint64_t reserved1, __unused uint8_t reserved2, const uint8_t *payload, const size_t payload_size) {
-  size_t header_size = 0;
-
-  // Write Magic Bytes
-  _buffer[_buffer_offset + header_size++] = WSL_MAGIC_BYTE_1;
-  _buffer[_buffer_offset + header_size++] = WSL_MAGIC_BYTE_2;
-
-  // Packet Type (1 byte)
-  _buffer[_buffer_offset + header_size++] = WSLPacketType::WSL_WRITE_ROW;
-
-  // Reserved (8 bytes)
-  _buffer[_buffer_offset + header_size++] = 0x00;
-  _buffer[_buffer_offset + header_size++] = 0x00;
-  _buffer[_buffer_offset + header_size++] = 0x00;
-  _buffer[_buffer_offset + header_size++] = 0x00;
-  _buffer[_buffer_offset + header_size++] = 0x00;
-  _buffer[_buffer_offset + header_size++] = 0x00;
-  _buffer[_buffer_offset + header_size++] = 0x00;
-  _buffer[_buffer_offset + header_size++] = 0x00;
-
-  // Reserved (1 byte)
-  _buffer[_buffer_offset + header_size++] = 0x00;
-
-  // Message Length (2 bytes)
-  memset(_buffer + _buffer_offset + header_size, (uint16_t)payload_size, sizeof((uint16_t)payload_size));
-  header_size += sizeof((uint16_t)payload_size);
-
-  // Set Message
-  memcpy(_buffer + _buffer_offset + header_size, payload, payload_size);
-
-  // Return total packet size
-  return header_size + payload_size;
-}
-
 size_t WebSerialClass::_write_row(uint8_t *data, size_t len) {
   // Split the logData into multiple packets
   size_t remaining_size = len;
@@ -178,7 +253,7 @@ size_t WebSerialClass::_write_row(uint8_t *data, size_t len) {
     _buffer_mutex = true;
 
     // Write Packet to Buffer
-    _buffer_offset += _write_row_packet(0, 0, current_ptr, packet_size);
+    _buffer_offset += _write_row_packet(_buffer, current_ptr, packet_size);
 
     // Unlock Mutex
     _buffer_mutex = false;
@@ -223,11 +298,13 @@ void WebSerialClass::_flush_global_buffer() {
     _buffer_mutex = false;
   }
 }
+#endif // WSL_HIGH_PERF
 
 void WebSerialClass::loop() {
+#ifndef WSL_HIGH_PERF
   if ((unsigned long)(millis() - _last_cleanup_time) > WSL_CLEANUP_TIME_MS) {
     _last_cleanup_time = millis();
-    _ws->cleanupClients();
+    _ws->cleanupClients(WSL_MAX_WS_CLIENTS);
   }
 
   // If FLUSH_TIME ms has been passed since last packet time, flush logs
@@ -243,6 +320,30 @@ void WebSerialClass::loop() {
       _flush_global_buffer();
     }
   }
+#endif // WSL_HIGH_PERF
+}
+
+void WebSerialClass::setBuffer(size_t initialCapacity) {
+#ifdef WSL_HIGH_PERF
+  assert(initialCapacity <= UINT16_MAX);
+  _initialBufferCapacity = initialCapacity;
+  _buffer = String();
+  _buffer.reserve(initialCapacity);
+#endif
+}
+
+size_t WebSerialClass::_write_row_packet(uint8_t* dest, const uint8_t *payload, size_t payload_size) {
+  // sanity check to ensure the payload size is within the hard limit
+  if(payload_size > UINT16_MAX)
+    payload_size = UINT16_MAX;
+  // Write header
+  memmove(dest, WSL_HEAD, WSL_HEAD_LEN);
+  // Message Length (2 bytes)
+  memset(dest + WSL_HEAD_LEN, static_cast<uint16_t>(payload_size), WSL_MSG_SIZE_LEN);
+  // Set Message
+  memmove(dest + WSL_HEAD_LEN + WSL_MSG_SIZE_LEN, payload, payload_size);
+  // Return total packet size
+  return WSL_HEAD_LEN + WSL_MSG_SIZE_LEN + payload_size;
 }
 
 WebSerialClass WebSerial;

+ 84 - 46
src/WebSerial.h

@@ -35,44 +35,60 @@ License: AGPL-3.0 (https://www.gnu.org/licenses/agpl-3.0.html)
     #include "ESPAsyncWebServer.h"
 #endif
 
-// Global buffer ( buffers all packets )
-#ifndef WSL_BUFFER_SIZE
-#define WSL_BUFFER_SIZE                       2048
-#endif
-#ifndef WSL_PRINT_BUFFER_SIZE
-#define WSL_PRINT_BUFFER_SIZE                 1024
-#endif
-#ifndef WSL_MAX_ROW_PACKET_PAYLOAD_SIZE
-#define WSL_MAX_ROW_PACKET_PAYLOAD_SIZE       512
+#ifndef WSL_MAX_WS_CLIENTS
+#define WSL_MAX_WS_CLIENTS DEFAULT_MAX_WS_CLIENTS
 #endif
 
-#ifndef WSL_PRINT_FLUSH_TIME_US
-#define WSL_PRINT_FLUSH_TIME_US               100
-#endif
-#ifndef WSL_GLOBAL_FLUSH_TIME_MS
-#define WSL_GLOBAL_FLUSH_TIME_MS              100
-#endif
-#ifndef WSL_CLEANUP_TIME_MS
-#define WSL_CLEANUP_TIME_MS                   5000
-#endif
-
-#if WSL_BUFFER_SIZE < 512
-  #error "WSL_BUFFER_SIZE must be >= 512 bytes"
-#endif
-
-#if WSL_BUFFER_SIZE < WSL_PRINT_BUFFER_SIZE
-  #error "WSL_BUFFER_SIZE must be >= WSL_PRINT_BUFFER_SIZE"
-#endif
-
-#if WSL_PRINT_FLUSH_TIME_US < 1
-  #error "WSL_PRINT_FLUSH_TIME_US must be greater than 1us"
-#endif
-
-#if WSL_GLOBAL_FLUSH_TIME_MS < 50
-  #error "WSL_GLOBAL_FLUSH_TIME_MS must be greater than 50ms"
-#endif
+// High performance mode:
+// - Low memory footprint (no stack allocation, no global buffer by default)
+// - Low latency (messages sent immediately to the WebSocket queue)
+// - High throughput (up to 20 messages per second, no locking mechanism)
+// Activation with: -D WSL_HIGH_PERFORMANCE
+// Also recommended to tweak AsyncTCP and ESPAsyncWebServer settings, for example:
+//  -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128  // AsyncTCP queue size
+//  -D CONFIG_ASYNC_TCP_RUNNING_CORE=1  // core for the async_task
+//  -D WS_MAX_QUEUED_MESSAGES=128       // WS message queue size
+#ifndef WSL_HIGH_PERF
+  // Global buffer ( buffers all packets )
+  #ifndef WSL_BUFFER_SIZE
+  #define WSL_BUFFER_SIZE                       2048
+  #endif
+  #ifndef WSL_PRINT_BUFFER_SIZE
+  #define WSL_PRINT_BUFFER_SIZE                 1024
+  #endif
+  #ifndef WSL_MAX_ROW_PACKET_PAYLOAD_SIZE
+  #define WSL_MAX_ROW_PACKET_PAYLOAD_SIZE       512
+  #endif
+
+  #ifndef WSL_PRINT_FLUSH_TIME_US
+  #define WSL_PRINT_FLUSH_TIME_US               100
+  #endif
+  #ifndef WSL_GLOBAL_FLUSH_TIME_MS
+  #define WSL_GLOBAL_FLUSH_TIME_MS              100
+  #endif
+  #ifndef WSL_CLEANUP_TIME_MS
+  #define WSL_CLEANUP_TIME_MS                   5000
+  #endif
+
+  #if WSL_BUFFER_SIZE < 512
+    #error "WSL_BUFFER_SIZE must be >= 512 bytes"
+  #endif
+
+  #if WSL_BUFFER_SIZE < WSL_PRINT_BUFFER_SIZE
+    #error "WSL_BUFFER_SIZE must be >= WSL_PRINT_BUFFER_SIZE"
+  #endif
+
+  #if WSL_PRINT_FLUSH_TIME_US < 1
+    #error "WSL_PRINT_FLUSH_TIME_US must be greater than 1us"
+  #endif
+
+  #if WSL_GLOBAL_FLUSH_TIME_MS < 50
+    #error "WSL_GLOBAL_FLUSH_TIME_MS must be greater than 50ms"
+  #endif
+#endif // WSL_HIGH_PERFORMANCE
 
 typedef std::function<void(uint8_t *data, size_t len)> WSLMessageHandler;
+typedef std::function<void(const String& msg)> WSLStringMessageHandler;
 
 class WebSerialClass : public Print {
   public:
@@ -80,11 +96,40 @@ class WebSerialClass : public Print {
     inline void setAuthentication(const char* username, const char* password) { setAuthentication(String(username), String(password)); }
     void setAuthentication(const String& username, const String& password);
     void onMessage(WSLMessageHandler recv);
-    size_t write(uint8_t);
-    size_t write(uint8_t* buffer, size_t size);
+    void onMessage(WSLStringMessageHandler recv);
+    size_t write(uint8_t) override;
+    size_t write(const uint8_t* buffer, size_t size) override;
+    
+    // Only valid if WSL_HIGH_PERF is not activated (which is the default)
+    // Housekeeping for WebSerial internals.
+    // Calling this loop has no effect if WSL_HIGH_PERF is activated
     void loop();
+    
+    // Only valid if WSL_HIGH_PERF is activated
+    // A buffer (shared across cores) can be initialised with an initial capacity to be able to use any Print functions event those that are not buffered and would
+    // create a performance impact for WS calls. The goal of this buffer is to be used with lines ending with '\n', like log messages.
+    // The buffer size will eventually grow until a '\n' is found, then the message will be sent to the WS clients and a new buffer will be created.
+    // Set initialCapacity to 0 to disable buffering.
+    // Must be called before begin(): calling it after will erase the buffer and its content will be lost.
+    // The buffer is not enabled by default.
+    void setBuffer(size_t initialCapacity);
 
   private:
+    // Server
+    AsyncWebServer *_server;
+    AsyncWebSocket *_ws;
+    WSLMessageHandler _recv = nullptr;
+    bool _authenticate = false;
+    String _username;
+    String _password;
+
+#ifdef WSL_HIGH_PERF
+    size_t _initialBufferCapacity = 0;
+    String _buffer;
+    void _send(const uint8_t* buffer, size_t size);
+#else
+    unsigned long _last_cleanup_time = 0;
+
     // Global Buffer
     bool _buffer_mutex = false;
     size_t _buffer_offset = 0;
@@ -97,25 +142,18 @@ class WebSerialClass : public Print {
     unsigned long _last_print_buffer_write_time = 0;
     unsigned long _last_print_buffer_flush_time = 0;
 
-    // Server
-    AsyncWebServer *_server;
-    AsyncWebSocket *_ws;
-    WSLMessageHandler _recv = nullptr;
-    unsigned long _last_cleanup_time = 0;
-    bool _authenticate = false;
-    String _username;
-    String _password;
-
     // Print
     void _wait_for_global_mutex();
     void _wait_for_print_mutex();
     bool _has_enough_space(size_t size);
     size_t _start_row();
     size_t _write_row(uint8_t *data, size_t len);
-    size_t _write_row_packet(uint64_t reserved1, uint8_t reserved2, const uint8_t *payload, const size_t payload_size);
     size_t _end_row();
     void _flush_print_buffer();
     void _flush_global_buffer();
+#endif
+
+    static size_t _write_row_packet(uint8_t* dest, const uint8_t *payload, size_t payload_size);
 };
 
 extern WebSerialClass WebSerial;