mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-06-23 13:30:48 +02:00
Mi Band 8: Handle incoming chunked packets
This commit is contained in:
parent
f0188f3499
commit
44be081e86
|
@ -56,6 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBlePro
|
||||||
public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback, GattServerCallback {
|
public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback, GattServerCallback {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractBTLEDeviceSupport.class);
|
private static final Logger LOG = LoggerFactory.getLogger(AbstractBTLEDeviceSupport.class);
|
||||||
|
|
||||||
|
private int mMTU = 0;
|
||||||
private BtLEQueue mQueue;
|
private BtLEQueue mQueue;
|
||||||
private Map<UUID, BluetoothGattCharacteristic> mAvailableCharacteristics;
|
private Map<UUID, BluetoothGattCharacteristic> mAvailableCharacteristics;
|
||||||
private final Set<UUID> mSupportedServices = new HashSet<>(4);
|
private final Set<UUID> mSupportedServices = new HashSet<>(4);
|
||||||
|
@ -380,7 +381,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||||
|
this.mMTU = mtu;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -407,4 +408,12 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
|
||||||
public boolean onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
|
public boolean onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current MTU, or 0 if unknown
|
||||||
|
* @return the current MTU, 0 if unknown
|
||||||
|
*/
|
||||||
|
public int getMTU() {
|
||||||
|
return mMTU;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/* 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,10 +40,10 @@ public class XiaomiConstants {
|
||||||
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0003 = UUID.fromString((String.format(BASE_UUID, "0003")));
|
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0003 = UUID.fromString((String.format(BASE_UUID, "0003")));
|
||||||
|
|
||||||
// TODO not like this
|
// TODO not like this
|
||||||
public static final byte[] PAYLOAD_CHUNKED_START = new byte[]{0, 0, 0, 1};
|
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_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_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_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_HEADER_CMD = new byte[]{0, 0, 2, 1};
|
||||||
public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0};
|
public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0};
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ import android.content.Context;
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -34,6 +33,7 @@ import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -52,7 +52,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
|
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||||
|
@ -138,6 +137,8 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final Map<UUID, XiaomiChunkedHandler> mChunkedHandlers = new HashMap<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
|
public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
|
||||||
if (super.onCharacteristicChanged(gatt, characteristic)) {
|
if (super.onCharacteristicChanged(gatt, characteristic)) {
|
||||||
|
@ -147,6 +148,10 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
|
||||||
final UUID characteristicUUID = characteristic.getUuid();
|
final UUID characteristicUUID = characteristic.getUuid();
|
||||||
final byte[] value = characteristic.getValue();
|
final byte[] value = characteristic.getValue();
|
||||||
|
|
||||||
|
if (Arrays.equals(value, PAYLOAD_ACK)) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE.equals(characteristicUUID)) {
|
if (UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE.equals(characteristicUUID)) {
|
||||||
if (Arrays.equals(value, PAYLOAD_ACK)) {
|
if (Arrays.equals(value, PAYLOAD_ACK)) {
|
||||||
LOG.debug("Got command write ack");
|
LOG.debug("Got command write ack");
|
||||||
|
@ -158,48 +163,78 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ.equals(characteristicUUID)) {
|
if (UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ.equals(characteristicUUID)) {
|
||||||
sendAck(characteristic);
|
final ByteBuffer buf = ByteBuffer.wrap(characteristic.getValue())
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
final int header = BLETypeConversions.toUint16(value, 0);
|
final int chunk = buf.getShort();
|
||||||
final int type = BLETypeConversions.toUnsigned(value, 2);
|
if (chunk != 0) {
|
||||||
final int encryption = BLETypeConversions.toUnsigned(value, 3);
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
if (header != 0) {
|
|
||||||
LOG.warn("Non-zero header not supported");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
if (type == 0) {
|
|
||||||
// Chunked
|
|
||||||
}
|
|
||||||
if (type != 2) {
|
|
||||||
LOG.warn("Unsupported type {}", type);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] plainValue;
|
|
||||||
if (encryption == 1) {
|
|
||||||
plainValue = authService.decrypt(ArrayUtils.subarray(value, 4, value.length));
|
|
||||||
} else {
|
} else {
|
||||||
plainValue = ArrayUtils.subarray(value, 4, value.length);
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.debug("Got command: {}", GB.hexdump(plainValue));
|
|
||||||
|
|
||||||
final XiaomiProto.Command cmd;
|
|
||||||
try {
|
|
||||||
cmd = XiaomiProto.Command.parseFrom(plainValue);
|
|
||||||
} catch (final Exception e) {
|
|
||||||
LOG.error("Failed to parse bytes as protobuf command payload", e);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
final AbstractXiaomiService service = mServiceMap.get(cmd.getType());
|
|
||||||
if (service != null) {
|
|
||||||
service.handleCommand(cmd);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG.warn("Unexpected watch command type {}", cmd.getType());
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,6 +242,26 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void handleCommandBytes(final byte[] plainValue) {
|
||||||
|
LOG.debug("Got command: {}", GB.hexdump(plainValue));
|
||||||
|
|
||||||
|
final XiaomiProto.Command cmd;
|
||||||
|
try {
|
||||||
|
cmd = XiaomiProto.Command.parseFrom(plainValue);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("Failed to parse bytes as protobuf command payload", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final AbstractXiaomiService service = mServiceMap.get(cmd.getType());
|
||||||
|
if (service != null) {
|
||||||
|
service.handleCommand(cmd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.warn("Unexpected watch command type {}", cmd.getType());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendConfiguration(final String config) {
|
public void onSendConfiguration(final String config) {
|
||||||
final Prefs prefs = getDevicePrefs();
|
final Prefs prefs = getDevicePrefs();
|
||||||
|
@ -429,6 +484,18 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
|
||||||
builder.queue(getQueue());
|
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;
|
private short encryptedIndex = 0;
|
||||||
|
|
||||||
public void sendCommand(final String taskName, final XiaomiProto.Command command) {
|
public void sendCommand(final String taskName, final XiaomiProto.Command command) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user