- name
- shiny-obd
- description
- Generate code using Shiny.Obd, an OBD-II vehicle communication library for .NET with command-object pattern, adapter auto-detection, and BLE + WiFi (TCP) + serial (USB/UART) transports
- auto_invoke
- true
- triggers
- ["obd","obd-ii","obd2","vehicle diagnostics","elm327","obdlink","IObdCommand","IObdConnection","IObdTransport","IObdDeviceScanner","ObdDiscoveredDevice","BleObdDeviceScanner","SerialObdTransport","SerialObdConfiguration","SerialObdDeviceScanner","SerialPortEnumerator","SerialPortInfo","AddShinyObdSerial","serial transport","usb obd","ttyUSB","WifiObdTransport","WifiObdConfiguration","WifiObdDeviceScanner","WifiObdEndpoint","AddShinyObdWifi","wifi transport","wifi obd","elm327 wifi","obdlink mx wifi","ObdCommand","ObdConnection","[Truncated]"]
# Shiny.Obd Skill
You are an expert in Shiny.Obd, a .NET library for communicating with vehicles through OBD-II adapters. It uses a command-object pattern with generic return types, pluggable transports (BLE, WiFi, serial), and adapter auto-detection for ELM327 and OBDLink (STN) adapters.
## When to Use This Skill
Invoke this skill when the user wants to:
- Read vehicle data (speed, RPM, coolant temp, VIN, etc.) through OBD-II
- Create custom OBD commands with typed return values
- Connect to an OBD-II adapter over Bluetooth LE, WiFi or serial (USB/UART)
- Scan for / discover available OBD adapters
- Configure ELM327 or OBDLink adapter initialization
- Implement a custom transport (Android USB Host, J2534, a replay harness) for OBD communication
- Send raw AT commands to an OBD adapter
- Handle OBD response parsing and error handling
- Build a MAUI app with OBD integration
## Library Overview
- **Repository**: https://github.com/shinyorg/obd
- **Namespaces**: `Shiny.Obd`, `Shiny.Obd.Ble`, `Shiny.Obd.Wifi`, `Shiny.Obd.Serial`, `Shiny.Obd.Commands`, `Shiny.Obd.Emulator`
- **NuGet**: `Shiny.Obd` (core), `Shiny.Obd.Ble` (BLE), `Shiny.Obd.Wifi` (WiFi/TCP), `Shiny.Obd.Serial` (USB/UART), `Shiny.Obd.Emulator` (+ `.Ble`) — be an adapter instead of reading one
- **Targets**: `net10.0` throughout
## Core Types
### IObdCommand<T> — Command interface
Every OBD command implements this. `T` is the parsed result type.
```csharp
public interface IObdCommand<T>
{
string RawCommand { get; }
T Parse(byte[] data);
}
```
### ObdCommand<T> — Base class for standard Mode/PID commands
Validates mode+PID response header, strips it, and delegates to `ParseData`.
```csharp
public abstract class ObdCommand<T> : IObdCommand<T>
{
protected ObdCommand(byte mode, byte pid);
public byte Mode { get; }
public byte Pid { get; }
public virtual string RawCommand { get; } // "{Mode:X2}{Pid:X2}"
protected abstract T ParseData(byte[] data); // data after header
}
```
### IObdConnection — Connection interface
```csharp
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);
}
```
### IObdTransport — Transport abstraction
```csharp
public interface IObdTransport : IAsyncDisposable
{
bool IsConnected { get; }
Task Connect(CancellationToken ct = default);
Task Disconnect();
Task<string> Send(string command, CancellationToken ct = default);
}
```
Three implementations ship: `BleObdTransport` (`Shiny.Obd.Ble`), `WifiObdTransport`
(`Shiny.Obd.Wifi`) and `SerialObdTransport` (`Shiny.Obd.Serial`). All three also implement
`IDisposable` so a container can tear them down on its synchronous path.
### IObdDeviceScanner — Device discovery
```csharp
public interface IObdDeviceScanner
{
Task Scan(Action<ObdDiscoveredDevice> onDeviceFound, CancellationToken ct = default);
}
```
Cancel the token to stop scanning. Each discovered device invokes the callback.
### ObdDiscoveredDevice — Discovered adapter
```csharp
public class ObdDiscoveredDevice
{
public string Name { get; } // e.g. "OBDLink MX+"
public string Id { get; } // unique identifier (BLE UUID, IP, etc.)
public object NativeDevice { get; } // IPeripheral for BLE, IPEndPoint for WiFi, etc.
}
```
### ObdConnection — ELM327 protocol handler
Two constructors:
- `ObdConnection(IObdTransport transport)` — auto-detects adapter via ATI
- `ObdConnection(IObdTransport transport, IObdAdapterProfile profile)` — uses explicit profile, skips detection
Properties:
- `DetectedAdapter` — `ObdAdapterInfo?` with `RawIdentifier` (string) and `Type` (ObdAdapterType enum: Unknown, Elm327, ObdLink). Null when explicit profile used.
- `Protocol` — `string?`, settable before `Connect`. The ELM protocol number to pin with `ATSP` instead of searching. Ignored when an explicit profile was supplied — pass it to the profile's constructor instead.
- `NegotiatedProtocol` — `string?`, the protocol the adapter reports it is on. Refreshed by `Connect` and by `RefreshNegotiatedProtocol()`.
Methods:
- `RefreshNegotiatedProtocol(CancellationToken)` — re-reads `ATDPN` and updates `NegotiatedProtocol`.
**Always pin the protocol on reconnect.** `ATSP0` does not choose a protocol — it defers the choice to
the first command that needs the bus, and that command pays the whole ELM search: seconds of it,
routinely longer than a command timeout. `ATZ` discards the result, so an unpinned adapter pays it
again on every reconnect. Generate this pattern whenever an app reconnects to a remembered adapter:
```csharp
var connection = new ObdConnection(transport) { Protocol = savedProtocol }; // null on a first run
await connection.Connect();
// ⚠️ Ask AFTER something has needed the bus. ATSP0 has chosen nothing at the end of Connect, so
// asking there reports null and the app never learns a number to save.
await connection.Execute(new SupportedPidsCommand(0x00));
savedProtocol = await connection.RefreshNegotiatedProtocol();
```
A stale pin is safe — it is verified with mode 01 during initialization and dropped for a search when
nothing answers.
Handles:
- ELM327 hex response parsing (single-line and multi-frame CAN)
- Multi-frame framing: discards the leading byte-count line and the `N:` frame index, then concatenates frames in order
- Spaced and unspaced hex alike (`0: 49 02 01` and `0:490201`)
- Error detection: "NO DATA", "UNABLE TO CONNECT", "BUS INIT: ...ERROR", "?"
- Strips "SEARCHING..." and "BUS INIT" prefixes
### IObdAdapterProfile — Adapter initialization
```csharp
public interface IObdAdapterProfile
{
string Name { get; }
Task Initialize(IObdConnection connection, CancellationToken ct = default);
}
```
Built-in profiles:
- `Elm327AdapterProfile(string? protocol = null)` — ATZ, ATE0, ATL0, ATS1, ATH0, then `ATSP{protocol}` or `ATSP0`
- `ObdLinkAdapterProfile(string? protocol = null)` — STFAC, then the Elm327 sequence, then ATCAF1
`ATZ` is sent **once** per connect, by the profile. Auto-detection probes with `ATI` alone and does not
reset first. `STFAC` restores factory defaults, so it precedes the ELM327 configuration rather than
following it.
## Standard Commands (StandardCommands static class)
| Property | Type | Command | Return | Parse Formula |
|----------|------|---------|--------|---------------|
| `VehicleSpeed` | `VehicleSpeedCommand` | `010D` | `int` (km/h) | `A` |
| `EngineRpm` | `EngineRpmCommand` | `010C` | `int` (RPM) | `((A*256)+B)/4` |
| `CoolantTemperature` | `CoolantTemperatureCommand` | `0105` | `int` (°C) | `A-40` |
| `ThrottlePosition` | `ThrottlePositionCommand` | `0111` | `double` (%) | `(A*100)/255` |
| `FuelLevel` | `FuelLevelCommand` | `012F` | `double` (%) | `(A*100)/255` |
| `CalculatedEngineLoad` | `CalculatedEngineLoadCommand` | `0104` | `double` (%) | `(A*100)/255` |
| `IntakeAirTemperature` | `IntakeAirTemperatureCommand` | `010F` | `int` (°C) | `A-40` |
| `RuntimeSinceStart` | `RuntimeSinceStartCommand` | `011F` | `TimeSpan` | `(A*256)+B` seconds |
| `Vin` | `VinCommand` | `0902` | `string` | skip count byte, ASCII decode |
| `Odometer` | `OdometerCommand` | `01A6` | `double` (km) | `((A<<24)\|(B<<16)\|(C<<8)\|D)/10` |
| `DistanceSinceCodesCleared` | `DistanceSinceCodesClearedCommand` | `0131` | `int` (km) | `(A*256)+B` |
| `ControlModuleVoltage` | `ControlModuleVoltageCommand` | `0142` | `double` (V) | `((A*256)+B)/1000` |
| `MassAirFlow` | `MassAirFlowCommand` | `0110` | `double` (g/s) | `((A*256)+B)/100` |
| `EngineFuelRate` | `EngineFuelRateCommand` | `015E` | `double` (L/h) | `((A*256)+B)/20` |
| `EngineOilTemperature` | `EngineOilTemperatureCommand` | `015C` | `int` (°C) | `A-40` |
| `FuelType` | `FuelTypeCommand` | `0151` | `byte` | `A` (J1979 code) |
| `HybridBatteryLife` | `HybridBatteryLifeCommand` | `015B` | `double` (%) | `(A*100)/255` |
| `MonitorStatus` | `MonitorStatusCommand` | `0101` | `MonitorStatus` | see readiness section below |
| `MonitorStatusThisDriveCycle` | `MonitorStatusThisDriveCycleCommand` | `0141` | `MonitorStatus` | same layout; byte A reserved |
| `FuelSystemStatus` | `FuelSystemStatusCommand` | `0103` | `FuelSystemStatus` | enumerated loop state, `A` and optional `B` |
| `IntakeManifoldPressure` | `IntakeManifoldPressureCommand` | `010B` | `int` (kPa) | `A` |
| `BarometricPressure` | `BarometricPressureCommand` | `0133` | `int` (kPa) | `A` |
| `TimingAdvance` | `TimingAdvanceCommand` | `010E` | `double` (° BTDC) | `(A/2)-64` |
| `AmbientAirTemperature` | `AmbientAirTemperatureCommand` | `0146` | `int` (°C) | `A-40` |
| `RelativeAcceleratorPedalPosition` | `RelativeAcceleratorPedalPositionCommand` | `015A` | `double` (%) | `(A*100)/255` |
| `CommandedThrottleActuator` | `CommandedThrottleActuatorCommand` | `014C` | `double` (%) | `(A*100)/255` |
| `DistanceWithMilOn` | `DistanceWithMilOnCommand` | `0121` | `int` (km) | `(A*256)+B` |
| `TimeRunWithMilOn` | `TimeRunWithMilOnCommand` | `014D` | `TimeSpan` | `(A*256)+B` **minutes** |
| `TimeSinceCodesCleared` | `TimeSinceCodesClearedCommand` | `014E` | `TimeSpan` | `(A*256)+B` **minutes** |
| `CalibrationId` | `CalibrationIdCommand` | `0904` | `IReadOnlyList<string>` | 16-byte ASCII blocks, null-padded |
| `CommandedAirFuelRatio` | `CommandedAirFuelRatioCommand` | `0144` | `double` (lambda) | `2/65536*((A*256)+B)` |
| `CommandedEgr` | `CommandedEgrCommand` | `012C` | `double` (%) | `(A*100)/255` |
| `EgrError` | `EgrErrorCommand` | `012D` | `double` (%) | `(A*100/128)-100` |
| `CommandedEvaporativePurge` | `CommandedEvaporativePurgeCommand` | `012E` | `double` (%) | `(A*100)/255` |
| `EvapVaporPressure` | `EvapVaporPressureCommand` | `0132` | `double` (Pa) | **signed** `((A*256)+B)/4` |
| `AbsoluteEvapVaporPressure` | `AbsoluteEvapVaporPressureCommand` | `0153` | `double` (kPa) | `((A*256)+B)/200` |
| `EvapVaporPressureWideRange` | `EvapVaporPressureWideRangeCommand` | `0154` | `double` (Pa) | **signed** `(A*256)+B` |
| `DriverDemandTorque` | `DriverDemandTorqueCommand` | `0161` | `int` (%) | `A-125` |
| `ActualEngineTorque` | `ActualEngineTorqueCommand` | `0162` | `int` (%) | `A-125` |
| `ReferenceTorque` | `ReferenceTorqueCommand` | `0163` | `int` (N·m) | `(A*256)+B` |
| `EnginePercentTorqueData` | `EnginePercentTorqueDataCommand` | `0164` | `EnginePercentTorqueData` | five points, each `X-125` |
| `FuelPressure` | `FuelPressureCommand` | `010A` | `int` (kPa) | `A*3` |
| `FuelRailPressure` | `FuelRailPressureCommand` | `0122` | `double` (kPa) | `0.079*((A*256)+B)` |
| `FuelRailGaugePressure` | `FuelRailGaugePressureCommand` | `0123` | `int` (kPa) | `10*((A*256)+B)` |
| `FuelRailAbsolutePressure` | `FuelRailAbsolutePressureCommand` | `0159` | `int` (kPa) | `10*((A*256)+B)` |
| `EthanolFuelPercent` | `EthanolFuelPercentCommand` | `0152` | `double` (%) | `(A*100)/255` |
| `AbsoluteLoadValue` | `AbsoluteLoadValueCommand` | `0143` | `double` (%) | `((A*256)+B)*100/255` — **not capped at 100** |
| `WarmUpsSinceCodesCleared` | `WarmUpsSinceCodesClearedCommand` | `0130` | `int` (count) | `A` |
Ver no GitHub