mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
Xiaomi: change BLE max chunk size with MTU changes
This commit is contained in:
parent
53a7cc5b30
commit
339859c829
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user