Skip to content

BLEManager: Reconnection fails after disconnect due to accumulated state and silent guard #41

@Maxymvs

Description

@Maxymvs

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

  1. Connect to OBD adapter successfully
  2. Disconnect (programmatically via stopConnection())
  3. Immediately attempt to reconnect
  4. Result: Connection silently fails (no error, no connection)
  5. Repeat steps 2-4 about 5 times
  6. 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)

  1. Waiting 1.5s after disconnect - Helped sometimes, not reliable
  2. Polling until connectionState == .disconnected - State updates but internal state still corrupted
  3. Creating new OBDService instance - New CBCentralManager conflicts with iOS Bluetooth stack
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions