1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-07-22 14:52:25 +02:00

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 {
private static final Logger LOG = LoggerFactory.getLogger(AbstractBTLEDeviceSupport.class);
private int mMTU = 0;
private int mMTU = 23;
private BtLEQueue mQueue;
private Map<UUID, BluetoothGattCharacteristic> mAvailableCharacteristics;
private final Set<UUID> mSupportedServices = new HashSet<>(4);

View File

@ -79,32 +79,16 @@ public class XiaomiAuthService extends AbstractXiaomiService {
return encryptionInitialized;
}
// TODO also implement for spp
protected void startEncryptedHandshake(final XiaomiBleSupport support, final nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder) {
protected void startEncryptedHandshake() {
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);
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) {
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()));
protected void startClearTextHandshake() {
final XiaomiProto.Auth auth = XiaomiProto.Auth.newBuilder()
.setUserId(getUserId(getSupport().getDevice()))
.build();
@ -115,7 +99,7 @@ public class XiaomiAuthService extends AbstractXiaomiService {
.setAuth(auth)
.build();
support.sendCommand(builder, command);
getSupport().sendCommand("auth step 1", command);
}
@Override

View File

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

View File

@ -42,9 +42,6 @@ public class XiaomiCharacteristic {
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 BluetoothGattCharacteristic bluetoothGattCharacteristic;
@ -56,6 +53,10 @@ public class XiaomiCharacteristic {
public boolean incrementNonce = true;
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
private int numChunks = 0;
private int currentChunk = 0;
@ -145,6 +146,17 @@ public class XiaomiCharacteristic {
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) {
final ByteBuffer buf = ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN);
@ -199,8 +211,16 @@ public class XiaomiCharacteristic {
case 1:
// Chunked ack
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) {
case 0:
case 0: {
LOG.debug("Got chunked ack end");
if (currentPayload != null && currentPayload.getCallback() != null) {
currentPayload.getCallback().onSend();
@ -209,23 +229,21 @@ public class XiaomiCharacteristic {
sendingChunked = false;
sendNext(null);
return;
case 1:
}
case 1: {
LOG.debug("Got chunked ack start");
final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunks for " + currentPayload.getTaskName());
final byte[] payload = currentPayload.getBytesToSend();
for (int i = 0; i * MAX_WRITE_SIZE < payload.length; i++) {
final int startIndex = i * MAX_WRITE_SIZE;
final int endIndex = Math.min((i + 1) * MAX_WRITE_SIZE, payload.length);
LOG.debug("Sending chunk {} from {} to {} for {}", i, startIndex, endIndex, currentPayload.getTaskName());
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);
final int chunkPayloadSize = maxWriteSizeForCurrentMessage - 2;
for (int i = 0; i * chunkPayloadSize < payload.length; i++) {
sendChunk(builder, i, chunkPayloadSize);
}
builder.queue(mSupport.getQueue());
return;
case 2:
}
case 2: {
LOG.warn("Got chunked nack for {}", currentPayload.getTaskName());
if (currentPayload != null && currentPayload.getCallback() != null) {
currentPayload.getCallback().onNack();
@ -234,6 +252,35 @@ public class XiaomiCharacteristic {
sendingChunked = false;
sendNext(null);
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());
@ -276,6 +323,7 @@ public class XiaomiCharacteristic {
currentPayload.getCallback().onNack();
}
}
currentPayload = null;
waitingAck = false;
sendNext(null);
@ -306,6 +354,9 @@ public class XiaomiCharacteristic {
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 (encrypt && incrementNonce) {
// Prepend encrypted index for the nonce
@ -325,7 +376,7 @@ public class XiaomiCharacteristic {
buf.putShort((short) 0);
buf.put((byte) 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;
builder.write(bluetoothGattCharacteristic, buf.array());
@ -368,7 +419,7 @@ public class XiaomiCharacteristic {
}
// 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() {
@ -389,6 +440,11 @@ public class XiaomiCharacteristic {
builder.queue(mSupport.getQueue());
}
public void setMtu(final int newMtu) {
// subtract ATT packet header size
maxWriteSize = newMtu - 3;
}
private static class Payload {
private final String taskName;
private final byte[] bytes;

View File

@ -76,7 +76,14 @@ public class XiaomiSppSupport extends XiaomiConnectionSupport {
}
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;
}