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:
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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")));
|
||||
|
||||
// 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};
|
||||
}
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user