Xiaomi: change BLE max chunk size with MTU changes

This commit is contained in:
MrYoranimo 2024-01-05 16:51:54 +01:00 committed by José Rebelo
parent 53a7cc5b30
commit 339859c829
5 changed files with 138 additions and 52 deletions

View File

@ -55,7 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBlePro
public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback, GattServerCallback { public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback, GattServerCallback {
private static final Logger LOG = LoggerFactory.getLogger(AbstractBTLEDeviceSupport.class); private static final Logger LOG = LoggerFactory.getLogger(AbstractBTLEDeviceSupport.class);
private int mMTU = 0; private int mMTU = 23;
private BtLEQueue mQueue; private BtLEQueue mQueue;
private Map<UUID, BluetoothGattCharacteristic> mAvailableCharacteristics; private Map<UUID, BluetoothGattCharacteristic> mAvailableCharacteristics;
private final Set<UUID> mSupportedServices = new HashSet<>(4); private final Set<UUID> mSupportedServices = new HashSet<>(4);

View File

@ -79,32 +79,16 @@ public class XiaomiAuthService extends AbstractXiaomiService {
return encryptionInitialized; return encryptionInitialized;
} }
// TODO also implement for spp protected void startEncryptedHandshake() {
protected void startEncryptedHandshake(final XiaomiBleSupport support, final nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder) {
encryptionInitialized = false; encryptionInitialized = false;
builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.AUTHENTICATING, getSupport().getContext()));
System.arraycopy(getSecretKey(getSupport().getDevice()), 0, secretKey, 0, 16); System.arraycopy(getSecretKey(getSupport().getDevice()), 0, secretKey, 0, 16);
new SecureRandom().nextBytes(nonce); new SecureRandom().nextBytes(nonce);
support.sendCommand(builder, buildNonceCommand(nonce)); getSupport().sendCommand("auth step 1", buildNonceCommand(nonce));
} }
protected void startEncryptedHandshake(final XiaomiSppSupport support, final nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder builder) { protected void startClearTextHandshake() {
encryptionInitialized = false;
builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.AUTHENTICATING, getSupport().getContext()));
System.arraycopy(getSecretKey(getSupport().getDevice()), 0, secretKey, 0, 16);
new SecureRandom().nextBytes(nonce);
support.sendCommand(builder, buildNonceCommand(nonce));
}
protected void startClearTextHandshake(final XiaomiBleSupport support, final nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder) {
builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.AUTHENTICATING, getSupport().getContext()));
final XiaomiProto.Auth auth = XiaomiProto.Auth.newBuilder() final XiaomiProto.Auth auth = XiaomiProto.Auth.newBuilder()
.setUserId(getUserId(getSupport().getDevice())) .setUserId(getUserId(getSupport().getDevice()))
.build(); .build();
@ -115,7 +99,7 @@ public class XiaomiAuthService extends AbstractXiaomiService {
.setAuth(auth) .setAuth(auth)
.build(); .build();
support.sendCommand(builder, command); getSupport().sendCommand("auth step 1", command);
} }
@Override @Override

View File

@ -116,29 +116,54 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport {
return builder; return builder;
} }
XiaomiBleSupport.this.characteristicCommandRead = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandRead, mXiaomiSupport.getAuthService()); // FIXME:
XiaomiBleSupport.this.characteristicCommandRead.setEncrypted(uuidSet.isEncrypted()); // Because the first handshake packet is sent before the actions in the builder are run,
XiaomiBleSupport.this.characteristicCommandRead.setChannelHandler(mXiaomiSupport::handleCommandBytes); // the maximum message size is not properly initialized if the device itself does not request
XiaomiBleSupport.this.characteristicCommandWrite = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandWrite, mXiaomiSupport.getAuthService()); // the MTU to be upgraded. However, since we will upgrade the MTU ourselves to the highest
XiaomiBleSupport.this.characteristicCommandWrite.setEncrypted(uuidSet.isEncrypted()); // possible (512) and the device will (likely) respond with something higher than 247,
XiaomiBleSupport.this.characteristicActivityData = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicActivityData, mXiaomiSupport.getAuthService()); // we will initialize the characteristics with that MTU.
XiaomiBleSupport.this.characteristicActivityData.setChannelHandler(mXiaomiSupport.getHealthService().getActivityFetcher()::addChunk); final int expectedMtu = 247;
XiaomiBleSupport.this.characteristicActivityData.setEncrypted(uuidSet.isEncrypted()); characteristicCommandRead = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandRead, mXiaomiSupport.getAuthService());
XiaomiBleSupport.this.characteristicDataUpload = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicDataUpload, mXiaomiSupport.getAuthService()); characteristicCommandRead.setEncrypted(uuidSet.isEncrypted());
XiaomiBleSupport.this.characteristicDataUpload.setEncrypted(uuidSet.isEncrypted()); characteristicCommandRead.setChannelHandler(mXiaomiSupport::handleCommandBytes);
XiaomiBleSupport.this.characteristicDataUpload.setIncrementNonce(false); characteristicCommandRead.setMtu(expectedMtu);
characteristicCommandWrite = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandWrite, mXiaomiSupport.getAuthService());
characteristicCommandWrite.setEncrypted(uuidSet.isEncrypted());
characteristicCommandWrite.setMtu(expectedMtu);
characteristicActivityData = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicActivityData, mXiaomiSupport.getAuthService());
characteristicActivityData.setChannelHandler(mXiaomiSupport.getHealthService().getActivityFetcher()::addChunk);
characteristicActivityData.setEncrypted(uuidSet.isEncrypted());
characteristicActivityData.setMtu(expectedMtu);
characteristicDataUpload = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicDataUpload, mXiaomiSupport.getAuthService());
characteristicDataUpload.setEncrypted(uuidSet.isEncrypted());
characteristicDataUpload.setIncrementNonce(false);
characteristicDataUpload.setMtu(expectedMtu);
builder.requestMtu(247); // request highest possible MTU; device should response with the highest supported MTU anyway
builder.requestMtu(512);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
builder.notify(btCharacteristicCommandWrite, true); builder.notify(btCharacteristicCommandWrite, true);
builder.notify(btCharacteristicCommandRead, true); builder.notify(btCharacteristicCommandRead, true);
builder.notify(btCharacteristicActivityData, true); builder.notify(btCharacteristicActivityData, true);
builder.notify(btCharacteristicDataUpload, true); builder.notify(btCharacteristicDataUpload, true);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext()));
if (uuidSet.isEncrypted()) { if (uuidSet.isEncrypted()) {
mXiaomiSupport.getAuthService().startEncryptedHandshake(XiaomiBleSupport.this, builder); builder.add(new PlainAction() {
@Override
public boolean run(BluetoothGatt gatt) {
mXiaomiSupport.getAuthService().startEncryptedHandshake();
return true;
}
});
} else { } else {
mXiaomiSupport.getAuthService().startClearTextHandshake(XiaomiBleSupport.this, builder); builder.add(new PlainAction() {
@Override
public boolean run(BluetoothGatt gatt) {
mXiaomiSupport.getAuthService().startClearTextHandshake();
return true;
}
});
} }
return builder; return builder;
@ -175,6 +200,20 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport {
public boolean getImplicitCallbackModify() { public boolean getImplicitCallbackModify() {
return mXiaomiSupport.getImplicitCallbackModify(); return mXiaomiSupport.getImplicitCallbackModify();
} }
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
if (characteristicCommandRead != null)
characteristicCommandRead.setMtu(mtu);
if (characteristicCommandWrite != null)
characteristicCommandWrite.setMtu(mtu);
if (characteristicDataUpload != null)
characteristicDataUpload.setMtu(mtu);
if (characteristicActivityData != null)
characteristicActivityData.setMtu(mtu);
}
}; };
public XiaomiBleSupport(final XiaomiSupport xiaomiSupport) { public XiaomiBleSupport(final XiaomiSupport xiaomiSupport) {

View File

@ -42,9 +42,6 @@ public class XiaomiCharacteristic {
public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0}; public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0};
// max chunk size, including headers
public static final int MAX_WRITE_SIZE = 242;
private final XiaomiBleSupport mSupport; private final XiaomiBleSupport mSupport;
private final BluetoothGattCharacteristic bluetoothGattCharacteristic; private final BluetoothGattCharacteristic bluetoothGattCharacteristic;
@ -56,6 +53,10 @@ public class XiaomiCharacteristic {
public boolean incrementNonce = true; public boolean incrementNonce = true;
private int encryptedIndex = 0; private int encryptedIndex = 0;
// max chunk size, including headers
private int maxWriteSize = 244; // MTU of 247 - 3 bytes for the ATT overhead (based on lowest MTU observed after increasing MTU to 512)
private int maxWriteSizeForCurrentMessage;
// Chunking // Chunking
private int numChunks = 0; private int numChunks = 0;
private int currentChunk = 0; private int currentChunk = 0;
@ -145,6 +146,17 @@ public class XiaomiCharacteristic {
sendNext(builder); sendNext(builder);
} }
private void sendChunk(final TransactionBuilder builder, final int index, final int chunkPayloadSize) {
final byte[] payload = currentPayload.getBytesToSend();
final int startIndex = index * chunkPayloadSize;
final int endIndex = Math.min((index + 1) * chunkPayloadSize, payload.length);
LOG.debug("Sending chunk {} from {} to {} for {}", index, startIndex, endIndex, currentPayload.getTaskName());
final byte[] chunkToSend = new byte[2 + endIndex - startIndex];
BLETypeConversions.writeUint16(chunkToSend, 0, index + 1);
System.arraycopy(payload, startIndex, chunkToSend, 2, endIndex - startIndex);
builder.write(bluetoothGattCharacteristic, chunkToSend);
}
public void onCharacteristicChanged(final byte[] value) { public void onCharacteristicChanged(final byte[] value) {
final ByteBuffer buf = ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN); final ByteBuffer buf = ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN);
@ -199,8 +211,16 @@ public class XiaomiCharacteristic {
case 1: case 1:
// Chunked ack // Chunked ack
final byte subtype = buf.get(); final byte subtype = buf.get();
final byte[] remaining = new byte[buf.remaining()];
if (buf.hasRemaining()) {
buf.get(remaining);
LOG.debug("Operation CHUNK_ACK of type {} has additional payload: {}",
subtype, GB.hexdump(remaining));
}
switch (subtype) { switch (subtype) {
case 0: case 0: {
LOG.debug("Got chunked ack end"); LOG.debug("Got chunked ack end");
if (currentPayload != null && currentPayload.getCallback() != null) { if (currentPayload != null && currentPayload.getCallback() != null) {
currentPayload.getCallback().onSend(); currentPayload.getCallback().onSend();
@ -209,23 +229,21 @@ public class XiaomiCharacteristic {
sendingChunked = false; sendingChunked = false;
sendNext(null); sendNext(null);
return; return;
case 1: }
case 1: {
LOG.debug("Got chunked ack start"); LOG.debug("Got chunked ack start");
final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunks for " + currentPayload.getTaskName()); final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunks for " + currentPayload.getTaskName());
final byte[] payload = currentPayload.getBytesToSend(); final byte[] payload = currentPayload.getBytesToSend();
for (int i = 0; i * MAX_WRITE_SIZE < payload.length; i++) { final int chunkPayloadSize = maxWriteSizeForCurrentMessage - 2;
final int startIndex = i * MAX_WRITE_SIZE;
final int endIndex = Math.min((i + 1) * MAX_WRITE_SIZE, payload.length); for (int i = 0; i * chunkPayloadSize < payload.length; i++) {
LOG.debug("Sending chunk {} from {} to {} for {}", i, startIndex, endIndex, currentPayload.getTaskName()); sendChunk(builder, i, chunkPayloadSize);
final byte[] chunkToSend = new byte[2 + endIndex - startIndex];
BLETypeConversions.writeUint16(chunkToSend, 0, i + 1);
System.arraycopy(payload, startIndex, chunkToSend, 2, endIndex - startIndex);
builder.write(bluetoothGattCharacteristic, chunkToSend);
} }
builder.queue(mSupport.getQueue()); builder.queue(mSupport.getQueue());
return; return;
case 2: }
case 2: {
LOG.warn("Got chunked nack for {}", currentPayload.getTaskName()); LOG.warn("Got chunked nack for {}", currentPayload.getTaskName());
if (currentPayload != null && currentPayload.getCallback() != null) { if (currentPayload != null && currentPayload.getCallback() != null) {
currentPayload.getCallback().onNack(); currentPayload.getCallback().onNack();
@ -234,6 +252,35 @@ public class XiaomiCharacteristic {
sendingChunked = false; sendingChunked = false;
sendNext(null); sendNext(null);
return; return;
}
case 5: {
short[] invalidChunks = new short[remaining.length / 2];
if (remaining.length > 0) {
ByteBuffer remainingBuffer = ByteBuffer.wrap(remaining).order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < remaining.length / 2; i++) {
invalidChunks[i] = remainingBuffer.getShort();
}
LOG.info("Got chunk request, requested chunks: {}", Arrays.toString(invalidChunks));
final TransactionBuilder builder = mSupport.createTransactionBuilder("resend chunks for " + currentPayload.getTaskName());
for (short chunkIndex : invalidChunks) {
// chunk indices start at 1
sendChunk(builder, chunkIndex - 1, maxWriteSizeForCurrentMessage - 2);
}
} else {
LOG.warn("Got chunk request, no chunk indices requested");
if (maxWriteSize != maxWriteSizeForCurrentMessage) {
LOG.info("MTU changed while sending message, prepending message to queue and resending");
((LinkedList<Payload>) payloadQueue).addFirst(currentPayload);
currentPayload = null;
sendingChunked = false;
sendNext(null);
return;
}
}
}
} }
LOG.warn("Unknown chunked ack subtype {} for {}", subtype, currentPayload.getTaskName()); LOG.warn("Unknown chunked ack subtype {} for {}", subtype, currentPayload.getTaskName());
@ -276,6 +323,7 @@ public class XiaomiCharacteristic {
currentPayload.getCallback().onNack(); currentPayload.getCallback().onNack();
} }
} }
currentPayload = null; currentPayload = null;
waitingAck = false; waitingAck = false;
sendNext(null); sendNext(null);
@ -306,6 +354,9 @@ public class XiaomiCharacteristic {
currentPayload.setBytesToSend(authService.encrypt(currentPayload.getBytesToSend(), incrementNonce ? encryptedIndex : 0)); currentPayload.setBytesToSend(authService.encrypt(currentPayload.getBytesToSend(), incrementNonce ? encryptedIndex : 0));
} }
// before checking whether message should be chunked, read the maximum message size for this transaction
maxWriteSizeForCurrentMessage = maxWriteSize;
if (shouldWriteChunked(currentPayload.getBytesToSend())) { if (shouldWriteChunked(currentPayload.getBytesToSend())) {
if (encrypt && incrementNonce) { if (encrypt && incrementNonce) {
// Prepend encrypted index for the nonce // Prepend encrypted index for the nonce
@ -325,7 +376,7 @@ public class XiaomiCharacteristic {
buf.putShort((short) 0); buf.putShort((short) 0);
buf.put((byte) 0); buf.put((byte) 0);
buf.put((byte) (encrypt ? 1 : 0)); buf.put((byte) (encrypt ? 1 : 0));
buf.putShort((short) Math.ceil(currentPayload.getBytesToSend().length / (float) MAX_WRITE_SIZE)); buf.putShort((short) Math.ceil(currentPayload.getBytesToSend().length / (float) (maxWriteSizeForCurrentMessage - 2)));
final TransactionBuilder builder = b == null ? mSupport.createTransactionBuilder("send chunked start for " + currentPayload.getTaskName()) : b; final TransactionBuilder builder = b == null ? mSupport.createTransactionBuilder("send chunked start for " + currentPayload.getTaskName()) : b;
builder.write(bluetoothGattCharacteristic, buf.array()); builder.write(bluetoothGattCharacteristic, buf.array());
@ -368,7 +419,7 @@ public class XiaomiCharacteristic {
} }
// payload + 6 bytes at the start with the encryption stuff // payload + 6 bytes at the start with the encryption stuff
return payload.length + 6 > MAX_WRITE_SIZE; return payload.length + 6 > maxWriteSizeForCurrentMessage;
} }
private void sendAck() { private void sendAck() {
@ -389,6 +440,11 @@ public class XiaomiCharacteristic {
builder.queue(mSupport.getQueue()); builder.queue(mSupport.getQueue());
} }
public void setMtu(final int newMtu) {
// subtract ATT packet header size
maxWriteSize = newMtu - 3;
}
private static class Payload { private static class Payload {
private final String taskName; private final String taskName;
private final byte[] bytes; private final byte[] bytes;

View File

@ -76,7 +76,14 @@ public class XiaomiSppSupport extends XiaomiConnectionSupport {
} }
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
mXiaomiSupport.getAuthService().startEncryptedHandshake(XiaomiSppSupport.this, builder); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext()));
builder.add(new PlainAction() {
@Override
public boolean run(BluetoothSocket socket) {
mXiaomiSupport.getAuthService().startEncryptedHandshake();
return true;
}
});
return builder; return builder;
} }