mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Xiaomi-protobuf: Add BLE V2
This commit is contained in:
+1
-1
@@ -77,7 +77,7 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
public Collection<? extends ScanFilter> createBLEScanFilters() {
|
||||
final List<ScanFilter> 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);
|
||||
|
||||
+18
-12
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+28
-7
@@ -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<String, String> mServices = new HashMap<>(100);
|
||||
private static HashMap<String, String> mCharacteristics = new HashMap<>(600);
|
||||
private static SparseArray<String> mValueFormats = new SparseArray<>(10);
|
||||
private static SparseArray<String> mAppearance = new SparseArray<>(20);
|
||||
private static SparseArray<String> mHeartRateSensorLocation = new SparseArray<>(10);
|
||||
private static final Map<String, String> mServices = new HashMap<>(100);
|
||||
private static final Map<String, String> mCharacteristics = new HashMap<>(600);
|
||||
private static final SparseArray<String> mValueFormats = new SparseArray<>(10);
|
||||
private static final SparseArray<String> mAppearance = new SparseArray<>(20);
|
||||
private static final SparseArray<String> 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");
|
||||
|
||||
+31
@@ -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);
|
||||
}
|
||||
+246
@@ -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<UUID, XiaomiUuids.XiaomiBleUuidSet> 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);
|
||||
}
|
||||
}
|
||||
+353
@@ -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<XiaomiChannelHandler.Channel, XiaomiChannelHandler> 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);
|
||||
}
|
||||
}
|
||||
+43
-169
@@ -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<UUID> 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<UUID, XiaomiUuids.XiaomiBleUuidSet> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-15
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -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) {}
|
||||
}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
|
||||
|
||||
public interface XiaomiSendCallback {
|
||||
void onSend();
|
||||
|
||||
void onNack();
|
||||
}
|
||||
+3
-2
@@ -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);
|
||||
}
|
||||
|
||||
+7
-1
@@ -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<UUID, XiaomiBleUuidSet> BLE_UUIDS = new LinkedHashMap<UUID, XiaomiBleUuidSet>() {{
|
||||
|
||||
// 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<UUID, XiaomiBleUuidSet> BLE_V1_UUIDS = new LinkedHashMap<UUID, XiaomiBleUuidSet>() {{
|
||||
// all encrypted devices seem to share the same characteristics
|
||||
// Mi Band 8
|
||||
// Redmi Watch 3 Active
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user