esp-nimble-cpp icon indicating copy to clipboard operation
esp-nimble-cpp copied to clipboard

Wi-Fi + BLE resulting in BLE disconnections from iOS after some amount of time (but not a Linux computer)

Open cmorganBE opened this issue 9 months ago • 7 comments

esp32s3, esp-idf v5.3, esp-nimble-cpp 63a3301696 (Add missing const in NimblEConnInfo).

I'm not suggesting this is an esp-nimble-cpp issue but I'm also not sure what to do next in terms of how to gather the next piece of information.

What I'm seeing:

  • Wi-Fi connected
  • Connect with BLE (iOS + lightblue), pair and bond
  • Retrieve some characteristics, works fine
  • Sit for a minute or two
  • BLE disconnected

It's possible to reconnect as soon as the disconnection occurs, the same thing happens after this reconnection, after a few minutes.

If I connect with a Linux PC I can transfer megabytes of data without disconnection (and with Wi-Fi connected), so it looks like this is something related to iOS but I'm wondering if its a timeout setting or something on the connection that can be configured.

Tried to look at 'NimBLE' output via 'log_level NimBLE debug', and I see this when the disconnection occurs:

no new log output here until BLE disconnects, then the below happens

D (2006502) NimBLE: ble_hs_event_rx_hci_ev; opcode=0x5 
D (2006502) NimBLE: 

I (2006502) BLE: Client disconnected
I (2006502) NimBLE: GAP procedure initiated: advertise; 
I (2006502) NimBLE: disc_mode=2
I (2006502) NimBLE:  adv_channel_map=0 own_addr_type=0 adv_filter_policy=2 adv_itvl_min=0 adv_itvl_max=0
I (2006502) NimBLE: 

D (2006502) NimBLE: ble_hs_hci_cmd_send: ogf=0x08 ocf=0x0006 len=15

D (2006502) NimBLE: 0x06 
D (2006502) NimBLE: 0x20 
D (2006502) NimBLE: 0x0f 
D (2006502) NimBLE: 0x30 
D (2006502) NimBLE: 0x00 
D (2006502) NimBLE: 0x60 
D (2006502) NimBLE: 0x00 
D (2006502) NimBLE: 0x00 
D (2006502) NimBLE: 0x00 
D (2006502) NimBLE: 0x00 
D (2006502) NimBLE: 0x00 
D (2006502) NimBLE: 0x00 
D (2006502) NimBLE: 0x00 
D (2006502) NimBLE: 0x00 
D (2006502) NimBLE: 0x00 
D (2006502) NimBLE: 0x00 
D (2006502) NimBLE: 0x07 
D (2006502) NimBLE: 0x02 
D (2006502) NimBLE: 

D (2006502) NimBLE: ble_hs_hci_cmd_send: ogf=0x08 ocf=0x000a len=1

D (2006502) NimBLE: 0x0a 
D (2006502) NimBLE: 0x20 
D (2006502) NimBLE: 0x01 
D (2006502) NimBLE: 0x01 
D (2006502) NimBLE: 

I (2006512) EESystem: BLE lastState (3) != state (0)
I (2006512) EESystem: BleRestricted

Thoughts on how to gather the data to debug?

cmorganBE avatar Mar 04 '25 21:03 cmorganBE

Using nRFConnect you might be able to see something in the logs on the iOS side. Would be good to see the reason code in the NimBLE logs as well.

h2zero avatar Mar 06 '25 18:03 h2zero

@h2zero how do I enable the reason code from the nimble side? I did turn on debugging as you can see above but maybe this isn't everything?

cmorganBE avatar Mar 06 '25 19:03 cmorganBE

@cmorganBE The logs only show the stack debug logs. Please enable the esp-nimble-cpp debug logs as well. Also the onDisconnect callback provides the disconnect reason.

h2zero avatar Mar 12 '25 15:03 h2zero

Hey @cmorganBE I think I ran into something similar to this previously. It may have to do with your connection parameters. If you look at the Accessory Design Guidelines for Apple Devices here https://www.bluetooth.com/wp-content/uploads/attachments/BluetoothDesignGuidelines.pdf

You will see the following.

Connection Parameters

1.6 Connection Parameters

The accessory is responsible for the connection parameters used for the Low Energy connection. The accessory should request connection parameters appropriate for its use case by sending an L2CAP Connection Parameter Update Request at the appropriate time. See the Bluetooth 4.0 specification, Volume 3, Part A, Section 4.20 for details.

The connection parameter request may be rejected if it does not comply with all of these rules:

  • Slave Latency ≤ 30
  • 2 seconds ≤ connSupervisionTimeout ≤ 6 seconds
  • Interval Min modulo 15 ms == 0
  • Interval Min ≥ 15 ms
  • One of the following:
    • Interval Min + 15 ms ≤ Interval Max
    • Interval Min == Interval Max == 15 ms
    • Interval Max * (Slave Latency + 1) ≤ 2 seconds
    • Interval Max * (Slave Latency + 1) * 3 < connSupervisionTimeout

Note: If an accessory requests Interval Min == Interval Max == 15 ms, some devices will scale the interval to 30 ms to balance power and performance constraints.

If Bluetooth Low Energy HID is one of the connected services of an accessory, connection interval down to 1.25 ms may be accepted by the device.

The device will not read or use the parameters in the Peripheral Preferred Connection Parameters characteristic. See the Bluetooth 4.0 specification, Volume 3, Part C, Section 12.5.

So you may want to check those parameters and see if you are correctly setting those.

Here are some Macros I made for my own implementation to make sure I abide by the IOS rules when I tweak the values


// Input Parameters (can be adjusted)
#define INTERVAL_MIN 30.0f              // Minimum connection interval in ms (must be >= 15 ms, multiple of 15 ms)
#define INTERVAL_MAX (INTERVAL_MIN + 15.0f) // Interval Max is 15 ms more than Min
#define PERIPHERAL_LATENCY 30           // Peripheral Latency (must be <= 30) - was 10 
#define SUPERVISION_TIMEOUT 6.0f        // Supervision timeout in seconds (2 <= timeout <= 6) // maybe try 17 seconds  was 6.0f before

// Derived Calculated Macros
#define MAX_LATENCY_INTERVAL (INTERVAL_MAX * (PERIPHERAL_LATENCY + 1))
#define REQUIRED_SUPERVISION_TIMEOUT (MAX_LATENCY_INTERVAL * 3)

// Calculation Macros (for dynamic recalculations)
#define CALCULATE_INTERVAL_MAX(min) ((min) + 15.0f)
#define CALCULATE_MAX_LATENCY_INTERVAL(intervalMax, latency) ((intervalMax) * ((latency) + 1))
// Convert MAX_LATENCY_INTERVAL from milliseconds to seconds before using it
#define CALCULATE_SUPERVISION_TIMEOUT(intervalMax, latency) \
    (CALCULATE_MAX_LATENCY_INTERVAL(intervalMax, latency) / 1000.0f * 3)

// Validation Macros (for specific checks)
#define VALIDATE_INTERVAL_MIN(min) \
    ((min) >= 15.0f && static_cast<int>(min) % 15 == 0)

#define VALIDATE_INTERVAL_MAX(min, max) \
    ((max) >= (min) + 15.0f || ((min) == (max) && (min) == 15.0f))

#define VALIDATE_LATENCY(latency) \
    ((latency) <= 30)

#define VALIDATE_MAX_LATENCY_INTERVAL(maxLatencyInterval) \
    ((maxLatencyInterval) <= 2000.0f)  // 2 seconds max

// Validation Macro for Supervision Timeout
#define VALIDATE_SUPERVISION_TIMEOUT(supervisionTimeout, requiredTimeout) \
    ((supervisionTimeout) >= 2.0f && (supervisionTimeout) <= 6.0f && \
    (supervisionTimeout) > requiredTimeout)

// Advertising Interval Constants (Apple-specified intervals)
#define INITIAL_ADVERTISING_INTERVAL 20.0f
#define LONG_ADVERTISING_INTERVAL_1 152.5f
#define LONG_ADVERTISING_INTERVAL_2 211.25f
#define LONG_ADVERTISING_INTERVAL_3 318.75f
#define LONG_ADVERTISING_INTERVAL_4 417.5f
#define LONG_ADVERTISING_INTERVAL_5 546.25f
#define LONG_ADVERTISING_INTERVAL_6 760.0f
#define LONG_ADVERTISING_INTERVAL_7 852.5f
#define LONG_ADVERTISING_INTERVAL_8 1022.5f
#define LONG_ADVERTISING_INTERVAL_9 1285.0f

// Advertising Validation Macro (ensures a valid interval for discoverability)
#define VALIDATE_ADVERTISING_INTERVAL(interval) \
    ((interval) == INITIAL_ADVERTISING_INTERVAL || \
    (interval) == LONG_ADVERTISING_INTERVAL_1 || \
    (interval) == LONG_ADVERTISING_INTERVAL_2 || \
    (interval) == LONG_ADVERTISING_INTERVAL_3 || \
    (interval) == LONG_ADVERTISING_INTERVAL_4 || \
    (interval) == LONG_ADVERTISING_INTERVAL_5 || \
    (interval) == LONG_ADVERTISING_INTERVAL_6 || \
    (interval) == LONG_ADVERTISING_INTERVAL_7 || \
    (interval) == LONG_ADVERTISING_INTERVAL_8 || \
    (interval) == LONG_ADVERTISING_INTERVAL_9)

michaelboeding avatar Mar 12 '25 20:03 michaelboeding

Modified onDisconnect() to print out the reason code.

I (77824) BLE: Client disconnected - reason 0x208 (520)

Which from https://mynewt.apache.org/latest/network/ble_hs/ble_hs_return_codes.html appears to indicate:

0x0208
0x08
BLE_ERR_CONN_SPVN_TMO
Connection Timeout

And sure enough I'm messing with connection parameters:

// updateConnParams() to shorten the connection interval to
// improve performance
auto minInterval = 6; // 6 * 1.25 = 7.5ms
auto maxInterval = minInterval;
auto latency_packets = 0;
auto timeout = 10; // 10 * 10ms = 100ms
pServer->updateConnParams(connInfo.getConnHandle(),
                           minInterval,
                           maxInterval,
                           latency_packets,
                           timeout);
pServer->setDataLen(connInfo.getConnHandle(), 251);

@michaelboeding I'm guessing I'm violating some iOS rules. After dropping the call to updateConnParams() everything seems stable here. No connections dropped for some time.

cmorganBE avatar Mar 26 '25 12:03 cmorganBE

@cmorganBE Thanks for the update, glad you found the cause. Yes, connection parameters are essential but also problematic.

h2zero avatar Mar 27 '25 23:03 h2zero

BLE and iOS is a delicate story, in particular with respect to performance and stability. It doesn't help that iOS comes with its own shares of bugs. We discussed the connection parameters in particular in https://github.com/espressif/esp-idf/issues/12789 and https://github.com/espressif/esp-idf/issues/13637. It seems you need to experiment a lot to find a stable/performant set of values. And be sure to test every major (and sometimes, minor) iOS release as they are still doing changes to the stack.

mickeyl avatar May 23 '25 13:05 mickeyl