ble: MTU handling in TransactionBuilder including MTUs > 515 (#5146)

This is especially relevant for Android 14
https://developer.android.com/about/versions/14/behavior-changes-all#mtu-set-to-517

AbstractBTLEDeviceSupport.calcMaxWriteChunk:
new function to properly calculate chunk size

TransactionBuilder.writeChunkedData:
if required automatically resize the chunk size

TransactionBuilder.write and TransactionBuilder.writeLegacy:
for now log an error but still attempt to write the data
This commit is contained in:
Thomas Kuehne
2025-07-26 10:23:22 +02:00
committed by José Rebelo
parent 1445f35799
commit 8794e11720
4 changed files with 57 additions and 6 deletions
@@ -22,7 +22,7 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothProfile;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import java.util.UUID;
@@ -84,4 +84,18 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport
@Nullable
abstract BluetoothGattCharacteristic getCharacteristic(UUID uuid, int deviceIdx);
abstract int getMTU(int deviceIdx);
/// the maximum payload length supported for one write action
@IntRange(from = 20L, to = 512L)
public static int calcMaxWriteChunk(int mtu) {
// the minimum MTU is 23 (Bluetooth spec)
int safeMtu = Math.max(23, mtu);
// GATT_MAX_ATTR_LEN: no larger than 512 (Bluetooth spec)
// MTU: overhead of simple write must be supported. Some other operations like
// ATT_PREPARE_WRITE_REQ have even larger overhead so the max BLE MTU is larger than 512+3
return Math.min(512, safeMtu - 3);
}
}
@@ -584,6 +584,7 @@ public abstract class AbstractBTLEMultiDeviceSupport extends AbstractBTLEDeviceS
/**
* Get the current MTU, or the minimum 23 if unknown
*/
@Override
public int getMTU(int deviceIdx) {
validateDeviceIndex(deviceIdx);
return mMTUs[deviceIdx];
@@ -524,4 +524,12 @@ public abstract class AbstractBTLESingleDeviceSupport extends AbstractBTLEDevice
public int getMTU() {
return mMTU;
}
@Override
int getMTU(int deviceIdx) {
if(deviceIdx != 0){
throw new IllegalArgumentException("deviceIdx is " + deviceIdx);
}
return getMTU();
}
}
@@ -112,6 +112,14 @@ public class TransactionBuilder {
LOG.warn("Unable to write characteristic: null");
return this;
}
int maxChunk = getMaxWriteChunk();
if (data.length > maxChunk) {
LOG.error("writeLegacy - payload for {} is too long: {} > {}",
characteristic.getUuid(), data.length, maxChunk);
// TODO throw exception after reviewing device specific code (performConnected...)
}
WriteAction action = new WriteAction(characteristic, data, true);
return add(action);
}
@@ -119,15 +127,23 @@ public class TransactionBuilder {
/// Invokes a write operation on a given characteristic
/// The result status will be made available asynchronously through
/// {@link GattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int)}
/// @see #write(UUID, byte...)
/// @see #write(UUID, byte...)
/// @see #writeChunkedData(BluetoothGattCharacteristic, byte[], int)
/// @see #writeLegacy(BluetoothGattCharacteristic, byte...)
/// @see #writeLegacy(BluetoothGattCharacteristic, byte...)
@NonNull
public TransactionBuilder write(@Nullable BluetoothGattCharacteristic characteristic, byte... data) {
if (characteristic == null) {
LOG.warn("Unable to write characteristic: null");
return this;
}
int maxChunk = getMaxWriteChunk();
if (data.length > maxChunk) {
LOG.error("write - payload for {} is too long: {} > {}",
characteristic.getUuid(), data.length, maxChunk);
// TODO throw exception after reviewing device specific code (performConnected...)
}
WriteAction action = new WriteAction(characteristic, data);
return add(action);
}
@@ -135,7 +151,7 @@ public class TransactionBuilder {
/// Invokes a write operation on a given characteristic
/// The result status will be made available asynchronously through
/// {@link GattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int)}
/// @see #write(BluetoothGattCharacteristic, byte...)
/// @see #write(BluetoothGattCharacteristic, byte...)
/// @see #writeChunkedData(BluetoothGattCharacteristic, byte[], int)
/// @see #writeLegacy(BluetoothGattCharacteristic, byte...)
@NonNull
@@ -151,17 +167,22 @@ public class TransactionBuilder {
/// Invokes one or more write operations on a given characteristic
/// The result status will be made available asynchronously through
/// {@link GattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int)}
/// @param requestedChunkLength will be automatically reduced if required for this connection
/// @see #write(BluetoothGattCharacteristic, byte...)
/// @see #write(UUID, byte...)
/// @see #writeLegacy(BluetoothGattCharacteristic, byte...)
@NonNull
public TransactionBuilder writeChunkedData(@Nullable BluetoothGattCharacteristic characteristic,
@NonNull byte[] data, int chunkSize)
{
@NonNull byte[] data,
@IntRange(from = 1L) int requestedChunkLength) {
if (characteristic == null) {
LOG.warn("Unable to write characteristic: null");
return this;
}
// no larger than requested
int chunkSize = Math.min(requestedChunkLength, getMaxWriteChunk());
for (int start = 0; start < data.length; start += chunkSize) {
int end = start + chunkSize;
if (end > data.length) end = data.length;
@@ -172,6 +193,13 @@ public class TransactionBuilder {
return this;
}
/// the maximum payload length supported for one write action
@IntRange(from = 20L, to = 512L)
public int getMaxWriteChunk() {
int mtu = mDeviceSupport.getMTU(mDeviceIdx);
return AbstractBTLEDeviceSupport.calcMaxWriteChunk(mtu);
}
/// Calls {@link BluetoothGatt#requestMtu(int)}. Results are returned asynchronously through
/// {@link GattCallback#onMtuChanged(BluetoothGatt, int, int)}
@NonNull