System designTroubleshooting

SerialPort close and reopen: fix barcode scanner data loss

Why a barcode scanner goes silent after SerialPort close and reopen: the dispose-and-rebuild lifecycle, session settings and a close-timing budget.

LumaSpec Vision12 min read Share on LinkedIn

On this page 10 sections
  1. 01The symptom signature
  2. 02Causes, in order of probability
  3. 03The fix: one lifecycle for every session
  4. 04A close-timing budget you can compute
  5. 05Open once, keep it open
  6. 06When the usual answer is wrong
  7. 07Checks before you blame the scanner
  8. 08Prevention checklist
  9. 09Wired readers that hold a steady port
  10. 10Questions

A SerialPort that decodes the first scan and then stays silent on every open after it is a teardown bug in your code, not a failing reader. The second Open() call succeeds and IsOpen reads true, the scanner even beeps at a scan, and DataReceived never fires — while a serial terminal program on the same port reopens cleanly time after time. That contrast is the diagnosis: the device tolerates repeated opens when they are closed in a good order, so the application is tearing the port down half-way. This guide gives you the symptom signature, ranks the causes from most to least likely, and hands you the open–read–close lifecycle that makes session two decode as reliably as session one, plus a close-timing budget you can compute for your own baud rate.

The symptom signature

Run one session: open, scan, decode, close. Then run the identical session again and look for these markers:

  • The second Open() succeeds and IsOpen reads true, so nothing looks wrong at the API surface.
  • The bytes never arrive: DataReceived does not fire at all, or fires once with a fragment.
  • The reader sounds an error tone when the session closes — many serial scanners watch the host handshake lines and announce a mid-session drop as a fault.
  • Device Manager still lists the same COM port after an unplug and replug, so the driver is stable.
  • A serial terminal program reopens the same port over and over and decodes every time.

If the terminal program passes and your application fails, stop suspecting the scanner. The USB-to-COM driver keeps the device alive; the difference between the two programs is what happens to the port handle during close.

Causes, in order of probability

Work down the list; the first two causes account for most cases, and each carries a test that confirms it.

1. The old SerialPort is never disposed. Close() closes the streams but leaves the object alive. Building a fresh SerialPort for session two while the stale one lingers puts two objects over one COM handle, and the stale object's background read thread can swallow the bytes meant for the new one. Test: keep a reference to session one's object, check its state after the second Open(), and log how many instances your session code has created. Fix: after Close(), call Dispose(), set the field to null, and only then build the next instance.

2. You reopen before teardown has finished. Close() returns as soon as the close is queued, not when the driver has finished releasing the handle, so Open() on a half-released port can succeed and still never deliver data. Test: insert a 300 ms delay between Dispose() and the next Open(); if data comes back, the race was yours. Fix: replace the blind delay with the quiet-time budget below.

3. The handler is still attached while the port closes. Unsubscribing after Close() leaves a window in which a pending event lands on a stream that is going away. Closing from inside the handler is worse: the event runs on a framework thread-pool thread, so the close waits on itself and the handler never returns. Test: log the thread id and timestamp at every handler entry and compare against the close timestamp. Fix: unsubscribe first, then Close(), then Dispose(), and never close from inside the handler.

4. Session settings reset on the new instance. Every property you do not set explicitly reverts to its default: DtrEnable false, RtsEnable false, Handshake None, ReceivedBytesThreshold 1. A reader configured for hardware handshake now sees a host that never asserts its lines and stays quiet despite a valid open. Test: dump every property of both instances and compare them side by side. Fix: set the full set explicitly, in one method, every session. The same settings mismatch is why a reader can accept your open and then ignore scan commands — the barcode scanner serial trigger diagnostics cover that cousin symptom.

5. USB power management parks the device. Selective suspend can drop the reader into a low-power state once your handle is gone, and the next open enumerates a device that wakes late or not at all. Test: disable USB selective suspend in the power plan, or run the adapter through a powered hub, and repeat the open–close cycle. Fix: if the setting changes the behaviour, keep the port open for the shift instead of cycling it per scan, and check the adapter itself with the barcode scanner power and interface checks.

The fix: one lifecycle for every session

Whatever the trigger, the cure has one shape: own the whole lifecycle, detach before you close, dispose before you rebuild, keep every setting explicit, and let exactly one owner object open and close the port.

  1. Create a new SerialPort for each session and never reuse an instance after disposing it.
  2. Set every property explicitly — port name, 9600 baud, 8 data bits, Parity.None, StopBits.One, Handshake, DtrEnable, RtsEnable, ReceivedBytesThreshold — including the ones that look like defaults.
  3. Subscribe DataReceived before calling Open(), so early bytes are not missed.
  4. In the handler, call ReadExisting into a StringBuilder, cut the complete code out at the '\r' terminator and hand it to the rest of the application; no dialogs, no decoding and no database work on that thread. If the reader sends no terminator, add one in its prefix and suffix settings — the variable-length input fix with a suffix shows the pattern, and framing serial data without headers covers the same cut-out on the host side.
  5. Close in this order: unsubscribe the handler, then Close(), then Dispose(), then wait out the teardown before the next Open().
  6. Build the next session's instance from scratch with the same explicit settings.
  7. Raise the decode result to the subscriber, which marshals it to the UI thread (Dispatcher.BeginInvoke in a WPF application); never show a modal dialog from the serial thread.

ScannerPort, _port and _rx are illustrative names; SerialPort, DataReceived, ReadExisting, Close() and Dispose() are the framework's own members:

using System;
using System.IO.Ports;
using System.Text;

public sealed class ScannerPort : IDisposable
{
    private SerialPort _port;                  // one instance per session
    private readonly StringBuilder _rx = new StringBuilder();

    public event Action<string> ScanComplete;  // raised on a thread-pool thread

    public void Open(string portName)
    {
        _port = new SerialPort(portName, 9600, Parity.None, 8, StopBits.One);
        _port.Handshake = Handshake.None;
        _port.DtrEnable = true;                // assert the host lines every session
        _port.RtsEnable = true;
        _port.ReceivedBytesThreshold = 1;
        _port.DataReceived += OnDataReceived;  // subscribe BEFORE Open
        _port.Open();
    }

    private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        _rx.Append(_port.ReadExisting());      // pump bytes; decide nothing else here
        int end = _rx.ToString().IndexOf('\r');
        if (end < 0) return;
        string code = _rx.ToString(0, end);
        _rx.Remove(0, end + 1);
        ScanComplete?.Invoke(code);
    }

    public void Close()
    {
        if (_port == null) return;
        _port.DataReceived -= OnDataReceived;  // detach first
        _port.Close();
        _port.Dispose();                       // release the handle before a rebuild
        _port = null;
    }

    public void Dispose() => Close();
}

The test that proves it: run three open–scan–close cycles in a row and require a decode on every cycle. The broken pattern goes silent on cycle two; this lifecycle decodes all three. When the application shuts down, the same ordering discipline applies to every device handle you hold — the camera SDK shutdown order article walks the matching stream teardown for cameras, and the USB barcode scanner SerialPort and POS API walkthrough covers the reception pattern from the Windows side.

A close-timing budget you can compute

Open() succeeds and data still goes missing when the close lands inside the burst. Turn 'wait a bit' into a number:

line time (s) = bytes × 10 / baud quiet time (s) ≈ 10 × line time

The ×10 in the first line is serial framing: at 8 data bits, 1 stop bit and no parity, every byte costs 10 bits on the wire. The ×10 in the second line is a margin for the handler's reaction time and the driver's batching.

Worked example: a 12-character Code 39 payload plus the '\r' terminator is 13 bytes at 9600 bit/s.

  • Line time: 13 × 10 / 9600 = 130 / 9600 = 0.0135 s ≈ 13.5 ms.
  • Quiet time before Close(): 10 × 13.5 ms ≈ 135 ms, which rounds up to 150 ms of silence on the line before tearing the session down.
  • The same burst at 115200 bit/s takes 13 × 10 / 115200 = 1.13 ms, so about 12 ms of quiet covers it; scale the margin with line time instead of copying 150 ms everywhere.

The capacity check that rules out overflow: the framework's default receive buffer is 4096 bytes, and a 9600 bit/s line delivers 9600 / 10 = 960 bytes per second, so the buffer fills in 4096 / 960 ≈ 4.3 s. A handler running a few tens of milliseconds late has nowhere near that much data waiting — which is why silent reopen failures are teardown races, not capacity problems. Fragments across several events are normal framing behaviour; prefix and suffix framing for scanner input shows how to make the reader frame the message for you.

Open once, keep it open

The strongest fix removes the cycle. A dedicated reader on a dedicated port should open once at start-up and close only at shutdown: scan after scan then lands on a session that was never torn down, and the whole failure mode disappears. Per-scan open and close earns its place only when the port must be shared between stations or jobs, and then the rebuild lifecycle above is mandatory. If the port is being cycled as a workaround for hangs, fix the hang instead — recycling resets the symptom, not the state that caused it. When the decoded string has to travel onward to a controller, the barcode scanner input methods to a PLC comparison sets serial, USB and Ethernet side by side, so the transport decision is made once at design time rather than debugged at the bench.

When the usual answer is wrong

  • The reader's own SDK is the right tool when you need more than decode strings: device events, image capture, symbology setting sync and health monitoring do not fit through a raw byte stream, and the SDK manages its own connection lifecycle, so the COM handle stops being your problem.
  • Ethernet ends the COM lifecycle altogether. A fixed code reader with an Ethernet interface sends decode strings over TCP, and the host becomes a client or a listener — see TCP client or listener for network barcode readers.
  • A Linux host changes the API but not the discipline of open, frame on a terminator, close only at shutdown; the handheld barcode scanner USB or RS232 choice for a Linux PLC covers the hardware half of that decision.
  • If the first scan after every open is dropped but later scans arrive, that is an application-state bug rather than a port bug; the null previousValue first-scan fix is the closer relative.

Checks before you blame the scanner

  1. Three open–scan–close cycles in a serial terminal program: if all three decode, the device and driver are proven and the application lifecycle is the suspect.
  2. Unplug and replug, then confirm the COM number did not move; a moving port number breaks session two before your code runs.
  3. UnauthorizedAccessException on Open() means another process — a crashed previous instance or a live debug session — still owns the port; close it before testing your teardown.
  4. An error tone from the reader at close means the host dropped the handshake lines mid-session; a terminal program's clean close of the same reader proves the device tolerates ordered teardown.
  5. Disable USB selective suspend before drawing any conclusion about the reader; a parked device mimics every symptom on this page.

Prevention checklist

  • One session owner: a single object in the application opens, reads and closes the port, and the handler never closes anything.
  • Explicit settings: one method sets port name, baud, framing, handshake and thresholds for every session, so no default sneaks back in.
  • Teardown order: unsubscribe, Close(), Dispose(), quiet-time wait, then rebuild.
  • Framing: buffer every ReadExisting chunk and cut codes out on the terminator.
  • UI hygiene: one Dispatcher call per decode; nothing modal on the serial thread.
  • Bench test: a DE-9 loopback jumper, pin 2 to pin 3, exercises full open–read–close cycles at 9600 baud with no scanner in sight.

Wired readers that hold a steady port

Wired handhelds are the hardware half of this pattern: one cable, one enumeration, one COM port that stays put for a whole shift. Start in the handheld code readers category, or filter by interface and connection in the /shop/cameras/ finder.

Handheld code readersHandheld industrial barcode reader, 1.0 MP, USB 2.0 (LS-HCR-0001)LS-HCR-0001
Resolution
Up to 3 MP
Shutter
Global
$539.00View part
Handheld code readersHikrobot MV-IDH5000P/13SR/04RN/U General-purpose wired industrial handheld code readerMV-IDH5000P/13SR/04RN/U
Interface
USB
Connection
Wired
$719.00View part

Both are wired handhelds, so the connection holds for the shift, and the USB unit enumerates through its vendor driver as a COM port — exactly what the single-session-owner pattern wants. When the station cycles readers between jobs and the COM lifecycle is more than the software should own, take the port out of the design: a wired handheld with a Fast Ethernet interface sends decode strings over TCP instead.

Code readers & vision sensorsFixed code readers

1D/2D codes on the line

658 models · from $399.00

On a conveyor or fixture, fixed readers with a Fast Ethernet or GigE interface take the identical TCP path, so the host talks to a socket instead of juggling a COM handle.

Build the whole station

The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.

Start with the camera

Rebuild the lifecycle first — detach, Close(), Dispose(), wait, fresh instance — and three clean cycles at the bench settle it. If the hardware side is the open question, the configurator steps through reader, interface and trigger choices, or send the symptom to us through the contact page and we will help you pin it down. The systems topic page collects the rest of the integration fixes like this one.

Questions

Why does the scanner beep an error when my program closes the port?

Many serial scanners watch the host handshake lines, DTR and RTS, and treat a mid-session drop as a communication fault, announced with an error tone. The tone means the device saw the port disappear, not that the device is broken. If it appears only on closes your code performs, run the same close in a serial terminal program: a clean close there proves the reader tolerates an ordered teardown.

Do I have to unplug and replug the USB cable to recover the COM port?

No. The USB-to-COM driver keeps the port enumerated; only your handle is broken. Close and dispose the SerialPort, wait out the teardown and Open() a fresh instance on the same port name. A replug is only needed when Device Manager shows the port disappearing, which points to power management, cable or hub problems rather than to your lifecycle.

Should I use the scanner vendor's SDK instead of SerialPort?

For plain decode strings ending in a terminator, SerialPort is enough, and the lifecycle fix above solves the reopen problem. Reach for the SDK when you need device events, image capture, symbology setting sync or health monitoring that a raw byte stream cannot carry. The SDK owns its own connection lifecycle, so the COM handle stops being your problem too.

Can I call Close() from inside the DataReceived handler?

No. The event fires on a framework thread-pool thread, and closing the port while that thread sits inside the handler deadlocks the close or throws ObjectDisposedException. Set a flag or post the work to the UI thread, let the handler return, and close from the thread that owns the session.

How do I test serial reception without scanning a real code?

Fit a DE-9 loopback jumper: pin 2 (RX) to pin 3 (TX). Every byte you Write now comes straight back to DataReceived, so full open–read–close cycles run at the bench with no scanner at all. At 9600 baud with 8 data bits and 1 stop bit, a 13-byte write occupies the line for 13.5 ms, so any handler slower than that shows up in the log.

Why does ReadExisting return fragments or nothing in the handler?

DataReceived fires as soon as at least ReceivedBytesThreshold bytes — default 1 — sit in the driver buffer, so one scan can arrive as two or three events. Append every ReadExisting result to a buffer and cut the code out on the terminator character. Raising ReceivedBytesThreshold to one message length reduces the event count, but the buffer-and-terminator pattern is the one that survives every framing quirk.