From 4c01beabe873339c0d370c9c344c97bbf0ea0359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sat, 19 Jul 2025 22:11:06 +0100 Subject: [PATCH] Xiaomi-protobuf: Add BLE V2 --- .../devices/xiaomi/XiaomiCoordinator.java | 2 +- .../watches/MiBand9ActiveCoordinator.java | 30 +- .../service/btle/BleNamesResolver.java | 35 +- .../xiaomi/AbstractXiaomiBleProtocol.java | 31 ++ .../devices/xiaomi/XiaomiBleProtocolV1.java | 246 ++++++++++++ .../devices/xiaomi/XiaomiBleProtocolV2.java | 353 ++++++++++++++++++ .../devices/xiaomi/XiaomiBleSupport.java | 212 +++-------- ...istic.java => XiaomiCharacteristicV1.java} | 27 +- .../xiaomi/XiaomiConnectionSupport.java | 3 +- .../devices/xiaomi/XiaomiSendCallback.java | 7 + .../devices/xiaomi/XiaomiSppSupport.java | 5 +- .../service/devices/xiaomi/XiaomiUuids.java | 8 +- .../services/XiaomiDataUploadService.java | 4 +- 13 files changed, 752 insertions(+), 211 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/AbstractXiaomiBleProtocol.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleProtocolV1.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleProtocolV2.java rename app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/{XiaomiCharacteristic.java => XiaomiCharacteristicV1.java} (97%) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSendCallback.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java index c82ad47a8d..2d93f41715 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java @@ -77,7 +77,7 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator { @Override public Collection createBLEScanFilters() { final List filters = new ArrayList<>(); - for (final UUID uuid : XiaomiUuids.BLE_UUIDS.keySet()) { + for (final UUID uuid : XiaomiUuids.BLE_V1_UUIDS.keySet()) { final ParcelUuid service = new ParcelUuid(uuid); final ScanFilter filter = new ScanFilter.Builder().setServiceUuid(service).build(); filters.add(filter); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/watches/MiBand9ActiveCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/watches/MiBand9ActiveCoordinator.java index 95df18d4ec..00bfb37c5a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/watches/MiBand9ActiveCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/watches/MiBand9ActiveCoordinator.java @@ -20,8 +20,16 @@ import java.util.regex.Pattern; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; public class MiBand9ActiveCoordinator extends XiaomiCoordinator { + @Override + public boolean isExperimental() { + // Activity fetching is broken + // Some settings are broken + return true; + } + @Override public int getDeviceNameResource() { return R.string.devicetype_miband9active; @@ -32,20 +40,18 @@ public class MiBand9ActiveCoordinator extends XiaomiCoordinator { return Pattern.compile("^Xiaomi( Smart)? Band 9 Active [0-9A-F]{4}$"); } - @Override - public boolean isExperimental() { - // Untested - based on btsnoops provided on matrix - return true; - } - - @Override - public ConnectionType getConnectionType() { - // Unconfirmed - but actually looks like SPPv2 packets over BLE? - return ConnectionType.BOTH; - } - @Override public int getDefaultIconResource() { return R.drawable.ic_device_default; } + + @Override + public boolean supportsFindDevice() { + return false; + } + + @Override + public boolean supportsManualHeartRateMeasurement(final GBDevice device) { + return false; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java index fe44df908f..c9fd689b6d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java @@ -24,13 +24,14 @@ import android.bluetooth.BluetoothProfile; import android.util.SparseArray; import java.util.HashMap; +import java.util.Map; public class BleNamesResolver { - private static HashMap mServices = new HashMap<>(100); - private static HashMap mCharacteristics = new HashMap<>(600); - private static SparseArray mValueFormats = new SparseArray<>(10); - private static SparseArray mAppearance = new SparseArray<>(20); - private static SparseArray mHeartRateSensorLocation = new SparseArray<>(10); + private static final Map mServices = new HashMap<>(100); + private static final Map mCharacteristics = new HashMap<>(600); + private static final SparseArray mValueFormats = new SparseArray<>(10); + private static final SparseArray mAppearance = new SparseArray<>(20); + private static final SparseArray mHeartRateSensorLocation = new SparseArray<>(10); static public String resolveServiceName(final String uuid) { String result = mServices.get(uuid); @@ -39,8 +40,7 @@ public class BleNamesResolver { } static public String resolveValueTypeDescription(final int format) { - Integer tmp = Integer.valueOf(format); - return mValueFormats.get(tmp, "Unknown Format"); + return mValueFormats.get(format, "Unknown Format"); } static public String resolveCharacteristicName(final String uuid) { @@ -1000,6 +1000,27 @@ public class BleNamesResolver { mCharacteristics.put("6a4e2823-667b-11e3-949a-0800200c9a66", "(Propr: Garmin ML 3 TX)"); mCharacteristics.put("6a4e2814-667b-11e3-949a-0800200c9a66", "(Propr: Garmin ML 4 RX)"); mCharacteristics.put("6a4e2824-667b-11e3-949a-0800200c9a66", "(Propr: Garmin ML 4 TX)"); + mCharacteristics.put("00000051-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi V1 Command Read)"); + mCharacteristics.put("00000052-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi V1 Command Write)"); + mCharacteristics.put("00000053-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi V1 Activity Data)"); + mCharacteristics.put("00000055-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi V1 Data Upload)"); + mCharacteristics.put("16186f01-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi V1 Command Read)"); + mCharacteristics.put("16186f02-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi V1 Command Write)"); + mCharacteristics.put("16186f03-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi V1 Activity Data)"); + mCharacteristics.put("16186f04-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi V1 Data Upload)"); + mCharacteristics.put("16187f02-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi V1 Command Read)"); + mCharacteristics.put("16187f01-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi V1 Command Write)"); + mCharacteristics.put("16187f03-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi V1 Activity Data)"); + mCharacteristics.put("16187f04-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi V1 Data Upload)"); + mCharacteristics.put("1314f005-1000-9000-7000-301291e21220", "(Propr: Xiaomi V1 Command Read)"); + mCharacteristics.put("1314f001-1000-9000-7000-301291e21220", "(Propr: Xiaomi V1 Command Write)"); + mCharacteristics.put("1314f002-1000-9000-7000-301291e21220", "(Propr: Xiaomi V1 Activity Data)"); + mCharacteristics.put("1314f007-1000-9000-7000-301291e21220", "(Propr: Xiaomi V1 Data Upload)"); + mCharacteristics.put("74950002-a7f3-424b-92dd-4a006a3aef56", "(Propr: Xiaomi V1 Command Read)"); + mCharacteristics.put("74950001-a7f3-424b-92dd-4a006a3aef56", "(Propr: Xiaomi V1 Command Write)"); + mCharacteristics.put("74950003-a7f3-424b-92dd-4a006a3aef56", "(Propr: Xiaomi V1 Activity Data)"); + mCharacteristics.put("0000005e-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi V2 RX)"); + mCharacteristics.put("0000005f-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi V2 TX)"); mCharacteristics.put("0000fff1-0000-1000-8000-00805f9b34fb", "(Propr: Nothing CMF Command Read"); mCharacteristics.put("0000fff2-0000-1000-8000-00805f9b34fb", "(Propr: Nothing CMF Command Write"); mCharacteristics.put("02f00000-0000-0000-0000-00000000ffe1", "(Propr: Nothing CMF Data Write"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/AbstractXiaomiBleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/AbstractXiaomiBleProtocol.java new file mode 100644 index 0000000000..26b229cc5e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/AbstractXiaomiBleProtocol.java @@ -0,0 +1,31 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; + +import androidx.annotation.Nullable; + +import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; + +public abstract class AbstractXiaomiBleProtocol { + public abstract boolean initializeDevice(final TransactionBuilder builder); + + public abstract void reset(); + + public abstract void dispose(); + + public abstract boolean onCharacteristicChanged(final BluetoothGatt gatt, + final BluetoothGattCharacteristic characteristic, + final byte[] value); + + public abstract void onMtuChanged(BluetoothGatt gatt, int mtu, int status); + + public abstract void onAuthSuccess(); + + public abstract void sendCommand(final String taskName, final XiaomiProto.Command command); + + public abstract void sendCommand(final TransactionBuilder builder, final XiaomiProto.Command command); + + public abstract void sendDataChunk(final String taskName, final byte[] chunk, @Nullable final XiaomiSendCallback callback); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleProtocolV1.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleProtocolV1.java new file mode 100644 index 0000000000..d181e7d622 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleProtocolV1.java @@ -0,0 +1,246 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; + +import androidx.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; + +/** @noinspection LoggingSimilarMessage*/ +public class XiaomiBleProtocolV1 extends AbstractXiaomiBleProtocol { + private static final Logger LOG = LoggerFactory.getLogger(XiaomiBleProtocolV1.class); + + private final XiaomiBleSupport xiaomiBleSupport; + private final XiaomiSupport xiaomiSupport; + private final AbstractBTLESingleDeviceSupport commsSupport; + private final Context context; + private final GBDevice gbDevice; + + private XiaomiCharacteristicV1 characteristicCommandRead; + private XiaomiCharacteristicV1 characteristicCommandWrite; + private XiaomiCharacteristicV1 characteristicActivityData; + @Nullable + private XiaomiCharacteristicV1 characteristicDataUpload; + + public XiaomiBleProtocolV1(final XiaomiBleSupport xiaomiBleSupport) { + this.xiaomiBleSupport = xiaomiBleSupport; + this.xiaomiSupport = xiaomiBleSupport.getXiaomiSupport(); + this.commsSupport = xiaomiBleSupport.getCommsSupport(); + this.context = xiaomiSupport.getContext(); + this.gbDevice = xiaomiSupport.getDevice(); + } + + @Override + public boolean initializeDevice(final TransactionBuilder builder) { + reset(); + + XiaomiUuids.XiaomiBleUuidSet uuidSet = null; + BluetoothGattCharacteristic btCharacteristicCommandRead = null; + BluetoothGattCharacteristic btCharacteristicCommandWrite = null; + BluetoothGattCharacteristic btCharacteristicActivityData = null; + BluetoothGattCharacteristic btCharacteristicDataUpload = null; + + // Attempt to find a known xiaomi service + for (Map.Entry xiaomiUuid : XiaomiUuids.BLE_V1_UUIDS.entrySet()) { + final XiaomiUuids.XiaomiBleUuidSet currentUuidSet = xiaomiUuid.getValue(); + UUID currentChar; + + if ((currentChar = currentUuidSet.getCharacteristicCommandRead()) == null || + (btCharacteristicCommandRead = commsSupport.getCharacteristic(currentChar)) == null) { + continue; + } + + if ((currentChar = currentUuidSet.getCharacteristicCommandWrite()) == null || + (btCharacteristicCommandWrite = commsSupport.getCharacteristic(currentChar)) == null) { + continue; + } + + if ((currentChar = currentUuidSet.getCharacteristicActivityData()) == null || + (btCharacteristicActivityData = commsSupport.getCharacteristic(currentChar)) == null) { + continue; + } + + if ((currentChar = currentUuidSet.getCharacteristicDataUpload()) == null || + (btCharacteristicDataUpload = commsSupport.getCharacteristic(currentChar)) == null) { + LOG.warn("btCharacteristicDataUpload characteristic is null"); + // this characteristic may not be supported by all models + } + + LOG.debug("Found Xiaomi service: {}", xiaomiUuid.getKey()); + uuidSet = xiaomiUuid.getValue(); + + break; + } + + if (uuidSet == null) { + return false; + } + + // FIXME unsetDynamicState unsets the fw version, which causes problems.. + if (gbDevice.getFirmwareVersion() == null) { + gbDevice.setFirmwareVersion(xiaomiSupport.getCachedFirmwareVersion() != null ? + xiaomiSupport.getCachedFirmwareVersion() : + "N/A" + ); + } + + // 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 XiaomiCharacteristicV1(xiaomiBleSupport, btCharacteristicCommandRead, xiaomiSupport.getAuthService()); + characteristicCommandRead.setEncrypted(uuidSet.isEncrypted()); + characteristicCommandRead.setChannelHandler(xiaomiSupport::handleCommandBytes); + characteristicCommandRead.setMtu(expectedMtu); + characteristicCommandWrite = new XiaomiCharacteristicV1(xiaomiBleSupport, btCharacteristicCommandWrite, xiaomiSupport.getAuthService()); + characteristicCommandWrite.setEncrypted(uuidSet.isEncrypted()); + characteristicCommandWrite.setMtu(expectedMtu); + characteristicActivityData = new XiaomiCharacteristicV1(xiaomiBleSupport, btCharacteristicActivityData, xiaomiSupport.getAuthService()); + characteristicActivityData.setChannelHandler(xiaomiSupport.getHealthService().getActivityFetcher()::addChunk); + characteristicActivityData.setEncrypted(uuidSet.isEncrypted()); + characteristicActivityData.setMtu(expectedMtu); + if (btCharacteristicDataUpload != null) { + characteristicDataUpload = new XiaomiCharacteristicV1(xiaomiBleSupport, btCharacteristicDataUpload, xiaomiSupport.getAuthService()); + characteristicDataUpload.setEncrypted(uuidSet.isEncrypted()); + characteristicDataUpload.setIncrementNonce(false); + characteristicDataUpload.setMtu(expectedMtu); + } + + // request highest possible MTU; device should response with the highest supported MTU anyway + builder.requestMtu(512); + builder.add(new SetDeviceStateAction(gbDevice, GBDevice.State.INITIALIZING, context)); + builder.notify(btCharacteristicCommandWrite, true); + builder.notify(btCharacteristicCommandRead, true); + builder.notify(btCharacteristicActivityData, true); + builder.notify(btCharacteristicDataUpload, true); + builder.add(new SetDeviceStateAction(gbDevice, GBDevice.State.AUTHENTICATING, context)); + + if (uuidSet.isEncrypted()) { + builder.run(() -> xiaomiSupport.getAuthService().startEncryptedHandshake()); + } else { + builder.run(() -> xiaomiSupport.getAuthService().startClearTextHandshake()); + } + + return true; + } + + @Override + public void reset() { + if (characteristicCommandRead != null) + characteristicCommandRead.reset(); + if (characteristicCommandWrite != null) + characteristicCommandWrite.reset(); + if (characteristicDataUpload != null) + characteristicDataUpload.reset(); + if (characteristicActivityData != null) + characteristicActivityData.reset(); + } + + @Override + public void dispose() { + if (characteristicCommandRead != null) + characteristicCommandRead.dispose(); + if (characteristicCommandWrite != null) + characteristicCommandWrite.dispose(); + if (characteristicDataUpload != null) + characteristicDataUpload.dispose(); + if (characteristicActivityData != null) + characteristicActivityData.dispose(); + } + + @Override + public boolean onCharacteristicChanged(final BluetoothGatt gatt, + final BluetoothGattCharacteristic characteristic, + final byte[] value) { + final UUID characteristicUUID = characteristic.getUuid(); + + if (characteristicCommandRead.getCharacteristicUUID().equals(characteristicUUID)) { + characteristicCommandRead.onCharacteristicChanged(value); + return true; + } else if (characteristicCommandWrite.getCharacteristicUUID().equals(characteristicUUID)) { + characteristicCommandWrite.onCharacteristicChanged(value); + return true; + } else if (characteristicActivityData.getCharacteristicUUID().equals(characteristicUUID)) { + characteristicActivityData.onCharacteristicChanged(value); + return true; + } else if (characteristicDataUpload != null && characteristicDataUpload.getCharacteristicUUID().equals(characteristicUUID)) { + characteristicDataUpload.onCharacteristicChanged(value); + return true; + } + + return false; + } + + @Override + public void onMtuChanged(final BluetoothGatt gatt, final int mtu, final int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + return; + } + + if (characteristicCommandRead != null) + characteristicCommandRead.setMtu(mtu); + if (characteristicCommandWrite != null) + characteristicCommandWrite.setMtu(mtu); + if (characteristicDataUpload != null) + characteristicDataUpload.setMtu(mtu); + if (characteristicActivityData != null) + characteristicActivityData.setMtu(mtu); + } + + @Override + public void onAuthSuccess() { + characteristicCommandRead.reset(); + characteristicCommandWrite.reset(); + characteristicActivityData.reset(); + if (characteristicDataUpload != null) { + characteristicDataUpload.reset(); + } + } + + @Override + public void sendCommand(final String taskName, final XiaomiProto.Command command) { + if (this.characteristicCommandWrite == null) { + // Can sometimes happen in race conditions when connecting + receiving calendar event or weather updates + LOG.warn("Unable to {}, characteristicCommandWrite is null", taskName); + return; + } + + this.characteristicCommandWrite.write(taskName, command.toByteArray()); + } + + @Override + public void sendCommand(final TransactionBuilder builder, final XiaomiProto.Command command) { + if (this.characteristicCommandWrite == null) { + // Can sometimes happen in race conditions when connecting + receiving calendar event or weather updates + LOG.warn("Unable to {}, characteristicCommandWrite is null", builder.getTaskName()); + return; + } + + this.characteristicCommandWrite.write(builder, command.toByteArray()); + } + + @Override + public void sendDataChunk(final String taskName, final byte[] chunk, @Nullable final XiaomiSendCallback callback) { + if (this.characteristicDataUpload == null) { + LOG.warn("Unable to {}, characteristicDataUpload is null!", taskName); + return; + } + + this.characteristicDataUpload.write(taskName, chunk, callback); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleProtocolV2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleProtocolV2.java new file mode 100644 index 0000000000..e62cb2722c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleProtocolV2.java @@ -0,0 +1,353 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi; + +import static nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSppPacketV2.PACKET_PREAMBLE; +import static nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSppPacketV2.PACKET_TYPE_ACK; +import static nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSppPacketV2.PACKET_TYPE_DATA; +import static nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSppPacketV2.PACKET_TYPE_SESSION_CONFIG; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; + +import androidx.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** @noinspection LoggingSimilarMessage*/ +public class XiaomiBleProtocolV2 extends AbstractXiaomiBleProtocol { + private static final Logger LOG = LoggerFactory.getLogger(XiaomiBleProtocolV2.class); + + private final XiaomiSupport xiaomiSupport; + private final AbstractBTLESingleDeviceSupport commsSupport; + private final GBDevice mGbDevice; + + private BluetoothGattCharacteristic btCharacteristicRead; + private BluetoothGattCharacteristic btCharacteristicWrite; + + private final AtomicInteger packetSequenceCounter = new AtomicInteger(0); + private int maxWriteSize = 244; // MTU of 247 - 3 bytes for the ATT overhead + + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private final Map mChannelHandlers = new HashMap<>(); + + public XiaomiBleProtocolV2(final XiaomiBleSupport xiaomiBleSupport) { + this.xiaomiSupport = xiaomiBleSupport.getXiaomiSupport(); + this.commsSupport = xiaomiBleSupport.getCommsSupport(); + this.mGbDevice = xiaomiSupport.getDevice(); + + mChannelHandlers.put(XiaomiChannelHandler.Channel.Version, this::handleVersionPacket); + mChannelHandlers.put(XiaomiChannelHandler.Channel.ProtobufCommand, this.xiaomiSupport::handleCommandBytes); + mChannelHandlers.put(XiaomiChannelHandler.Channel.Activity, this.xiaomiSupport.getHealthService().getActivityFetcher()::addChunk); + } + + @Override + public boolean initializeDevice(final TransactionBuilder builder) { + reset(); + + btCharacteristicRead = commsSupport.getCharacteristic(XiaomiUuids.BLE_V2_CHARACTERISTIC_RX_UUID); + btCharacteristicWrite = commsSupport.getCharacteristic(XiaomiUuids.BLE_V2_CHARACTERISTIC_TX_UUID); + + if (btCharacteristicRead == null || btCharacteristicWrite == null) { + return false; + } + + // FIXME unsetDynamicState unsets the fw version, which causes problems.. + if (mGbDevice.getFirmwareVersion() == null) { + mGbDevice.setFirmwareVersion(xiaomiSupport.getCachedFirmwareVersion() != null ? + xiaomiSupport.getCachedFirmwareVersion() : + "N/A"); + } + + // request highest possible MTU; device should response with the highest supported MTU anyway + builder.requestMtu(512); + builder.setDeviceState(GBDevice.State.INITIALIZING); + builder.notify(btCharacteristicRead, true); + builder.setDeviceState(GBDevice.State.AUTHENTICATING); + + writeChunks( + builder, + XiaomiSppPacketV2.newSessionConfigPacketBuilder() + .setOpCode(XiaomiSppPacketV2.SessionConfigPacket.OPCODE_START_SESSION_REQUEST) + .setSequenceNumber(0) + .build() + .encode(null) + ); + + return true; + } + + @Override + public void reset() { + packetSequenceCounter.set(0); + buffer.reset(); + } + + @Override + public void dispose() { + + } + + @Override + public boolean onCharacteristicChanged(final BluetoothGatt gatt, + final BluetoothGattCharacteristic characteristic, + final byte[] value) { + final UUID characteristicUUID = characteristic.getUuid(); + + if (btCharacteristicRead.getUuid().equals(characteristicUUID)) { + try { + buffer.write(value); + } catch (IOException ex) { + LOG.error("Exception while writing buffer: ", ex); + } + + processBuffer(); + return true; + } + + return false; + } + + private void skipBuffer(int newStart) { + final byte[] bufferState = buffer.toByteArray(); + buffer.reset(); + + if (newStart < 0) { + newStart = bufferState.length; + } + + if (newStart >= bufferState.length) { + return; + } + + buffer.write(bufferState, newStart, bufferState.length - newStart); + } + + private void processBuffer() { + boolean shouldProcess = true; + while (shouldProcess) { + final byte[] bufferState = buffer.toByteArray(); + final AbstractXiaomiSppProtocol.ParseResult parseResult = processPacket(bufferState); + LOG.debug("processBuffer(): protocol.processPacket() returned status {}", parseResult.status); + int skipBytes; + + switch (parseResult.status) { + case Incomplete: + skipBytes = 0; + shouldProcess = false; + break; + case Complete: + skipBytes = parseResult.packetSize; + break; + case Invalid: + skipBytes = findNextPacketOffset(bufferState); + if (skipBytes < 0) { + skipBytes = bufferState.length; + } + break; + default: + throw new IllegalStateException(String.format("Unhandled parse state %s", parseResult.status)); + } + + if (skipBytes > 0) { + LOG.debug("processBuffer(): skipping {} bytes for state {}", skipBytes, parseResult.status); + skipBuffer(skipBytes); + } + } + } + + protected void onPacketReceived(final XiaomiChannelHandler.Channel channel, final byte[] payload) { + final XiaomiChannelHandler handler = mChannelHandlers.get(channel); + if (handler != null) { + handler.handle(payload); + } else { + LOG.warn("Unhandled SppPacket on channel {}", channel); + } + } + + @Override + public void onMtuChanged(final BluetoothGatt gatt, final int mtu, final int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + return; + } + this.maxWriteSize = mtu - 3; + } + + @Override + public void onAuthSuccess() { + + } + + @Override + public void sendCommand(final String taskName, final XiaomiProto.Command command) { + if (this.btCharacteristicWrite == null) { + // Can sometimes happen in race conditions when connecting + receiving calendar event or weather updates + LOG.warn("Unable to send command for {} - btCharacteristicWrite is null!", taskName); + return; + } + + try { + final TransactionBuilder builder = this.commsSupport.createTransactionBuilder("send " + taskName); + sendCommand(builder, command); + builder.queue(); + } catch (final Exception ex) { + LOG.error("Caught unexpected exception while sending command, device may not have been informed!", ex); + } + } + + @Override + public void sendCommand(final TransactionBuilder builder, final XiaomiProto.Command command) { + if (this.btCharacteristicWrite == null) { + // Can sometimes happen in race conditions when connecting + receiving calendar event or weather updates + LOG.warn("Unable to send command for {} - btCharacteristicWrite is null!", builder.getTaskName()); + return; + } + + LOG.debug("sendCommand(): encoded command for task '{}': {}", builder.getTaskName(), GB.hexdump(command.toByteArray())); + if (command.getType() == XiaomiAuthService.COMMAND_TYPE) { + writeChunks(builder, encodePacket(XiaomiChannelHandler.Channel.Authentication, command.toByteArray())); + } else { + writeChunks(builder, encodePacket(XiaomiChannelHandler.Channel.ProtobufCommand, command.toByteArray())); + } + // do not queue here, that's the job of the caller + } + + @Override + public void sendDataChunk(final String taskName, final byte[] chunk, @Nullable final XiaomiSendCallback callback) { + LOG.debug("sendDataChunk(): encoded data chunk for task '{}': {}", taskName, GB.hexdump(chunk)); + final TransactionBuilder builder = this.commsSupport.createTransactionBuilder("send " + taskName); + writeChunks(builder, encodePacket(XiaomiChannelHandler.Channel.Data, chunk)); + builder.queue(); + + if (callback != null) { + // callback puts a SetProgressAction onto the queue + callback.onSend(); + } + } + + private void handleVersionPacket(final byte[] payloadBytes) { + if (payloadBytes != null && payloadBytes.length > 0) { + LOG.debug("Received SPP protocol version: {}", GB.hexdump(payloadBytes)); + + // show in details + final GBDeviceEventUpdateDeviceInfo event = new GBDeviceEventUpdateDeviceInfo("SPP_PROTOCOL: ", GB.hexdump(payloadBytes)); + xiaomiSupport.evaluateGBDeviceEvent(event); + + // TODO handle different protocol versions + // Right now, we expect 3 + } + + xiaomiSupport.getAuthService().startEncryptedHandshake(); + } + + private void sendAck(final int sequenceNumber) { + final TransactionBuilder builder = commsSupport.createTransactionBuilder(String.format(Locale.ROOT, "send ack for %d", sequenceNumber)); + writeChunks(builder, new XiaomiSppPacketV2.AckPacket.Builder() + .setSequenceNumber(sequenceNumber) + .build() + .encode(null)); + builder.queue(); + } + + public int findNextPacketOffset(byte[] buffer) { + for (int i = 1; i < buffer.length; i++) { + if (buffer[i] == PACKET_PREAMBLE[0]) + return i; + } + + return -1; + } + + public AbstractXiaomiSppProtocol.ParseResult processPacket(byte[] rxBuf) { + if (rxBuf.length < 8) { + LOG.debug("processPacket(): not enough bytes in buffer to process packet (got {} of required {} bytes)", + rxBuf.length, + 8); + return new AbstractXiaomiSppProtocol.ParseResult(AbstractXiaomiSppProtocol.ParseResult.Status.Incomplete); + } + + final ByteBuffer buffer = ByteBuffer.wrap(rxBuf).order(ByteOrder.LITTLE_ENDIAN); + final byte[] headerMagic = new byte[PACKET_PREAMBLE.length]; + buffer.get(headerMagic); + + if (!Arrays.equals(PACKET_PREAMBLE, headerMagic)) { + LOG.warn("processPacket(): invalid header magic (expected {}, got {})", + GB.hexdump(PACKET_PREAMBLE), + GB.hexdump(headerMagic)); + return new AbstractXiaomiSppProtocol.ParseResult(AbstractXiaomiSppProtocol.ParseResult.Status.Invalid); + } + + buffer.get(); // flags and packet type + buffer.get(); // packet sequence number + final int packetSize = 8 + (buffer.getShort() & 0xffff); + buffer.getShort(); // checksum + + if (rxBuf.length < packetSize) { + LOG.debug("processPacket(): missing {} bytes (got {}/{} bytes)", + packetSize - rxBuf.length, + rxBuf.length, + packetSize); + return new AbstractXiaomiSppProtocol.ParseResult(AbstractXiaomiSppProtocol.ParseResult.Status.Incomplete); + } + + final XiaomiSppPacketV2 decodedPacket = XiaomiSppPacketV2.decode(rxBuf); + if (decodedPacket != null) { + switch (decodedPacket.getPacketType()) { + case PACKET_TYPE_SESSION_CONFIG: + // TODO handle device's session config + LOG.info("Received session config, opcode={}", ((XiaomiSppPacketV2.SessionConfigPacket) decodedPacket).getOpCode()); + xiaomiSupport.getAuthService().startEncryptedHandshake(); + break; + case PACKET_TYPE_DATA: + XiaomiSppPacketV2.DataPacket dataPacket = (XiaomiSppPacketV2.DataPacket) decodedPacket; + try { + onPacketReceived(dataPacket.getChannel(), dataPacket.getPayloadBytes(xiaomiSupport.getAuthService())); + } catch (final Exception ex) { + LOG.error("Exception while handling received packet", ex); + } + // TODO: only directly ack protobuf packets, bulk ack others + sendAck(decodedPacket.getSequenceNumber()); + break; + case PACKET_TYPE_ACK: + LOG.debug("receive ack for packet {}", decodedPacket.getSequenceNumber()); + break; + default: + LOG.warn("Unhandled packet with type {} (decoded type {})", decodedPacket.getPacketType(), decodedPacket.getClass().getSimpleName()); + break; + } + } + + return new AbstractXiaomiSppProtocol.ParseResult(AbstractXiaomiSppProtocol.ParseResult.Status.Complete, packetSize); + } + + public byte[] encodePacket(final XiaomiChannelHandler.Channel channel, final byte[] payloadBytes) { + return XiaomiSppPacketV2.newDataPacketBuilder() + .setChannel(channel) + .setSequenceNumber(packetSequenceCounter.getAndIncrement()) + .setOpCode(XiaomiSppPacketV2.DataPacket.getOpCodeForChannel(channel)) + .setPayload(payloadBytes) + .build() + .encode(xiaomiSupport.getAuthService()); + } + + private void writeChunks(final TransactionBuilder builder, final byte[] value) { + builder.writeChunkedData(btCharacteristicWrite, value, maxWriteSize); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java index b292d93d34..1cc90cdada 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java @@ -27,7 +27,6 @@ import androidx.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Map; import java.util.Set; import java.util.UUID; @@ -41,14 +40,10 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB; public class XiaomiBleSupport extends XiaomiConnectionSupport { private static final Logger LOG = LoggerFactory.getLogger(XiaomiBleSupport.class); - private XiaomiCharacteristic characteristicCommandRead; - private XiaomiCharacteristic characteristicCommandWrite; - private XiaomiCharacteristic characteristicActivityData; - private XiaomiCharacteristic characteristicDataUpload; - private final XiaomiSupport mXiaomiSupport; + private AbstractXiaomiBleProtocol bleProtocol; - final AbstractBTLESingleDeviceSupport commsSupport = new AbstractBTLESingleDeviceSupport(LOG) { + private final AbstractBTLESingleDeviceSupport commsSupport = new AbstractBTLESingleDeviceSupport(LOG) { @Override public boolean useAutoConnect() { return mXiaomiSupport.useAutoConnect(); @@ -56,7 +51,8 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport { @Override protected Set getSupportedServices() { - return XiaomiUuids.BLE_UUIDS.keySet(); + // This actually includes V2 too + return XiaomiUuids.BLE_V1_UUIDS.keySet(); } @Override @@ -66,102 +62,19 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport { @Override protected TransactionBuilder initializeDevice(final TransactionBuilder builder) { - XiaomiBleSupport.this.reset(); - - XiaomiUuids.XiaomiBleUuidSet uuidSet = null; - BluetoothGattCharacteristic btCharacteristicCommandRead = null; - BluetoothGattCharacteristic btCharacteristicCommandWrite = null; - BluetoothGattCharacteristic btCharacteristicActivityData = null; - BluetoothGattCharacteristic btCharacteristicDataUpload = null; - - // Attempt to find a known xiaomi service - for (Map.Entry xiaomiUuid : XiaomiUuids.BLE_UUIDS.entrySet()) { - final XiaomiUuids.XiaomiBleUuidSet currentUuidSet = xiaomiUuid.getValue(); - UUID currentChar; - - if ((currentChar = currentUuidSet.getCharacteristicCommandRead()) == null || - (btCharacteristicCommandRead = getCharacteristic(currentChar)) == null) { - continue; - } - - if ((currentChar = currentUuidSet.getCharacteristicCommandWrite()) == null || - (btCharacteristicCommandWrite = getCharacteristic(currentChar)) == null) { - continue; - } - - if ((currentChar = currentUuidSet.getCharacteristicActivityData()) == null || - (btCharacteristicActivityData = getCharacteristic(currentChar)) == null) { - continue; - } - - if ((currentChar = currentUuidSet.getCharacteristicDataUpload()) == null || - (btCharacteristicDataUpload= getCharacteristic(currentChar)) == null) { - LOG.warn("btCharacteristicDataUpload characteristic is null"); - // this characteristic may not be supported by all models - } - - LOG.debug("Found Xiaomi service: {}", xiaomiUuid.getKey()); - uuidSet = xiaomiUuid.getValue(); - - break; - } - - if (uuidSet == null) { - GB.toast(getContext(), "Failed to find known Xiaomi service", Toast.LENGTH_LONG, GB.ERROR); - LOG.warn("Failed to find known Xiaomi service"); - builder.setDeviceState(GBDevice.State.NOT_CONNECTED); - return builder; - } - - // FIXME unsetDynamicState unsets the fw version, which causes problems.. - if (getDevice().getFirmwareVersion() == null) { - getDevice().setFirmwareVersion(mXiaomiSupport.getCachedFirmwareVersion() != null ? - mXiaomiSupport.getCachedFirmwareVersion() : - "N/A"); - } - - if (btCharacteristicCommandRead == null || btCharacteristicCommandWrite == null) { - LOG.warn("Characteristics are null, will attempt to reconnect"); - builder.setDeviceState(GBDevice.State.WAITING_FOR_RECONNECT); - return builder; - } - - // 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); - - // request highest possible MTU; device should response with the highest supported MTU anyway - builder.requestMtu(512); - builder.setDeviceState(GBDevice.State.INITIALIZING); - builder.notify(btCharacteristicCommandWrite, true); - builder.notify(btCharacteristicCommandRead, true); - builder.notify(btCharacteristicActivityData, true); - builder.notify(btCharacteristicDataUpload, true); - builder.setDeviceState(GBDevice.State.AUTHENTICATING); - - if (uuidSet.isEncrypted()) { - builder.run(() -> mXiaomiSupport.getAuthService().startEncryptedHandshake()); + final XiaomiBleProtocolV1 protocolV1 = new XiaomiBleProtocolV1(XiaomiBleSupport.this); + if (protocolV1.initializeDevice(builder)) { + bleProtocol = protocolV1; } else { - builder.run(() -> mXiaomiSupport.getAuthService().startClearTextHandshake()); + final XiaomiBleProtocolV2 protocolV2 = new XiaomiBleProtocolV2(XiaomiBleSupport.this); + if (!protocolV2.initializeDevice(builder)) { + GB.toast(getContext(), "Failed to find a known Xiaomi BLE protocol", Toast.LENGTH_LONG, GB.ERROR); + LOG.warn("Failed to find a known Xiaomi BLE protocol"); + builder.setDeviceState(GBDevice.State.NOT_CONNECTED); + return builder; + } + + bleProtocol = protocolV2; } return builder; @@ -173,41 +86,23 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport { return true; } - final UUID characteristicUUID = characteristic.getUuid(); - - if (characteristicCommandRead.getCharacteristicUUID().equals(characteristicUUID)) { - characteristicCommandRead.onCharacteristicChanged(value); - return true; - } else if (characteristicCommandWrite.getCharacteristicUUID().equals(characteristicUUID)) { - characteristicCommandWrite.onCharacteristicChanged(value); - return true; - } else if (characteristicActivityData.getCharacteristicUUID().equals(characteristicUUID)) { - characteristicActivityData.onCharacteristicChanged(value); - return true; - } else if (characteristicDataUpload.getCharacteristicUUID().equals(characteristicUUID)) { - characteristicDataUpload.onCharacteristicChanged(value); + if (bleProtocol != null && bleProtocol.onCharacteristicChanged(gatt, characteristic, value)) { return true; } - LOG.warn("Unhandled characteristic changed: {} {}", characteristicUUID, GB.hexdump(value)); + LOG.warn("Unhandled characteristic changed: {} {}", characteristic.getUuid(), GB.hexdump(value)); return false; } @Override - public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { + public void onMtuChanged(final BluetoothGatt gatt, final int mtu, final int status) { super.onMtuChanged(gatt, mtu, status); if (status != BluetoothGatt.GATT_SUCCESS) { return; } - - if (characteristicCommandRead != null) - characteristicCommandRead.setMtu(mtu); - if (characteristicCommandWrite != null) - characteristicCommandWrite.setMtu(mtu); - if (characteristicDataUpload != null) - characteristicDataUpload.setMtu(mtu); - if (characteristicActivityData != null) - characteristicActivityData.setMtu(mtu); + if (bleProtocol != null) { + bleProtocol.onMtuChanged(gatt, mtu, status); + } } @Override @@ -219,16 +114,23 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport { } }; + XiaomiSupport getXiaomiSupport() { + return mXiaomiSupport; + } + + AbstractBTLESingleDeviceSupport getCommsSupport() { + return commsSupport; + } + public XiaomiBleSupport(final XiaomiSupport xiaomiSupport) { this.mXiaomiSupport = xiaomiSupport; } @Override public void onAuthSuccess() { - characteristicCommandRead.reset(); - characteristicCommandWrite.reset(); - characteristicActivityData.reset(); - characteristicDataUpload.reset(); + if (bleProtocol != null) { + bleProtocol.onAuthSuccess(); + } } @Override @@ -238,23 +140,16 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport { @Override public void sendCommand(final String taskName, final XiaomiProto.Command command) { - if (this.characteristicCommandWrite == null) { - // Can sometimes happen in race conditions when connecting + receiving calendar event or weather updates - LOG.warn("characteristicCommandWrite is null!"); - return; + if (bleProtocol != null) { + bleProtocol.sendCommand(taskName, command); } - - this.characteristicCommandWrite.write(taskName, command.toByteArray()); } @Override - public void sendDataChunk(String taskName, byte[] chunk, @Nullable XiaomiCharacteristic.SendCallback callback) { - if (this.characteristicDataUpload == null) { - LOG.warn("characteristicDataUpload is null!"); - return; + public void sendDataChunk(String taskName, byte[] chunk, @Nullable XiaomiSendCallback callback) { + if (bleProtocol != null) { + bleProtocol.sendDataChunk(taskName, chunk, callback); } - - this.characteristicDataUpload.write(taskName, chunk, callback); } @Override @@ -268,13 +163,9 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport { * order. */ public void sendCommand(final TransactionBuilder builder, final XiaomiProto.Command command) { - if (this.characteristicCommandWrite == null) { - // Can sometimes happen in race conditions when connecting + receiving calendar event or weather updates - LOG.warn("characteristicCommandWrite is null!"); - return; + if (bleProtocol != null) { + bleProtocol.sendCommand(builder, command); } - - this.characteristicCommandWrite.write(builder, command.toByteArray()); } public TransactionBuilder createTransactionBuilder(String taskName) { @@ -305,18 +196,6 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport { return commsSupport.connect(); } - @Override - public void reset() { - if (characteristicCommandRead != null) - characteristicCommandRead.reset(); - if (characteristicCommandWrite != null) - characteristicCommandWrite.reset(); - if (characteristicDataUpload != null) - characteristicDataUpload.reset(); - if (characteristicActivityData != null) - characteristicActivityData.reset(); - } - @Override public void runOnQueue(String taskName, Runnable runnable) { final TransactionBuilder b = commsSupport.createTransactionBuilder("run task " + taskName + " on queue"); @@ -327,13 +206,8 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport { @Override public void dispose() { commsSupport.dispose(); - if (characteristicCommandRead != null) - characteristicCommandRead.dispose(); - if (characteristicCommandWrite != null) - characteristicCommandWrite.dispose(); - if (characteristicDataUpload != null) - characteristicDataUpload.dispose(); - if (characteristicActivityData != null) - characteristicActivityData.dispose(); + if (bleProtocol != null) { + bleProtocol.dispose(); + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristicV1.java similarity index 97% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristicV1.java index b980342cc5..7ace10ed38 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristicV1.java @@ -43,8 +43,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.util.GB; -public class XiaomiCharacteristic { - private final Logger LOG = LoggerFactory.getLogger(XiaomiCharacteristic.class); +public class XiaomiCharacteristicV1 { + private final Logger LOG = LoggerFactory.getLogger(XiaomiCharacteristicV1.class); private static final long TIMEOUT_TASK_DELAY = 5000L; public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0}; @@ -78,9 +78,9 @@ public class XiaomiCharacteristic { private XiaomiChannelHandler channelHandler = null; - public XiaomiCharacteristic(final XiaomiBleSupport support, - final BluetoothGattCharacteristic bluetoothGattCharacteristic, - @Nullable final XiaomiAuthService authService) { + public XiaomiCharacteristicV1(final XiaomiBleSupport support, + final BluetoothGattCharacteristic bluetoothGattCharacteristic, + @Nullable final XiaomiAuthService authService) { this.mSupport = support; this.bluetoothGattCharacteristic = bluetoothGattCharacteristic; this.authService = authService; @@ -119,7 +119,7 @@ public class XiaomiCharacteristic { * Write bytes to this characteristic, encrypting and splitting it into chunks if necessary. * Callback will be notified when a (n)ack has been received by the remote device. */ - public void write(final String taskName, final byte[] value, final SendCallback callback) { + public void write(final String taskName, final byte[] value, final XiaomiSendCallback callback) { write(null, new Payload(taskName, value, callback)); } @@ -136,7 +136,7 @@ public class XiaomiCharacteristic { * commands. The callback will be notified when a (n)ack has been received from the remote * device in response to the payload being sent. */ - public void write(final TransactionBuilder builder, final byte[] value, final SendCallback callback) { + public void write(final TransactionBuilder builder, final byte[] value, final XiaomiSendCallback callback) { write(builder, new Payload(builder.getTaskName(), value, callback)); } @@ -536,9 +536,9 @@ public class XiaomiCharacteristic { // Bytes that will actually be sent (might be encrypted) private byte[] bytesToSend; - private final SendCallback callback; + private final XiaomiSendCallback callback; - public Payload(final String taskName, final byte[] bytes, final SendCallback callback) { + public Payload(final String taskName, final byte[] bytes, final XiaomiSendCallback callback) { this.taskName = taskName; this.bytes = bytes; this.callback = callback; @@ -559,11 +559,8 @@ public class XiaomiCharacteristic { public byte[] getBytesToSend() { return bytesToSend != null ? bytesToSend : bytes; } - public SendCallback getCallback() { return this.callback; } - } - - public interface SendCallback { - void onSend(); - void onNack(); + public XiaomiSendCallback getCallback() { + return this.callback; + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConnectionSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConnectionSupport.java index 37a07b5fa5..58643eb936 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConnectionSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConnectionSupport.java @@ -29,10 +29,9 @@ public abstract class XiaomiConnectionSupport { public void onAuthSuccess() {} public abstract void onUploadProgress(int textRsrc, int progressPercent, boolean ongoing); public abstract void runOnQueue(String taskName, Runnable run); - public abstract void reset(); public abstract void dispose(); public abstract void setContext(final GBDevice device, final BluetoothAdapter adapter, final Context context); public abstract void sendCommand(final String taskName, final XiaomiProto.Command command); - public abstract void sendDataChunk(final String taskName, final byte[] chunk, @Nullable final XiaomiCharacteristic.SendCallback callback); + public abstract void sendDataChunk(final String taskName, final byte[] chunk, @Nullable final XiaomiSendCallback callback); public void setAutoReconnect(final boolean enabled) {} } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSendCallback.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSendCallback.java new file mode 100644 index 0000000000..dde2e5015b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSendCallback.java @@ -0,0 +1,7 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi; + +public interface XiaomiSendCallback { + void onSend(); + + void onNack(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java index f7065cfd87..b54260f810 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java @@ -122,6 +122,7 @@ public class XiaomiSppSupport extends XiaomiConnectionSupport { @Override public void dispose() { commsSupport.dispose(); + mVersionResponseTimeoutHandler.removeCallbacksAndMessages(null); } protected XiaomiAuthService getAuthService() { @@ -244,7 +245,7 @@ public class XiaomiSppSupport extends XiaomiConnectionSupport { } @Override - public void sendDataChunk(final String taskName, final byte[] chunk, @Nullable final XiaomiCharacteristic.SendCallback callback) { + public void sendDataChunk(final String taskName, final byte[] chunk, @Nullable final XiaomiSendCallback callback) { LOG.debug("sendDataChunk(): encoded data chunk for task '{}': {}", taskName, GB.hexdump(chunk)); this.commsSupport.createTransactionBuilder("send " + taskName) .write(mProtocol.encodePacket(Channel.Data, chunk)) @@ -279,9 +280,9 @@ public class XiaomiSppSupport extends XiaomiConnectionSupport { } } - @Override public void reset() { buffer.reset(); + mVersionResponseTimeoutHandler.removeCallbacksAndMessages(null); // FIXME this is a bit ugly, reset the protocol back to V1 so we're able to parse the version packet mProtocol = new XiaomiSppProtocolV1(this); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiUuids.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiUuids.java index 62534d70a7..5531e96334 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiUuids.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiUuids.java @@ -24,7 +24,13 @@ import java.util.UUID; public class XiaomiUuids { public static final UUID UUID_SERVICE_SERIAL_PORT_PROFILE = UUID.fromString(String.format(BASE_UUID, "1101")); - public static final Map BLE_UUIDS = new LinkedHashMap() {{ + + // Mi Band 9 Active + public static final UUID BLE_V2_SERVICE_UUID = UUID.fromString("0000fe95-0000-1000-8000-00805f9b34fb"); + public static final UUID BLE_V2_CHARACTERISTIC_RX_UUID = UUID.fromString("0000005e-0000-1000-8000-00805f9b34fb"); + public static final UUID BLE_V2_CHARACTERISTIC_TX_UUID = UUID.fromString("0000005f-0000-1000-8000-00805f9b34fb"); + + public static final Map BLE_V1_UUIDS = new LinkedHashMap() {{ // all encrypted devices seem to share the same characteristics // Mi Band 8 // Redmi Watch 3 Active diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java index 40688e9bcd..379953305b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java @@ -29,7 +29,7 @@ import java.util.Objects; import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; -import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSendCallback; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; import nodomain.freeyourgadget.gadgetbridge.util.CheckSums; @@ -159,7 +159,7 @@ public class XiaomiDataUploadService extends AbstractXiaomiService { BLETypeConversions.writeUint16(chunkToSend, 2, currentPart); System.arraycopy(payload, startIndex, chunkToSend, 4, endIndex - startIndex); - getSupport().getConnectionSpecificSupport().sendDataChunk("upload part " + currentPart + " of " + totalParts, chunkToSend, new XiaomiCharacteristic.SendCallback() { + getSupport().getConnectionSpecificSupport().sendDataChunk("upload part " + currentPart + " of " + totalParts, chunkToSend, new XiaomiSendCallback() { @Override public void onSend() { final int progressPercent = Math.round((100.0f * currentPart) / totalParts);