mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Zepp OS 3/4: Refactor file transfer service, fix screenshots
This commit is contained in:
+3
-2
@@ -41,7 +41,7 @@ public class HuamiService {
|
||||
* Alarms, Display and other configuration.
|
||||
*/
|
||||
public static final UUID UUID_CHARACTERISTIC_3_CONFIGURATION = UUID.fromString("00000003-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_UNKNOWN_CHARACTERISTIC4 = UUID.fromString("00000004-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_5_ACTIVITY_CONTROL = UUID.fromString("00000004-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_5_ACTIVITY_DATA = UUID.fromString("00000005-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_6_BATTERY_INFO = UUID.fromString("00000006-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_7_REALTIME_STEPS = UUID.fromString("00000007-0000-3512-2118-0009af100700");
|
||||
@@ -58,7 +58,8 @@ public class HuamiService {
|
||||
public static final UUID UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ = UUID.fromString("00000017-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_CHUNKEDTRANSFER = UUID.fromString("00000020-0000-3512-2118-0009af100700");
|
||||
|
||||
public static final UUID UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3 = UUID.fromString("00000023-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3_SEND = UUID.fromString("00000023-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3_RECEIVE = UUID.fromString("00000024-0000-3512-2118-0009af100700");
|
||||
|
||||
public static final int ALERT_LEVEL_NONE = 0;
|
||||
public static final int ALERT_LEVEL_MESSAGE = 1;
|
||||
|
||||
-5
@@ -70,11 +70,6 @@ public class AmazfitActive2Coordinator extends ZeppOsCoordinator {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScreenshots(final GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToDoList() {
|
||||
return true;
|
||||
|
||||
+21
@@ -98,6 +98,7 @@ public class BleNamesResolver {
|
||||
mServices.put("0000fe95-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi Wear Service)");
|
||||
mServices.put("0000fee0-0000-3512-2118-0009af100700", "(Propr: Xiaomi MiLi Service)");
|
||||
mServices.put("00001530-0000-3512-2118-0009af100700", "(Propr: Xiaomi Weight Service)");
|
||||
mServices.put("0000fee0-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi Huami Service)");
|
||||
mServices.put("14701820-620a-3973-7c78-9cfff0876abd", "(Propr: HPLUS Service)");
|
||||
mServices.put("16186f00-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi Wear Service - Mi Watch Lite/Redmi Watch)");
|
||||
mServices.put("16187f00-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi Wear Service - Mi Smart Watch 4C/Redmi Band)");
|
||||
@@ -219,6 +220,26 @@ public class BleNamesResolver {
|
||||
//mCharacteristics.put("ebe0ccbc-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd HISTORY)");
|
||||
//mCharacteristics.put("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd LIVE_DATA)");
|
||||
//mCharacteristics.put("ebe0ccba-7a0a-4b0c-8a1a-6ff2997da3a6", "(Propr: Lywsd HISTORY_LAST_ID)");
|
||||
mCharacteristics.put("00000001-0000-3512-2118-0009af100700", "(Propr: Huami Raw Sensor Control)");
|
||||
mCharacteristics.put("00000002-0000-3512-2118-0009af100700", "(Propr: Huami Raw Sensor Data)");
|
||||
mCharacteristics.put("00000003-0000-3512-2118-0009af100700", "(Propr: Huami Configuration)");
|
||||
mCharacteristics.put("00000004-0000-3512-2118-0009af100700", "(Propr: Huami Activity Control)");
|
||||
mCharacteristics.put("00000005-0000-3512-2118-0009af100700", "(Propr: Huami Activity Data)");
|
||||
mCharacteristics.put("00000006-0000-3512-2118-0009af100700", "(Propr: Huami Battery Info)");
|
||||
mCharacteristics.put("00000007-0000-3512-2118-0009af100700", "(Propr: Huami Realtime Steps)");
|
||||
mCharacteristics.put("00000008-0000-3512-2118-0009af100700", "(Propr: Huami User Settings)");
|
||||
mCharacteristics.put("00000009-0000-3512-2118-0009af100700", "(Propr: Huami Auth)");
|
||||
mCharacteristics.put("0000000f-0000-3512-2118-0009af100700", "(Propr: Huami Workout)");
|
||||
mCharacteristics.put("00000010-0000-3512-2118-0009af100700", "(Propr: Huami Device Event)");
|
||||
mCharacteristics.put("00000012-0000-3512-2118-0009af100700", "(Propr: Huami Audio Control)");
|
||||
mCharacteristics.put("00000013-0000-3512-2118-0009af100700", "(Propr: Huami Audio Data)");
|
||||
mCharacteristics.put("00000016-0000-3512-2118-0009af100700", "(Propr: Huami 2021 Chunked Write)");
|
||||
mCharacteristics.put("00000017-0000-3512-2118-0009af100700", "(Propr: Huami 2021 Chunked Read)");
|
||||
mCharacteristics.put("00000020-0000-3512-2118-0009af100700", "(Propr: Huami Chunked Transfer)");
|
||||
mCharacteristics.put("00000023-0000-3512-2118-0009af100700", "(Propr: Zepp OS File Transfer V3 Send)");
|
||||
mCharacteristics.put("00000024-0000-3512-2118-0009af100700", "(Propr: Zepp OS File Transfer V3 Receive)");
|
||||
mCharacteristics.put("00001531-0000-3512-2118-0009af100700", "(Propr: Huami Firmware Control)");
|
||||
mCharacteristics.put("00001532-0000-3512-2118-0009af100700", "(Propr: Huami Firmware Data)");
|
||||
|
||||
mValueFormats.put(52, "32bit float");
|
||||
mValueFormats.put(50, "16bit float");
|
||||
|
||||
+2
-3
@@ -22,7 +22,6 @@ import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.SharedPreferences;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@@ -100,7 +99,7 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
|
||||
characteristicActivityData = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_5_ACTIVITY_DATA);
|
||||
builder.notify(characteristicActivityData, false);
|
||||
|
||||
characteristicFetch = getCharacteristic(HuamiService.UUID_UNKNOWN_CHARACTERISTIC4);
|
||||
characteristicFetch = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_5_ACTIVITY_CONTROL);
|
||||
builder.notify(characteristicFetch, true);
|
||||
|
||||
startFetching(builder);
|
||||
@@ -123,7 +122,7 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
|
||||
if (HuamiService.UUID_CHARACTERISTIC_5_ACTIVITY_DATA.equals(characteristicUUID)) {
|
||||
handleActivityData(characteristic.getValue());
|
||||
return true;
|
||||
} else if (HuamiService.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) {
|
||||
} else if (HuamiService.UUID_CHARACTERISTIC_5_ACTIVITY_CONTROL.equals(characteristicUUID)) {
|
||||
handleActivityMetadata(characteristic.getValue());
|
||||
return true;
|
||||
} else {
|
||||
|
||||
+5
-2
@@ -1378,8 +1378,11 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
public boolean onCharacteristicChanged(final BluetoothGatt gatt,
|
||||
final BluetoothGattCharacteristic characteristic) {
|
||||
final UUID characteristicUUID = characteristic.getUuid();
|
||||
if (HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3.equals(characteristicUUID)) {
|
||||
fileTransferService.onCharacteristicChanged(characteristic.getValue());
|
||||
if (HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3_SEND.equals(characteristicUUID)) {
|
||||
fileTransferService.onCharacteristicChanged(characteristic);
|
||||
return true;
|
||||
} else if (HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3_RECEIVE.equals(characteristicUUID)) {
|
||||
fileTransferService.onCharacteristicChanged(characteristic);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+41
-546
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2023-2024 José Rebelo
|
||||
/* Copyright (C) 2023-2025 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@@ -16,51 +16,24 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.zip.DataFormatException;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.filetransfer.AbstractFileTransferImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.filetransfer.FileTransferImplV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.filetransfer.FileTransferImplV3;
|
||||
|
||||
public class ZeppOsFileTransferService extends AbstractZeppOsService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsFileTransferService.class);
|
||||
|
||||
private static final short ENDPOINT = 0x000d;
|
||||
|
||||
private static final byte CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
private static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
private static final byte CMD_TRANSFER_REQUEST = 0x03;
|
||||
private static final byte CMD_TRANSFER_RESPONSE = 0x04;
|
||||
private static final byte CMD_DATA_SEND = 0x10;
|
||||
private static final byte CMD_DATA_ACK = 0x11;
|
||||
private static final byte CMD_DATA_V3_SEND = 0x12;
|
||||
private static final byte CMD_DATA_V3_ACK = 0x13;
|
||||
|
||||
private static final byte FLAG_FIRST_CHUNK = 0x01;
|
||||
private static final byte FLAG_LAST_CHUNK = 0x02;
|
||||
private static final byte FLAG_CRC = 0x04;
|
||||
|
||||
private final Map<Byte, FileTransferRequest> mSessionRequests = new HashMap<>();
|
||||
|
||||
private int mVersion = -1;
|
||||
private int mChunkSize = -1;
|
||||
private int mCompressedChunkSize = -1;
|
||||
private AbstractFileTransferImpl impl;
|
||||
|
||||
public ZeppOsFileTransferService(final ZeppOsSupport support) {
|
||||
super(support, false);
|
||||
@@ -71,538 +44,60 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
|
||||
return ENDPOINT;
|
||||
}
|
||||
|
||||
/**
|
||||
* HACK: Expose the method to the impl.
|
||||
*/
|
||||
@Override
|
||||
public void write(final String taskName, final byte[] data) {
|
||||
super.write(taskName, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(final byte[] payload) {
|
||||
byte session;
|
||||
byte status;
|
||||
|
||||
switch (payload[0]) {
|
||||
case CMD_CAPABILITIES_RESPONSE:
|
||||
mVersion = payload[1] & 0xff;
|
||||
if (mVersion != 1 && mVersion != 2 && mVersion != 3) {
|
||||
LOG.error("Unsupported file transfer service version: {}", mVersion);
|
||||
return;
|
||||
}
|
||||
mChunkSize = BLETypeConversions.toUint16(payload, 2);
|
||||
if (mVersion == 3) {
|
||||
// TODO parse the rest for v3
|
||||
mCompressedChunkSize = BLETypeConversions.toUint32(payload, 4);
|
||||
final TransactionBuilder builder = getSupport().createTransactionBuilder("enable file transfer v3 notifications");
|
||||
builder.notify(getSupport().getCharacteristic(HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3), true);
|
||||
builder.queue(getSupport().getQueue());
|
||||
}
|
||||
LOG.info(
|
||||
"Got file transfer service: version={}, chunkSize={}, compressedChunkSize={}",
|
||||
mVersion,
|
||||
mChunkSize,
|
||||
mCompressedChunkSize
|
||||
);
|
||||
return;
|
||||
case CMD_TRANSFER_REQUEST:
|
||||
handleFileTransferRequest(payload);
|
||||
return;
|
||||
case CMD_TRANSFER_RESPONSE:
|
||||
session = payload[1];
|
||||
status = payload[2];
|
||||
final int existingProgress = BLETypeConversions.toUint32(payload, 3);
|
||||
LOG.info("Band acknowledged file transfer request: session={}, status={}, existingProgress={}", session, status, existingProgress);
|
||||
if (status != 0) {
|
||||
LOG.error("Unexpected status from band for session {}, aborting", session);
|
||||
onUploadFinish(session, false);
|
||||
return;
|
||||
}
|
||||
if (existingProgress != 0) {
|
||||
LOG.info("Updating existing progress for session {} to {}", session, existingProgress);
|
||||
final FileTransferRequest request = mSessionRequests.get(session);
|
||||
if (request == null) {
|
||||
LOG.error("No request found for session {}", session);
|
||||
return;
|
||||
}
|
||||
request.setProgress(existingProgress);
|
||||
}
|
||||
sendNextQueuedData(session);
|
||||
return;
|
||||
case CMD_DATA_SEND:
|
||||
handleFileTransferData(payload);
|
||||
return;
|
||||
case CMD_DATA_ACK:
|
||||
session = payload[1];
|
||||
status = payload[2];
|
||||
LOG.info("Band acknowledged file transfer data: session={}, status={}", session, status);
|
||||
if (status != 0) {
|
||||
LOG.error("Unexpected status from band, aborting session {}", session);
|
||||
onUploadFinish(session, false);
|
||||
return;
|
||||
}
|
||||
sendNextQueuedData(session);
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unexpected file transfer byte {}", String.format("0x%02x", payload[0]));
|
||||
if (impl != null) {
|
||||
impl.handlePayload(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload[0] != AbstractFileTransferImpl.CMD_CAPABILITIES_RESPONSE) {
|
||||
LOG.warn("Got file transfer command, but impl is not initialized");
|
||||
return;
|
||||
}
|
||||
final int version = payload[1] & 0xff;
|
||||
if (version == 1 || version == 2) {
|
||||
impl = new FileTransferImplV2(this, getSupport());
|
||||
} else if (version == 3) {
|
||||
impl = new FileTransferImplV3(this, getSupport());
|
||||
} else {
|
||||
LOG.error("Unsupported file transfer service version: {}", version);
|
||||
return;
|
||||
}
|
||||
|
||||
impl.handlePayload(payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(final TransactionBuilder builder) {
|
||||
requestCapability(builder);
|
||||
}
|
||||
|
||||
public void requestCapability(final TransactionBuilder builder) {
|
||||
write(builder, new byte[]{CMD_CAPABILITIES_REQUEST});
|
||||
}
|
||||
|
||||
private void handleFileTransferRequest(final byte[] payload) {
|
||||
// File transfer request initialized from watch
|
||||
int pos = 1;
|
||||
final byte session = payload[pos++];
|
||||
final String url = StringUtils.untilNullTerminator(payload, pos);
|
||||
if (url == null) {
|
||||
LOG.error("Unable to parse url from transfer request");
|
||||
return;
|
||||
}
|
||||
pos += url.length() + 1;
|
||||
final String filename = StringUtils.untilNullTerminator(payload, pos);
|
||||
if (filename == null) {
|
||||
LOG.error("Unable to parse filename from transfer request");
|
||||
return;
|
||||
}
|
||||
pos += filename.length() + 1;
|
||||
final int length = BLETypeConversions.toUint32(payload, pos);
|
||||
pos += 4;
|
||||
final int crc32 = BLETypeConversions.toUint32(payload, pos);
|
||||
pos += 4;
|
||||
|
||||
final boolean compressed;
|
||||
if (pos < payload.length) {
|
||||
final Boolean compressedBoolean = booleanFromByte(payload[pos]);
|
||||
if (compressedBoolean == null) {
|
||||
LOG.warn("Unknown compression type {}", payload[pos]);
|
||||
return;
|
||||
}
|
||||
compressed = compressedBoolean;
|
||||
} else {
|
||||
compressed = false;
|
||||
}
|
||||
|
||||
LOG.info("Got transfer request: session={}, url={}, filename={}, length={}, compressed={}", session, url, filename, length, compressed);
|
||||
|
||||
final FileTransferRequest request = new FileTransferRequest(
|
||||
url,
|
||||
filename,
|
||||
new byte[length],
|
||||
compressed,
|
||||
compressed ? mCompressedChunkSize : mChunkSize,
|
||||
getSupport()
|
||||
);
|
||||
request.setCrc32(crc32);
|
||||
|
||||
if (mVersion < 3) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(7).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_TRANSFER_RESPONSE);
|
||||
buf.put(session);
|
||||
buf.put((byte) 0x00);
|
||||
buf.putInt(0);
|
||||
|
||||
write("send file transfer response", buf.array());
|
||||
} else {
|
||||
// FIXME: Receive files on v3
|
||||
LOG.error("Receiving files on V3 is not implemented");
|
||||
return;
|
||||
}
|
||||
|
||||
mSessionRequests.put(session, request);
|
||||
}
|
||||
|
||||
private void handleFileTransferData(final byte[] payload) {
|
||||
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.get(); // Discard first byte
|
||||
final byte flags = buf.get();
|
||||
final boolean firstPacket = (flags == 1);
|
||||
final boolean lastPacket = (flags == 2);
|
||||
final byte session = buf.get();
|
||||
final byte index = buf.get();
|
||||
|
||||
if ((flags & 0x01) != 0) {
|
||||
buf.getInt(); // ?
|
||||
}
|
||||
|
||||
final short size = buf.getShort();
|
||||
|
||||
final FileTransferRequest request = mSessionRequests.get(session);
|
||||
if (request == null) {
|
||||
LOG.error("No request found for session {}", session);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index != request.index) {
|
||||
LOG.warn("Unexpected index {}, expected {}", index, request.index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstPacket && request.getProgress() != 0) {
|
||||
LOG.warn("Got first packet, but progress is {}", request.getProgress());
|
||||
return;
|
||||
}
|
||||
|
||||
buf.get(request.getBytes(), request.getProgress(), size);
|
||||
request.setIndex((byte) (index + 1));
|
||||
request.setProgress(request.getProgress() + size);
|
||||
|
||||
LOG.debug("Got data for session={}, progress={}/{}", session, request.getProgress(), request.getSize());
|
||||
|
||||
write("ack file data", new byte[]{CMD_DATA_ACK, session, 0x00});
|
||||
|
||||
if (lastPacket) {
|
||||
mSessionRequests.remove(session);
|
||||
|
||||
final byte[] data;
|
||||
if (request.isCompressed()) {
|
||||
data = decompress(request.getBytes());
|
||||
if (data == null) {
|
||||
LOG.error("Failed to decompress bytes for session={}", session);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
data = request.getBytes();
|
||||
}
|
||||
|
||||
final int checksum = CheckSums.getCRC32(data);
|
||||
if (checksum != request.getCrc32()) {
|
||||
LOG.warn("Checksum mismatch: expected {}, got {}", request.getCrc32(), checksum);
|
||||
return;
|
||||
}
|
||||
|
||||
request.getCallback().onFileDownloadFinish(request.getUrl(), request.getFilename(), data);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] compress(final byte[] data) {
|
||||
final Deflater deflater = new Deflater();
|
||||
deflater.setInput(data);
|
||||
deflater.finish();
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream(data.length);
|
||||
final byte[] buf = new byte[8096];
|
||||
int read;
|
||||
while ((read = deflater.deflate(buf)) > 0) {
|
||||
baos.write(buf, 0, read);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static byte[] decompress(final byte[] data) {
|
||||
final Inflater inflater = new Inflater();
|
||||
final byte[] output = new byte[data.length];
|
||||
inflater.setInput(data);
|
||||
try {
|
||||
inflater.inflate(output);
|
||||
} catch (final DataFormatException e) {
|
||||
LOG.error("Failed to decompress data", e);
|
||||
return null;
|
||||
} finally {
|
||||
inflater.end();
|
||||
}
|
||||
|
||||
return output;
|
||||
write(builder, new byte[]{AbstractFileTransferImpl.CMD_CAPABILITIES_REQUEST});
|
||||
}
|
||||
|
||||
public void sendFile(final String url, final String filename, final byte[] bytes, final boolean compress, final Callback callback) {
|
||||
if (mChunkSize < 0) {
|
||||
if (impl == null) {
|
||||
LOG.error("Service not initialized, refusing to send {}", url);
|
||||
callback.onFileUploadFinish(false);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Sending {} bytes to {} in {}", bytes.length, filename, url);
|
||||
|
||||
final FileTransferRequest request = new FileTransferRequest(
|
||||
url,
|
||||
filename,
|
||||
bytes,
|
||||
compress && mCompressedChunkSize > 0,
|
||||
compress && mCompressedChunkSize > 0 ? mCompressedChunkSize : mChunkSize,
|
||||
callback
|
||||
);
|
||||
|
||||
if (mVersion == 3 && !mSessionRequests.isEmpty()) {
|
||||
// FIXME non-zero session on v3
|
||||
LOG.error("File transfer v3 only supports single session, not sending file");
|
||||
callback.onFileUploadFinish(false);
|
||||
return;
|
||||
}
|
||||
|
||||
byte session = (byte) mSessionRequests.size();
|
||||
while (mSessionRequests.containsKey(session)) {
|
||||
session++;
|
||||
}
|
||||
|
||||
int payloadSize = 2 + url.length() + 1 + filename.length() + 1 + 4 + 4;
|
||||
if (mVersion == 3) {
|
||||
payloadSize += 2;
|
||||
if (compress) {
|
||||
payloadSize += 4;
|
||||
}
|
||||
}
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(payloadSize);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_TRANSFER_REQUEST);
|
||||
buf.put(session);
|
||||
buf.put(url.getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0x00);
|
||||
buf.put(filename.getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0x00);
|
||||
buf.putInt(bytes.length);
|
||||
buf.putInt(request.getCrc32());
|
||||
if (mVersion == 3) {
|
||||
buf.put((byte) (compress ? 1 : 0));
|
||||
if (compress) {
|
||||
buf.putInt(mCompressedChunkSize);
|
||||
}
|
||||
buf.put((byte) 0);
|
||||
}
|
||||
|
||||
write("send file upload request", buf.array());
|
||||
|
||||
mSessionRequests.put(session, request);
|
||||
impl.uploadFile(url, filename, bytes, compress, callback);
|
||||
}
|
||||
|
||||
private void sendNextQueuedData(final byte session) {
|
||||
final FileTransferRequest request = mSessionRequests.get(session);
|
||||
if (request == null) {
|
||||
LOG.error("No request found for session {}", session);
|
||||
public void onCharacteristicChanged(final BluetoothGattCharacteristic characteristic) {
|
||||
if (impl == null) {
|
||||
LOG.error("Service not initialized, ignoring characteristic change for {}", characteristic.getUuid());
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.getProgress() >= request.getSize()) {
|
||||
LOG.info("Finished sending {}", request.getUrl());
|
||||
onUploadFinish(session, true);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Sending file data for session={}, progress={}, index={}", session, request.getProgress(), request.getIndex());
|
||||
|
||||
if (mVersion < 3) {
|
||||
writeChunkV1(request, session);
|
||||
} else {
|
||||
if (session != 0) {
|
||||
// FIXME non-zero session on v3
|
||||
LOG.error("Sending non-zero session on v3 is not supported, got session={}", session);
|
||||
mSessionRequests.remove(session);
|
||||
return;
|
||||
}
|
||||
writeChunkV3(request);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeChunkV1(final FileTransferRequest request, final byte session) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(10 + request.getChunkSize());
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_DATA_SEND);
|
||||
|
||||
byte flags = 0;
|
||||
if (request.getProgress() == 0) {
|
||||
flags |= FLAG_FIRST_CHUNK;
|
||||
}
|
||||
if (request.getProgress() + request.getChunkSize() >= request.getSize()) {
|
||||
flags |= FLAG_LAST_CHUNK;
|
||||
}
|
||||
|
||||
buf.put(flags);
|
||||
buf.put(session);
|
||||
buf.put(request.getIndex());
|
||||
if ((flags & FLAG_FIRST_CHUNK) > 0) {
|
||||
buf.put((byte) 0x00); // ?
|
||||
buf.put((byte) 0x00); // ?
|
||||
buf.put((byte) 0x00); // ?
|
||||
buf.put((byte) 0x00); // ?
|
||||
}
|
||||
|
||||
final byte[] payload = ArrayUtils.subarray(
|
||||
request.getBytes(),
|
||||
request.getProgress(),
|
||||
request.getProgress() + request.getChunkSize()
|
||||
);
|
||||
|
||||
buf.putShort((short) payload.length);
|
||||
buf.put(payload);
|
||||
|
||||
request.setProgress(request.getProgress() + payload.length);
|
||||
request.setIndex((byte) (request.getIndex() + 1));
|
||||
request.getCallback().onFileUploadProgress(request.getProgress());
|
||||
|
||||
write("send file data", buf.array());
|
||||
}
|
||||
|
||||
private void writeChunkV3(final FileTransferRequest request) {
|
||||
final byte[] chunk = ArrayUtils.subarray(
|
||||
request.getBytes(),
|
||||
request.getProgress(),
|
||||
request.getProgress() + request.getChunkSize()
|
||||
);
|
||||
|
||||
byte flags = 0;
|
||||
if (request.getProgress() == 0) {
|
||||
flags |= FLAG_FIRST_CHUNK;
|
||||
}
|
||||
if (request.getProgress() + request.getChunkSize() >= request.getSize()) {
|
||||
flags |= FLAG_LAST_CHUNK;
|
||||
}
|
||||
|
||||
final int partSize = getSupport().getMTU() - 3;
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(chunk.length + 5);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_DATA_V3_SEND);
|
||||
buf.put(flags);
|
||||
buf.put(request.getIndex());
|
||||
buf.putShort((short) chunk.length);
|
||||
buf.put(chunk);
|
||||
|
||||
final byte[] payload = buf.array();
|
||||
|
||||
final TransactionBuilder builder = getSupport().createTransactionBuilder("send chunk v3");
|
||||
for (int i = 0; i < payload.length; i += partSize) {
|
||||
final byte[] part = ArrayUtils.subarray(payload, i, i + partSize);
|
||||
builder.write(
|
||||
getSupport().getCharacteristic(HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3),
|
||||
part
|
||||
);
|
||||
}
|
||||
builder.queue(getSupport().getQueue());
|
||||
|
||||
request.setProgress(request.getProgress() + chunk.length);
|
||||
request.setIndex((byte) (request.getIndex() + 1));
|
||||
request.getCallback().onFileUploadProgress(request.getProgress());
|
||||
}
|
||||
|
||||
private void onUploadFinish(final byte session, final boolean success) {
|
||||
final FileTransferRequest request = mSessionRequests.get(session);
|
||||
if (request == null) {
|
||||
LOG.error("No request found for session {}", session);
|
||||
return;
|
||||
}
|
||||
|
||||
mSessionRequests.remove(session);
|
||||
|
||||
request.getCallback().onFileUploadFinish(success);
|
||||
}
|
||||
|
||||
public void onCharacteristicChanged(final byte[] value) {
|
||||
if (value[0] != CMD_DATA_V3_ACK) {
|
||||
LOG.error("Got non-ack on file transfer characteristic");
|
||||
return;
|
||||
}
|
||||
|
||||
final byte session = (byte) 0; // FIXME non-zero session on v3
|
||||
final byte status = value[1];
|
||||
final byte chunkIndex = value[2];
|
||||
final byte unk1 = value[3]; // 1/2?
|
||||
|
||||
LOG.info(
|
||||
"Band acknowledged file transfer data: session={}, status={}, chunkIndex={}, unk1={}",
|
||||
session,
|
||||
status,
|
||||
chunkIndex,
|
||||
unk1
|
||||
);
|
||||
|
||||
final FileTransferRequest request = mSessionRequests.get(session);
|
||||
if (request == null) {
|
||||
LOG.error("No request found for v3 session {}", session);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status != 0) {
|
||||
LOG.error("Unexpected status from band, aborting session {}", session);
|
||||
onUploadFinish(session, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.getIndex() - 1 != chunkIndex) {
|
||||
LOG.error("Got ack for unexpected chunk index {}, expected {}", chunkIndex, request.getIndex() - 1);
|
||||
onUploadFinish(session, false);
|
||||
return;
|
||||
}
|
||||
|
||||
sendNextQueuedData(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class to keep track of ongoing file send requests and their progress.
|
||||
*/
|
||||
public static class FileTransferRequest {
|
||||
private final String url;
|
||||
private final String filename;
|
||||
private final byte[] bytes;
|
||||
private final boolean compressed;
|
||||
private final int chunkSize;
|
||||
private final Callback callback;
|
||||
private int progress = 0;
|
||||
private byte index = 0;
|
||||
private int crc32;
|
||||
|
||||
public FileTransferRequest(final String url, final String filename, final byte[] bytes, boolean compressed, int chunkSize, final Callback callback) {
|
||||
this.url = url;
|
||||
this.filename = filename;
|
||||
this.bytes = compressed ? compress(bytes) : bytes;
|
||||
this.compressed = compressed;
|
||||
this.chunkSize = chunkSize;
|
||||
this.callback = callback;
|
||||
this.crc32 = CheckSums.getCRC32(bytes);
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return bytes.length;
|
||||
}
|
||||
|
||||
public boolean isCompressed() {
|
||||
return compressed;
|
||||
}
|
||||
|
||||
public int getChunkSize() {
|
||||
return chunkSize;
|
||||
}
|
||||
|
||||
public Callback getCallback() {
|
||||
return callback;
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public void setProgress(final int progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public byte getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setIndex(final byte index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public int getCrc32() {
|
||||
return crc32;
|
||||
}
|
||||
|
||||
public void setCrc32(final int crc32) {
|
||||
this.crc32 = crc32;
|
||||
}
|
||||
impl.onCharacteristicChanged(characteristic);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.filetransfer;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.DataFormatException;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileTransferService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public abstract class AbstractFileTransferImpl {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractFileTransferImpl.class);
|
||||
|
||||
public static final byte CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
protected static final byte CMD_TRANSFER_REQUEST = 0x03;
|
||||
protected static final byte CMD_TRANSFER_RESPONSE = 0x04;
|
||||
protected static final byte CMD_DATA_SEND = 0x10;
|
||||
protected static final byte CMD_DATA_ACK = 0x11;
|
||||
protected static final byte CMD_DATA_V3_SEND = 0x12;
|
||||
protected static final byte CMD_DATA_V3_ACK = 0x13;
|
||||
|
||||
protected static final byte FLAG_FIRST_CHUNK = 0x01;
|
||||
protected static final byte FLAG_LAST_CHUNK = 0x02;
|
||||
protected static final byte FLAG_CRC = 0x04;
|
||||
|
||||
protected final ZeppOsFileTransferService mFileTransferService;
|
||||
protected final ZeppOsSupport mSupport;
|
||||
|
||||
protected int mVersion = -1;
|
||||
protected int mChunkSize = -1;
|
||||
protected int mCompressedChunkSize = -1;
|
||||
protected final List<String> supportedServices = new ArrayList<>();
|
||||
|
||||
public AbstractFileTransferImpl(final ZeppOsFileTransferService fileTransferService,
|
||||
final ZeppOsSupport support) {
|
||||
this.mFileTransferService = fileTransferService;
|
||||
this.mSupport = support;
|
||||
}
|
||||
|
||||
public void handlePayload(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case CMD_CAPABILITIES_RESPONSE:
|
||||
handleCapabilitiesResponse(payload);
|
||||
return;
|
||||
case CMD_TRANSFER_REQUEST:
|
||||
handleFileTransferRequest(payload);
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unexpected file transfer payload byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void uploadFile(final FileTransferRequest request);
|
||||
|
||||
public abstract void handleFileDownloadRequest(final byte session, final FileTransferRequest request);
|
||||
|
||||
public abstract void onCharacteristicChanged(final BluetoothGattCharacteristic characteristic);
|
||||
|
||||
public void uploadFile(final String url,
|
||||
final String filename,
|
||||
final byte[] bytes,
|
||||
final boolean compress,
|
||||
final ZeppOsFileTransferService.Callback callback) {
|
||||
LOG.info("Sending {} bytes to {} in {}", bytes.length, filename, url);
|
||||
|
||||
final FileTransferRequest request = new FileTransferRequest(
|
||||
url,
|
||||
filename,
|
||||
bytes.length,
|
||||
compress && mCompressedChunkSize > 0 ? compress(bytes) : bytes,
|
||||
compress && mCompressedChunkSize > 0,
|
||||
CheckSums.getCRC32(bytes),
|
||||
compress && mCompressedChunkSize > 0 ? mCompressedChunkSize : mChunkSize,
|
||||
callback
|
||||
);
|
||||
|
||||
uploadFile(request);
|
||||
}
|
||||
|
||||
private void handleCapabilitiesResponse(final byte[] payload) {
|
||||
supportedServices.clear();
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.wrap(payload)
|
||||
.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.get(); // discard command byte
|
||||
|
||||
mVersion = buf.get() & 0xff;
|
||||
mChunkSize = buf.getShort();
|
||||
if (mVersion >= 3) {
|
||||
mCompressedChunkSize = buf.getInt();
|
||||
final int numServices = buf.getShort();
|
||||
for (int i = 0; i < numServices; i++) {
|
||||
supportedServices.add(StringUtils.untilNullTerminator(buf));
|
||||
}
|
||||
// TODO: 3 unknown bytes for v3
|
||||
|
||||
final TransactionBuilder builder = mSupport.createTransactionBuilder("enable file transfer v3 notifications");
|
||||
builder.notify(mSupport.getCharacteristic(HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3_SEND), true);
|
||||
builder.notify(mSupport.getCharacteristic(HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3_RECEIVE), true);
|
||||
builder.queue(mSupport.getQueue());
|
||||
}
|
||||
|
||||
LOG.info(
|
||||
"Got file transfer service: version={}, chunkSize={}, compressedChunkSize={}, supportedServices=[{}]",
|
||||
mVersion,
|
||||
mChunkSize,
|
||||
mCompressedChunkSize,
|
||||
String.join(",", supportedServices)
|
||||
);
|
||||
}
|
||||
|
||||
private void handleFileTransferRequest(final byte[] payload) {
|
||||
// File transfer request initialized from watch
|
||||
final ByteBuffer buf = ByteBuffer.wrap(payload)
|
||||
.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.get(); // discard command byte
|
||||
|
||||
final byte session = buf.get();
|
||||
final String url = StringUtils.untilNullTerminator(buf);
|
||||
if (url == null) {
|
||||
LOG.error("Unable to parse url from transfer request");
|
||||
return;
|
||||
}
|
||||
final String filename = StringUtils.untilNullTerminator(buf);
|
||||
if (filename == null) {
|
||||
LOG.error("Unable to parse filename from transfer request");
|
||||
return;
|
||||
}
|
||||
final int length = buf.getInt();
|
||||
final int crc32 = buf.getInt();
|
||||
|
||||
final boolean compressed;
|
||||
if (buf.hasRemaining()) {
|
||||
final byte compressedByte = buf.get();
|
||||
final Boolean compressedBoolean = booleanFromByte(compressedByte);
|
||||
if (compressedBoolean == null) {
|
||||
LOG.warn("Unknown compression type {}", String.format("0x%02x", compressedByte));
|
||||
return;
|
||||
}
|
||||
compressed = compressedBoolean;
|
||||
} else {
|
||||
compressed = false;
|
||||
}
|
||||
|
||||
LOG.info(
|
||||
"Got transfer request: session={}, url={}, filename={}, length={}, compressed={}",
|
||||
session,
|
||||
url,
|
||||
filename,
|
||||
length,
|
||||
compressed
|
||||
);
|
||||
|
||||
final FileTransferRequest request = new FileTransferRequest(
|
||||
url,
|
||||
filename,
|
||||
length,
|
||||
new byte[length],
|
||||
compressed,
|
||||
crc32,
|
||||
compressed ? mCompressedChunkSize : mChunkSize,
|
||||
mSupport
|
||||
);
|
||||
|
||||
handleFileDownloadRequest(session, request);
|
||||
}
|
||||
|
||||
public static byte[] compress(final byte[] data) {
|
||||
final Deflater deflater = new Deflater();
|
||||
deflater.setInput(data);
|
||||
deflater.finish();
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream(data.length);
|
||||
final byte[] buf = new byte[8096];
|
||||
int read;
|
||||
while ((read = deflater.deflate(buf)) > 0) {
|
||||
baos.write(buf, 0, read);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static byte[] decompress(final byte[] data) {
|
||||
final Inflater inflater = new Inflater();
|
||||
final byte[] output = new byte[data.length];
|
||||
inflater.setInput(data);
|
||||
try {
|
||||
inflater.inflate(output);
|
||||
} catch (final DataFormatException e) {
|
||||
LOG.error("Failed to decompress data", e);
|
||||
return null;
|
||||
} finally {
|
||||
inflater.end();
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected static Boolean booleanFromByte(final byte b) {
|
||||
switch (b) {
|
||||
case 0x00:
|
||||
return false;
|
||||
case 0x01:
|
||||
return true;
|
||||
default:
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
/* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.filetransfer;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileTransferService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
|
||||
public class FileTransferImplV2 extends AbstractFileTransferImpl {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileTransferImplV2.class);
|
||||
|
||||
private final Map<Byte, FileTransferRequest> mSessionRequests = new HashMap<>();
|
||||
|
||||
public FileTransferImplV2(final ZeppOsFileTransferService fileTransferService,
|
||||
final ZeppOsSupport support) {
|
||||
super(fileTransferService, support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(final byte[] payload) {
|
||||
byte session;
|
||||
byte status;
|
||||
|
||||
switch (payload[0]) {
|
||||
case CMD_TRANSFER_RESPONSE:
|
||||
session = payload[1];
|
||||
status = payload[2];
|
||||
final int existingProgress = BLETypeConversions.toUint32(payload, 3);
|
||||
LOG.info("Band acknowledged file transfer request: session={}, status={}, existingProgress={}", session, status, existingProgress);
|
||||
if (status != 0) {
|
||||
LOG.error("Unexpected status from band for session {}, aborting", session);
|
||||
onUploadFinish(session, false);
|
||||
return;
|
||||
}
|
||||
if (existingProgress != 0) {
|
||||
LOG.info("Updating existing progress for session {} to {}", session, existingProgress);
|
||||
final FileTransferRequest request = mSessionRequests.get(session);
|
||||
if (request == null) {
|
||||
LOG.error("No request found for session {}", session);
|
||||
return;
|
||||
}
|
||||
request.setProgress(existingProgress);
|
||||
}
|
||||
sendNextChunk(session);
|
||||
return;
|
||||
case CMD_DATA_SEND:
|
||||
handleFileTransferData(payload);
|
||||
return;
|
||||
case CMD_DATA_ACK:
|
||||
session = payload[1];
|
||||
status = payload[2];
|
||||
LOG.info("Band acknowledged file transfer data: session={}, status={}", session, status);
|
||||
if (status != 0) {
|
||||
LOG.error("Unexpected status from band, aborting session {}", session);
|
||||
onUploadFinish(session, false);
|
||||
return;
|
||||
}
|
||||
sendNextChunk(session);
|
||||
return;
|
||||
default:
|
||||
super.handlePayload(payload);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadFile(final FileTransferRequest request) {
|
||||
if (request.isCompressed()) {
|
||||
throw new IllegalArgumentException("V1/V2 does not support compressed transfers");
|
||||
}
|
||||
|
||||
byte session = (byte) mSessionRequests.size();
|
||||
while (mSessionRequests.containsKey(session)) {
|
||||
session++;
|
||||
}
|
||||
|
||||
final int payloadSize = 2 + request.getUrl().length() + 1 + request.getFilename().length() + 1 + 4 + 4;
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(payloadSize);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_TRANSFER_REQUEST);
|
||||
buf.put(session);
|
||||
buf.put(request.getUrl().getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0x00);
|
||||
buf.put(request.getFilename().getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0x00);
|
||||
buf.putInt(request.getRawLength());
|
||||
buf.putInt(request.getCrc32());
|
||||
|
||||
mFileTransferService.write("send file upload request", buf.array());
|
||||
|
||||
mSessionRequests.put(session, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFileDownloadRequest(final byte session, final FileTransferRequest request) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(7).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_TRANSFER_RESPONSE);
|
||||
buf.put(session);
|
||||
buf.put((byte) 0x00);
|
||||
buf.putInt(0);
|
||||
|
||||
mFileTransferService.write("send file transfer response", buf.array());
|
||||
|
||||
mSessionRequests.put(session, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(final BluetoothGattCharacteristic characteristic) {
|
||||
LOG.error("Unknown characteristic changed: {}", characteristic.getUuid());
|
||||
}
|
||||
|
||||
private void handleFileTransferData(final byte[] payload) {
|
||||
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.get(); // Discard first byte
|
||||
|
||||
final byte flags = buf.get();
|
||||
final boolean firstChunk = (flags & FLAG_FIRST_CHUNK) != 0;
|
||||
final boolean lastChunk = (flags & FLAG_LAST_CHUNK) != 0;
|
||||
final byte session = buf.get();
|
||||
final byte index = buf.get();
|
||||
|
||||
if (firstChunk) {
|
||||
buf.getInt(); // ?
|
||||
}
|
||||
|
||||
final short size = buf.getShort();
|
||||
|
||||
final FileTransferRequest request = mSessionRequests.get(session);
|
||||
if (request == null) {
|
||||
LOG.error("No download request found for V1 session {}", session);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index != request.getIndex()) {
|
||||
LOG.warn("Unexpected index {}, expected {}", index, request.getIndex());
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstChunk && request.getProgress() != 0) {
|
||||
LOG.warn("Got first packet, but progress is {}", request.getProgress());
|
||||
return;
|
||||
}
|
||||
|
||||
buf.get(request.getBytes(), request.getProgress(), size);
|
||||
request.setIndex((byte) (index + 1));
|
||||
request.setProgress(request.getProgress() + size);
|
||||
|
||||
LOG.debug("Got data for session={}, progress={}/{}", session, request.getProgress(), request.getSize());
|
||||
|
||||
mFileTransferService.write("ack file data", new byte[]{CMD_DATA_ACK, session, 0x00});
|
||||
|
||||
if (lastChunk) {
|
||||
mSessionRequests.remove(session);
|
||||
|
||||
final byte[] data;
|
||||
if (request.isCompressed()) {
|
||||
data = decompress(request.getBytes());
|
||||
if (data == null) {
|
||||
LOG.error("Failed to decompress bytes for session={}", session);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
data = request.getBytes();
|
||||
}
|
||||
|
||||
final int checksum = CheckSums.getCRC32(data);
|
||||
if (checksum != request.getCrc32()) {
|
||||
LOG.warn("Checksum mismatch: expected {}, got {}", request.getCrc32(), checksum);
|
||||
return;
|
||||
}
|
||||
|
||||
request.getCallback().onFileDownloadFinish(request.getUrl(), request.getFilename(), data);
|
||||
}
|
||||
}
|
||||
|
||||
private void onUploadFinish(final byte session, final boolean success) {
|
||||
final FileTransferRequest request = mSessionRequests.get(session);
|
||||
if (request == null) {
|
||||
LOG.error("No request found for session {} to finish upload", session);
|
||||
return;
|
||||
}
|
||||
|
||||
mSessionRequests.remove(session);
|
||||
|
||||
request.getCallback().onFileUploadFinish(success);
|
||||
}
|
||||
|
||||
private void sendNextChunk(final byte session) {
|
||||
final FileTransferRequest request = mSessionRequests.get(session);
|
||||
if (request == null) {
|
||||
LOG.error("No request found for session {} to send next chunk", session);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.getProgress() >= request.getSize()) {
|
||||
LOG.info("Finished sending {}", request.getUrl());
|
||||
onUploadFinish(session, true);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Sending file data for session={}, progress={}, index={}", session, request.getProgress(), request.getIndex());
|
||||
|
||||
writeChunk(session, request);
|
||||
}
|
||||
|
||||
private void writeChunk(final byte session, final FileTransferRequest request) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(10 + request.getChunkSize());
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_DATA_SEND);
|
||||
|
||||
byte flags = 0;
|
||||
if (request.getProgress() == 0) {
|
||||
flags |= FLAG_FIRST_CHUNK;
|
||||
}
|
||||
if (request.getProgress() + request.getChunkSize() >= request.getSize()) {
|
||||
flags |= FLAG_LAST_CHUNK;
|
||||
}
|
||||
|
||||
buf.put(flags);
|
||||
buf.put(session);
|
||||
buf.put(request.getIndex());
|
||||
if ((flags & FLAG_FIRST_CHUNK) > 0) {
|
||||
buf.put((byte) 0x00); // ?
|
||||
buf.put((byte) 0x00); // ?
|
||||
buf.put((byte) 0x00); // ?
|
||||
buf.put((byte) 0x00); // ?
|
||||
}
|
||||
|
||||
final byte[] payload = ArrayUtils.subarray(
|
||||
request.getBytes(),
|
||||
request.getProgress(),
|
||||
request.getProgress() + request.getChunkSize()
|
||||
);
|
||||
|
||||
buf.putShort((short) payload.length);
|
||||
buf.put(payload);
|
||||
|
||||
request.setProgress(request.getProgress() + payload.length);
|
||||
request.setIndex((byte) (request.getIndex() + 1));
|
||||
request.getCallback().onFileUploadProgress(request.getProgress());
|
||||
|
||||
mFileTransferService.write("send file data", buf.array());
|
||||
}
|
||||
}
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
/* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.filetransfer;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
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.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileTransferService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
|
||||
public class FileTransferImplV3 extends AbstractFileTransferImpl {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileTransferImplV3.class);
|
||||
|
||||
private static final byte CMD_DATA_V3_SEND = 0x12;
|
||||
private static final byte CMD_DATA_V3_ACK = 0x13;
|
||||
|
||||
private static final long TRANSFER_TIMEOUT_THRESHOLD_MILLIS = 5_000L;
|
||||
|
||||
private byte nextSession = 0;
|
||||
|
||||
private FileTransferRequest currentSendRequest;
|
||||
private byte currentSendSession = -1;
|
||||
private long lastSendActivityMillis = -1;
|
||||
|
||||
private FileTransferRequest currentReceiveRequest;
|
||||
private byte currentReceiveSession = -1;
|
||||
private long lastReceiveActivityMillis = -1;
|
||||
|
||||
private int currentReceiveChunkSize = -1;
|
||||
private boolean currentReceiveChunkIsLast = false;
|
||||
private final ByteArrayOutputStream receivePacketBuffer = new ByteArrayOutputStream();
|
||||
|
||||
public FileTransferImplV3(final ZeppOsFileTransferService fileTransferService,
|
||||
final ZeppOsSupport support) {
|
||||
super(fileTransferService, support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case CMD_TRANSFER_RESPONSE:
|
||||
final byte session = payload[1];
|
||||
final byte status = payload[2];
|
||||
final int existingProgress = BLETypeConversions.toUint32(payload, 3);
|
||||
LOG.info("Band acknowledged file transfer request: session={}, status={}, existingProgress={}", session, status, existingProgress);
|
||||
if (currentSendRequest == null) {
|
||||
LOG.error("No ongoing send request found");
|
||||
return;
|
||||
}
|
||||
if (status != 0) {
|
||||
LOG.error("Unexpected status from band for session {}, aborting", session);
|
||||
onUploadFinish(false);
|
||||
resetSend();
|
||||
return;
|
||||
}
|
||||
if (currentSendSession != session) {
|
||||
LOG.error("Unexpected send session from band {}, expected {}, aborting", session, currentSendSession);
|
||||
onUploadFinish(false);
|
||||
resetSend();
|
||||
return;
|
||||
}
|
||||
if (existingProgress != 0) {
|
||||
LOG.info("Updating existing progress for session {} to {}", session, existingProgress);
|
||||
currentSendRequest.setProgress(existingProgress);
|
||||
}
|
||||
sendNextQueuedData();
|
||||
return;
|
||||
default:
|
||||
super.handlePayload(payload);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadFile(final FileTransferRequest request) {
|
||||
if (currentSendRequest != null) {
|
||||
if (System.currentTimeMillis() - lastSendActivityMillis < TRANSFER_TIMEOUT_THRESHOLD_MILLIS) {
|
||||
LOG.warn("Already uploading {}", currentSendRequest.getFilename());
|
||||
request.getCallback().onFileUploadFinish(false);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.warn("Timing out existing upload request for {}", currentSendRequest.getFilename());
|
||||
currentSendRequest.getCallback().onFileUploadFinish(false);
|
||||
resetSend();
|
||||
}
|
||||
|
||||
final byte session = nextSession++;
|
||||
|
||||
int payloadSize = 2 +
|
||||
request.getUrl().getBytes(StandardCharsets.UTF_8).length + 1 +
|
||||
request.getFilename().getBytes(StandardCharsets.UTF_8).length + 1 +
|
||||
4 + 4 + 2;
|
||||
if (request.isCompressed()) {
|
||||
payloadSize += 4;
|
||||
}
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(payloadSize);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_TRANSFER_REQUEST);
|
||||
buf.put(session);
|
||||
buf.put(request.getUrl().getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0x00);
|
||||
buf.put(request.getFilename().getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0x00);
|
||||
buf.putInt(request.getRawLength());
|
||||
buf.putInt(request.getCrc32());
|
||||
buf.put((byte) (request.isCompressed() ? 1 : 0));
|
||||
if (request.isCompressed()) {
|
||||
buf.putInt(mCompressedChunkSize);
|
||||
}
|
||||
buf.put((byte) 0);
|
||||
|
||||
mFileTransferService.write("send v3 file upload request", buf.array());
|
||||
|
||||
currentSendSession = session;
|
||||
currentSendRequest = request;
|
||||
lastSendActivityMillis = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFileDownloadRequest(final byte session, final FileTransferRequest request) {
|
||||
if (currentReceiveRequest != null) {
|
||||
if (System.currentTimeMillis() - lastReceiveActivityMillis < TRANSFER_TIMEOUT_THRESHOLD_MILLIS) {
|
||||
LOG.warn("Already downloading {}", currentReceiveRequest.getFilename());
|
||||
// TODO how to send nack?
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.warn("Timing out existing download request for {}", currentReceiveRequest.getFilename());
|
||||
currentReceiveRequest.getCallback().onFileUploadFinish(false);
|
||||
|
||||
resetReceive();
|
||||
}
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_TRANSFER_RESPONSE);
|
||||
buf.put(session);
|
||||
buf.put((byte) 0x00);
|
||||
buf.putInt(0);
|
||||
buf.put((byte) 0x01);
|
||||
|
||||
mFileTransferService.write("send file transfer response", buf.array());
|
||||
|
||||
currentReceiveChunkIsLast = false;
|
||||
currentReceiveChunkSize = -1;
|
||||
receivePacketBuffer.reset();
|
||||
currentReceiveSession = session;
|
||||
currentReceiveRequest = request;
|
||||
lastReceiveActivityMillis = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(final BluetoothGattCharacteristic characteristic) {
|
||||
final UUID characteristicUUID = characteristic.getUuid();
|
||||
final byte[] value = characteristic.getValue();
|
||||
|
||||
if (HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3_RECEIVE.equals(characteristicUUID)) {
|
||||
handleFileReceiveData(value);
|
||||
} else if (HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3_SEND.equals(characteristicUUID)) {
|
||||
if (value[0] != CMD_DATA_V3_ACK) {
|
||||
LOG.error("Got non-ack on file send characteristic");
|
||||
return;
|
||||
}
|
||||
|
||||
final byte status = value[1];
|
||||
final byte chunkIndex = value[2];
|
||||
final byte unk1 = value[3]; // 1/2?
|
||||
|
||||
LOG.info(
|
||||
"Band acknowledged file transfer data: session={}, status={}, chunkIndex={}, unk1={}",
|
||||
currentSendSession,
|
||||
status,
|
||||
chunkIndex,
|
||||
unk1
|
||||
);
|
||||
|
||||
if (currentSendRequest == null) {
|
||||
LOG.error("Got ack for file send, but we are not uploading");
|
||||
return;
|
||||
}
|
||||
|
||||
if (status != 0) {
|
||||
LOG.error("Unexpected status from band, aborting session {}", currentSendSession);
|
||||
onUploadFinish(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSendRequest.getIndex() - 1 != chunkIndex) {
|
||||
LOG.error("Got ack for unexpected chunk index {}, expected {}", chunkIndex, currentSendRequest.getIndex() - 1);
|
||||
onUploadFinish(false);
|
||||
return;
|
||||
}
|
||||
|
||||
sendNextQueuedData();
|
||||
} else {
|
||||
LOG.warn("Unknown characteristic changed: {}", characteristicUUID);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeChunk(final FileTransferRequest request) {
|
||||
final byte[] chunk = ArrayUtils.subarray(
|
||||
request.getBytes(),
|
||||
request.getProgress(),
|
||||
request.getProgress() + request.getChunkSize()
|
||||
);
|
||||
|
||||
byte flags = 0;
|
||||
if (request.getProgress() == 0) {
|
||||
flags |= FLAG_FIRST_CHUNK;
|
||||
}
|
||||
if (request.getProgress() + request.getChunkSize() >= request.getSize()) {
|
||||
flags |= FLAG_LAST_CHUNK;
|
||||
}
|
||||
|
||||
final int partSize = mSupport.getMTU() - 3;
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(chunk.length + 5);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_DATA_V3_SEND);
|
||||
buf.put(flags);
|
||||
buf.put(request.getIndex());
|
||||
buf.putShort((short) chunk.length);
|
||||
buf.put(chunk);
|
||||
|
||||
final byte[] payload = buf.array();
|
||||
|
||||
final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunk v3");
|
||||
for (int i = 0; i < payload.length; i += partSize) {
|
||||
final byte[] part = ArrayUtils.subarray(payload, i, i + partSize);
|
||||
builder.write(
|
||||
mSupport.getCharacteristic(HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3_SEND),
|
||||
part
|
||||
);
|
||||
}
|
||||
builder.queue(mSupport.getQueue());
|
||||
|
||||
request.setProgress(request.getProgress() + chunk.length);
|
||||
request.setIndex((byte) (request.getIndex() + 1));
|
||||
request.getCallback().onFileUploadProgress(request.getProgress());
|
||||
}
|
||||
|
||||
private void handleFileReceiveData(final byte[] payload) {
|
||||
if (currentReceiveRequest == null) {
|
||||
LOG.error("No receive request found for V3 session {}", currentReceiveSession);
|
||||
return;
|
||||
}
|
||||
|
||||
lastReceiveActivityMillis = System.currentTimeMillis();
|
||||
|
||||
if (currentReceiveChunkSize > 0) {
|
||||
// We are currently receiving a chunk
|
||||
try {
|
||||
receivePacketBuffer.write(payload);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to write packet to chunk buffer", e);
|
||||
resetReceive();
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug(
|
||||
"Received {} ({}/{}) bytes for chunk at index {}",
|
||||
payload.length,
|
||||
receivePacketBuffer.size(),
|
||||
currentReceiveChunkSize,
|
||||
currentReceiveRequest.getIndex()
|
||||
);
|
||||
} else {
|
||||
// Start of a chunk
|
||||
receivePacketBuffer.reset();
|
||||
|
||||
LOG.debug("Got start of chunk - {} bytes", payload.length);
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.get(); // Discard first byte
|
||||
final byte flags = buf.get();
|
||||
final boolean firstChunk = (flags == FLAG_FIRST_CHUNK);
|
||||
currentReceiveChunkIsLast = (flags == FLAG_LAST_CHUNK);
|
||||
final byte index = buf.get();
|
||||
currentReceiveChunkSize = buf.getShort();
|
||||
|
||||
if (index != currentReceiveRequest.getIndex()) {
|
||||
LOG.warn("Unexpected V3 index {}, expected {}", index, currentReceiveRequest.getIndex());
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstChunk && currentReceiveRequest.getProgress() != 0) {
|
||||
LOG.warn("Got first V3 packet, but progress is {}", currentReceiveRequest.getProgress());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Starting a new chunk of {} bytes at index {}", currentReceiveChunkSize, currentReceiveRequest.getIndex());
|
||||
|
||||
receivePacketBuffer.write(payload, buf.position(), buf.limit() - buf.position());
|
||||
}
|
||||
|
||||
if (receivePacketBuffer.size() >= currentReceiveChunkSize) {
|
||||
// Finished a chunk
|
||||
|
||||
System.arraycopy(
|
||||
receivePacketBuffer.toByteArray(),
|
||||
0,
|
||||
currentReceiveRequest.getBytes(),
|
||||
currentReceiveRequest.getProgress(),
|
||||
currentReceiveChunkSize
|
||||
);
|
||||
|
||||
currentReceiveRequest.setIndex((byte) (currentReceiveRequest.getIndex() + 1));
|
||||
currentReceiveRequest.setProgress(currentReceiveRequest.getProgress() + currentReceiveChunkSize);
|
||||
|
||||
LOG.debug("Got V3 data for session={}, progress={}/{}", currentReceiveSession, currentReceiveRequest.getProgress(), currentReceiveRequest.getSize());
|
||||
|
||||
final TransactionBuilder builder = mSupport.createTransactionBuilder("send ack v3 file data");
|
||||
builder.write(
|
||||
mSupport.getCharacteristic(HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3_RECEIVE),
|
||||
new byte[]{
|
||||
CMD_DATA_V3_ACK,
|
||||
0x00,
|
||||
(byte) (currentReceiveRequest.getIndex() - 1),
|
||||
(byte) 0x01,
|
||||
(byte) 0x00,
|
||||
(byte) 0x00,
|
||||
(byte) 0x00
|
||||
}
|
||||
);
|
||||
builder.queue(mSupport.getQueue());
|
||||
|
||||
if (currentReceiveChunkIsLast) {
|
||||
final byte[] data;
|
||||
if (currentReceiveRequest.isCompressed()) {
|
||||
data = decompress(currentReceiveRequest.getBytes());
|
||||
if (data == null) {
|
||||
LOG.error("Failed to decompress V3 bytes for {}", currentReceiveRequest.getFilename());
|
||||
resetReceive();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
data = currentReceiveRequest.getBytes();
|
||||
}
|
||||
|
||||
final int checksum = CheckSums.getCRC32(data);
|
||||
if (checksum != currentReceiveRequest.getCrc32()) {
|
||||
LOG.warn("V3 Checksum mismatch: expected {}, got {}", currentReceiveRequest.getCrc32(), checksum);
|
||||
resetReceive();
|
||||
return;
|
||||
}
|
||||
|
||||
currentReceiveRequest.getCallback().onFileDownloadFinish(
|
||||
currentReceiveRequest.getUrl(),
|
||||
currentReceiveRequest.getFilename(),
|
||||
data
|
||||
);
|
||||
|
||||
resetReceive();
|
||||
}
|
||||
|
||||
currentReceiveChunkSize = -1;
|
||||
receivePacketBuffer.reset();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendNextQueuedData() {
|
||||
if (currentSendRequest == null) {
|
||||
LOG.error("No ongoing V3 send request found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSendRequest.getProgress() >= currentSendRequest.getSize()) {
|
||||
LOG.info("Finished sending {}", currentSendRequest.getUrl());
|
||||
onUploadFinish(true);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug(
|
||||
"Sending file data for session={}, progress={}, index={}",
|
||||
currentSendSession,
|
||||
currentSendRequest.getProgress(),
|
||||
currentSendRequest.getIndex()
|
||||
);
|
||||
|
||||
lastSendActivityMillis = System.currentTimeMillis();
|
||||
|
||||
writeChunk(currentSendRequest);
|
||||
}
|
||||
|
||||
private void onUploadFinish(final boolean success) {
|
||||
if (currentSendRequest == null) {
|
||||
LOG.error("No request found for session {} to finish upload", currentSendSession);
|
||||
return;
|
||||
}
|
||||
|
||||
currentSendRequest.getCallback().onFileUploadFinish(success);
|
||||
|
||||
resetSend();
|
||||
}
|
||||
|
||||
private void resetSend() {
|
||||
currentSendSession = -1;
|
||||
currentSendRequest = null;
|
||||
lastSendActivityMillis = -1;
|
||||
}
|
||||
|
||||
private void resetReceive() {
|
||||
currentReceiveSession = -1;
|
||||
currentReceiveRequest = null;
|
||||
lastReceiveActivityMillis = -1;
|
||||
currentReceiveChunkIsLast = false;
|
||||
currentReceiveChunkSize = -1;
|
||||
receivePacketBuffer.reset();
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.filetransfer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileTransferService;
|
||||
|
||||
/**
|
||||
* Wrapper class to keep track of ongoing file send requests and their progress.
|
||||
*/
|
||||
public class FileTransferRequest {
|
||||
private final String url;
|
||||
private final String filename;
|
||||
private final int rawLength;
|
||||
private final byte[] bytes;
|
||||
private final boolean compressed;
|
||||
private final int crc32;
|
||||
private final int chunkSize;
|
||||
private final ZeppOsFileTransferService.Callback callback;
|
||||
|
||||
private int progress = 0;
|
||||
private byte index = 0;
|
||||
|
||||
public FileTransferRequest(final String url,
|
||||
final String filename,
|
||||
final int rawLength,
|
||||
final byte[] bytes,
|
||||
final boolean compressed,
|
||||
final int crc32,
|
||||
final int chunkSize,
|
||||
final ZeppOsFileTransferService.Callback callback) {
|
||||
this.url = url;
|
||||
this.filename = filename;
|
||||
this.rawLength = rawLength;
|
||||
this.bytes = bytes;
|
||||
this.compressed = compressed;
|
||||
this.crc32 = crc32;
|
||||
this.chunkSize = chunkSize;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public int getRawLength() {
|
||||
return rawLength;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return bytes.length;
|
||||
}
|
||||
|
||||
public boolean isCompressed() {
|
||||
return compressed;
|
||||
}
|
||||
|
||||
public int getCrc32() {
|
||||
return crc32;
|
||||
}
|
||||
|
||||
public int getChunkSize() {
|
||||
return chunkSize;
|
||||
}
|
||||
|
||||
public ZeppOsFileTransferService.Callback getCallback() {
|
||||
return callback;
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public void setProgress(final int progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public byte getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setIndex(final byte index) {
|
||||
this.index = index;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user