diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java index e66520c76..910a50395 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java @@ -70,8 +70,6 @@ public class XiaomiCharacteristic { private XiaomiChannelHandler channelHandler = null; - private SendCallback callback; - public XiaomiCharacteristic(final XiaomiBleSupport support, final BluetoothGattCharacteristic bluetoothGattCharacteristic, @Nullable final XiaomiAuthService authService) { @@ -90,10 +88,6 @@ public class XiaomiCharacteristic { this.channelHandler = handler; } - public void setCallback(final SendCallback callback) { - this.callback = callback; - } - public void setEncrypted(final boolean encrypted) { this.isEncrypted = encrypted; } @@ -113,11 +107,29 @@ public class XiaomiCharacteristic { this.currentPayload = null; } + /** + * Write bytes to this characteristic, encrypting and splitting it into chunks if necessary. + * Callback will be notified when a (n)ack has been received by the remote device. + */ + public void write(final String taskName, final byte[] value, final SendCallback callback) { + write(null, new Payload(taskName, value, callback)); + } + /** * Write bytes to this characteristic, encrypting and splitting it into chunks if necessary. */ public void write(final String taskName, final byte[] value) { - write(null, new Payload(taskName, value)); + write(taskName, value, null); + } + + /** + * Write bytes to this characteristic, encrypting and splitting it into chunks if necessary. Uses + * the provided builder if we need to schedule something, otherwise it will be queued as other + * commands. The callback will be notified when a (n)ack has been received from the remote + * device in response to the payload being sent. + */ + public void write(final TransactionBuilder builder, final byte[] value, final SendCallback callback) { + write(builder, new Payload(builder.getTaskName(), value, callback)); } /** @@ -125,7 +137,7 @@ public class XiaomiCharacteristic { * the provided if we need to schedule something, otherwise it will be queued as other commands. */ public void write(final TransactionBuilder builder, final byte[] value) { - write(builder, new Payload(builder.getTaskName(), value)); + write(builder, value, null); } private void write(final TransactionBuilder builder, final Payload payload) { @@ -134,17 +146,6 @@ public class XiaomiCharacteristic { } public void onCharacteristicChanged(final byte[] value) { - if (Arrays.equals(value, PAYLOAD_ACK)) { - LOG.debug("Got ack"); - currentPayload = null; - waitingAck = false; - if (callback != null) { - callback.onSend(payloadQueue.size()); - } - sendNext(null); - return; - } - final ByteBuffer buf = ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN); final int chunk = buf.getShort(); @@ -201,11 +202,11 @@ public class XiaomiCharacteristic { switch (subtype) { case 0: LOG.debug("Got chunked ack end"); + if (currentPayload != null && currentPayload.getCallback() != null) { + currentPayload.getCallback().onSend(); + } currentPayload = null; sendingChunked = false; - if (callback != null) { - callback.onSend(payloadQueue.size()); - } sendNext(null); return; case 1: @@ -226,17 +227,16 @@ public class XiaomiCharacteristic { return; case 2: LOG.warn("Got chunked nack for {}", currentPayload.getTaskName()); + if (currentPayload != null && currentPayload.getCallback() != null) { + currentPayload.getCallback().onNack(); + } currentPayload = null; sendingChunked = false; - if (callback != null) { - callback.onSend(payloadQueue.size()); - } sendNext(null); return; } LOG.warn("Unknown chunked ack subtype {} for {}", subtype, currentPayload.getTaskName()); - return; case 2: // Single command @@ -261,8 +261,28 @@ public class XiaomiCharacteristic { return; case 3: // ack - LOG.debug("Got ack"); + final byte result = buf.get(); + + if (result == 0) { + LOG.debug("Got ack for {}", currentPayload.getTaskName()); + + if (currentPayload != null && currentPayload.getCallback() != null) { + currentPayload.getCallback().onSend(); + } + } else { + LOG.warn("Got single cmd NACK ({}) for {}", result, currentPayload.getTaskName()); + + if (currentPayload != null && currentPayload.getCallback() != null) { + currentPayload.getCallback().onNack(); + } + } + currentPayload = null; + waitingAck = false; + sendNext(null); + return; } + + LOG.warn("Unhandled command type {}", type); } } @@ -375,10 +395,16 @@ public class XiaomiCharacteristic { // Bytes that will actually be sent (might be encrypted) private byte[] bytesToSend; + private final SendCallback callback; - public Payload(final String taskName, final byte[] bytes) { + public Payload(final String taskName, final byte[] bytes, final SendCallback callback) { this.taskName = taskName; this.bytes = bytes; + this.callback = callback; + } + + public Payload(final String taskName, final byte[] bytes) { + this(taskName, bytes, null); } public String getTaskName() { @@ -392,9 +418,11 @@ public class XiaomiCharacteristic { public byte[] getBytesToSend() { return bytesToSend != null ? bytesToSend : bytes; } + public SendCallback getCallback() { return this.callback; } } public interface SendCallback { - void onSend(int remaining); + void onSend(); + void onNack(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java index 5695ced16..a0190610d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java @@ -65,8 +65,7 @@ public class XiaomiDataUploadService extends AbstractXiaomiService { if (dataUploadAck.getUnknown2() != 0 || dataUploadAck.getResumePosition() != 0) { LOG.warn("Unexpected response"); - this.currentType = 0; - this.currentBytes = null; + onUploadFinish(false); return; } @@ -76,6 +75,7 @@ public class XiaomiDataUploadService extends AbstractXiaomiService { chunkSize = 2048; } + LOG.debug("Using chunk size of {} bytes", chunkSize); doUpload(currentType, currentBytes); return; } @@ -143,38 +143,35 @@ public class XiaomiDataUploadService extends AbstractXiaomiService { final int partSize = chunkSize - 4; // 2 + 2 at beginning of each for total and progress final int totalParts = (int) Math.ceil(payload.length / (float) partSize); - characteristic.setCallback(remainingParts -> { - final int totalBytes = totalParts * 4 + payload.length; - int progressBytes = totalParts * 4 + payload.length; - if (remainingParts > 1) { - progressBytes -= (remainingParts - 1) * partSize; - } - if (remainingParts > 0) { - progressBytes -= (payload.length % partSize); - } - - final int progressPercent = Math.round((100.0f * progressBytes) / totalBytes); - - LOG.debug("Data upload progress: {} parts remaining ({}%)", remainingParts, progressPercent); - - if (remainingParts > 0) { - if (callback != null) { - callback.onUploadProgress(progressPercent); - } - } else { - onUploadFinish(true); - } - }); - for (int i = 0; i * partSize < payload.length; i++) { + final int currentPart = i + 1; final int startIndex = i * partSize; - final int endIndex = Math.min((i + 1) * partSize, payload.length); - LOG.debug("Uploading part {} of {}, from {} to {}", (i + 1), totalParts, startIndex, endIndex); + final int endIndex = Math.min(currentPart * partSize, payload.length); + LOG.debug("Uploading part {} of {}, from {} to {}", currentPart, totalParts, startIndex, endIndex); final byte[] chunkToSend = new byte[4 + endIndex - startIndex]; BLETypeConversions.writeUint16(chunkToSend, 0, totalParts); - BLETypeConversions.writeUint16(chunkToSend, 2, i + 1); + BLETypeConversions.writeUint16(chunkToSend, 2, currentPart); System.arraycopy(payload, startIndex, chunkToSend, 4, endIndex - startIndex); - characteristic.write("upload part " + (i + 1) + " of " + totalParts, chunkToSend); + + characteristic.write("upload part " + currentPart + " of " + totalParts, chunkToSend, new XiaomiCharacteristic.SendCallback() { + @Override + public void onSend() { + final int progressPercent = Math.round((100.0f * currentPart) / totalParts); + LOG.debug("Data upload progress: {}/{} parts sent ({}% done)", currentPart, totalParts, progressPercent); + + if (currentPart >= totalParts) { + onUploadFinish(true); + } else if (callback != null) { + callback.onUploadProgress(progressPercent); + } + } + + @Override + public void onNack() { + LOG.warn("NACK received while uploading part {}/{}", currentPart, totalParts); + // TODO callback.onUploadFinish(false); ? + } + }); } } @@ -189,8 +186,6 @@ public class XiaomiDataUploadService extends AbstractXiaomiService { if (callback != null) { callback.onUploadFinish(success); } - - characteristic.setCallback(null); } public interface Callback {