StreamUtils for Arduino

repository·master·Indexed 43 days ago

https://github.com/bblanchon/arduinostreamutils

A collection of decorator classes for Arduino's Stream, Client, and Print abstractions. It provides advanced features including read/write buffering, logging, Hamming(7, 4) error-correction, HTTP chunk decoding, and the ability to treat String, EEPROM, and PROGMEM as streams.

Tokens
7K
Snippets
23
Records
28
Agent score
55%

What's inside StreamUtils

  1. Use Error-Correction Codes (ECC) with HammingStream

    master

    StreamUtils supports the Hamming(7, 4) error-correction code, which adds three parity bits to every 4 bits of data. This allows correcting any single-bit error within a 7-bit block.

    Available Classes:

    • HammingEncodingStream<7, 4>: Decorates a stream to include parity bits in every write operation.
    • HammingDecodingStream<7, 4>: Decorates a stream to decode parity bits in every read operation.
    • HammingStream<7, 4>: Combines both encoding and decoding, ideal for two-way communication.

    Optimization Tip: When using HardwareSerial (like Serial1), you can reduce overhead by configuring the port as a 7-bit channel using SERIAL_7N1.

    // For two-way communication with error correction
    HammingStream<7, 4> eccSerial(Serial1);
    
    eccSerial.println("Hello world!");
    
    char buffer[256];
    size_t n = eccSerial.readBytes(buffer, sizeof(buffer));
  2. Buffer read operations with ReadBufferingStream

    master

    To improve performance when reading from unbuffered streams (like files on SPIFFS), decorate the original Stream with ReadBufferingStream. This allows reading data in larger chunks rather than byte-by-byte.

    Important Considerations:

    • Only use this for unbuffered streams. Adding a buffer to already buffered streams (like Serial ports) provides no benefit.
    • This optimization is most effective when Stream::readBytes() is virtual and the derived class has an optimized implementation.
    • Recommendation: Prefer ReadBufferingClient over ReadBufferingStream because Client::read() is virtual on all platforms, ensuring better compatibility.
    • If memory allocation fails, the class falls back to forwarding all calls to the upstream Stream without buffering.
    File file = SPIFFS.open("example.json", "r");
    ReadBufferingStream bufferedFile{file, 64};
    // Use bufferedFile for reading operations
    deserializeJson(doc, bufferedFile);
  3. Retry write operations with WriteWaitingStream

    master

    If a stream is limited by its internal buffer capacity, you can use WriteWaitingStream to automatically wait and retry writes until they succeed or a timeout occurs.

    You can provide a custom wait() function via a lambda. This is useful for hardware like the Wire library, where the wait() hook can be used to flush the transmission buffer.

    Example with Wire: When using Wire, pass a lambda that calls endTransmission(false) to flush the buffer without stopping the transmission.

    WriteWaitingStream wireStream(Wire, [](){
      Wire.endTransmission(false); // Flush buffer without stopping transmission
      Wire.beginTransmission(address);
    });
    
    Wire.beginTransmission(address); 
    wireStream.print("This is a very very long message!");
    Wire.endTransmission();
  4. Log stream operations with LoggingStream

    master

    You can debug your program by logging read and write operations to a serial port. While you can combine ReadLoggingStream and WriteLoggingStream, the LoggingStream class provides a simpler way to log both directions simultaneously.

    Usage: Decorate your client or stream with LoggingStream(originalStream, logStream). Everything written to the decorated stream is sent to the original stream and also logged to the logStream (e.g., Serial).

    Caution: If you are logging from one serial port to another, ensure the logging port runs at a significantly higher baud rate (at least 10x faster). Otherwise, the logging process may slow down the receiving port and cause dropped bytes.

    LoggingStream loggingClient(client, Serial);
    
    // Both writes and reads will be logged to Serial
    loggingClient.println("GET / HTTP/1.1");
    char response[256];
    loggingClient.readBytes(response, 256);
  5. Use PROGMEM as a Stream

    master

    The ProgmemStream class allows you to read data stored in program memory (PROGMEM) using the standard Stream interface.

    Usage: Pass a pointer to the PROGMEM buffer to the constructor. The constructor also supports const __FlashStringHelper* (the type returned by the F() macro) and an optional size argument.

    const char buffer[] PROGMEM = "This string is in program memory";
    ProgmemStream stream{buffer};
    Serial.println(stream.readString());
  6. Buffer write operations with WriteBufferingStream

    master

    To improve performance when writing to unbuffered streams (like WiFiClient), decorate the original Stream with WriteBufferingStream. This reduces the number of packets sent by buffering data and sending it in larger chunks.

    Important Considerations:

    • You must call .flush() to send any remaining data in the buffer. If you forget, the end of your message will be missing.
    • Tip: The destructor of WriteBufferingStream calls flush(), so you can omit the explicit call if the decorator is destroyed immediately after use.
    • If memory allocation fails, the class falls back to forwarding all calls to the upstream Stream without buffering.
    WriteBufferingStream bufferedWifiClient{wifiClient, 64};
    serializeJson(doc, bufferedWifiClient);
    bufferedWifiClient.flush();
  7. Use EEPROM as a Stream

    master

    You can treat a region of EEPROM as a Stream using the EepromStream class. This is useful for saving or reading complex data structures like JSON documents directly to/from non-volatile memory.

    Usage: Initialize EepromStream with a starting address and the size of the region. On ESP platforms, calling .flush() on the stream will trigger EEPROM.commit().

    // Writing to EEPROM
    EepromStream eepromStream(0, 128);
    serializeJson(doc, eepromStream);
    eepromStream.flush(); 
    
    // Reading from EEPROM
    EepromStream eepromStream(0, 128);
    deserializeJson(doc, eepromStream);
  8. Use String as a Stream

    master

    StreamUtils provides two ways to use String objects as streams:

    1. StringPrint: Wraps a String to implement the Print interface. Use this when you want to redirect output (e.g., from a logger) into a String instead of a hardware port.
    2. StringStream: Wraps a String to implement the Stream interface. Use this when you have a String containing data that needs to be read by a function expecting a Stream.

    Example using StringPrint:

    StringPrint stream;
    stream.print("Temperature = ");
    stream.print(22.3);
    String result = stream.str(); // result contains "Temperature = 22.30"
  9. Decode HTTP chunks with ChunkDecodingStream

    master

    When an HTTP server uses Transfer-Encoding: chunked, the response arrives in multiple parts. ChunkDecodingStream (for Stream) and ChunkDecodingClient (for Client) decorate the raw stream to decode these chunks, presenting the data as a continuous, regular stream.

    Important Notes:

    • HTTPClient already performs chunk decoding if you use getString(). Use ChunkDecodingStream only if you are working with getStream().
    • You can avoid chunked encoding entirely by forcing HTTP 1.0 using http.useHTTP10(true) in HTTPClient.
    // ... after http.GET() and collecting headers ...
    Stream& rawStream = http.getStream();
    ChunkDecodingStream decodedStream(http.getStream());
    
    // Select the appropriate stream based on headers
    Stream& response = (http.header("Transfer-Encoding") == "chunked") ? decodedStream : rawStream;
    
    JsonDocument doc;
    deserializeJson(doc, response);
  10. Reference: StreamUtils Decorator Equivalence Table

    master

    The following table maps different purposes to their corresponding implementations for Client, Stream, and Print classes.

    | Purpose | `Client` | `Stream` | `Print` |
    |:---|:---|:---|:---|
    | Log *write* operations | `WriteLoggingClient` | `WriteLoggingStream` | `LoggingPrint` |
    | Log *read* operations | `ReadLoggingClient` | `ReadLoggingStream` | |
    | Log *read* and *write* op. | `LoggingClient` | `LoggingStream` | |
    | Buffer *write* operations | `WriteBufferingClient` | `WriteBufferingStream` | `BufferingPrint` |
    | Buffer *read* operations | `ReadBufferingClient` | `ReadBufferingStream` |
    | Repeat *write* operations | `WriteWaitingClient` | `WriteWaitingStream` | `WaitingPrint` |
    | Use `String` as a stream | | `StringStream` | `StringPrint` |
    | Use EEPROM as a stream | | `EepromStream` | |
    | Use `PROGMEM` as a stream | | `ProgmemStream` | |
    | Error correction (decode only) | `HammingDecodingClient` | `HammingDecodingStream` | |
    | Error correction (encode only) | `HammingEncodingClient` | `HammingEncodingStream` | `HammingPrint` |
    | Error correction (encode & decode) | `HammingClient` | `HammingStream` | |
    | Decode HTTP chunks | `ChunkDecodingClient` | `ChunkDecodingStream` | |
  11. Flush data using HammingEncodingPolicy

    master

    To ensure any pending encoded nibbles are written to the target stream, you must call the flush methods provided by the HammingEncodingPolicy.

    • flush(Stream &target): Flushes the remainder and calls target.flush().
    • flush(Print &target): Flushes the remainder and calls target.flush() if STREAMUTILS_PRINT_FLUSH_EXISTS is defined.
    • implicitFlush(Print &target): Only flushes the remainder without calling the target's flush() method.
    // If using a Print object as a target:
    policy.flush(target);
    
    // Or just flush the remainder without a full stream flush:
    policy.implicitFlush(target);
  12. Add read buffering to a stream with ReadBufferingClient

    master

    To add a read buffer to an existing Arduino Stream, use the ReadBufferingClient. This client wraps a target stream and uses an internal buffer to reduce the number of read operations performed on the underlying hardware.

    When initialized, it requires the target Stream and a capacity representing the size of the buffer in bytes. By default, it uses DefaultAllocator for memory management.

    If you need to provide a custom allocator, use BasicReadBufferingClient<TAllocator> instead.

    // Using the default client with DefaultAllocator
    StreamUtils::ReadBufferingClient bufferingClient(targetStream, 128);