Serial communication with payment terminals, cash drawers, and scale devices is one of the least-documented corners of D365 Commerce. When it works, nobody thinks about it. When it doesn't, the POS is down and the store is calling.
This post covers the failure modes we've seen repeatedly and the patterns that make Hardware Station connectors resilient.
The Hardware Station connector model
A Hardware Station peripheral connector implements INamedRequestHandler and lives in the Microsoft.Dynamics.Commerce.HardwareStation.Peripherals namespace. The connector is loaded by the Hardware Station runtime, which manages its lifecycle.
public class CustomScaleDevice : INamedRequestHandler
{
public string HandlerName => "CustomScale";
public IEnumerable<Type> SupportedRequestTypes =>
new[] { typeof(ScaleDeviceReadRequest) };
public Response Execute(Request request)
{
var readRequest = (ScaleDeviceReadRequest)request;
return ReadFromSerial(readRequest);
}
}
Common failure modes
1. Port claimed by another process
Windows holds serial port handles exclusively. If a previous Hardware Station process crashed without releasing the port, the next connection attempt throws UnauthorizedAccessException.
The fix: implement a retry with exponential backoff, and on persistent failures, attempt to enumerate and release orphaned handles via the Windows API before giving up.
2. Baud rate negotiation failure
Some devices auto-negotiate baud rate; others require an exact match. A mismatch produces garbage bytes — your reader loop will spin consuming invalid data without throwing an exception. The symptom is a response timeout rather than a clear error.
Always log the raw bytes received, not just the parsed result. When debugging baud issues, the raw byte stream tells you immediately whether you're receiving anything coherent.
private byte[] ReadRawResponse(SerialPort port, int expectedLength)
{
var buffer = new byte[expectedLength];
var timeout = DateTime.UtcNow.AddMilliseconds(port.ReadTimeout);
var offset = 0;
while (offset < expectedLength && DateTime.UtcNow < timeout)
{
var read = port.Read(buffer, offset, expectedLength - offset);
offset += read;
// Log partial reads — critical for baud rate debugging
if (read > 0)
Logger.LogTrace("Read {Bytes} bytes: {Hex}",
read, BitConverter.ToString(buffer, offset - read, read));
}
if (offset < expectedLength)
throw new TimeoutException($"Expected {expectedLength} bytes, got {offset}.");
return buffer;
}
3. Device disconnect mid-session
USB-serial adapters are particularly prone to this. The SerialPort object throws IOException on the next read or write — but not always on the same thread. If you have a background polling loop, the exception surfaces on the pool thread and can be swallowed.
The pattern that works: a dedicated connection-health token, checked before every operation, with a circuit breaker that puts the device into a faulted state. When faulted, operations fail fast with a clear message rather than hanging.
4. Hardware Station .NET 8 migration
If you're upgrading from .NET Framework to .NET 8 Hardware Station (required from Commerce 10.0.36+), System.IO.Ports.SerialPort is available but behaves differently on certain Windows versions regarding DTR/RTS signalling. Devices that relied on implicit DTR signals may stop responding.
Explicitly set DtrEnable = true and RtsEnable = true after opening the port, and verify signal state with a loopback test during initialisation.
A resilient connector template
The pattern we use for all serial connectors:
- Lazy initialisation — open the port on first use, not at startup
- Dispose + recreate — on any
IOException, dispose theSerialPortand recreate from config on the next call rather than attempting to recover a broken handle - Semaphore guard — a
SemaphoreSlim(1,1)ensures only one thread reads the device at a time - Structured logging — log every raw byte exchange at
Tracelevel, suppressed in production but invaluable in field debugging - Health endpoint — expose a
GET /health/peripheralson the Hardware Station that reports device status per connector
These five practices eliminate 80% of field calls on serial peripherals.
The field debugging toolkit
When a device stops responding in a store environment and you can't reproduce it locally:
- Enable Hardware Station trace logging (
appsettings.json,Logging:LogLevel:Microsoft.Dynamics: Trace) - Use Termite or PuTTY on the serial port to verify the device is responding at the OS level — if it doesn't respond there, it's hardware, not your connector
- Check Windows Device Manager for driver errors on the USB-serial adapter — driver reinstall resolves ~30% of "it just stopped working" calls
Serial debugging is methodical work. The bugs are almost never subtle — they're usually a wrong baud rate, a dead adapter, or a port handle that didn't get released. Systematic logging turns a four-hour support call into a fifteen-minute fix.