CBPeripheral gets stuck in connecting/disconnecting states

I work on an app that operates a HW device that acts as a BLE peripheral. Our BLE code stack has not changed much since 2017 and has been working very well over the years. We recently started seeing a lot of customer complaints and bad App Store reviews that the device was not working.

I have been investigating this for several weeks now and I'm struggling to narrow down the cause, but it seems to be a change in iOS. With the same app and device FW the issue is almost exclusively seen on iOS 17.x even though ~40% of our user base is still on iOS 16.x.

From my investigation what I see is the CBPeripheral getting stuck in connecting state. When it is in this state advertisements are seen in our app, and other apps are able to connect to the device (nRF Connect for example). If I cancel the connection the CBPeripheral then gets stuck in the disconnecting state. I can only toggle between these two states and it will remain like this for days.

I have found that initializing a new CBCentralManger will sometimes "fix" the issue. However, about 50% of the time the new CBCentralManager comes up in the unknown state so CoreBluetooth as a whole seems to be in a weird state.

More effective is killing the app and relaunching. But even then sometimes the CBPeripheral immediately gets stuck again and it takes multiple killing/launching the app to get back in a working state.

Few points that seem relevant:

  • App has central and peripheral background modes enabled.
  • App uses state restoration, though most of the times I see this issue there was not a state restore that happened.
  • To reproduce the issue the app needs to be in the background for some amount of time, and it happens on foregrounding.
  • We will in some cases scan/connect in the background, but I have reproduced this issue without that.

Is anyone else seeing this issue or have ideas what might be causing it?

I believe I have figured out the root cause. Starting in iOS 17, if you have state restoration enabled, CoreBluetooth can return a new instance of CBPeripheral with the same identifier!

As recommended by the docs we hold a strong reference to the CBPeripheral so that we can connect to it as needed. Also, per the docs, we have code implemented that checks identifier to see if that object needs updated. But we don't check that it's a new instance (ie memory address).

This is from the debugger on a single breakpoint in centralManager(_:didDiscover:advertisementData:rssi:):

(lldb) po peripheral
<CBPeripheral: 0x280aac8f0, identifier = BC077734-EAE6-5F1C-446C-B56305A18176, name = D2493B7P, mtu = 0, state = disconnected>
(lldb) po getPeripheral(peripheral).peripheral
<CBPeripheral: 0x280ab11e0, identifier = BC077734-EAE6-5F1C-446C-B56305A18176, name = D2493B7P, mtu = 23, state = connecting>

Notice how the identifiers match, but the memory address (and state) are different. The getPeripheral() function retrieves a previously retained peripheral as a wrapper object that holds a strong reference to the CBPeripheral, as well as some metadata specific to our app. I have verified that the CBCentralManager has not changed, and state restoration did not occur.

This seems like a bug in iOS 17. I would expect the identifier to change if there is a new object. Otherwise, why have an identifier? I will file a report with Apple.

Hi there! We are having the same issue with our application. Do you know if Apple has fixed this bug? We are seeing it heavily in the latest public iOS release. Have you tested it in the beta iOS 17.3 developer release?

I have the similar issue. I use state restoration. And when trying connect to CBPeripheral my device stuck in Connection state.

Has anyone ideas how to solve this issue ?

I had the similar issue. CBPeripheral device stucks in Connecting state.

My problem was that I had 2 different CBCentralManager.

First central manager just discovers CBPeripheral.

Second work as background central manager.

So when the first manager discovered peripheral I store this CBPeripheral. Next I tried to connect stored peripheral with second central manager. And connection stucks in connecting state.

I fix this issue by removing second central manager and use only the first.

I've the same issue during state restoration. The CBPeripheral restored has different address compared to the peripheral used when the app is in foreground.

The peripheral remains in connecting state and immediately after willRestoreState it is disconnected with error The connection has timed out unexpectedly

Any workaround for this problem?

CBPeripheral gets stuck in connecting/disconnecting states
 
 
Q