BLE Transport
The Shiny.Obd.Ble package provides a Bluetooth LE transport for communicating with ELM327-compatible OBD adapters using Shiny.BluetoothLE.
<PackageReference Include="Shiny.Obd" /><PackageReference Include="Shiny.Obd.Ble" />Configuration
Section titled “Configuration”OBD BLE adapters expose a GATT service with read (notify) and write characteristics. The UUIDs vary by manufacturer — defaults are set for common ELM327 BLE clones.
var config = new BleObdConfiguration{ // GATT UUIDs — defaults work for most ELM327 BLE clones ServiceUuid = "FFF0", ReadCharacteristicUuid = "FFF1", // notifications (RX from adapter) WriteCharacteristicUuid = "FFF2", // write commands (TX to adapter)
// Optional: filter scan results by device name DeviceNameFilter = "OBDLink",
// Timeout for a single command response CommandTimeout = TimeSpan.FromSeconds(10)};Common Adapter UUIDs
Section titled “Common Adapter UUIDs”| Adapter | Service | Read (RX) | Write (TX) |
|---|---|---|---|
| ELM327 BLE clones | FFF0 |
FFF1 |
FFF2 |
| OBDLink MX+ | Varies | Varies | Varies |
| Vgate iCar Pro | Varies | Varies | Varies |
Creating a Transport
Section titled “Creating a Transport”From a Discovered Device (recommended)
Section titled “From a Discovered Device (recommended)”Use BleObdDeviceScanner to find adapters, then pass the selected device:
IBleManager bleManager = /* from DI or Shiny setup */;
var scanner = new BleObdDeviceScanner(bleManager, new BleObdConfiguration{ DeviceNameFilter = "OBDII" // optional: matches any device name containing "OBDII"});
var cts = new CancellationTokenSource();var devices = new List<ObdDiscoveredDevice>();
await scanner.Scan(device =>{ devices.Add(device); Console.WriteLine($"Found: {device.Name} ({device.Id})");}, cts.Token);
// Connect to a selected devicevar transport = new BleObdTransport(devices[0], new BleObdConfiguration());var connection = new ObdConnection(transport);await connection.Connect();BleObdDeviceScanner deduplicates by peripheral UUID — each device is reported once. Cancel the token to stop scanning.
Auto-Scan
Section titled “Auto-Scan”Let the transport scan for the first device matching your filter:
IBleManager bleManager = /* from DI or Shiny setup */;
var transport = new BleObdTransport(bleManager, new BleObdConfiguration{ DeviceNameFilter = "OBDII" // matches any device name containing "OBDII"});
var connection = new ObdConnection(transport);await connection.Connect(); // scans, connects, initializesWhen DeviceNameFilter is null, the transport connects to the first BLE device found during scanning.
Pre-Scanned Peripheral
Section titled “Pre-Scanned Peripheral”If you’ve already discovered the peripheral (e.g. from a scan UI), pass it directly:
IPeripheral peripheral = /* from your BLE scan */;
var transport = new BleObdTransport(peripheral, new BleObdConfiguration());var connection = new ObdConnection(transport);await connection.Connect(); // connects to known peripheral, initializesMAUI Setup
Section titled “MAUI Setup”Register BLE and the scanner in your MauiProgram.cs:
using Shiny;
var builder = MauiApp.CreateBuilder();builder.UseMauiApp<App>();
builder.Services.AddBluetoothLE();builder.Services.AddShinyObdBluetoothLE(new BleObdConfiguration{ DeviceNameFilter = "OBD"});AddShinyObdBluetoothLE registers BleObdConfiguration and IObdDeviceScanner (BleObdDeviceScanner).
Full Example
Section titled “Full Example”using Shiny.Obd;using Shiny.Obd.Ble;using Shiny.Obd.Commands;using Shiny.BluetoothLE;
public class ObdService{ readonly IBleManager bleManager; IObdConnection? connection;
public ObdService(IBleManager bleManager) { this.bleManager = bleManager; }
public async Task Connect(CancellationToken ct = default) { var transport = new BleObdTransport(this.bleManager, new BleObdConfiguration { DeviceNameFilter = "OBDLink" }); this.connection = new ObdConnection(transport); await this.connection.Connect(ct); }
public async Task<DashboardData> ReadDashboard(CancellationToken ct = default) { if (this.connection == null || !this.connection.IsConnected) throw new InvalidOperationException("Not connected");
return new DashboardData { Speed = await this.connection.Execute(StandardCommands.VehicleSpeed, ct), Rpm = await this.connection.Execute(StandardCommands.EngineRpm, ct), CoolantTemp = await this.connection.Execute(StandardCommands.CoolantTemperature, ct), FuelLevel = await this.connection.Execute(StandardCommands.FuelLevel, ct), ThrottlePosition = await this.connection.Execute(StandardCommands.ThrottlePosition, ct) }; }
public async Task Disconnect() { if (this.connection != null) { await this.connection.Disconnect(); await this.connection.DisposeAsync(); this.connection = null; } }}
public class DashboardData{ public int Speed { get; set; } public int Rpm { get; set; } public int CoolantTemp { get; set; } public double FuelLevel { get; set; } public double ThrottlePosition { get; set; }}How It Works
Section titled “How It Works”The BLE transport:
- Scans for a peripheral matching the optional device name filter (or uses a pre-provided peripheral / discovered device). The scan itself is deliberately unfiltered — see Why the scan isn’t filtered by service UUID
- Connects using Shiny’s task-based
ConnectAsync - Subscribes to notifications on the read characteristic via
NotifyCharacteristic - Sends commands by writing bytes to the write characteristic via
WriteCharacteristicAsync - Collects response bytes from notifications into a buffer until the ELM327
>prompt is received - Returns the complete response string
Commands are serialized with a semaphore — only one command executes at a time, which matches the ELM327’s single-threaded request-response protocol.
Device names during a scan
Section titled “Device names during a scan”Both the scanner and the transport’s auto-scan match on the peripheral’s name if it has one, and fall back to the local name in the advertisement payload otherwise.
That fallback matters on iOS. CBPeripheral.Name is null while scanning a peripheral the device has never connected to — the name lives only in the advertisement — so matching on the peripheral name alone finds almost nothing on iPhone. Android and Windows populate the name from the scan record, which is why this only shows up on Apple platforms.
Why the scan isn’t filtered by service UUID
Section titled “Why the scan isn’t filtered by service UUID”ServiceUuid is used to talk to the adapter after connecting. It is deliberately not used as a scan filter.
CoreBluetooth matches a scan filter against the advertisement only, and most ELM327 clones advertise nothing but a local name — their GATT service is discoverable only once you connect. Filtering the scan on FFF0 would therefore find nothing at all on iOS.
Troubleshooting
Section titled “Troubleshooting”The adapter never shows up in the scan
Section titled “The adapter never shows up in the scan”BleObdDeviceScanner logs every advertisement it sees at Debug level, before any filtering is applied:
BLE advertisement - Name: VEEPEAK, Peripheral.Name: (null), Id: 9E4A..., RSSI: -63, Services: (none advertised)Enable debug logging to see it:
builder.Logging.AddDebug().SetMinimumLevel(LogLevel.Debug);Read the line as follows:
- The adapter isn’t in the log at all — it isn’t advertising, is out of range, or BLE permissions were denied.
- It’s in the log but not in your callback — your
DeviceNameFilterdoesn’t match theNameshown. Note that the filter is a case-insensitive substring match. Name: (none)— the adapter advertises no name at all, so no name filter can match it. LeaveDeviceNameFilternull and select the device byIdinstead.Services: (none advertised)— normal for ELM327 clones, and harmless. The service is found after connecting.
Connects, but every command times out
Section titled “Connects, but every command times out”The adapter’s GATT UUIDs don’t match your configuration. Services: in the log above only lists what was advertised, which usually isn’t the full picture — connect with a BLE scanner app (like nRF Connect) and read the real service and characteristic UUIDs off the device, then set them on BleObdConfiguration.


