Skip to content

Commit

Permalink
Merge pull request #158 from obermillerk/main
Browse files Browse the repository at this point in the history
Add stream variants for download and upload in FsManager
  • Loading branch information
philips77 authored Sep 6, 2024
2 parents 719bbf8 + 8acca24 commit 6565120
Show file tree
Hide file tree
Showing 9 changed files with 443 additions and 14 deletions.
152 changes: 149 additions & 3 deletions mcumgr-core/src/main/java/io/runtime/mcumgr/managers/FsManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -239,14 +244,37 @@ 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).
* <p>
* 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.
* <p>
* 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<String, Object> payloadMap = buildUploadPayload(name, data, offset, totalBytes);
return send(OP_WRITE, ID_FILE, payloadMap, SHORT_TIMEOUT, McuMgrFsUploadResponse.class);
}

/*
* Build the upload payload map.
*/
@NotNull
private HashMap<String, Object> 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.
Expand All @@ -266,6 +294,47 @@ private HashMap<String, Object> buildUploadPayload(@NotNull String name, byte @N
return payloadMap;
}

/*
* Build the upload payload map.
*/
@NotNull
private HashMap<String, Object> 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<String, Object> 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).
Expand Down Expand Up @@ -492,6 +561,43 @@ protected UploadResponse write(byte @NotNull [] data, int offset) throws McuMgrE
}
}

/**
* Start image upload.
* <p>
* Multiple calls will queue multiple uploads, executed sequentially. This includes file
* downloads executed from {@link #fileDownload}.
* <p>
* 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
//******************************************************************
Expand Down Expand Up @@ -532,6 +638,46 @@ protected DownloadResponse read(int offset) throws McuMgrException {
}
}

/**
* Start image upload.
* <p>
* Multiple calls will queue multiple uploads, executed sequentially. This includes file
* downloads executed from {@link #fileUpload}.
* <p>
* 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)
//******************************************************************
Expand Down Expand Up @@ -876,15 +1022,15 @@ 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<String, Object> overheadTestMap = new HashMap<>();
overheadTestMap.put("name", name);
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down Expand Up @@ -75,6 +76,7 @@ public McuMgrResponse send(int offset) throws McuMgrException {
public void reset() {
mOffset = 0;
mData = null;
mDataLength = -1;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Loading

0 comments on commit 6565120

Please sign in to comment.