Summary
After extensive debugging in a production app, we discovered several issues in BLEManager that cause reconnection to fail after disconnecting from an OBD adapter. The issues compound over multiple connect/disconnect cycles, eventually making reconnection impossible without killing the app.
Environment
- iOS 18.4+
- SwiftOBD2 (main branch, commit 2cd5cb1)
- Various ELM327-compatible Bluetooth adapters
Issues Found
1. connectAsync() silently returns if not disconnected
File: bleManager.swift:265-268
func connectAsync(timeout: TimeInterval, peripheral _: CBPeripheral? = nil) async throws {
if connectionState != .disconnected {
return // ← SILENT RETURN - no error thrown!
}
// ... connection never starts
}
Problem: cancelPeripheralConnection() is asynchronous - the didDisconnect callback fires 100-500ms later. If the app calls connectAsync() immediately after disconnectPeripheral(), the guard passes silently and no connection attempt is made.
Expected behavior: Should either:
- Wait for
connectionState == .disconnected before returning
- Throw an error like
BLEManagerError.connectionInProgress
2. foundPeripherals array grows unbounded
File: bleManager.swift:126-136
@Published var foundPeripherals: [CBPeripheral] = []
func appendFoundPeripheral(peripheral: CBPeripheral, ...) {
// Appends to array, never clears
foundPeripherals.append(peripheral)
}
Problem: foundPeripherals is never cleared in resetConfigure() or anywhere else. After multiple scan/connect cycles, this array grows and may contain stale peripheral references.
3. buffer not cleared on disconnect
File: bleManager.swift:56 and bleManager.swift:401-406
private var buffer = Data()
private func resetConfigure() {
ecuReadCharacteristic = nil
ecuWriteCharacteristic = nil
connectedPeripheral = nil
connectionState = .disconnected
// buffer is NOT cleared!
}
Problem: If a disconnect occurs mid-response, buffer retains partial data. On reconnection, the next response gets corrupted when this stale data is prepended.
4. Completion handlers can be left dangling
File: bleManager.swift:58-60
private var sendMessageCompletion: (([String]?, Error?) -> Void)?
private var foundPeripheralCompletion: ((CBPeripheral?, Error?) -> Void)?
private var connectionCompletion: ((CBPeripheral?, Error?) -> Void)?
If connection fails mid-operation (e.g., during didDiscoverCharacteristics), these handlers are never called or cleared. The next connection attempt hits:
guard sendMessageCompletion == nil else {
throw BLEManagerError.sendingMessagesInProgress // Line 299-301
}
5. OBDServiceError.noAdapterFound is defined but never thrown
File: obd2service.swift
enum OBDServiceError: Error {
case noAdapterFound // case 0
// ...
}
This error case exists but is never thrown anywhere in the codebase. When connection fails silently (Issue #1), the app receives wrapped errors that eventually show as "Error 0" which maps to this unused case.
Reproduction Steps
- Connect to OBD adapter successfully
- Disconnect (programmatically via
stopConnection())
- Immediately attempt to reconnect
- Result: Connection silently fails (no error, no connection)
- Repeat steps 2-4 about 5 times
- Result: "Error 0" appears, reconnection impossible even after force quit
Suggested Fixes
Fix 1: Make connectAsync() explicit about its preconditions
func connectAsync(timeout: TimeInterval, peripheral: CBPeripheral? = nil) async throws {
guard connectionState == .disconnected else {
throw BLEManagerError.connectionInProgress // New error case
}
// ... rest of method
}
Fix 2: Clear all state in resetConfigure()
private func resetConfigure() {
ecuReadCharacteristic = nil
ecuWriteCharacteristic = nil
connectedPeripheral = nil
connectionState = .disconnected
// ADD THESE:
buffer.removeAll()
foundPeripherals.removeAll()
sendMessageCompletion = nil
foundPeripheralCompletion = nil
connectionCompletion = nil
}
Fix 3: Add explicit reset() method for full cleanup
/// Fully resets BLEManager state for clean reconnection
public func reset() {
disconnectPeripheral()
resetConfigure()
// Cancel any pending scans
stopScan()
}
Workarounds We Tried (All Failed)
- Waiting 1.5s after disconnect - Helped sometimes, not reliable
- Polling until
connectionState == .disconnected - State updates but internal state still corrupted
- Creating new OBDService instance - New CBCentralManager conflicts with iOS Bluetooth stack
- Tracking service instance IDs - Over-engineering that didn't address root cause
Impact
These issues make SwiftOBD2 effectively single-use per app session. Users must force-quit the app to reconnect to their OBD adapter, which is a poor user experience for automotive diagnostic apps.
Related Files
Sources/SwiftOBD2/Communication/bleManager.swift
Sources/SwiftOBD2/obd2service.swift
Sources/SwiftOBD2/elm327.swift
Thank you for this library! Happy to help with a PR if you'd like to address these issues.
Summary
After extensive debugging in a production app, we discovered several issues in
BLEManagerthat cause reconnection to fail after disconnecting from an OBD adapter. The issues compound over multiple connect/disconnect cycles, eventually making reconnection impossible without killing the app.Environment
Issues Found
1.
connectAsync()silently returns if not disconnectedFile:
bleManager.swift:265-268Problem:
cancelPeripheralConnection()is asynchronous - thedidDisconnectcallback fires 100-500ms later. If the app callsconnectAsync()immediately afterdisconnectPeripheral(), the guard passes silently and no connection attempt is made.Expected behavior: Should either:
connectionState == .disconnectedbefore returningBLEManagerError.connectionInProgress2.
foundPeripheralsarray grows unboundedFile:
bleManager.swift:126-136Problem:
foundPeripheralsis never cleared inresetConfigure()or anywhere else. After multiple scan/connect cycles, this array grows and may contain stale peripheral references.3.
buffernot cleared on disconnectFile:
bleManager.swift:56andbleManager.swift:401-406Problem: If a disconnect occurs mid-response,
bufferretains partial data. On reconnection, the next response gets corrupted when this stale data is prepended.4. Completion handlers can be left dangling
File:
bleManager.swift:58-60If connection fails mid-operation (e.g., during
didDiscoverCharacteristics), these handlers are never called or cleared. The next connection attempt hits:5.
OBDServiceError.noAdapterFoundis defined but never thrownFile:
obd2service.swiftThis error case exists but is never thrown anywhere in the codebase. When connection fails silently (Issue #1), the app receives wrapped errors that eventually show as "Error 0" which maps to this unused case.
Reproduction Steps
stopConnection())Suggested Fixes
Fix 1: Make
connectAsync()explicit about its preconditionsFix 2: Clear all state in
resetConfigure()Fix 3: Add explicit
reset()method for full cleanupWorkarounds We Tried (All Failed)
connectionState == .disconnected- State updates but internal state still corruptedImpact
These issues make SwiftOBD2 effectively single-use per app session. Users must force-quit the app to reconnect to their OBD adapter, which is a poor user experience for automotive diagnostic apps.
Related Files
Sources/SwiftOBD2/Communication/bleManager.swiftSources/SwiftOBD2/obd2service.swiftSources/SwiftOBD2/elm327.swiftThank you for this library! Happy to help with a PR if you'd like to address these issues.