Skip to content
Shiny.NET
Shiny MAUI Shell v7 - App Links, App Shortcuts, & Navigation Interception!Shortcut me to it

Connection & Adapters

The ObdConnection class is the main entry point for communicating with a vehicle. It handles adapter detection, initialization, command execution, and ELM327 response parsing.

When no adapter profile is specified, Connect sends an ATI command to identify the adapter and selects the appropriate initialization profile.

var connection = new ObdConnection(transport);
await connection.Connect();
// Check what was detected
Console.WriteLine(connection.DetectedAdapter?.RawIdentifier); // "ELM327 v1.5"
Console.WriteLine(connection.DetectedAdapter?.Type); // Elm327
ATI Response Contains Detected As Profile Used
"ELM327" ObdAdapterType.Elm327 Elm327AdapterProfile
"STN" ObdAdapterType.ObdLink ObdLinkAdapterProfile
Anything else ObdAdapterType.Unknown Elm327AdapterProfile

Skip detection by providing a profile to the constructor:

var connection = new ObdConnection(transport, new ObdLinkAdapterProfile());
await connection.Connect(); // uses OBDLink init sequence, no ATI probe
// Typed command execution
var speed = await connection.Execute(StandardCommands.VehicleSpeed);
// Raw AT/OBD commands
var version = await connection.SendRaw("ATI"); // "ELM327 v1.5"
var protocol = await connection.SendRaw("ATDPN"); // current protocol number
var voltage = await connection.SendRaw("ATRV"); // battery voltage
var pids = await connection.SendRaw("0100"); // supported PIDs [01-20]

Elm327AdapterProfile — Standard ELM327 initialization:

Command Description
ATZ Reset adapter
ATE0 Echo off
ATL0 Linefeed off
ATS1 Spaces on
ATH0 Headers off
ATSP0 Auto protocol selection

ObdLinkAdapterProfile — Extends ELM327 with STN optimizations:

Command Description
STFAC Reset to STN factory defaults
(all ELM327 commands) Standard initialization
ATCAF1 CAN auto formatting on

STFAC restores factory defaults, so it runs before the ELM327 configuration rather than after it — sending it last would wipe the echo, spacing, header and protocol settings in the same breath as setting them.

ATSP0 does not choose a protocol. It defers the choice to the first command that needs the bus, and that command then pays the whole ELM search — seconds of it, routinely longer than a command timeout. ATZ discards the result, so an adapter left to search pays it again on every reconnect.

Read the number a session settled on and hand it back to the next one:

var connection = new ObdConnection(transport);
await connection.Connect();
// ATSP0 has not chosen anything yet, so ask again once something has needed the bus
await connection.Execute(new SupportedPidsCommand(0x00));
var protocol = await connection.RefreshNegotiatedProtocol(); // "6"
// Next session — no search at all
var next = new ObdConnection(transport) { Protocol = protocol };
await next.Connect();

A stale pin is safe. The protocol is verified with mode 01 during initialization and dropped for a search when nothing answers, so a number learned against a different vehicle costs one round trip rather than a session that can never read a PID.

Protocol is ignored when you supply your own profile — construct the profile with it instead (new Elm327AdapterProfile("6")).

Implement IObdAdapterProfile for adapters with special initialization needs:

public class MyAdapterProfile : IObdAdapterProfile
{
public string Name => "MyAdapter";
public async Task Initialize(IObdConnection connection, CancellationToken ct = default)
{
await connection.SendRaw("ATZ", ct);
await Task.Delay(500, ct);
await connection.SendRaw("ATE0", ct);
await connection.SendRaw("ATSP6", ct); // force CAN 11-bit 500kbaud
}
}
var connection = new ObdConnection(transport, new MyAdapterProfile());

ObdException is thrown for adapter-level and protocol errors:

try
{
var speed = await connection.Execute(StandardCommands.VehicleSpeed);
}
catch (ObdException ex) when (ex.Message.Contains("No data"))
{
// Vehicle not responding (engine off, unsupported PID, etc.)
}
catch (ObdException ex) when (ex.Message.Contains("Unable to connect"))
{
// Adapter can't reach the vehicle ECU
}

The connection automatically handles these ELM327 error responses:

Response Exception Message
NO DATA No data received from vehicle
UNABLE TO CONNECT Unable to connect to vehicle
BUS INIT: ...ERROR Bus initialization error
? Unknown command
(empty) Empty response received

Informational prefixes like SEARCHING... and BUS INIT: ...OK are stripped automatically before parsing.

A single-frame reply is one line of hex bytes:

41 0D 50

A reply too large for one CAN frame — the VIN (mode 09 PID 02), or mode 03 carrying three or more trouble codes — is printed as a byte count followed by numbered frames:

014
0: 49 02 01 57 42 41
1: 31 32 33 34 35 36 37
2: 38 39 30 31 32 33 34

The leading 014 is the total number of data bytes (0x14 = 20: the 49 02 01 header plus 17 VIN characters). It is framing, not payload, and is discarded — as is the N: index on each frame. The remaining bytes are concatenated in frame order and handed to the command’s parser.

Hex is read whether or not the adapter is spacing it, so 0: 49 02 01 and 0:490201 are equivalent. Adapters are asked for spaces during initialization (ATS1), but clones ignore it.

public interface IObdConnection : IAsyncDisposable
{
bool IsConnected { get; }
Task Connect(CancellationToken ct = default);
Task Disconnect();
Task<T> Execute<T>(IObdCommand<T> command, CancellationToken ct = default);
Task<string> SendRaw(string command, CancellationToken ct = default);
}