mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
Xiaomi: Refactor characteristics (wip, chunked is broken)
This commit is contained in:
parent
ae0a7bb806
commit
0ed169c153
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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};
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user