β Star this repo if you find SwiftOBD2 useful! Your support helps the project grow and reach more developers.
SwiftOBD2 is a Swift package designed to simplify communication with vehicles using an ELM327 OBD2 adapter. It provides a straightforward and powerful interface for interacting with your vehicle's onboard diagnostics system, allowing you to retrieve real-time data, perform diagnostics, and monitor raw CAN bus frames. Sample App.
Demo coming soon! We're preparing a comprehensive demo video showcasing real-time vehicle data retrieval, DTC scanning, and more.
- Real-time RPM, Speed, and Engine Load monitoring
- Diagnostic Trouble Code (DTC) scanning and clearing
- Live sensor data visualization
- Bluetooth and USB Serial connection management
Screenshots and demo GIF will be added in the next release
Get up and running in 2 minutes:
import SwiftOBD2
let obdService = OBDService(connectionType: .bluetooth)
let obd2Info = try await obdService.startConnection()
obdService.startContinuousUpdates([.mode1(.rpm), .mode1(.speed)])
.sink { measurements in
print("RPM: \(measurements[.mode1(.rpm)]?.value ?? 0)")
}- iOS 14.0+ / macOS 11.0+
- Xcode 13.0+
- Swift 5.0+
- Connects to ELM327 adapters via Bluetooth LE, Wi-Fi (TCP), or USB Serial.
- Handles full adapter initialisation (reset, echo off, header on, auto-protocol) and vehicle handshake automatically.
- Manages connection states:
disconnected,connecting,connectedToAdapter,connectedToVehicle,error. - Exposes both
@PublishedCombine properties and lightweight Swift closure callbacks so integrators can choose the reactive model that suits them.
Two platform-native serial backends have been added, replacing the previous demo mode placeholder:
iOS β MFi USB Serial (SerialManager)
Connects to MFi-certified USB OBD adapters (e.g. OBDLink EX) using Apple's ExternalAccessory framework over the com.scantool.stnobd protocol string. The adapter must be physically connected via USB-C or Lightning before calling startConnection. No scanning step is required β the adapter is enumerated directly from the list of connected accessories.
macOS β POSIX Serial (MacSerialManager)
Connects to any USB-to-serial OBD adapter exposed as a /dev/tty.* device, using POSIX file descriptors and termios directly. The device path is read from ConfigurationService.shared.serialPath. On connect the manager automatically probes baud rates in the order 115200 β 38400 β 57600 β 9600, confirming each by checking whether the adapter returns printable ASCII. The first rate that produces a valid response is used and logged; the connection fails cleanly if none does. Both backends feed into the same ELM327 initialisation flow as BLE and Wi-Fi.
- Host and port are now fully configurable via
ConfigurationService.shared.wifiHostand.wifiPortrather than being hardcoded. The defaults remain192.168.0.10and35000. ATZ(adapter reset) is handled specially: the command is sent fire-and-forget, the TCP connection is cancelled, the manager waits 1.5 seconds for the adapter to reboot, then reconnects transparently and returns a syntheticELM327 v2.1so the init sequence continues without error. This fixes a class of timeout failures seen with common Wi-Fi ELM327 clones that drop the TCP socket on reset.- The TCP receive loop now accumulates multiple chunks until the ELM327
>prompt arrives, fixing truncation on responses that span more than one TCP segment. - A
ResumeOncegate ensures that exactly one resume fires even when the 15-second hard-deadline timeout races with a normal receive callback.
sendMonitorCommand(_ command: String, duration: TimeInterval) is a new method on OBDService that puts the ELM327 into streaming monitor mode (e.g. AT MA β monitor all, or AT MT hh β monitor for header hh) for a fixed duration and returns all captured CAN frames as an array of hex strings. Each transport handles this differently:
- BLE: sets a
monitorModeflag on the message processor so that a timeout returns accumulated data instead of throwing. After the duration a bare carriage return is sent to stop monitoring and the resultingSTOPPED>acknowledgment is drained before returning, preventing it from corrupting the next regular command. - Wi-Fi: performs a single send-and-receive with a generous timeout.
- Serial: reads from the file descriptor until the duration expires.
This capability enables passive CAN bus observation and forms the foundation for proprietary protocol work where raw frame capture is needed alongside standard OBD diagnostics.
switchProtocol(_ proto: PROTOCOL) switches the ELM327 to a different CAN protocol (sends ATSPn and reasserts ATH1) without dropping the Bluetooth or serial connection. This is useful when a vehicle has multiple CAN buses operating on different protocols β the app layer can switch mid-session to target a specific bus.
scanForUDSDTCs(header: String) sends UDS Service $19 subfunction $02 (Read DTC by Status Mask, all statuses) to a specific ECU identified by its 11-bit or 29-bit CAN header. This extends DTC coverage beyond the standard OBD Mode 03 to manufacturer-specific ECUs that respond to UDS but not OBD. The response is parsed as 4-byte DTC groups (two DTC bytes, one status byte, one filler) and returned as the same TroubleCode type used by the standard scan. A new ECUID.becm case (raw value 0x04) has been added to the ECU identifier enumeration for Battery ECU targeting.
Mode 1 now covers the full SAE J1979 PID space from 0x00 through 0xC8. The additions include:
- PID group D (0x60β0x7F): driver and actual engine torque, reference torque, turbocharger RPM and temperatures, boost pressure control, VGT, wastegate, exhaust pressure, charge air cooler temperature, exhaust gas temperature (EGT) banks 1 and 2, DPF differential pressure, DPF status and temperature, NOx NTE and PM NTE control area status, total engine run time.
- PID group E (0x80β0x9F): AECD run-time counters (up to 20 entries), NOx sensor concentration, manifold surface temperature, NOx reagent system, PM sensor banks 1 and 2, intake manifold pressure (secondary), SCR inducement system, diesel aftertreatment, wide-range O2 sensor, throttle position G, engine friction torque, WWH-OBD vehicle information and counters, fuel system control, NOx warning and inducement system.
- PID group F / G (0xA0β0xC8): NOx sensor corrected concentrations, per-cylinder fuel rate, evap system pressure (alternate), transmission actual gear, commanded DEF dosing, odometer, NOx sensor concentrations at banks 3 and 4, ABS disable switch, fuel level inputs A and B, exhaust particulate diagnostics, fuel pressure A and B, particulate control status, distance since ECU reflash, NOx/PM warning lamp state.
All new PIDs carry the appropriate CommandProperties entries (mode byte, description, expected byte count, decoder type, and a flag indicating whether the PID needs vehicle-running conditions).
minBytesguard on UAS multi-byte decoders: many vehicles return a single-byte default response (0x11) for unsupported Mode 1 PIDs. All UAS decoder entries for physically meaningful quantities that require at least 2 bytes (RPM, speed, voltage, duration, resistance, temperature, pressure, angle, ratio, frequency, distance) now carryminBytes: 2and return.failure(.noData)instead of decoding the garbage byte as a real value.- Safe subscript extension: a
subscript(safe:)extension onCollectionprevents out-of-bounds crashes when bit-array operations or decoder index arithmetic runs against unexpectedly short responses. CVNDecoder: a new decoder for Mode 9 Calibration Verification Numbers (CVN), used to verify ECU software integrity.UASentry 0x34: addsUnitDuration.minutessupport for elapsed-time quantities that return values in minutes.CommandProperties.decode: the spurious.dropFirst()that was stripping the first payload byte before decoding has been removed. All decoders now receive the full data slice.FuelTypeDecoderandMaxMafDecoder: now use the safe subscript rather than direct index access to guard against empty response data.MonitorDecoder: convertsDatato[UInt8]before indexed access, avoidingDataindex-offset pitfalls.
Scan and connection lifecycle
ConnectionStatenow conforms toEquatable, enabling aremoveDuplicates()operator in the Combine state publisher so consumers do not receive redundant state updates on reconnect cycles.- Bluetooth power-on no longer auto-connects to a previously seen peripheral. Scanning is now always initiated explicitly by the caller, giving the app layer full control over when peripheral discovery begins.
- A new
connectionInProgressguard prevents stacking a second connection attempt on top of one already in flight; the attempt throwsBLEManagerError.connectionInProgressimmediately rather than silently racing. - State restoration (CoreBluetooth background reconnect) no longer promotes a restored peripheral to the managed slot automatically. Instead, restored peripherals are added to the discovered list so they appear in the UI, and the user chooses whether to connect. This prevents silent reconnects to a previously paired adapter the user may have switched away from.
peripheralManager.reset()is now called on connection failure to clear the peripheral delegate and any pending completion handlers, so a retry starts from a clean baseline.
Device Information Service
On GATT service discovery the handler now reads all characteristics from the standard Bluetooth Device Information Service (UUID 0x180A). Manufacturer name, model number, serial number, hardware revision, firmware revision, software revision, system ID, and IEEE certification are all decoded and published via the adapterInfoUpdated delegate callback and the adapterInfo: [String: String] published property on OBDService. Binary characteristics (System ID and IEEE cert) are formatted as colon-separated or space-separated hex.
ISSC/Microchip Transparent UART
The ISSC service (UUID 49535343-FE7D-...) and its TX/RX characteristics are now explicitly recognised and gracefully skipped rather than generating unknown-characteristic warnings. This removes spurious log noise when connecting to adapters based on RN4870 or ISP1807 Bluetooth modules.
Concurrent command assertion
The assertion that guards against concurrent BLE commands is now handled through Swift's structured concurrency task cancellation handler, which correctly resolves the continuation when a task is cancelled rather than leaving it dangling.
A structured logging pipeline has been added end-to-end:
OBDServiceDelegategains alogMessage(_ message: String)method with a default no-op implementation so existing conformances don't need to change.OBDServiceexposes anonLog: ((String) -> Void)?closure for apps that do not adopt the delegate pattern.- Every step of the ELM327 initialisation sequence (
ATZ,ATE0,ATL0,ATS0,ATH1,ATSP0) emits a log message with the raw response. - Protocol detection emits messages at each stage: preferred protocol test, ATSP0, 0100, ATDPN query, and final result (including whether the detected protocol passed the 0100 validation test).
- All OBD commands can be logged via
ConfigurationService.shared.obdCommandLogging = true, which causes everysendCommandcall to emitCMD <cmd> β <response>to both the system log and theonLogcallback. - Serial verbose logging is gated separately via
ConfigurationService.shared.serialVerboseLogging.
This makes it straightforward to surface a live connection log in the UI, which is particularly valuable during development and for diagnosing adapter compatibility issues with unfamiliar vehicles.
ConfigurationService.shared is now public static let (was static var) and all properties are public. New settings:
| Property | Key | Default | Description |
|---|---|---|---|
wifiHost |
wifiHost |
192.168.0.10 |
Wi-Fi adapter IP address |
wifiPort |
wifiPort |
35000 |
Wi-Fi adapter TCP port |
serialPath |
serialPath |
"" |
macOS serial device path (e.g. /dev/tty.usbserial-110) |
serialVerboseLogging |
serialVerboseLogging |
false |
Log every byte read/written on serial |
obdCommandLogging |
obdCommandLogging |
false |
Log every OBD command and response |
All values are persisted in UserDefaults.standard.
Published properties
peripherals: [CBPeripheral]β updated in real time as BLE discovery finds adapters. Drives any adapter picker UI directly.adapterInfo: [String: String]β key/value map of device information characteristics read from the connected adapter's GATT Device Information Service.
Closure callbacks
In addition to the OBDServiceDelegate protocol, OBDService now exposes plain Swift closures for integrators that prefer a callback model over delegation:
onConnectionStateChanged: ((ConnectionState) -> Void)?onPeripheralsUpdated: (([CBPeripheral]) -> Void)?onScanningChanged: ((Bool) -> Void)?onAdapterInfoUpdated: (([String: String]) -> Void)?onLog: ((String) -> Void)?
New methods
startConnection(preferedProtocol:timeout:peripheral:)β theperipheralparameter lets the caller connect directly to a specificCBPeripheral(e.g. one chosen from a scan list) rather than relying on the default scan-and-first-found behaviour.switchProtocol(_ proto: PROTOCOL)β switches the ELM327 CAN protocol mid-session without disconnecting.scanForUDSDTCs(header: String)β reads DTCs from a specific ECU using UDS Service $19.sendMonitorCommandInternal(_ command: String, duration: TimeInterval)β exposes the monitor-mode capture path.
VINInfo gains an optional Trim field decoded from the NHTSA VIN lookup response.
The CommProtocol protocol and CommunicationError enum have been moved from wifiManager.swift into their own file (CommProtocol.swift). The protocol now includes:
sendMonitorCommand(_ command: String, duration: TimeInterval)β monitor mode capture.reset()β returns the transport to a clean disconnected state, aborting any in-flight continuation.
All four transports (BLE, Wi-Fi, iOS Serial, macOS Serial) conform to the updated protocol.
OBDCommand and all its sub-enumerations (General, Protocols, Mode1, Mode3, Mode6, Mode9) now conform to Sendable. OBDService and ConfigurationService carry @unchecked Sendable to satisfy Swift 5.10 strict concurrency checks. These additions eliminate data-race warnings when using OBDCommand values across actor boundaries and enable the library to be used cleanly in async contexts.
-
Create a New Swift Project
Open Xcode and start a new iOS or macOS project. -
Add the SwiftOBD2 Package
In Xcode navigate to File > Add Packages... and enter this repository's URL:https://github.com/kkonteh97/SwiftOBD2/ -
Permissions and Capabilities
- Bluetooth: add
NSBluetoothAlwaysUsageDescriptiontoInfo.plistand enable Uses Bluetooth LE Accessories under the Background Modes capability. - USB Serial (iOS, MFi): add
com.scantool.stnobdto theUISupportedExternalAccessoryProtocolsarray inInfo.plist. The MFi entitlement is also required for App Store distribution. - USB Serial (macOS): no entitlement is needed for
termios/POSIX serial access. The user selects the/dev/tty.*path in your preferences UI and assigns it toConfigurationService.shared.serialPath.
- Bluetooth: add
OBDService: the primary entry point. Manages the selected transport, drives ELM327 initialisation, and exposes all vehicle interaction APIs.ConfigurationService: persists connection settings (type, Wi-Fi host/port, serial path, logging flags) toUserDefaults.CommProtocol: the internal transport abstraction. Implemented byBLEManager,WifiManager,SerialManager(iOS),MacSerialManager(macOS), andMOCKComm. Not part of the public API surface but useful to understand when building custom transports.OBDServiceDelegate: protocol for receiving connection state changes, peripheral list updates, adapter info, and log messages. Default no-op implementations are provided so conformances only need to implement the callbacks they care about.OBDCommand: typed enumeration of all supported OBD commands organised by mode. Each case carries aCommandPropertiesstruct that encodes the wire bytes, human-readable description, expected response length, decoder, and whether running-engine conditions are required.ConnectionState: value describing the current transport state. Conforms toSendableandEquatable.
Set the desired connection type and any required settings before connecting:
- For Wi-Fi, set
ConfigurationService.shared.wifiHostand.wifiPortto match your adapter. - For macOS Serial, set
ConfigurationService.shared.serialPathto the/dev/tty.*device. - For iOS USB Serial, ensure the adapter is physically connected; no path configuration is needed.
Subscribe to obdService.$connectionState (Combine) or assign obdService.onConnectionStateChanged to react to state transitions without adopting the delegate protocol.
Call startConnection(preferedProtocol:timeout:peripheral:). The optional peripheral argument connects directly to a specific BLE device from a prior scan. The call returns OBDInfo containing the detected OBD protocol, a list of supported PIDs, and vehicle identification data.
Call scanForPeripherals() to populate obdService.peripherals. Present the list in your UI and pass the chosen CBPeripheral to startConnection(peripheral:).
Use startContinuousUpdates(_ pids:) to poll a set of PIDs at a regular interval. The returned publisher emits a [OBDCommand: MeasurementResult] dictionary on each update cycle. Use addPID(_:) and removePID(_:) to adjust the active set without restarting the update loop.
scanForTroubleCodes()reads standard OBD Mode 03 DTCs.scanForUDSDTCs(header:)reads manufacturer-specific DTCs from an ECU identified by its CAN header, using UDS Service $19.clearTroubleCodes()sends Mode 04 to erase stored DTCs.
Call sendMonitorCommandInternal("AT MA", duration: 5.0) to capture 5 seconds of raw CAN frames from all IDs. Use "AT MT hh" to monitor a specific header. The returned array contains raw hex frame strings as reported by the ELM327.
Use switchProtocol(_ proto:) to move between CAN buses (e.g. from protocol6 ISO 15765-4 11-bit 500kbps to protocol9 ISO 15765-4 29-bit 500kbps) without disconnecting from the adapter.
After connecting, obdService.adapterInfo contains a dictionary of GATT Device Information Service fields ("Manufacturer", "Model", "Firmware Revision", etc.) read directly from the BLE adapter. Subscribe via obdService.$adapterInfo or the onAdapterInfoUpdated closure.
Enable ConfigurationService.shared.obdCommandLogging = true during development to see every OBD command and raw response. Assign obdService.onLog to route messages to your app's log view or console.
class ViewModel: ObservableObject {
@Published var measurements: [OBDCommand: MeasurementResult] = [:]
@Published var connectionState: ConnectionState = .disconnected
@Published var connectionLogs: [String] = []
var cancellables = Set<AnyCancellable>()
let obdService = OBDService(connectionType: .bluetooth)
init() {
obdService.$connectionState.assign(to: &$connectionState)
obdService.onLog = { [weak self] msg in
DispatchQueue.main.async { self?.connectionLogs.append(msg) }
}
}
func startConnection() async throws {
let info = try await obdService.startConnection(preferedProtocol: .protocol6)
print(info)
obdService.startContinuousUpdates([.mode1(.rpm), .mode1(.speed)])
.sink { _ in } receiveValue: { self.measurements = $0 }
.store(in: &cancellables)
}
func stopConnection() {
cancellables.removeAll()
obdService.stopConnection()
}
func getTroubleCodes() async {
let dtcs = try? await obdService.scanForTroubleCodes()
print(dtcs ?? "nil")
}
func getUDSDTCs(ecuHeader: String) async {
let dtcs = try? await obdService.scanForUDSDTCs(header: ecuHeader)
print(dtcs ?? "nil")
}
func monitorCANBus() async {
let frames = try? await obdService.sendMonitorCommandInternal("AT MA", duration: 5.0)
print(frames ?? [])
}
}| Type | Platform | Adapter Examples |
|---|---|---|
| Bluetooth LE | iOS, macOS | OBDLink MX+, BAFX, Veepeak BLE |
| Wi-Fi TCP | iOS, macOS | Veepeak Mini WiFi, most clone adapters |
| USB Serial (MFi) | iOS only | OBDLink EX |
| USB Serial (POSIX) | macOS only | Any USB-to-serial adapter at a /dev/tty.* path |
| Mode | Description |
|---|---|
| Mode 01 | Real-time data β PIDs 0x00β0xC8 (full SAE J1979 range) |
| Mode 03 | Stored DTCs |
| Mode 04 | Clear DTCs |
| Mode 06 | On-board monitoring test results (MIDs AβM) |
| Mode 09 | Vehicle information (VIN, calibration IDs, CVN) |
| UDS $19 | Manufacturer-specific DTCs via header targeting |
A complete list of Mode 1 PID cases is in OBDCommand.Mode1. Each case maps directly to its SAE J1979 PID byte.
Q: Bluetooth connection fails immediately
- Ensure
NSBluetoothAlwaysUsageDescriptionis inInfo.plist. - Make sure Bluetooth is on and permissions granted in iOS Settings.
- Verify your ELM327 adapter is powered (OBD port has ignition on).
- Try calling
scanForPeripherals()first and passing the resulting peripheral tostartConnection(peripheral:)rather than relying on auto-discovery.
Q: Wi-Fi adapter times out during protocol detection
- Some adapters take longer than 7 seconds on
SEARCHING.... Increase thetimeoutparameter tostartConnection(15β20 seconds is safe). - Confirm host and port match your adapter β set them via
ConfigurationService.shared. - If connection works but
ATZcauses a disconnect, this is handled automatically by the Wi-Fi reconnect logic in this release.
Q: macOS serial adapter not found
- Run
ls /dev/tty.*in Terminal after connecting the adapter to find the device path. - Assign the path to
ConfigurationService.shared.serialPathbefore callingstartConnection. - The baud auto-probe will try four rates; if none produces a valid response, check the cable and that the adapter is ELM327-compatible.
Q: iOS USB Serial adapter not detected
- Confirm the adapter carries the
com.scantool.stnobdMFi protocol string (OBDLink EX does; most clone adapters do not). - Add
UISupportedExternalAccessoryProtocolswithcom.scantool.stnobdtoInfo.plist. - The adapter must be physically connected before calling
startConnection.
Q: PIDs return zero or garbage values on some vehicles
- Enable
ConfigurationService.shared.obdCommandLogging = trueand inspect the raw responses viaonLog. - Single-byte default responses (e.g.
0x11) from unsupported PIDs now return.failure(.noData)rather than a decoded value β this is correct behaviour and means the vehicle ECU does not support that PID.
Q: No data received from vehicle
- Confirm the vehicle is OBD2 compatible (1996+ in the US).
- Some PIDs require the engine to be running β check the
requiresRunningEngineflag onCommandProperties. - Try connecting without a preferred protocol first (omit
preferedProtocol) to let auto-detection run.
β Tested ELM327 Adapters:
- BAFX Products Bluetooth OBD2
- OBDLink MX+ Bluetooth
- OBDLink EX USB (iOS serial)
- VEEPEAK Mini WiFi OBD2
- Generic ELM327 BLE clones (FFE0/FFF0/18F0 GATT profiles)
- Cheap ELM327 clones may drop the Wi-Fi TCP connection on ATZ; the automatic reconnect handles this transparently.
- iOS USB serial requires MFi certification β generic USB OBD adapters without the
com.scantool.stnobdprotocol string will not enumerate.
- π Open an issue for bug reports
- π‘ Start a discussion for questions
- π± Check out the sample app for implementation examples
- Permissions: Bluetooth requires
NSBluetoothAlwaysUsageDescriptioninInfo.plistand the Background Modes capability. USB serial on iOS additionally requires MFi entitlements. - Error Handling: implement robust error handling β adapter timeouts, unsupported PIDs, and CAN bus errors all surface as typed Swift errors.
- Thread Safety:
OBDServiceisObservableObjectand marshals@Publishedupdates to the main thread. TheonLogand other closures are also dispatched to the main queue. - Background Updates: if your app needs OBD data in the background, enable the Uses Bluetooth LE Accessories background mode and handle the CoreBluetooth state restoration path (peripherals are now restored to the scan list rather than auto-connected).
This project welcomes your contributions! Feel free to open issues for bug reports or feature requests. To contribute code:
- Fork the repository.
- Create your feature branch.
- Commit your changes with descriptive messages.
- Submit a pull request for review.
The Swift OBD package is distributed under the MIT license. See the LICENSE file for more details.
Love SwiftOBD2? Here's how you can help:
- β Star this repository - It really makes a difference!
- π Report bugs - Help us improve by reporting issues
- π‘ Suggest features - Share your ideas for new functionality
- π Contribute code - Submit PRs for fixes and enhancements
- π’ Spread the word - Share with other iOS/Swift developers
Current Stars: 106+ and growing! π
- SwiftOBD2App - Sample iOS app demonstrating SwiftOBD2
- Want your project listed here? Open a PR!
