Serial Communication App Pitfalls - Through Reconnection and Log Design

· · Serial Communication, RS-232, C#, .NET, Windows Development, Device Integration

Device integration, measuring instruments, PLCs, barcode readers, USB-to-serial adapters. Serial communication looks like old technology, yet it is still entirely commonplace in real Windows application work.

The slightly dangerous part is that serial communication can be started with nothing more than a single COM port and a single Read / Write. The connectivity check passes immediately, but once in production, symptoms like the following tend to appear.

  • Commands and responses occasionally get out of sync
  • It freezes exactly once a day
  • It fails to recover only after a USB unplug/replug
  • The UI sometimes stalls
  • The logs contain nothing but “Timeout”

What is genuinely hard in serial communication apps is not the send/receive API itself, but boundaries, timeouts, state transitions, reconnection, and observability.

1. The Conclusions First

Summarized up front, in practice-oriented terms.

  • Serial communication is an ordered byte stream; message boundaries do not appear on their own
  • Calling Read(100) does not guarantee exactly 100 bytes back
  • .NET’s DataReceived is not guaranteed to fire per received byte, and moreover it is not on the UI thread
  • ReadLine() / WriteLine() behave nicely only when the peer truly speaks a line-based text protocol
  • One timeout is not enough. Separating the meanings — open, inter-byte, response, reconnect — gives more stability
  • Rather than allowing Write from anywhere, leaning toward a single writer is harder to break
  • With USB-serial, it is more peaceful to assume from the start: unplug/replug, re-enumeration, COM number changes, and reconnection failures

In short, the hard part of a serial communication app is not “can you open the port,” but how you turn the byte stream into meaningful messages, and how you manage the time and state around it.

2. Serial Communication Is an “Ordered Byte Stream,” Not “Messages”

From the application’s point of view, serial communication looks like “send one command, receive one response.” But at the layer below, what actually flows is just an ordered sequence of bytes.

What you sent with one Write may appear to the other side as:

  • Arriving in one Read
  • Arriving split into two
  • Arriving concatenated with other data

Drop this premise and the app starts assuming “this Read must be this response.” That assumption is often the first land mine in serial communication apps.

Common assumption Reality
Read(16) returns exactly 16 bytes Depending on arrival and timeouts, you may only get part of it
DataReceived = one message arrived The event is not guaranteed per byte, and it is not on the UI thread
Write returned = the peer finished processing In most cases it is closer to “the sender queued it into a buffer”
The COM list = the current truth of what is connected Enumeration order is unspecified, and results can be stale

For this reason, in serial communication you must define message boundaries yourself, as a protocol. Fixed-length frames, delimiter-based, length + payload + checksum — any shape is fine, but entering implementation with this left vague almost guarantees pain later.

3. What to Decide First

Before building a serial communication app, decide at least the items listed here up front.

3.1 Frame Boundaries

Decide which byte sequences count as one message. Fixed length? Newline-delimited? Length-prefixed? Is there a checksum / CRC? If this is vague, the receiver cannot tell “not enough yet” from “corrupted.”

3.2 Text, Binary, or a Mix

Decide up front whether it is an ASCII / UTF-8 line protocol, pure binary, or a mixture. Especially with mixtures like “the command part is a string, the payload is binary, only the tail has a newline,” the boundary collapses quickly unless you make explicit what gets decoded and from where bytes are treated as raw.

3.3 The Meaning of Each Timeout

Timeouts are safer thought of not as one value but separated by meaning.

  • open timeout: until the port opens
  • inter-byte timeout: time with no byte arriving mid-frame
  • response timeout: from command issue to response completion
  • reconnect backoff: the wait interval between reconnection attempts

Timeouts are stable when held not as “insurance against slowness” but as rules that advance the state machine.

3.4 Flow Control and Line State

The settings you want to make explicit are around here.

  • BaudRate
  • DataBits
  • Parity
  • StopBits
  • Handshake
  • DTR / RTS

Settle this with “8N1 is roughly right” and, depending on the peer device, things will simply stop.

3.5 Separation of Responsibilities

Divide who is responsible for what.

  • Who reads
  • Who writes
  • Who parses
  • Who applies results to business state

Serial communication becomes more fragile the more the UI and the communication are mixed together.

3.6 Start / Stop / Reconnect State Transitions

At minimum, states like Closed, Opening, Ready, WaitingResponse, Fault, and Reconnecting should be part of the design. Right after an unplug/replug, the peer may still be booting, and there are times when you must not drag along the previous pending request.

3.7 Logging and Investigability

This is where the worst trouble comes later, almost always. At minimum, you want to record: open / close / reopen times, the port settings used, hex dumps of sent and received frames, checksum / CRC errors, frame timeouts / response timeouts, and the reason for each reconnect.

4. Common Pitfalls

4.1 Believing “One Read = One Message”

This is the most common. Say the peer returns a frame consisting of a header, length, payload, and CRC. If you call Read(buffer, 0, expectedLength) once and assume the return is one whole frame, partial reception breaks it easily.

The three usual breakage modes:

  • Only the length arrived; the payload has not come yet
  • One and a half frames arrived; the second half rolls into the next Read
  • Two frames arrived together; only the first is processed and the rest is discarded

The countermeasure is simple: accumulate received bytes first, and let a parser carve frames out of the buffer.

4.2 Treating DataReceived Directly as a Business Event

.NET’s SerialPort.DataReceived looks convenient, but treating it as “a message has arrived” is dangerous. In practice, regard DataReceived as merely “something seems to have arrived,” do no heavy work inside the handler, and always marshal UI updates back to the UI thread.

4.3 Believing Anyone May Write from Anywhere

A configuration where the UI button, the monitoring timer, the reconnection logic, and the keepalive each call Write directly is fragile. Serial is a byte stream, so depending on the design, command interleaving or follow-up sends while awaiting a response can occur. Especially for request-response protocols and RS-485-style buses, leaning toward a single writer is substantially more stable.

4.4 Pushing Everything Through ReadLine() / WriteLine()

For a line-based text protocol, ReadLine() / WriteLine() are convenient. But they are convenient only when it truly is a line protocol. NewLine mismatches, newlines inside the payload, character-encoding differences, or mixed binary will break the boundary quickly.

4.5 Leaving Timeouts Undesigned, at Their Defaults

Drop in a careless synchronous read and you get a plain infinite wait. Worse, a configured timeout does not necessarily apply to every way of reading. Implementations that do synchronous reads on the UI thread, try to express everything with a single timeout, or just add retries, tend to wedge.

4.6 Taking RTS/CTS, XON/XOFF, and DTR/RTS Lightly

Handshaking and control lines matter a great deal against real hardware. With mismatched settings, the symptoms tend to be: transmission occasionally stalls, data is dropped beyond a certain volume, or behavior differs only right after opening. Some devices even interpret DTR/RTS transitions as boot or mode-switch signals.

4.7 Believing a Repeated Open() Equals Reconnection

Especially with USB-serial, it is entirely normal for the port to disappear temporarily, the old handle to become invalid, and the previous pending request to lose its meaning. Reconnection is safer handled as a bundle covering at least: invalidating the session, failing pending requests, stopping the reader / writer, reopening after backoff, and re-running device initialization.

4.8 Treating COM Port Enumeration as Truth

GetPortNames() is convenient, but appearing in the list and being openable are not the same. Blindly trusting last time’s COM7, auto-selecting the first enumeration result, or treating presence in the list as validity — these implementations cause operational trouble.

4.9 Thin Send/Receive Logs

TimeoutException, IOException, and Port closed alone tell you almost nothing. If you record send/receive timestamps, the port profile, hex dumps of traffic, parser errors, which request a response belongs to, and the trigger for each reconnect, triage advances considerably.

5. Best Practices

What pays off most is separating responsibilities.

  • reader: only reads bytes from the port
  • writer: only writes, in order, from the outbound queue
  • parser: only carves frames out of the byte stream
  • protocol: handles request-response pairing and checksums
  • app state: only updates business state

For reception, rather than treating each Read return as a business unit, the stable configuration accumulates into a buffer first and lets the parser carve out frames. Consolidating transmission into one worker — pushing the actual Write toward a single writer — reduces ordering slips.

For timeouts too, rather than settling on one number, separating them by meaning — open, inter-byte, response, reconnect — makes root-cause triage easier. Hold the port settings as a profile rather than ad-hoc code values, and log them at startup; on-site investigation becomes much easier.

Think of reconnection not as a mere reopen but as session regeneration. Rebuild everything — receive buffer, parser state, pending requests, the initialization sequence, and the readiness check — and you reduce the “breaks only occasionally” class of reconnection bugs.

Finally, we recommend keeping both raw logs and summary logs. Raw hex dumps and open / close history are strong for investigation; summaries of request IDs and retry counts are strong for operations.

6. The Checklist to Run First

  • Are message boundaries written down explicitly?
  • Is reception structured as byte accumulation → frame extraction?
  • Are you treating DataReceived as message arrival?
  • Is there synchronous I/O on the UI thread?
  • Is transmission a single writer?
  • Are timeouts split by meaning rather than being one value?
  • Are Handshake / DTR / RTS explicit?
  • Does reconnection rebuild the session?
  • Are raw hex dumps recorded?
  • Have you tested physical unplug/replug and mid-stream disconnection?

If several of these items look shaky, it is worth pausing once before going to production.

7. Summary

Finally, the key points one more time.

  • Serial communication is a byte stream, not messages
  • Read units and message units do not coincide
  • Boundaries must be defined as a protocol
  • Treating DataReceived directly as a business event is fragile
  • Separate send/receive responsibilities, and push transmission toward a single writer
  • Split timeouts by meaning, and design reconnection at the session level
  • Logs that include raw hex dumps make later investigation much easier

In other words, in a serial communication app, how you interpret the byte stream and how you control time and state matters far more than opening the port. Just separating these concerns in the initial design substantially reduces the “breaks only occasionally” class of communication defects.

8. References

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Windows App Development

Windows applications that include serial communication are more stable when designed end to end, covering receive handling, state transitions, reconnection, and UI separation.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog