From 44be081e862a54a19fe497b65fd8525f81216ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Fri, 6 Oct 2023 20:10:11 +0100 Subject: [PATCH] Mi Band 8: Handle incoming chunked packets --- .../btle/AbstractBTLEDeviceSupport.java | 11 +- .../devices/xiaomi/XiaomiChunkedHandler.java | 59 ++++++++ .../devices/xiaomi/XiaomiConstants.java | 10 +- .../service/devices/xiaomi/XiaomiSupport.java | 143 +++++++++++++----- 4 files changed, 179 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiChunkedHandler.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java index 0ef82d27e..a07643e9d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java @@ -56,6 +56,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 BtLEQueue mQueue; private Map mAvailableCharacteristics; private final Set mSupportedServices = new HashSet<>(4); @@ -380,7 +381,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { - + this.mMTU = mtu; } @Override @@ -407,4 +408,12 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im public boolean onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { return false; } + + /** + * Gets the current MTU, or 0 if unknown + * @return the current MTU, 0 if unknown + */ + public int getMTU() { + return mMTU; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiChunkedHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiChunkedHandler.java new file mode 100644 index 000000000..458e80feb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiChunkedHandler.java @@ -0,0 +1,59 @@ +/* Copyright (C) 2023 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +public class XiaomiChunkedHandler { + private int numChunks = 0; + private int currentChunk = 0; + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + public XiaomiChunkedHandler() { + + } + + public void setNumChunks(final int numChunks) { + this.numChunks = numChunks; + this.currentChunk = 0; + this.baos.reset(); + } + + public void addChunk(final byte[] chunk) { + try { + baos.write(chunk); + } catch (final IOException e) { + throw new RuntimeException(e); + } + + currentChunk++; + } + + public int getNumChunks() { + return numChunks; + } + + public int getCurrentChunk() { + return currentChunk; + } + + public byte[] getArray() { + return baos.toByteArray(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConstants.java index 146bdcebc..b1bb6553b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConstants.java @@ -40,10 +40,10 @@ public class XiaomiConstants { public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0003 = UUID.fromString((String.format(BASE_UUID, "0003"))); // TODO not like this - public static final byte[] PAYLOAD_CHUNKED_START = new byte[]{0, 0, 0, 1}; + public static final byte[] PAYLOAD_CHUNKED_START = new byte[]{0, 0, 0, 1}; public static final byte[] PAYLOAD_CHUNKED_START_ACK = new byte[]{0, 0, 1, 1}; - public static final byte[] PAYLOAD_CHUNKED_END_ACK = new byte[]{0, 0, 1, 0}; - public static final byte[] PAYLOAD_HEADER_AUTH = new byte[]{0, 0, 2, 2}; - public static final byte[] PAYLOAD_HEADER_CMD = new byte[]{0, 0, 2, 1}; - public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0}; + public static final byte[] PAYLOAD_CHUNKED_END_ACK = new byte[]{0, 0, 1, 0}; + public static final byte[] PAYLOAD_HEADER_AUTH = new byte[]{0, 0, 2, 2}; + public static final byte[] PAYLOAD_HEADER_CMD = new byte[]{0, 0, 2, 1}; + public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0}; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java index 3fdc8facd..a61aed003 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java @@ -25,7 +25,6 @@ import android.content.Context; import android.location.Location; import android.net.Uri; -import org.apache.commons.lang3.ArrayUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,6 +33,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; @@ -52,7 +52,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.model.WorldClock; import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; -import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; @@ -138,6 +137,8 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { return builder; } + private final Map mChunkedHandlers = new HashMap<>(); + @Override public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { if (super.onCharacteristicChanged(gatt, characteristic)) { @@ -147,6 +148,10 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { final UUID characteristicUUID = characteristic.getUuid(); final byte[] value = characteristic.getValue(); + if (Arrays.equals(value, PAYLOAD_ACK)) { + + } + if (UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE.equals(characteristicUUID)) { if (Arrays.equals(value, PAYLOAD_ACK)) { LOG.debug("Got command write ack"); @@ -158,48 +163,78 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { } if (UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ.equals(characteristicUUID)) { - sendAck(characteristic); + final ByteBuffer buf = ByteBuffer.wrap(characteristic.getValue()) + .order(ByteOrder.LITTLE_ENDIAN); - final int header = BLETypeConversions.toUint16(value, 0); - final int type = BLETypeConversions.toUnsigned(value, 2); - final int encryption = BLETypeConversions.toUnsigned(value, 3); + final int chunk = buf.getShort(); + if (chunk != 0) { + // Chunked packet + final XiaomiChunkedHandler chunkedHandler = mChunkedHandlers.get(characteristicUUID); + if (chunkedHandler == null) { + LOG.warn("No chunked handler initialized for {}", characteristicUUID); + return true; + } + final byte[] chunkBytes = new byte[buf.limit() - buf.position()]; + buf.get(chunkBytes); + chunkedHandler.addChunk(chunkBytes); + if (chunk == chunkedHandler.getNumChunks()) { + // TODO handle reassembled chunk + final byte[] plainValue = authService.decrypt(chunkedHandler.getArray()); + handleCommandBytes(plainValue); + } - if (header != 0) { - LOG.warn("Non-zero header not supported"); return true; - } - if (type == 0) { - // Chunked - } - if (type != 2) { - LOG.warn("Unsupported type {}", type); - return true; - } - - final byte[] plainValue; - if (encryption == 1) { - plainValue = authService.decrypt(ArrayUtils.subarray(value, 4, value.length)); } else { - plainValue = ArrayUtils.subarray(value, 4, value.length); + // Not a chunk / single-packet + final byte type = buf.get(); + + switch (type) { + case 0: + // Chunked start request + final byte one = buf.get(); // ? + if (one != 1) { + LOG.warn("Chunked start request: expected 1, got {}", one); + return true; + } + final short numChunks = buf.getShort(); + LOG.debug("Got chunked start request for {} chunks", numChunks); + XiaomiChunkedHandler chunkedHandler = mChunkedHandlers.get(characteristicUUID); + if (chunkedHandler == null) { + chunkedHandler = new XiaomiChunkedHandler(); + mChunkedHandlers.put(UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ, chunkedHandler); + } + chunkedHandler.setNumChunks(numChunks); + sendChunkStartAck(characteristic); + return true; + case 1: + // Chunked start ack + LOG.debug("Got chunked start ack"); + return true; + case 2: + // Single command + sendAck(characteristic); + + final byte encryption = buf.get(); + final byte[] plainValue; + if (encryption == 1) { + final byte[] encryptedValue = new byte[buf.limit() - buf.position()]; + buf.get(encryptedValue); + plainValue = authService.decrypt(encryptedValue); + } else { + plainValue = new byte[buf.limit() - buf.position()]; + buf.get(plainValue); + } + + handleCommandBytes(plainValue); + + return true; + case 3: + // ack + LOG.debug("Got ack"); + return true; + } } - LOG.debug("Got command: {}", GB.hexdump(plainValue)); - - final XiaomiProto.Command cmd; - try { - cmd = XiaomiProto.Command.parseFrom(plainValue); - } catch (final Exception e) { - LOG.error("Failed to parse bytes as protobuf command payload", e); - return true; - } - - final AbstractXiaomiService service = mServiceMap.get(cmd.getType()); - if (service != null) { - service.handleCommand(cmd); - return true; - } - - LOG.warn("Unexpected watch command type {}", cmd.getType()); return true; } @@ -207,6 +242,26 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { return false; } + public void handleCommandBytes(final byte[] plainValue) { + LOG.debug("Got command: {}", GB.hexdump(plainValue)); + + final XiaomiProto.Command cmd; + try { + cmd = XiaomiProto.Command.parseFrom(plainValue); + } catch (final Exception e) { + LOG.error("Failed to parse bytes as protobuf command payload", e); + return; + } + + final AbstractXiaomiService service = mServiceMap.get(cmd.getType()); + if (service != null) { + service.handleCommand(cmd); + return; + } + + LOG.warn("Unexpected watch command type {}", cmd.getType()); + } + @Override public void onSendConfiguration(final String config) { final Prefs prefs = getDevicePrefs(); @@ -429,6 +484,18 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { builder.queue(getQueue()); } + private void sendChunkStartAck(final BluetoothGattCharacteristic characteristic) { + final TransactionBuilder builder = createTransactionBuilder("send chunked start ack"); + builder.write(characteristic, PAYLOAD_CHUNKED_START_ACK); + builder.queue(getQueue()); + } + + private void sendChunkEndAck(final BluetoothGattCharacteristic characteristic) { + final TransactionBuilder builder = createTransactionBuilder("send chunked end ack"); + builder.write(characteristic, PAYLOAD_CHUNKED_END_ACK); + builder.queue(getQueue()); + } + private short encryptedIndex = 0; public void sendCommand(final String taskName, final XiaomiProto.Command command) {