diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/managers/FsManager.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/managers/FsManager.java
index cd24f34..640f639 100644
--- a/mcumgr-core/src/main/java/io/runtime/mcumgr/managers/FsManager.java
+++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/managers/FsManager.java
@@ -13,6 +13,8 @@
import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.util.HashMap;
import io.runtime.mcumgr.McuMgrCallback;
@@ -33,6 +35,9 @@
import io.runtime.mcumgr.response.fs.McuMgrFsUploadResponse;
import io.runtime.mcumgr.transfer.Download;
import io.runtime.mcumgr.transfer.DownloadCallback;
+import io.runtime.mcumgr.transfer.StreamDownload;
+import io.runtime.mcumgr.transfer.StreamDownloadCallback;
+import io.runtime.mcumgr.transfer.StreamUpload;
import io.runtime.mcumgr.transfer.TransferController;
import io.runtime.mcumgr.transfer.TransferManager;
import io.runtime.mcumgr.transfer.Upload;
@@ -239,6 +244,29 @@ public McuMgrFsUploadResponse upload(@NotNull String name, byte @NotNull [] data
return send(OP_WRITE, ID_FILE, payloadMap, SHORT_TIMEOUT, McuMgrFsUploadResponse.class);
}
+ /**
+ * Send a packet of given data from the specified offset to the device (synchronous).
+ *
+ * The chunk size is limited by the current MTU. If the current MTU set by
+ * {@link #setUploadMtu(int)} is too large, the {@link InsufficientMtuException} error will be
+ * thrown. Use {@link InsufficientMtuException#getMtu()} to get the current MTU and
+ * pass it to {@link #setUploadMtu(int)} and try again.
+ *
+ * Use {@link #fileUpload} to upload the whole file asynchronously using one command.
+ *
+ * @param name the file name.
+ * @param data the file data.
+ * @param offset the offset, from which the chunk will be sent.
+ * @return The upload response.
+ * @see #fileUpload(String, byte[], UploadCallback)
+ */
+ @NotNull
+ public McuMgrFsUploadResponse upload(@NotNull String name, @NotNull InputStream data, int offset, int totalBytes)
+ throws McuMgrException {
+ HashMap payloadMap = buildUploadPayload(name, data, offset, totalBytes);
+ return send(OP_WRITE, ID_FILE, payloadMap, SHORT_TIMEOUT, McuMgrFsUploadResponse.class);
+ }
+
/*
* Build the upload payload map.
*/
@@ -246,7 +274,7 @@ public McuMgrFsUploadResponse upload(@NotNull String name, byte @NotNull [] data
private HashMap buildUploadPayload(@NotNull String name, byte @NotNull [] data, int offset) {
// Get the length of data (in bytes) to put into the upload packet. This calculated as:
// min(MTU - packetOverhead, imageLength - uploadOffset)
- int dataLength = Math.min(mMtu - calculatePacketOverhead(name, data, offset),
+ int dataLength = Math.min(mMtu - calculatePacketOverhead(name, data.length, offset),
data.length - offset);
// Copy the data from the image into a buffer.
@@ -266,6 +294,47 @@ private HashMap buildUploadPayload(@NotNull String name, byte @N
return payloadMap;
}
+ /*
+ * Build the upload payload map.
+ */
+ @NotNull
+ private HashMap buildUploadPayload(@NotNull String name, @NotNull InputStream data, int offset, int totalBytes)
+ throws McuMgrException {
+ // Get the length of data (in bytes) to put into the upload packet. This calculated as:
+ // min(MTU - packetOverhead, imageLength - uploadOffset)
+ int dataLength = Math.min(mMtu - calculatePacketOverhead(name, totalBytes, offset),
+ totalBytes - offset);
+
+ // Copy the data from the image into a buffer.
+ byte[] sendBuffer = new byte[dataLength];
+ try {
+ int totalRead = 0;
+ while(totalRead < dataLength) {
+ int read = data.read(sendBuffer, totalRead, dataLength - totalRead);
+ if (read < 0) {
+ throw new McuMgrException("Data InputStream reached end of file. Expected " +
+ (totalBytes - offset - totalRead) + " more bytes."
+ );
+ }
+ totalRead += read;
+ }
+ } catch(IOException e) {
+ throw new McuMgrException("Failed to read data for packet.", e);
+ }
+
+ // Create the map of key-values for the McuManager payload
+ HashMap payloadMap = new HashMap<>();
+ // Put the name, data and offset
+ payloadMap.put("name", name);
+ payloadMap.put("data", sendBuffer);
+ payloadMap.put("off", offset);
+ if (offset == 0) {
+ // Only send the length of the image in the first packet of the upload
+ payloadMap.put("len", totalBytes);
+ }
+ return payloadMap;
+ }
+
/**
* Command allows to retrieve status of an existing file from specified path of a target device
* (asynchronous).
@@ -492,6 +561,43 @@ protected UploadResponse write(byte @NotNull [] data, int offset) throws McuMgrE
}
}
+ /**
+ * Start image upload.
+ *
+ * Multiple calls will queue multiple uploads, executed sequentially. This includes file
+ * downloads executed from {@link #fileDownload}.
+ *
+ * The upload may be controlled using the {@link TransferController} returned by this method.
+ *
+ * @param data The file data to upload.
+ * @param callback Receives callbacks from the upload.
+ * @return The object used to control this upload.
+ * @see TransferController
+ */
+ @NotNull
+ public TransferController fileUpload(@NotNull String name, @NotNull InputStream data, int totalBytes, @NotNull UploadCallback callback) {
+ return startUpload(new FileStreamUpload(name, data, totalBytes, callback));
+ }
+
+ /**
+ * File Upload Implementation.
+ */
+ public class FileStreamUpload extends StreamUpload {
+
+ @NotNull
+ private final String mName;
+
+ protected FileStreamUpload(@NotNull String name, @NotNull InputStream data, int totalBytes, @NotNull UploadCallback callback) {
+ super(data, totalBytes, callback);
+ mName = name;
+ }
+
+ @Override
+ protected UploadResponse write(@NotNull InputStream data, int offset, int totalBytes) throws McuMgrException {
+ return upload(mName, data, offset, totalBytes);
+ }
+ }
+
//******************************************************************
// File Download
//******************************************************************
@@ -532,6 +638,46 @@ protected DownloadResponse read(int offset) throws McuMgrException {
}
}
+ /**
+ * Start image upload.
+ *
+ * Multiple calls will queue multiple uploads, executed sequentially. This includes file
+ * downloads executed from {@link #fileUpload}.
+ *
+ * The upload may be controlled using the {@link TransferController} returned by this method.
+ *
+ * @param callback Receives callbacks from the upload.
+ * @return The object used to control this upload.
+ * @see TransferController
+ */
+ @NotNull
+ public TransferController fileDownload(
+ @NotNull String name,
+ @NotNull OutputStream dataOutput,
+ @NotNull StreamDownloadCallback callback
+ ) {
+ return startDownload(new FileStreamDownload(name, dataOutput, callback));
+ }
+
+ public class FileStreamDownload extends StreamDownload {
+ @NotNull
+ private final String mName;
+
+ protected FileStreamDownload(
+ @NotNull String name,
+ @NotNull OutputStream dataOutput,
+ @NotNull StreamDownloadCallback callback
+ ) {
+ super(dataOutput, callback);
+ mName = name;
+ }
+
+ @Override
+ protected DownloadResponse read(int offset) throws McuMgrException {
+ return download(mName, offset);
+ }
+ }
+
//******************************************************************
// File Upload / Download (OLD, DEPRECATED)
//******************************************************************
@@ -876,7 +1022,7 @@ public void onError(@NotNull McuMgrException error) {
}
};
- private int calculatePacketOverhead(@NotNull String name, byte @NotNull [] data, int offset) {
+ private int calculatePacketOverhead(@NotNull String name, int totalBytes, int offset) {
try {
if (getScheme().isCoap()) {
HashMap overheadTestMap = new HashMap<>();
@@ -884,7 +1030,7 @@ private int calculatePacketOverhead(@NotNull String name, byte @NotNull [] data,
overheadTestMap.put("data", new byte[0]);
overheadTestMap.put("off", offset);
if (offset == 0) {
- overheadTestMap.put("len", data.length);
+ overheadTestMap.put("len", totalBytes);
}
byte[] header = {0, 0, 0, 0, 0, 0, 0, 0};
overheadTestMap.put("_h", header);
diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/Download.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/Download.java
index e4b2386..9b9b1f5 100644
--- a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/Download.java
+++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/Download.java
@@ -47,6 +47,7 @@ public McuMgrResponse send(int offset) throws McuMgrException {
// The first packet contains the file length.
if (response.off == 0) {
mData = new byte[response.len];
+ mDataLength = response.len;
}
if (mData == null) {
throw new McuMgrException("Download buffer is null, packet with offset 0 was never received.");
@@ -75,6 +76,7 @@ public McuMgrResponse send(int offset) throws McuMgrException {
public void reset() {
mOffset = 0;
mData = null;
+ mDataLength = -1;
}
@Override
diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamDownload.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamDownload.java
new file mode 100644
index 0000000..00abb25
--- /dev/null
+++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamDownload.java
@@ -0,0 +1,110 @@
+package io.runtime.mcumgr.transfer;
+
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import io.runtime.mcumgr.McuMgrErrorCode;
+import io.runtime.mcumgr.exception.McuMgrErrorException;
+import io.runtime.mcumgr.exception.McuMgrException;
+import io.runtime.mcumgr.response.DownloadResponse;
+import io.runtime.mcumgr.response.McuMgrResponse;
+
+@SuppressWarnings("unused")
+public abstract class StreamDownload extends StreamTransfer {
+
+ @NotNull
+ private final OutputStream mDataOutput;
+
+ @Nullable
+ private final StreamDownloadCallback mCallback;
+
+ protected StreamDownload(@NotNull OutputStream dataOutput) {
+ this(dataOutput, null);
+ }
+
+ protected StreamDownload(
+ @NotNull OutputStream dataOutput,
+ @Nullable StreamDownloadCallback callback
+ ) {
+ mDataOutput = dataOutput;
+ mCallback = callback;
+ }
+
+ /**
+ * Sends read request from given offset.
+ *
+ * @param offset the offset.
+ * @return received response.
+ * @throws McuMgrException a reason of a failure.
+ */
+ protected abstract DownloadResponse read(int offset) throws McuMgrException;
+
+ @Override
+ public McuMgrResponse send(int offset) throws McuMgrException {
+ DownloadResponse response = read(offset);
+ // Check for a McuManager error.
+ if (response.rc != 0) {
+ throw new McuMgrErrorException(McuMgrErrorCode.valueOf(response.rc));
+ }
+
+ // The first packet contains the file length.
+ if (response.off == 0) {
+ mDataLength = response.len;
+ }
+
+ // Validate response body
+ if (response.data == null) {
+ throw new McuMgrException("Download response data is null.");
+ }
+ if (mDataLength < 0) {
+ throw new McuMgrException("Download size not set.");
+ }
+
+ try {
+ mDataOutput.write(response.data);
+ } catch (IOException e) {
+ throw new McuMgrException("Download data failed to write to stream.", e);
+ }
+ mOffset = response.off + response.data.length;
+
+ return response;
+ }
+
+ @Override
+ public void reset() {
+ mOffset = 0;
+ mDataLength = -1;
+ }
+
+ @Override
+ public void onProgressChanged(int current, int total, long timestamp) {
+ if (mCallback != null) {
+ mCallback.onDownloadProgressChanged(current, total, timestamp);
+ }
+ }
+
+ @Override
+ public void onFailed(@NotNull McuMgrException e) {
+ if (mCallback != null) {
+ mCallback.onDownloadFailed(e);
+ }
+ }
+
+ @Override
+ public void onCompleted() {
+ if (mCallback != null) {
+ mCallback.onDownloadCompleted();
+ }
+ }
+
+ @Override
+ public void onCanceled() {
+ if (mCallback != null) {
+ mCallback.onDownloadCanceled();
+ }
+ }
+}
diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamDownloadCallback.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamDownloadCallback.java
new file mode 100644
index 0000000..1bfc522
--- /dev/null
+++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamDownloadCallback.java
@@ -0,0 +1,33 @@
+package io.runtime.mcumgr.transfer;
+
+import org.jetbrains.annotations.NotNull;
+
+import io.runtime.mcumgr.exception.McuMgrException;
+
+public interface StreamDownloadCallback {
+ /**
+ * Called when a response has been received successfully.
+ *
+ * @param current the number of bytes downloaded so far.
+ * @param total the total size of the download in bytes.
+ * @param timestamp the timestamp of when the response was received.
+ */
+ void onDownloadProgressChanged(int current, int total, long timestamp);
+
+ /**
+ * Called when the download has failed.
+ *
+ * @param error the error. See the cause for more info.
+ */
+ void onDownloadFailed(@NotNull McuMgrException error);
+
+ /**
+ * Called when the download has been canceled.
+ */
+ void onDownloadCanceled();
+
+ /**
+ * Called when the download has finished successfully.
+ */
+ void onDownloadCompleted();
+}
diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamTransfer.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamTransfer.java
new file mode 100644
index 0000000..d702ba5
--- /dev/null
+++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamTransfer.java
@@ -0,0 +1,33 @@
+package io.runtime.mcumgr.transfer;
+
+import org.jetbrains.annotations.Nullable;
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+public abstract class StreamTransfer extends Transfer {
+
+ StreamTransfer() {
+ this(0);
+ }
+
+ StreamTransfer(int offset) {
+ this(offset, -1);
+ }
+
+ StreamTransfer(int offset, int totalBytes) {
+ super(offset);
+ mDataLength = totalBytes;
+ }
+
+ @Override
+ @Deprecated
+ public byte @Nullable [] getData() {
+ throw new UnsupportedOperationException("StreamTransfer has no retrievable data.");
+ }
+
+ /**
+ * Returns true if transfer is complete.
+ */
+ public boolean isFinished() {
+ return mOffset == mDataLength;
+ }
+}
diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamUpload.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamUpload.java
new file mode 100644
index 0000000..5715650
--- /dev/null
+++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/StreamUpload.java
@@ -0,0 +1,83 @@
+package io.runtime.mcumgr.transfer;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.InputStream;
+
+import io.runtime.mcumgr.McuMgrErrorCode;
+import io.runtime.mcumgr.exception.McuMgrErrorException;
+import io.runtime.mcumgr.exception.McuMgrException;
+import io.runtime.mcumgr.response.McuMgrResponse;
+import io.runtime.mcumgr.response.UploadResponse;
+
+@SuppressWarnings("unused")
+public abstract class StreamUpload extends StreamTransfer {
+
+ @NotNull
+ private final InputStream mDataInput;
+
+ private final UploadCallback mCallback;
+
+ protected StreamUpload(@NotNull InputStream data, int totalBytes) {
+ this(data, totalBytes, null);
+ }
+
+ protected StreamUpload(
+ @NotNull InputStream data,
+ int totalBytes,
+ @Nullable UploadCallback callback
+ ) {
+ super(0, totalBytes);
+ mDataInput = data;
+ mCallback = callback;
+ }
+
+ protected abstract UploadResponse write(@NotNull InputStream data, int offset, int totalBytes) throws McuMgrException;
+
+ @Override
+ public McuMgrResponse send(int offset) throws McuMgrException {
+ UploadResponse response = write(mDataInput, offset, mDataLength);
+ // Check for a McuManager error.
+ if (response.rc != 0) {
+ throw new McuMgrErrorException(McuMgrErrorCode.valueOf(response.rc));
+ }
+
+ mOffset = response.off;
+
+ return response;
+ }
+
+ @Override
+ public void reset() {
+ mOffset = 0;
+ }
+
+ @Override
+ public void onProgressChanged(int current, int total, long timestamp) {
+ if (mCallback != null) {
+ mCallback.onUploadProgressChanged(current, total, timestamp);
+ }
+ }
+
+ @Override
+ public void onFailed(@NotNull McuMgrException e) {
+ if (mCallback != null) {
+ mCallback.onUploadFailed(e);
+ }
+ }
+
+ @Override
+ public void onCompleted() {
+ if (mCallback != null) {
+ mCallback.onUploadCompleted();
+ }
+ }
+
+ @Override
+ public void onCanceled() {
+ if (mCallback != null) {
+ mCallback.onUploadCanceled();
+ }
+ }
+}
diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/Transfer.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/Transfer.java
index f2bfd07..0a8fd5c 100644
--- a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/Transfer.java
+++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/Transfer.java
@@ -11,23 +11,23 @@ public abstract class Transfer implements TransferCallback {
byte @Nullable [] mData;
int mOffset;
+ int mDataLength;
+
Transfer() {
- mData = null;
- mOffset = 0;
+ this(null, 0);
}
Transfer(int offset) {
- mData = null;
- mOffset = offset;
+ this(null, offset);
}
Transfer(byte @Nullable [] data) {
- mData = data;
- mOffset = 0;
+ this(data, 0);
}
Transfer(byte @Nullable [] data, int offset) {
mData = data;
+ mDataLength = data == null ? -1 : data.length;
mOffset = offset;
}
diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/TransferCallable.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/TransferCallable.java
index e57660c..ea00e9a 100644
--- a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/TransferCallable.java
+++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/TransferCallable.java
@@ -97,12 +97,8 @@ public Transfer call() throws InsufficientMtuException {
return mTransfer;
}
- if (mTransfer.getData() == null) {
- throw new NullPointerException("Transfer data is null!");
- }
-
// Call the progress callback.
- mTransfer.onProgressChanged(mTransfer.getOffset(), mTransfer.getData().length,
+ mTransfer.onProgressChanged(mTransfer.getOffset(), mTransfer.mDataLength,
System.currentTimeMillis());
}
}
diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/TransferManager.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/TransferManager.java
index 107980d..9d6aed6 100644
--- a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/TransferManager.java
+++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/TransferManager.java
@@ -36,6 +36,19 @@ public TransferController startUpload(@NotNull Upload upload) {
return startTransfer(upload);
}
+ /**
+ * Start an upload.
+ *
+ * If there is an active transfer being executed on this manager, the transfer will be queued.
+ *
+ * @param upload The upload to start.
+ * @return The controller used to pause, resume, or cancel the upload.
+ */
+ @NotNull
+ public TransferController startUpload(@NotNull StreamUpload upload) {
+ return startTransfer(upload);
+ }
+
/**
* Start an download.
*
@@ -49,6 +62,19 @@ public TransferController startDownload(@NotNull Download download) {
return startTransfer(download);
}
+ /**
+ * Start an download.
+ *
+ * If there is an active transfer being executed on this manager, the download will be queued.
+ *
+ * @param download The upload to start.
+ * @return The controller used to pause, resume, or cancel the download.
+ */
+ @NotNull
+ public TransferController startDownload(@NotNull StreamDownload download) {
+ return startTransfer(download);
+ }
+
@NotNull
private synchronized TransferController startTransfer(@NotNull final Transfer transfer) {