Android Interop Problem with DeviceOS/GattClient Initiated MTU Negotiation

I am running into a BLE interoperability issue between an Android 14 central/client connecting to a DeviceOS 6.2.1 peripheral/server (Argon).

AFAIK, it is typically the central/client device that initiates MTU negotiation, and in Android this is done by a call to BluetoothGatt.requestMtu() after the device connects. In the case of DeviceOS, even when nominally configured as a peripheral/server, it also acts as a central/client and initiates MTU negotiation.

I don't understand the root cause on the Android side but I do know that when both sides initiate negotiation the Android client never finishes the service discovery: the callback onServicesDiscovered is never called after the call to discoverServices. If I remove the call to BluetoothGatt.requestMtu() in the Android app then no problem. If I comment out the call to sd_ble_gattc_exchange_mtu_request() in device-os/hal/src/nRF52840/ble_hal.cpp then no problem.

Here is a trace from the Android console for the problematic case:

Date/Time PID-TID Tag Package Name Log Level Message
2025-07-11 17:47:49.116 8165-8165 BluetoothAdapter io.secureforward.mybletest D getBleEnabledArray(): ON
2025-07-11 17:47:49.118 8165-8165 BluetoothGatt io.secureforward.mybletest D connect() - device: XX:XX:XX:XX:77:30, auto: false
2025-07-11 17:47:49.118 8165-8165 BluetoothGatt io.secureforward.mybletest D registerApp()
2025-07-11 17:47:49.119 8165-8165 BluetoothGatt io.secureforward.mybletest D registerApp() - UUID=a616f277-dd9b-4d9c-87c9-199cafe1cfc8
2025-07-11 17:47:49.124 8165-9333 BluetoothGatt io.secureforward.mybletest D onClientRegistered() - status=0 clientIf=12
2025-07-11 17:47:49.126 8165-9333 BluetoothAdapter io.secureforward.mybletest D getBleEnabledArray(): ON
2025-07-11 17:47:50.596 8165-9333 BluetoothGatt io.secureforward.mybletest D onClientConnectionState() - status=0 clientIf=12 device=XX:XX:XX:XX:77:30
2025-07-11 17:47:50.600 8165-9333 BluetoothGatt io.secureforward.mybletest D configureMTU() - device: XX:XX:XX:XX:77:30 mtu: 512
2025-07-11 17:47:50.603 8165-9333 BluetoothGatt io.secureforward.mybletest D discoverServices() - device: XX:XX:XX:XX:77:30
2025-07-11 17:47:51.120 8165-9333 BluetoothGatt io.secureforward.mybletest D onConnectionUpdated() - Device=XX:XX:XX:XX:77:30 interval=6 latency=0 timeout=500 status=0
2025-07-11 17:47:51.494 8165-9333 BluetoothGatt io.secureforward.mybletest D onConfigureMTU() - Device=XX:XX:XX:XX:77:30 mtu=247 status=0
2025-07-11 17:47:51.569 8165-9333 BluetoothGatt io.secureforward.mybletest D onConnectionUpdated() - Device=XX:XX:XX:XX:77:30 interval=36 latency=0 timeout=500 status=0

And the same thing but with the call to sd_ble_gattc_exchange_mtu_request() in ble_hal.cpp commented out:

Date/Time PID-TID Tag Package Name Log Level Message
2025-07-11 17:52:18.055 8165-8165 BluetoothAdapter io.secureforward.mybletest D getBleEnabledArray(): ON
2025-07-11 17:52:18.058 8165-8165 BluetoothGatt io.secureforward.mybletest D connect() - device: XX:XX:XX:XX:77:30, auto: false
2025-07-11 17:52:18.058 8165-8165 BluetoothGatt io.secureforward.mybletest D registerApp()
2025-07-11 17:52:18.058 8165-8165 BluetoothGatt io.secureforward.mybletest D registerApp() - UUID=c055ae54-e1d1-4f45-a276-9281537e3e2f
2025-07-11 17:52:18.063 8165-8181 BluetoothGatt io.secureforward.mybletest D onClientRegistered() - status=0 clientIf=13
2025-07-11 17:52:18.065 8165-8181 BluetoothAdapter io.secureforward.mybletest D getBleEnabledArray(): ON
2025-07-11 17:52:18.480 8165-8181 BluetoothGatt io.secureforward.mybletest D onClientConnectionState() - status=0 clientIf=13 device=XX:XX:XX:XX:77:30
2025-07-11 17:52:18.482 8165-8181 BluetoothGatt io.secureforward.mybletest D configureMTU() - device: XX:XX:XX:XX:77:30 mtu: 512
2025-07-11 17:52:18.484 8165-8181 BluetoothGatt io.secureforward.mybletest D discoverServices() - device: XX:XX:XX:XX:77:30
2025-07-11 17:52:19.001 8165-8181 BluetoothGatt io.secureforward.mybletest D onConnectionUpdated() - Device=XX:XX:XX:XX:77:30 interval=6 latency=0 timeout=500 status=0
2025-07-11 17:52:19.423 8165-8181 BluetoothGatt io.secureforward.mybletest D onConfigureMTU() - Device=XX:XX:XX:XX:77:30 mtu=247 status=0
2025-07-11 17:52:19.425 8165-8181 BluetoothGatt io.secureforward.mybletest D onSearchComplete() = Device=XX:XX:XX:XX:77:30 Status=0
2025-07-11 17:52:19.425 8165-8181 BluetoothGatt io.secureforward.mybletest D setCharacteristicNotification() - uuid: 85fc567e-31d9-4185-87c6-339924d1c5be enable: true
2025-07-11 17:52:19.496 8165-8181 BluetoothGatt io.secureforward.mybletest D onConnectionUpdated() - Device=XX:XX:XX:XX:77:30 interval=36 latency=0 timeout=500 status=0

I found several topics related to MTU negotiation in community.particle.io but none that really describe this problem. I also found this thread on Nordic's web site which suggests that peripheral initiated negotation may lead to Android interoperability issues albeit with older versions of Android.

FWIW, there is no problem with an iOS central/client because it initiates MTU negotiation first/before service discovery so the DeviceOS initiated negotiation never happens.

All of this to say, would Particle consider an adding and API to disable GattClient initiated MTU negotiation when the role of the connecting device is central? I need the Android client to work with various BLE peripherals without resorting to putting a manufacturer ID in the scan/advertisement data. I'm happy to create a PR - I believe the change is pretty simple.

Moving call to BluetoothGatt.requestMtu() after service discovery is complete, i.e. in onServicesDiscovered() callback, solves the problem. Thanks for your consideration, maybe this post will help someone else who bumps into this issue.

2 Likes

Thanks khp!

Appreciate the post AND the solution :slight_smile: :person_bowing:

Thanks

Nick.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.