Xiaomi-protobuf: Add BLE V2

This commit is contained in:
José Rebelo
2025-07-19 22:11:06 +01:00
parent eca17af9fb
commit 4c01beabe8
13 changed files with 752 additions and 211 deletions
@@ -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);
@@ -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;
}
}
@@ -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");
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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;
}
}
}
@@ -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) {}
}
@@ -0,0 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
public interface XiaomiSendCallback {
void onSend();
void onNack();
}
@@ -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);
}
@@ -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
@@ -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);