SerialPortStream

repository·master·Indexed 20 days ago

https://github.com/jcurl/rjcp.dll.serialportstream

A high-reliability, buffered implementation of serial port streams for .NET, designed as an independent alternative to the standard Microsoft and Mono serial port implementations. It provides a stream-based API with internal buffering to reduce driver underruns and overruns, reliable writes, and improved closing/disposing behavior. Supports Windows (.NET 4.0, 4.5, 4.8.1, Core 6.0, 8.0) and Linux (via Mono and the libnserial support library).

Tokens
6.6K
Snippets
19
Records
35
Agent score
69%

What's inside SerialPortStream

  1. What is SerialPortStream and how does it differ from MS SerialPort?

    master

    SerialPortStream is an independent implementation of System.IO.Ports.SerialPort and SerialStream designed for better reliability, maintainability, and portability (including Mono on Linux).

    Key Differences

    • Stream-based API: Unlike the MS implementation which focuses on a SerialPort API, SerialPortStream provides a Stream implementation.
    • Internal Buffering: All data is buffered in memory using a dedicated I/O thread. This reduces the risk of driver underruns and overruns. While this adds a small amount of latency due to context switching, it allows your application to be less sensitive to timing constraints.
    • Reliable Writes: In the MS implementation, asynchronous Write() calls may return the number of bytes actually transferred, requiring manual retries. SerialPortStream copies data to a local buffer and handles the background transfer. If the data cannot be sent, it throws a TimeoutException.
    • Improved Closing/Disposing: Disposing or closing the port during a blocking write operation will abort the operation with a System.IO.IOException, whereas the MS implementation may not abort the write.
  2. How the LogSource abstraction works

    master

    The LogSource abstraction (provided by the RJCP.Diagnostics.Trace library) acts as a wrapper around TraceSource.

    • In .NET Framework: It relies on the native TraceSource behavior, where the singleton is managed by the runtime using the app.config file.
    • In .NET Core: Since .NET Core does not automatically load app.config for tracing or provide a built-in singleton for TraceSource, LogSource provides a factory method (SetLoggerFactory) to bridge the gap. This allows developers to inject an ILoggerFactory that the library uses to create loggers under the namespace RJCP.IO.Ports.SerialPortStream.
  3. Understand SerialPortStream Buffering and Flushing

    master

    Buffering Behavior

    SerialPortStream uses independent Read and Write buffers that are decoupled from the low-level driver:

    • Write Buffer: Can be as large as 128KB. The background thread handles issuing multiple write calls to the driver as necessary.
    • Read Buffer: Can be as large as 5MB. The background thread reads from the serial port whenever data arrives.

    This buffering allows your application to sleep or perform other tasks without losing data, provided the .NET I/O thread can execute every 100-200ms.

    Flushing Writes

    Because writes are buffered and return to the application immediately (rather than waiting for the hardware to finish), you should use the Flush() method to ensure all buffered data has been successfully sent to the serial port.

  4. Understand parity error behavior across chipsets

    master

    When testing data transmission between different parity configurations (e.g., from 8,N,1 to 7,E,1 or 7,O,1) where parityreplace is set to zero, some chipsets exhibit implementation-defined behavior. Instead of only the corrupted byte being affected, multiple subsequent bytes may appear incorrect or be marked incorrectly (especially if PARMRK is set).

    Chipset Reliability for Parity Errors:

    • 16550A: PASS (Gold standard: only the affected byte is set to zero; all other bytes remain correct).
    • PL2303RA: FAIL
    • FTDI: FAIL
    • PL2303H: FAIL

    If you require strict data integrity where only the specific byte with a parity error is modified, use a chipset based on the 16550A standard.

  5. Best practices for designing serial protocols

    master

    Due to known issues with hardware/software flow control, parity, and data integrity across various drivers (especially USB-to-Serial), follow these guidelines when designing your own serial protocol:

    1. Assume Unreliable Layer 2: Assume data can be inserted, modified, or deleted at the serial bus level.
    2. Use Frame-Based Data: Define data as frames containing:
      • A marker byte (start of frame).
      • A length field.
      • A checksum (e.g., CRC16) for integrity.
    3. Avoid Hardware/Software Flow Control: These can lead to deadlocks or protocol complications. Instead, implement a Link Control Protocol (LCP) within your frames to manage data flow.
    4. Avoid Parity: Parity can insert arbitrary bytes or corrupted data. Use frame checksums (FCS) instead.
    5. Bundle Frames: To improve performance and reduce context switching overhead caused by the SerialPortStream buffer thread, bundle multiple frames together rather than sending many small, individually acknowledged frames.
  6. Developer Notes: Project Format and Targets

    master

    The project uses the modern Microsoft SDK project format.

    Important for Contributors:

    • File Inclusion: The project requires explicit inclusion of files in .csproj files for safety. When adding new files, you must manually modify the .csproj files; Visual Studio 2022 may not automatically place them in the correct <ItemGroup/>.
    • Target Frameworks: The library targets .NET 4.0, .NET 4.5, .NET Core 6.0, and .NET Core 8.0.
  7. Build the libnserial Docker image

    master

    To create a Docker image for the build environment, use the docker build command. The image is based on a specific Ubuntu flavor defined by the CODENAME environment variable. The architecture of the resulting image will match your host machine.

    export CODENAME=focal
    docker build --build-arg CODE_VERSION=${CODENAME} -t libnserial:${CODENAME} .
  8. Configure CMake to find libnserial

    master

    To use the libnserial.so library in a C project, use find_package(nserial CONFIG REQUIRED) in your CMakeLists.txt. This requires that the nserialConfig.cmake file is present in your library directories (typically provided by installing the libnserial and libnserial-dev Debian packages). You must also include the library's include directories and link against the nserial libraries and system threads.

    cmake_minimum_required(VERSION 2.8)
    project(helloworld)
    add_executable(helloworld hello.c)
    find_package(Threads REQUIRED)
    find_package(nserial CONFIG REQUIRED)
    include_directories(${nserial_INCLUDE_DIRS})
    target_link_libraries(helloworld ${nserial_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
  9. Configure and Run Unit Tests on Windows

    master

    The test suite contains both unit and integration tests. For integration tests, you can use the Com0Com driver.

    Configuring Ports: Edit test\SerialPortStreamTest\App.config to define the source and destination ports. If using real hardware, replace the default values with your actual COM ports.

    Default Configuration:

    <appSettings>
      <add key="Win32SourcePort" value="CNCA0"/>
      <add key="Win32DestPort" value="CNCB0"/>
      <add key="LinuxSourcePort" value="/dev/ttyUSB0"/>
      <add key="LinuxDestPort" value="/dev/ttyUSB1"/>
    </appSettings>

    Running Tests:

    • Debug: dotnet test
    • Release: dotnet test -c Release --logger "trx"
    • Skip Manual Tests: Use the -Trait:ManualTest filter in Visual Studio.

    Note on Com0Com: The following tests will fail when using the Com0Com driver because it does not handle parity correctly, though they will pass with real hardware: EvenParityLoopback, OddParityLoopback, and ParityChangeLoopback.

    # Test DEBUG mode
    dotnet test
    
    # Test RELEASE mode
    dotnet test -c Release --logger "trx"
  10. Check Linux driver support for TIOCGICOUNT

    master

    On Linux, the reliability of monitoring pins (CTS, DSR, RI, DCD) depends on whether the driver supports the ioctl(TIOCGICOUNT) call. Chips like PL2303H and PL2303RA do not support it, meaning short pin toggles might not be reliably detected. 16550A and FTDI chips generally support it.

    You can verify your driver's support by running the icount test program from the comptest directory.

    # If supported:
    $ ./icount /dev/ttyS0
    Your driver supports TIOCGICOUNT
    
    # If NOT supported:
    $ ./icount /dev/ttyUSB0
    Your driver doesn't support TIOCGICOUNT
      Error: 25 (Inappropriate ioctl for device)
  11. Build using Docker (Ubuntu Images)

    master

    You can use Docker containers to build the library for a specific Ubuntu image. This method installs necessary build packages inside a container based on a specific Ubuntu release and performs the build within that environment. The source code and build results are synchronized using mount points provided in the docker run command.

    For detailed instructions, refer to the README.md file located in the docker directory of the repository.