1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-08-24 16:21:16 +02:00

Xiaomi: Refactor characteristics (wip, chunked is broken)

This commit is contained in:
José Rebelo 2023-10-16 21:01:02 +01:00
parent ae0a7bb806
commit 0ed169c153
8 changed files with 476 additions and 294 deletions

View File

@ -47,9 +47,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
@NonNull
@ -415,10 +413,4 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
public AbstractNotificationPattern[] getNotificationLedPatterns() {
return new AbstractNotificationPattern[0];
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return XiaomiSupport.class;
}
}

View File

@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.regex.Pattern;
@ -26,6 +27,8 @@ import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiEncryptedSupport;
public class MiBand8Coordinator extends XiaomiCoordinator {
@Override
@ -54,4 +57,10 @@ public class MiBand8Coordinator extends XiaomiCoordinator {
public int getDisabledIconResource() {
return R.drawable.ic_device_miband6_disabled;
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return XiaomiEncryptedSupport.class;
}
}

View File

@ -16,8 +16,6 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiConstants.UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE;
import android.content.SharedPreferences;
import android.os.Build;
@ -59,6 +57,8 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class XiaomiAuthService extends AbstractXiaomiService {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiAuthService.class);
public static final byte[] PAYLOAD_HEADER_AUTH = new byte[]{0, 0, 2, 2};
public static final int COMMAND_TYPE = 1;
public static final int CMD_NONCE = 26;
@ -75,15 +75,16 @@ public class XiaomiAuthService extends AbstractXiaomiService {
super(support);
}
protected void startAuthentication(final TransactionBuilder builder) {
protected void startEncryptedHandshake(final TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.AUTHENTICATING, getSupport().getContext()));
System.arraycopy(getSecretKey(getSupport().getDevice()), 0, secretKey, 0, 16);
new SecureRandom().nextBytes(nonce);
// TODO use sendCommand
builder.write(
getSupport().getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE),
ArrayUtils.addAll(XiaomiConstants.PAYLOAD_HEADER_AUTH, buildNonceCommand(nonce))
getSupport().getCharacteristic(XiaomiEncryptedSupport.UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE),
ArrayUtils.addAll(PAYLOAD_HEADER_AUTH, buildNonceCommand(nonce))
);
}
@ -105,10 +106,10 @@ public class XiaomiAuthService extends AbstractXiaomiService {
}
final TransactionBuilder builder = getSupport().createTransactionBuilder("auth step 2");
// TODO maybe move these writes to support class?
// TODO use sendCommand
builder.write(
getSupport().getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE),
ArrayUtils.addAll(XiaomiConstants.PAYLOAD_HEADER_AUTH, reply.toByteArray())
getSupport().getCharacteristic(XiaomiEncryptedSupport.UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE),
ArrayUtils.addAll(PAYLOAD_HEADER_AUTH, reply.toByteArray())
);
builder.queue(getSupport().getQueue());
break;

View File

@ -0,0 +1,311 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
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.LinkedList;
import java.util.Queue;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
public class XiaomiCharacteristic {
public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0};
public static final byte[] PAYLOAD_CHUNKED_START_ACK = new byte[]{0, 0, 1, 1};
public static final byte[] PAYLOAD_CHUNKED_END_ACK = new byte[]{0, 0, 1, 0};
private final Logger LOG;
private final XiaomiSupport mSupport;
private final BluetoothGattCharacteristic bluetoothGattCharacteristic;
private final UUID characteristicUUID;
// Encryption
private XiaomiAuthService authService = null;
private boolean isEncrypted = false;
private short encryptedIndex = 0;
// Chunking
private int numChunks = 0;
private int currentChunk = 0;
private final ByteArrayOutputStream chunkBuffer = new ByteArrayOutputStream();
// Scheduling
// TODO timeouts
private final Queue<byte[]> payloadQueue = new LinkedList<>();
private boolean waitingAck = false;
private boolean sendingChunked = false;
private byte[] currentSending = null;
private Handler handler = null;
public XiaomiCharacteristic(final XiaomiSupport support,
final BluetoothGattCharacteristic bluetoothGattCharacteristic,
@Nullable final XiaomiAuthService authService) {
this.mSupport = support;
this.bluetoothGattCharacteristic = bluetoothGattCharacteristic;
this.authService = authService;
this.isEncrypted = authService != null;
this.LOG = LoggerFactory.getLogger("XiaomiCharacteristic [" + bluetoothGattCharacteristic.getUuid().toString() + "]");
this.characteristicUUID = bluetoothGattCharacteristic.getUuid();
}
public UUID getCharacteristicUUID() {
return characteristicUUID;
}
public void setHandler(final Handler handler) {
this.handler = handler;
}
public void setEncrypted(final boolean encrypted) {
this.isEncrypted = encrypted;
}
public void reset() {
this.numChunks = 0;
this.currentChunk = 0;
this.encryptedIndex = 1; // 0 is used by auth service
this.chunkBuffer.reset();
this.payloadQueue.clear();
this.waitingAck = false;
this.sendingChunked = false;
this.currentSending = null;
}
/**
* Write bytes to this characteristic, encrypting and splitting it into chunks if necessary.
*/
public void write(final byte[] value) {
payloadQueue.add(value);
sendNext();
}
/**
* Write bytes to this characteristic directly.
*/
public void writeDirect(final TransactionBuilder builder, final byte[] value) {
builder.write(bluetoothGattCharacteristic, value);
}
public void onCharacteristicChanged(final byte[] value) {
if (Arrays.equals(value, PAYLOAD_ACK)) {
LOG.debug("Got ack");
currentSending = null;
waitingAck = false;
sendNext();
return;
}
final ByteBuffer buf = ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN);
final int chunk = buf.getShort();
if (chunk != 0) {
// Chunked packet
final byte[] chunkBytes = new byte[buf.limit() - buf.position()];
buf.get(chunkBytes);
try {
chunkBuffer.write(chunkBytes);
} catch (final IOException e) {
throw new RuntimeException(e);
}
currentChunk++;
LOG.debug("Got chunk {} of {}", currentChunk, numChunks);
if (chunk == numChunks) {
sendChunkEndAck();
if (authService != null) {
// chunks are always encrypted if an auth service is available
handler.handle(authService.decrypt(chunkBuffer.toByteArray()));
} else {
handler.handle(chunkBuffer.toByteArray());
}
}
} else {
// Not a chunk / single-packet
final byte type = buf.get();
switch (type) {
case 0:
// Chunked start request
final byte one = buf.get(); // ?
if (one != 1) {
LOG.warn("Chunked start request: expected 1, got {}", one);
return;
}
numChunks = buf.getShort();
LOG.debug("Got chunked start request for {} chunks", numChunks);
sendChunkStartAck();
return;
case 1:
// Chunked ack
final byte subtype = buf.get();
switch (subtype) {
case 0:
LOG.debug("Got chunked ack end");
currentSending = null;
sendingChunked = false;
sendNext();
return;
case 1:
LOG.debug("Got chunked ack start");
final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunks");
for (int i = 0; i * 242 < currentSending.length; i ++) {
final int startIndex = i * 242;
final int endIndex = Math.min((i + 1) * 242, currentSending.length);
LOG.debug("Sending chunk {} from {} to {}", i, startIndex, endIndex);
final byte[] chunkToSend = new byte[2 + endIndex - startIndex];
BLETypeConversions.writeUint16(chunkToSend, 0, i + 1);
System.arraycopy(currentSending, startIndex, chunkToSend, 2, endIndex - startIndex);
builder.write(bluetoothGattCharacteristic, chunkToSend);
}
builder.queue(mSupport.getQueue());
return;
}
LOG.warn("Unknown chunked ack subtype {}", subtype);
return;
case 2:
// Single command
sendAck();
final byte encryption = buf.get();
final byte[] plainValue;
if (encryption == 1) {
final byte[] encryptedValue = new byte[buf.limit() - buf.position()];
buf.get(encryptedValue);
plainValue = authService.decrypt(encryptedValue);
} else {
plainValue = new byte[buf.limit() - buf.position()];
buf.get(plainValue);
}
handler.handle(plainValue);
return;
case 3:
// ack
LOG.debug("Got ack");
}
}
}
private void sendNext() {
if (waitingAck || sendingChunked) {
LOG.debug("Already sending something");
return;
}
final byte[] payload = payloadQueue.poll();
if (payload == null) {
LOG.debug("Nothing to send");
return;
}
if (isEncrypted) {
currentSending = authService.encrypt(payload, encryptedIndex);
} else {
currentSending = payload;
}
if (shouldWriteChunked(currentSending)) {
LOG.debug("Sending next - chunked");
// FIXME this is not efficient - re-encrypt with the correct key for chunked (assumes
// final encrypted size is the same - need to check)
if (isEncrypted) {
currentSending = authService.encrypt(payload, (short) 0);
}
sendingChunked = true;
final ByteBuffer buf = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN);
buf.putShort((short) 0);
buf.put((byte) 0);
buf.put((byte) 1);
buf.putShort((short) Math.round(currentSending.length / 247.0));
final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunked start");
builder.write(bluetoothGattCharacteristic, buf.array());
builder.queue(mSupport.getQueue());
} else {
LOG.debug("Sending next - single");
// Encrypt single command
final int commandLength = 6 + currentSending.length;
final ByteBuffer buf = ByteBuffer.allocate(commandLength).order(ByteOrder.LITTLE_ENDIAN);
buf.putShort((short) 0);
buf.put((byte) 2); // 2 for command
buf.put((byte) 1); // 1 for encrypted
buf.putShort(encryptedIndex++);
buf.put(currentSending); // it's already encrypted
waitingAck = true;
final TransactionBuilder builder = mSupport.createTransactionBuilder("send single command");
builder.write(bluetoothGattCharacteristic, buf.array());
builder.queue(mSupport.getQueue());
}
}
private boolean shouldWriteChunked(final byte[] payload) {
if (!isEncrypted) {
// non-encrypted are always chunked
return true;
}
return payload.length + 6 > 244;
}
private void sendAck() {
final TransactionBuilder builder = mSupport.createTransactionBuilder("send ack");
builder.write(bluetoothGattCharacteristic, PAYLOAD_ACK);
builder.queue(mSupport.getQueue());
}
private void sendChunkStartAck() {
final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunked start ack");
builder.write(bluetoothGattCharacteristic, PAYLOAD_CHUNKED_START_ACK);
builder.queue(mSupport.getQueue());
}
private void sendChunkEndAck() {
final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunked end ack");
builder.write(bluetoothGattCharacteristic, PAYLOAD_CHUNKED_END_ACK);
builder.queue(mSupport.getQueue());
}
public interface Handler {
void handle(final byte[] payload);
}
}

View File

@ -1,59 +0,0 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class XiaomiChunkedHandler {
private int numChunks = 0;
private int currentChunk = 0;
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
public XiaomiChunkedHandler() {
}
public void setNumChunks(final int numChunks) {
this.numChunks = numChunks;
this.currentChunk = 0;
this.baos.reset();
}
public void addChunk(final byte[] chunk) {
try {
baos.write(chunk);
} catch (final IOException e) {
throw new RuntimeException(e);
}
currentChunk++;
}
public int getNumChunks() {
return numChunks;
}
public int getCurrentChunk() {
return currentChunk;
}
public byte[] getArray() {
return baos.toByteArray();
}
}

View File

@ -1,49 +0,0 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
import java.util.UUID;
public class XiaomiConstants {
public static final String BASE_UUID = "0000%s-0000-1000-8000-00805f9b34fb";
public static final UUID UUID_SERVICE_XIAOMI_FE95 = UUID.fromString((String.format(BASE_UUID, "fe95")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0050 = UUID.fromString((String.format(BASE_UUID, "0050")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ = UUID.fromString((String.format(BASE_UUID, "0051")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE = UUID.fromString((String.format(BASE_UUID, "0052")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_ACTIVITY_DATA = UUID.fromString((String.format(BASE_UUID, "0053")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0054 = UUID.fromString((String.format(BASE_UUID, "0054")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_WATCHFACE = UUID.fromString((String.format(BASE_UUID, "0055")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0056 = UUID.fromString((String.format(BASE_UUID, "0056")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0057 = UUID.fromString((String.format(BASE_UUID, "0057")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0058 = UUID.fromString((String.format(BASE_UUID, "0058")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0059 = UUID.fromString((String.format(BASE_UUID, "0059")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_005A = UUID.fromString((String.format(BASE_UUID, "005a")));
public static final UUID UUID_SERVICE_XIAOMI_FDAB = UUID.fromString((String.format(BASE_UUID, "fdab")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0001 = UUID.fromString((String.format(BASE_UUID, "0001")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0002 = UUID.fromString((String.format(BASE_UUID, "0002")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0003 = UUID.fromString((String.format(BASE_UUID, "0003")));
// TODO not like this
public static final byte[] PAYLOAD_CHUNKED_START = new byte[]{0, 0, 0, 1};
public static final byte[] PAYLOAD_CHUNKED_START_ACK = new byte[]{0, 0, 1, 1};
public static final byte[] PAYLOAD_CHUNKED_END_ACK = new byte[]{0, 0, 1, 0};
public static final byte[] PAYLOAD_HEADER_AUTH = new byte[]{0, 0, 2, 2};
public static final byte[] PAYLOAD_HEADER_CMD = new byte[]{0, 0, 2, 1};
public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0};
}

View File

@ -0,0 +1,102 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
import android.bluetooth.BluetoothGattCharacteristic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
public class XiaomiEncryptedSupport extends XiaomiSupport {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiEncryptedSupport.class);
public static final String BASE_UUID = "0000%s-0000-1000-8000-00805f9b34fb";
public static final UUID UUID_SERVICE_XIAOMI_FE95 = UUID.fromString((String.format(BASE_UUID, "fe95")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0050 = UUID.fromString((String.format(BASE_UUID, "0050")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ = UUID.fromString((String.format(BASE_UUID, "0051")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE = UUID.fromString((String.format(BASE_UUID, "0052")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_ACTIVITY_DATA = UUID.fromString((String.format(BASE_UUID, "0053")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0054 = UUID.fromString((String.format(BASE_UUID, "0054")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_DATA_UPLOAD = UUID.fromString((String.format(BASE_UUID, "0055")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0056 = UUID.fromString((String.format(BASE_UUID, "0056")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0057 = UUID.fromString((String.format(BASE_UUID, "0057")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0058 = UUID.fromString((String.format(BASE_UUID, "0058")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0059 = UUID.fromString((String.format(BASE_UUID, "0059")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_005A = UUID.fromString((String.format(BASE_UUID, "005a")));
public static final UUID UUID_SERVICE_XIAOMI_FDAB = UUID.fromString((String.format(BASE_UUID, "fdab")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0001 = UUID.fromString((String.format(BASE_UUID, "0001")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0002 = UUID.fromString((String.format(BASE_UUID, "0002")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0003 = UUID.fromString((String.format(BASE_UUID, "0003")));
public XiaomiEncryptedSupport() {
super();
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
addSupportedService(GattService.UUID_SERVICE_HUMAN_INTERFACE_DEVICE);
addSupportedService(UUID_SERVICE_XIAOMI_FE95);
addSupportedService(UUID_SERVICE_XIAOMI_FDAB);
}
@Override
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
final BluetoothGattCharacteristic btCharacteristicCommandRead = getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ);
final BluetoothGattCharacteristic btCharacteristicCommandWrite = getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE);
final BluetoothGattCharacteristic btCharacteristicActivityData = getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_ACTIVITY_DATA);
final BluetoothGattCharacteristic btCharacteristicDataUpload = getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_DATA_UPLOAD);
if (btCharacteristicCommandRead == null || btCharacteristicCommandWrite == null || btCharacteristicActivityData == null || btCharacteristicDataUpload == null) {
LOG.warn("Characteristics are null, will attempt to reconnect");
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.WAITING_FOR_RECONNECT, getContext()));
return builder;
}
this.characteristicCommandRead = new XiaomiCharacteristic(this, btCharacteristicCommandRead, authService);
this.characteristicCommandRead.setEncrypted(true);
this.characteristicCommandRead.setHandler(this::handleCommandBytes);
this.characteristicCommandWrite = new XiaomiCharacteristic(this, btCharacteristicCommandWrite, authService);
this.characteristicCommandRead.setEncrypted(true);
this.characteristicActivityData = new XiaomiCharacteristic(this, btCharacteristicActivityData, authService);
this.characteristicCommandRead.setEncrypted(true);
this.characteristicDataUpload = new XiaomiCharacteristic(this, btCharacteristicDataUpload, authService);
this.characteristicCommandRead.setEncrypted(true);
// FIXME why is this needed?
getDevice().setFirmwareVersion("...");
//getDevice().setFirmwareVersion2("...");
builder.requestMtu(247);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ), true);
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE), true);
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_ACTIVITY_DATA), true);
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_DATA_UPLOAD), true);
authService.startEncryptedHandshake(builder);
return builder;
}
}

View File

@ -16,7 +16,6 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiConstants.*;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothGatt;
@ -29,16 +28,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
@ -52,9 +48,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.AbstractXiaomiService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiCalendarService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiHealthService;
@ -66,17 +60,22 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.Xiao
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class XiaomiSupport extends AbstractBTLEDeviceSupport {
public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiSupport.class);
private final XiaomiAuthService authService = new XiaomiAuthService(this);
private final XiaomiMusicService musicService = new XiaomiMusicService(this);
private final XiaomiHealthService healthService = new XiaomiHealthService(this);
private final XiaomiNotificationService notificationService = new XiaomiNotificationService(this);
private final XiaomiScheduleService scheduleService = new XiaomiScheduleService(this);
private final XiaomiWeatherService weatherService = new XiaomiWeatherService(this);
private final XiaomiSystemService systemService = new XiaomiSystemService(this);
private final XiaomiCalendarService calendarService = new XiaomiCalendarService(this);
protected XiaomiCharacteristic characteristicCommandRead;
protected XiaomiCharacteristic characteristicCommandWrite;
protected XiaomiCharacteristic characteristicActivityData;
protected XiaomiCharacteristic characteristicDataUpload;
protected final XiaomiAuthService authService = new XiaomiAuthService(this);
protected final XiaomiMusicService musicService = new XiaomiMusicService(this);
protected final XiaomiHealthService healthService = new XiaomiHealthService(this);
protected final XiaomiNotificationService notificationService = new XiaomiNotificationService(this);
protected final XiaomiScheduleService scheduleService = new XiaomiScheduleService(this);
protected final XiaomiWeatherService weatherService = new XiaomiWeatherService(this);
protected final XiaomiSystemService systemService = new XiaomiSystemService(this);
protected final XiaomiCalendarService calendarService = new XiaomiCalendarService(this);
private final Map<Integer, AbstractXiaomiService> mServiceMap = new LinkedHashMap<Integer, AbstractXiaomiService>() {{
put(XiaomiAuthService.COMMAND_TYPE, authService);
@ -91,12 +90,6 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
public XiaomiSupport() {
super(LOG);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
addSupportedService(GattService.UUID_SERVICE_HUMAN_INTERFACE_DEVICE);
addSupportedService(UUID_SERVICE_XIAOMI_FE95);
addSupportedService(UUID_SERVICE_XIAOMI_FDAB);
}
@Override
@ -117,31 +110,6 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
}
}
@Override
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
final BluetoothGattCharacteristic characteristicCommandWrite = getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE);
final BluetoothGattCharacteristic characteristicCommandRead = getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ);
if (characteristicCommandWrite == null || characteristicCommandRead == null) {
LOG.warn("Command characteristics are null, will attempt to reconnect");
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.WAITING_FOR_RECONNECT, getContext()));
return builder;
}
// FIXME why is this needed?
getDevice().setFirmwareVersion("...");
//getDevice().setFirmwareVersion2("...");
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ), true);
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE), true);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
authService.startAuthentication(builder);
return builder;
}
private final Map<UUID, XiaomiChunkedHandler> mChunkedHandlers = new HashMap<>();
@Override
public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
if (super.onCharacteristicChanged(gatt, characteristic)) {
@ -151,93 +119,17 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
final UUID characteristicUUID = characteristic.getUuid();
final byte[] value = characteristic.getValue();
if (Arrays.equals(value, PAYLOAD_ACK)) {
}
if (UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE.equals(characteristicUUID)) {
if (Arrays.equals(value, PAYLOAD_ACK)) {
LOG.debug("Got command write ack");
} else {
LOG.warn("Unexpected notification from command write: {}", GB.hexdump(value));
}
if (characteristicCommandRead.getCharacteristicUUID().equals(characteristicUUID)) {
characteristicCommandRead.onCharacteristicChanged(value);
return true;
}
if (UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ.equals(characteristicUUID)) {
final ByteBuffer buf = ByteBuffer.wrap(characteristic.getValue())
.order(ByteOrder.LITTLE_ENDIAN);
final int chunk = buf.getShort();
if (chunk != 0) {
// Chunked packet
final XiaomiChunkedHandler chunkedHandler = mChunkedHandlers.get(characteristicUUID);
if (chunkedHandler == null) {
LOG.warn("No chunked handler initialized for {}", characteristicUUID);
return true;
}
final byte[] chunkBytes = new byte[buf.limit() - buf.position()];
buf.get(chunkBytes);
chunkedHandler.addChunk(chunkBytes);
if (chunk == chunkedHandler.getNumChunks()) {
// TODO handle reassembled chunk
final byte[] plainValue = authService.decrypt(chunkedHandler.getArray());
handleCommandBytes(plainValue);
}
return true;
} else {
// Not a chunk / single-packet
final byte type = buf.get();
switch (type) {
case 0:
// Chunked start request
final byte one = buf.get(); // ?
if (one != 1) {
LOG.warn("Chunked start request: expected 1, got {}", one);
return true;
}
final short numChunks = buf.getShort();
LOG.debug("Got chunked start request for {} chunks", numChunks);
XiaomiChunkedHandler chunkedHandler = mChunkedHandlers.get(characteristicUUID);
if (chunkedHandler == null) {
chunkedHandler = new XiaomiChunkedHandler();
mChunkedHandlers.put(UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ, chunkedHandler);
}
chunkedHandler.setNumChunks(numChunks);
sendChunkStartAck(characteristic);
return true;
case 1:
// Chunked start ack
LOG.debug("Got chunked start ack");
return true;
case 2:
// Single command
sendAck(characteristic);
final byte encryption = buf.get();
final byte[] plainValue;
if (encryption == 1) {
final byte[] encryptedValue = new byte[buf.limit() - buf.position()];
buf.get(encryptedValue);
plainValue = authService.decrypt(encryptedValue);
} else {
plainValue = new byte[buf.limit() - buf.position()];
buf.get(plainValue);
}
handleCommandBytes(plainValue);
return true;
case 3:
// ack
LOG.debug("Got ack");
return true;
}
}
} 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);
return true;
}
@ -290,8 +182,12 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
}
systemService.setCurrentTime(builder);
// TODO this should not be done here
calendarService.syncCalendar(builder);
final XiaomiCoordinator coordinator = getCoordinator();
if (coordinator.supportsCalendarEvents()) {
// TODO this should not be done here
calendarService.syncCalendar(builder);
}
builder.queue(getQueue());
}
@ -470,9 +366,17 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
weatherService.onSendWeather(weatherSpec);
}
public XiaomiCoordinator getCoordinator() {
return (XiaomiCoordinator) gbDevice.getDeviceCoordinator();
}
protected void phase2Initialize(final TransactionBuilder builder) {
LOG.info("phase2Initialize");
encryptedIndex = 1; // TODO not here
characteristicCommandRead.reset();
characteristicCommandWrite.reset();
characteristicActivityData.reset();
characteristicDataUpload.reset();
if (GBApplication.getPrefs().getBoolean("datetime_synconconnect", true)) {
systemService.setCurrentTime(builder);
@ -483,26 +387,6 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
}
}
private void sendAck(final BluetoothGattCharacteristic characteristic) {
final TransactionBuilder builder = createTransactionBuilder("send ack");
builder.write(characteristic, PAYLOAD_ACK);
builder.queue(getQueue());
}
private void sendChunkStartAck(final BluetoothGattCharacteristic characteristic) {
final TransactionBuilder builder = createTransactionBuilder("send chunked start ack");
builder.write(characteristic, PAYLOAD_CHUNKED_START_ACK);
builder.queue(getQueue());
}
private void sendChunkEndAck(final BluetoothGattCharacteristic characteristic) {
final TransactionBuilder builder = createTransactionBuilder("send chunked end ack");
builder.write(characteristic, PAYLOAD_CHUNKED_END_ACK);
builder.queue(getQueue());
}
private short encryptedIndex = 0;
public void sendCommand(final String taskName, final XiaomiProto.Command command) {
final TransactionBuilder builder = createTransactionBuilder(taskName);
sendCommand(builder, command);
@ -511,21 +395,12 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
public void sendCommand(final TransactionBuilder builder, final XiaomiProto.Command command) {
final byte[] commandBytes = command.toByteArray();
final byte[] encryptedCommandBytes = authService.encrypt(commandBytes, encryptedIndex);
final int commandLength = 6 + encryptedCommandBytes.length;
if (getMTU() != 0 && commandLength > getMTU()) {
// TODO MTU is 0 sometimes?
LOG.warn("Command with {} bytes is too large for MTU of {}", commandLength, getMTU());
}
final ByteBuffer buf = ByteBuffer.allocate(commandLength).order(ByteOrder.LITTLE_ENDIAN);
buf.putShort((short) 0);
buf.put((byte) 2); // 2 for command
buf.put((byte) 1); // 1 for encrypted
buf.putShort(encryptedIndex++);
buf.put(encryptedCommandBytes);
LOG.debug("Sending command {}", GB.hexdump(commandBytes));
builder.write(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE), buf.array());
this.characteristicCommandWrite.write(commandBytes);
}
public void sendCommand(final TransactionBuilder builder, final byte[] commandBytes) {
this.characteristicCommandWrite.write(commandBytes);
}
public void sendCommand(final TransactionBuilder builder, final int type, final int subtype) {