1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-29 05:16:51 +01:00

Mi Band 8: Handle incoming chunked packets

This commit is contained in:
José Rebelo 2023-10-06 20:10:11 +01:00
parent f0188f3499
commit 44be081e86
4 changed files with 179 additions and 44 deletions

View File

@ -56,6 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBlePro
public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback, GattServerCallback {
private static final Logger LOG = LoggerFactory.getLogger(AbstractBTLEDeviceSupport.class);
private int mMTU = 0;
private BtLEQueue mQueue;
private Map<UUID, BluetoothGattCharacteristic> mAvailableCharacteristics;
private final Set<UUID> mSupportedServices = new HashSet<>(4);
@ -380,7 +381,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
this.mMTU = mtu;
}
@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) {
return false;
}
/**
* Gets the current MTU, or 0 if unknown
* @return the current MTU, 0 if unknown
*/
public int getMTU() {
return mMTU;
}
}

View File

@ -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();
}
}

View File

@ -40,10 +40,10 @@ public class XiaomiConstants {
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 = 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};
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

@ -25,7 +25,6 @@ import android.content.Context;
import android.location.Location;
import android.net.Uri;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -34,6 +33,7 @@ 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;
@ -52,7 +52,6 @@ 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.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
@ -138,6 +137,8 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
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)) {
@ -147,6 +148,10 @@ 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");
@ -158,48 +163,78 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
}
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 type = BLETypeConversions.toUnsigned(value, 2);
final int encryption = BLETypeConversions.toUnsigned(value, 3);
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);
}
if (header != 0) {
LOG.warn("Non-zero header not supported");
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 {
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;
}
@ -207,6 +242,26 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
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
public void onSendConfiguration(final String config) {
final Prefs prefs = getDevicePrefs();
@ -429,6 +484,18 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport {
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) {