CosmoSerialPort 1.1.0
dotnet add package CosmoSerialPort --version 1.1.0
NuGet\Install-Package CosmoSerialPort -Version 1.1.0
<PackageReference Include="CosmoSerialPort" Version="1.1.0" />
<PackageVersion Include="CosmoSerialPort" Version="1.1.0" />
<PackageReference Include="CosmoSerialPort" />
paket add CosmoSerialPort --version 1.1.0
#r "nuget: CosmoSerialPort, 1.1.0"
#:package CosmoSerialPort@1.1.0
#addin nuget:?package=CosmoSerialPort&version=1.1.0
#tool nuget:?package=CosmoSerialPort&version=1.1.0
CosmoSerialPort
A modern, cross-platform serial port library for .NET 10 — a semantic port of serialport-rs built for industrial automation, robotics, SCADA, telemetry and other long-running, latency-sensitive workloads.
- Windows (Win32 overlapped I/O + IOCP), Linux (termios2/
BOTHER, poll), macOS (termios +IOSSIOSPEED, IOKit enumeration) SerialPortderives fromStream: works withStreamReader,System.IO.Pipelines, etc.- Real async: pooled
IValueTaskSourceoperations over a poll reactor (Unix) / IOCP (Windows) — noTask.Runwrappers, zero steady-state allocations - Deterministic disposal:
Dispose()wakes blocked readers, even with infinite timeouts - Port enumeration with USB VID/PID, serial number, manufacturer and product strings
- Modem pin control (RTS/DTR out, CTS/DSR/RI/CD in), break, buffer discard, XON/XOFF and RTS/CTS flow control
- Opt-in text layer (
ReadLine/WriteLine,NewLine,Encoding) and a race-freeDataReceivedevent — the binary hot path stays untouched until you use them - RS-485 direction control (
Rs485Settings:TIOCSRS485on Linux,RTS_CONTROL_TOGGLEon Windows), inter-byte timeouts (InterByteTimeout, pySerial'sinter_byte_timeout), and in-memorySerialPort.OpenLoopback()/SerialPort.OpenPair()transports for hardware-free testing on every platform - URL-style ports, pySerial-compatible:
rfc2217://host:port(Telnet COM-port control, works with ser2net),socket://host:port(raw TCP) andloop://— just pass the URL as the port name; plus DSR/DTR flow control and 1.5 stop bits (Windows/RFC 2217), and an interactive terminal sample (samples/CosmoSerialPort.Terminal, the miniterm equivalent) - Nullable, analyzer-clean, AOT- and trimming-compatible
- Targets
net10.0andnetstandard2.0— usable from .NET Framework 4.7.2+, validated on .NET Framework 4.8 (seesamples/CosmoSerialPort.Probe48). The ns2.0 build uses classicDllImportinterop and a small polyfill layer; behavior is identical, though async continuations there always dispatch via the thread pool
Documentation
Full guides live in docs/: getting started,
configuration, reading & writing,
events, signals & status,
enumeration, virtual & network ports,
error handling, API reference,
platform notes, migration,
examples, and the FAQ.
Quick start
using CosmoSerialPort;
// Discover ports
foreach (SerialPortInfo info in SerialPort.GetPortInfos())
Console.WriteLine($"{info.PortName} [{info.PortType}] {info.UsbInfo?.Product}");
// Open with the builder (an immutable record — share and reconfigure freely)
using SerialPort port = SerialPort.Create("/dev/ttyUSB0", 115200)
.WithParity(Parity.None)
.WithStopBits(StopBits.One)
.WithFlowControl(FlowControl.None)
.WithReadTimeout(TimeSpan.FromSeconds(2))
.Open();
// Sync: returns as soon as at least one byte is available,
// throws SerialPortTimeoutException after ReadTimeout.
byte[] buffer = new byte[256];
int read = port.Read(buffer);
// Async with cancellation
int n = await port.ReadAsync(buffer.AsMemory(), cancellationToken);
await port.WriteAsync("AT\r\n"u8.ToArray());
// Pipelines for protocol parsing
var reader = port.CreatePipeReader();
// Control surface
port.SetRts(true);
ModemPins pins = port.GetModemPins();
port.DiscardBuffers(ClearBuffer.Input);
// Text layer (opt-in; UTF-8 and "\n" by default)
port.WriteLine("AT");
string? reply = port.ReadLine();
string? reply2 = await port.ReadLineAsync(cancellationToken);
// DataReceived: a background watcher owns the receive side while subscribed,
// so reading inside the handler is safe (unlike System.IO.Ports).
port.DataReceived += (_, e) =>
{
if (!e.IsEndOfStream && port.ReadLine() is { } line)
Console.WriteLine($"got: {line}");
};
Feature tour
Network and URL-style ports
Port names can be URLs (pySerial's serial_for_url scheme set) — the builder and every feature
above work unchanged:
// RFC 2217 / Telnet COM-port control (ser2net, terminal servers):
// line settings, RTS/DTR/break and purges are forwarded to the remote UART.
using SerialPort remote = SerialPort.Create("rfc2217://192.168.1.50:4001", 115200)
.WithParity(Parity.Even)
.WithReadTimeout(TimeSpan.FromSeconds(5))
.Open();
remote.WriteLine("STATUS?");
Console.WriteLine(remote.ReadLine());
Console.WriteLine(remote.GetModemPins()); // served from NOTIFY_MODEMSTATE
// Raw TCP bridge (no control channel):
using SerialPort tcp = SerialPort.Open("socket://10.0.0.7:7777", 9600);
Hardware-free testing
// loop://—everything written comes back on the same port; RTS/DTR reflect as CTS/DSR.
using SerialPort loop = SerialPort.OpenLoopback();
loop.WriteLine("echo");
Assert.Equal("echo", loop.ReadLine());
// A cross-connected null-modem pair — drive both ends of your protocol in one process.
(SerialPort device, SerialPort host) = SerialPort.OpenPair();
device.Write(response);
host.Read(buffer); // reads what "device" wrote
device.Dispose(); // "host" now reads end-of-stream, like a hot unplug
Status events
port.PinChanged += (_, e) =>
Console.WriteLine($"changed: {e.ChangedPins}, now: {e.CurrentPins}");
port.ErrorReceived += (_, e) =>
Console.WriteLine($"line errors: {e.Errors}"); // Frame | Overrun | RxParity | RxOver | Break
Both are driven by a ~50 ms sampling watcher that starts with the first subscription and stops
with the last; pin pulses shorter than the interval can be missed, and ErrorReceived has no
macOS source. DataReceived supports ReceivedBytesThreshold to batch notifications.
Industrial extras
// RS-485 half-duplex direction control (Linux TIOCSRS485 / Windows RTS toggle).
using SerialPort bus = SerialPort.Create("/dev/ttyS2", 19200)
.WithRs485Mode(new Rs485Settings { DelayBeforeSend = TimeSpan.FromMilliseconds(1) })
.Open();
// Inter-byte timeout: Read collects a burst and returns when the line goes quiet,
// instead of returning the first fragment — ideal for framed RTU-style protocols.
port.InterByteTimeout = 50; // ms
int frame = port.Read(buffer);
// Parity beyond N/E/O, plus error-byte replacement and null stripping.
using SerialPort legacy = SerialPort.Create("COM2", 9600)
.WithParity(Parity.Mark) // Mark/Space: Windows & Linux
.WithParityReplace((byte)'?') // substitute bytes received in error
.WithDiscardNull(true)
.WithStopBits(StopBits.OnePointFive) // Windows & RFC 2217
.Open();
Timeouts
| Value | Sync behavior | Async behavior |
|---|---|---|
Timeout.Infinite (default) |
Block until data | Wait until data or cancellation |
> 0 |
SerialPortTimeoutException after the timeout |
Same |
0 |
Fail immediately if nothing buffered (the serialport-rs default) | Same |
SerialPortTimeoutException derives from TimeoutException for System.IO.Ports
compatibility; all other errors derive from SerialPortException (an IOException) with the
native error code preserved.
Thread safety
One thread may read while another writes on the same instance. Multiple concurrent readers (or writers) are rejected. Control operations are individually thread-safe.
CosmoSerialPort vs System.IO.Ports
How this library compares to the classic System.IO.Ports.SerialPort:
| Feature | System.IO.Ports | CosmoSerialPort |
|---|---|---|
| API model | Mutable component: set properties, then Open() |
Immutable SerialPortBuilder record → Open() returns the port |
| Stream integration | Indirect, via the BaseStream property |
SerialPort is a Stream |
| Async I/O | BaseStream.ReadAsync wraps the old APM pattern; not truly asynchronous |
Real async: ValueTask + CancellationToken; poll reactor on Unix, IOCP on Windows |
| Cancellation | Not supported — close the port to abort | CancellationToken on every async operation |
| Allocations | Internal buffering and event machinery allocate per operation | Zero steady-state allocations; Span/Memory overloads |
| Dispose while reading | Notorious hangs and ObjectDisposedException crashes on hot unplug |
Deterministic: Dispose() wakes blocked readers; hot-unplug tested |
DataReceived event |
Fires on a pool thread; reading from the handler races other reads | Serialized invocations; a watcher owns the receive side, so handler reads are race-free with natural backpressure |
| Text layer | ReadLine/WriteLine/ReadExisting/Write(string) (ASCII default) |
Same surface, opt-in with zero cost when unused (UTF-8 default) |
| Port enumeration | GetPortNames() — names only (registry SERIALCOMM) |
Names plus USB VID/PID, serial number, manufacturer, product, transport type (SetupAPI / sysfs / IOKit) |
| Baud rates | Standard rates; non-standard rates fail on Linux/macOS | Arbitrary rates: BOTHER (Linux), IOSSIOSPEED (macOS), any DCB value (Windows) |
| Exclusive access | None on Unix — two processes can open the same tty | flock + TIOCEXCL by default, opt-out via WithExclusive(false) |
| Modem pins | CtsHolding/DsrHolding/CDHolding — three calls, no Ring Indicator |
GetModemPins() — all four input pins in one native call |
| Error model | Mixed IOException/InvalidOperationException/UnauthorizedAccessException; native codes lost |
Typed hierarchy (PortNotFound, AccessDenied, PortInUse, InvalidConfiguration, NativeIO) with NativeErrorCode preserved |
| Timeout exception | TimeoutException |
SerialPortTimeoutException : TimeoutException — existing catch blocks keep working |
| Pipelines | Manual wiring over BaseStream |
CreatePipeReader() / CreatePipeWriter() built in |
| Parity options | Mark/Space parity, ParityReplace, DiscardNull |
Same — Mark/Space on Windows and Linux (not macOS); ParityReplace and DiscardNull everywhere |
| Handshake options | RequestToSendXOnXOff combined mode |
Same — FlowControl.HardwareAndSoftware |
PinChanged/ErrorReceived events |
Available | Available — sampling-based (~50ms); richer args (all pins + change mask, error flags); ErrorReceived not on macOS |
ReceivedBytesThreshold |
Available | Available |
| AOT / trimming | Not annotated | AOT- and trim-compatible (net10.0 target) |
| Frameworks | .NET Framework and .NET (Windows focus; limited Unix fidelity) | net10.0 + netstandard2.0 (.NET Framework 4.7.2+), first-class Windows/Linux/macOS |
Migration notes. Most call sites map one-to-one: IsOpen → CanRead/CanWrite, Close() → Dispose(), DiscardInBuffer()/DiscardOutBuffer() → DiscardBuffers(ClearBuffer.…), DtrEnable/RtsEnable → WithDtrOnOpen(…)/SetRts(…), Handshake → FlowControl. Write(string) and ReadExisting() exist with the same semantics (mind the UTF-8 vs ASCII default Encoding). Ports open at construction time via the builder, so code that configured a closed port and opened it later should defer creation to the point of first use.
CosmoSerialPort vs pySerial
For teams coming from Python, the feature sets now largely mirror each other:
| Feature | pySerial 3.5 | CosmoSerialPort |
|---|---|---|
| Async model | Blocking + threads; pyserial-asyncio is separate and experimental |
First-class ValueTask async with CancellationToken |
| Cancellation | cancel_read()/cancel_write() (may cancel a future call on POSIX) |
Deterministic per-operation tokens |
| Data/parity/stop | 5–8 bits, N/E/O/Mark/Space, stop bits 1 / 1.5 / 2 | Same (Mark/Space not on macOS; 1.5 on Windows/RFC 2217) |
| Flow control | XON/XOFF, RTS/CTS, DSR/DTR | Same, plus the combined RTS/CTS+XON/XOFF mode |
inter_byte_timeout |
Yes | Yes — InterByteTimeout |
| Custom baud rates | BOTHER / IOSSIOSPEED |
Same mechanisms |
| Exclusive access | Opt-in, POSIX only | Default-on (flock+TIOCEXCL); inherent on Windows |
| RS-485 mode | rs485_mode + software rs485.RS485 |
Rs485Settings (Linux TIOCSRS485, Windows RTS toggle) plus Emulated software mode on any platform |
| URL handlers | rfc2217://, socket://, loop://, spy://, hwgrep://, alt://, cp2110:// |
rfc2217://, socket://, loop://, spy:// + OpenPair() + custom scheme registration; hwgrep:// → query over GetPortInfos(); alt:///cp2110:// not built in |
| Enumeration fields | device, hwid, vid, pid, serial, location, manufacturer, product, interface | name, type, vid, pid, serial, manufacturer, product, location, interface |
| Timed break / buffer size | send_break(duration), set_buffer_size() |
SendBreak(TimeSpan) / BreakState, SetBufferSize() (Windows) |
| Events | None — poll or the experimental ReaderThread |
Native DataReceived (thresholded), PinChanged, ErrorReceived |
| Framing helpers | read_until, Packetizer |
ReadLine/NewLine, System.IO.Pipelines |
| Terminal tool | miniterm |
samples/CosmoSerialPort.Terminal |
| Error model | SerialException + SerialTimeoutException |
Typed hierarchy with preserved native error codes |
| Performance | Interpreter-bound | Native code, zero steady-state allocations |
Repository layout
| Path | Contents |
|---|---|
src/CosmoSerialPort |
The library |
tests/CosmoSerialPort.Tests |
xunit suite: PTY loopback on Unix, in-memory and in-process network servers everywhere |
bench/CosmoSerialPort.Benchmarks |
BenchmarkDotNet loopback benchmarks |
samples/CosmoSerialPort.Probe |
Hardware smoke test / enumeration demo for a real or virtual port |
samples/CosmoSerialPort.Probe48 |
The same probe on .NET Framework 4.8, consuming the netstandard2.0 build |
samples/CosmoSerialPort.Terminal |
Interactive terminal (miniterm equivalent); accepts device names and URLs |
ARCHITECTURE.md |
Analysis of serialport-rs and the porting decisions |
dotnet build -c Release # warning-clean, analyzers on
dotnet test -c Release # 71 tests; PTY suite on macOS/Linux, virtual transports everywhere
dotnet run --project samples/CosmoSerialPort.Probe -c Release -- COM1 # against an echo peer
dotnet run --project samples/CosmoSerialPort.Terminal -c Release -- loop:// # try it without hardware
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.IO.Pipelines (>= 10.0.9)
- System.Memory (>= 4.6.3)
- System.Runtime.CompilerServices.Unsafe (>= 6.1.2)
- System.Threading.Tasks.Extensions (>= 4.6.3)
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.