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) {