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.