Skip to content
Document DB 13 - MCP Server, REST API, Field Level Encryption, Transactional Outbox, & More!SHOW ME!!

Serial Transport (USB / UART)

Shiny.Obd.Serial connects to any ELM327-compatible adapter that presents as a serial port — OBDLink SX/EX, ELM327 clones on CH340/FTDI/CP210x bridges, and adapters wired directly to a board’s TX/RX pins.

It is built on System.IO.Ports, which is supported on Windows, Linux, macOS and Mac Catalyst.

Platform Supported Notes
Windows, Linux, macOS Yes
Mac Catalyst Yes Marked [SupportedOSPlatform] on the assembly
Android No Compiles and loads, but cannot open a port — see below
iOS, tvOS, Browser/WASM No [UnsupportedOSPlatform]; throws PlatformNotSupportedException
<PackageReference Include="Shiny.Obd" />
<PackageReference Include="Shiny.Obd.Serial" />
services.AddShinyObdSerial(config =>
{
config.PortNameFilter = "OBDLink";
config.BaudRate = 115200;
});
// or, against a known port
services.AddShinyObdSerial("/dev/ttyUSB0");

IObdTransport, IObdConnection and IObdDeviceScanner are registered as singletons. An OBD adapter is a single physical resource; a scoped or transient registration would leave two consumers fighting over one serial port.

var transport = new SerialObdTransport(new SerialObdConfiguration
{
PortName = null, // null discovers a port
PortNameFilter = "OBDLink",
AutoDetectBaudRate = true,
CommandTimeout = TimeSpan.FromSeconds(10)
});
var connection = new ObdConnection(transport);
await connection.Connect();
Console.WriteLine($"Opened {transport.ConnectedPortName} at {transport.ConnectedBaudRate} baud");
Property Default Notes
PortName null Null discovers one. Prefer a /dev/serial/by-id/... path on Linux
BaudRate 38400 The ELM327 default. OBDLink/STN adapters run happily at 115200 and up
AutoDetectBaudRate true Probes BaudRateCandidates at connect
BaudRateCandidates 38400, 115200, 9600, 500000
PortNameFilter null Substring match against the port name or description
DtrEnable / RtsEnable true Most USB bridges hold the adapter in reset until DTR is raised
OpenSettleDelay 500ms Bridges that reset on DTR swallow anything sent during the reset
CommandTimeout 10s Per command

A UART at the wrong baud rate does not go quiet — it returns framing garbage. The probe therefore checks the shape of the ATI reply (a known adapter string, or at least mostly-printable ASCII) rather than merely that a reply arrived. It costs a second or two at connect and removes an entire category of “it connects but returns nonsense” problem.

var scanner = new SerialObdDeviceScanner();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await scanner.Scan(device =>
{
var info = (SerialPortInfo)device.NativeDevice;
Console.WriteLine($"{info.PortName}{info.Description} (likely: {info.IsLikelyAdapter})");
}, cts.Token);

Unlike a raw SerialPort.GetPortNames(), discovery is platform-aware:

Platform Enumerated Why
Linux /dev/serial/by-id/*, then ttyUSB* / ttyACM* / ttyAMA* / serial* by-id names are built from the USB descriptor, so they carry the vendor and serial number and survive a reboot
macOS /dev/cu.* only Opening the matching tty. node blocks until the device asserts carrier detect, which a USB-serial bridge never does
Windows SerialPort.GetPortNames() Backed by the SERIALCOMM device map, and accurate

Candidates are returned likely-adapter first, matched against known OBD brands (OBDLink, Veepeak, Vgate, ScanTool) and the USB-serial bridge chips they are built on (FTDI, CH340, CP210x, PL2303). The bridges are included deliberately: a genuine OBDLink SX presents as a stock FTDI device with no OBD branding anywhere in its USB descriptor, so matching only on “OBD” would skip the best adapter on the list.

Opening a serial port requires the dialout group:

Terminal window
sudo usermod -aG dialout $USER # log out and back in

ModemManager also probes every serial device it sees and will hold an OBD adapter open for several seconds sending AT commands at it, which makes connects fail intermittently and unreproducibly. Tell it to ignore the bridges:

/etc/udev/rules.d/77-no-modemmanager-obd.rules
ATTRS{idVendor}=="0403", ENV{ID_MM_DEVICE_IGNORE}="1" # FTDI
ATTRS{idVendor}=="1a86", ENV{ID_MM_DEVICE_IGNORE}="1" # CH340
ATTRS{idVendor}=="10c4", ENV{ID_MM_DEVICE_IGNORE}="1" # CP210x